automation_model 1.0.440-dev → 1.0.440
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/lib/api.d.ts +43 -1
- package/lib/api.js +241 -43
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +5 -2
- package/lib/auto_page.js +215 -53
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +7 -3
- package/lib/browser_manager.js +170 -85
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.d.ts +2 -0
- package/lib/bruno.js +381 -0
- package/lib/bruno.js.map +1 -0
- package/lib/command_common.d.ts +6 -0
- package/lib/command_common.js +198 -0
- package/lib/command_common.js.map +1 -0
- package/lib/environment.d.ts +4 -0
- package/lib/environment.js +6 -2
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.d.ts +6 -0
- package/lib/error-messages.js +206 -0
- package/lib/error-messages.js.map +1 -0
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +61 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/generation_scripts.d.ts +4 -0
- package/lib/generation_scripts.js +2 -0
- package/lib/generation_scripts.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +5 -2
- package/lib/init_browser.js +128 -11
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.d.ts +7 -0
- package/lib/locate_element.js +215 -0
- package/lib/locate_element.js.map +1 -0
- package/lib/locator.d.ts +36 -0
- package/lib/locator.js +165 -0
- package/lib/locator.js.map +1 -1
- package/lib/locator_log.d.ts +26 -0
- package/lib/locator_log.js +69 -0
- package/lib/locator_log.js.map +1 -0
- package/lib/network.d.ts +3 -0
- package/lib/network.js +183 -0
- package/lib/network.js.map +1 -0
- package/lib/scripts/axe.mini.js +12 -0
- package/lib/snapshot_validation.d.ts +37 -0
- package/lib/snapshot_validation.js +280 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +108 -36
- package/lib/stable_browser.js +1930 -1238
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +13 -0
- package/lib/table.js +187 -0
- package/lib/table.js.map +1 -0
- package/lib/table_helper.d.ts +19 -0
- package/lib/table_helper.js +116 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +7 -0
- package/lib/test_context.js +15 -10
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +22 -2
- package/lib/utils.js +672 -11
- package/lib/utils.js.map +1 -1
- package/package.json +15 -12
package/lib/stable_browser.js
CHANGED
|
@@ -2,21 +2,32 @@
|
|
|
2
2
|
import { expect } from "@playwright/test";
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
4
|
import fs from "fs";
|
|
5
|
+
import { Jimp } from "jimp";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import reg_parser from "regex-parser";
|
|
7
|
-
import sharp from "sharp";
|
|
8
8
|
import { findDateAlternatives, findNumberAlternatives } from "./analyze_helper.js";
|
|
9
9
|
import { getDateTimeValue } from "./date_time.js";
|
|
10
10
|
import drawRectangle from "./drawRect.js";
|
|
11
11
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
-
import { decrypt } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
|
|
15
14
|
import csv from "csv-parser";
|
|
16
15
|
import { Readable } from "node:stream";
|
|
17
16
|
import readline from "readline";
|
|
18
|
-
|
|
17
|
+
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
18
|
+
import { getTestData } from "./auto_page.js";
|
|
19
|
+
import { locate_element } from "./locate_element.js";
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
22
|
+
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
23
|
+
import { LocatorLog } from "./locator_log.js";
|
|
24
|
+
import axios from "axios";
|
|
25
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
27
|
+
import { snapshotValidation } from "./snapshot_validation.js";
|
|
28
|
+
export const Types = {
|
|
19
29
|
CLICK: "click_element",
|
|
30
|
+
WAIT_ELEMENT: "wait_element",
|
|
20
31
|
NAVIGATE: "navigate",
|
|
21
32
|
FILL: "fill_element",
|
|
22
33
|
EXECUTE: "execute_page_method",
|
|
@@ -26,6 +37,8 @@ const Types = {
|
|
|
26
37
|
GET_PAGE_STATUS: "get_page_status",
|
|
27
38
|
CLICK_ROW_ACTION: "click_row_action",
|
|
28
39
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
40
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
41
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
29
42
|
ANALYZE_TABLE: "analyze_table",
|
|
30
43
|
SELECT: "select_combobox",
|
|
31
44
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
@@ -36,21 +49,44 @@ const Types = {
|
|
|
36
49
|
UNCHECK: "uncheck_element",
|
|
37
50
|
EXTRACT: "extract_attribute",
|
|
38
51
|
CLOSE_PAGE: "close_page",
|
|
52
|
+
TABLE_OPERATION: "table_operation",
|
|
39
53
|
SET_DATE_TIME: "set_date_time",
|
|
40
54
|
SET_VIEWPORT: "set_viewport",
|
|
41
55
|
VERIFY_VISUAL: "verify_visual",
|
|
42
56
|
LOAD_DATA: "load_data",
|
|
43
57
|
SET_INPUT: "set_input",
|
|
58
|
+
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
59
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
60
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
61
|
+
BRUNO: "bruno",
|
|
62
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
63
|
+
SET_INPUT_FILES: "set_input_files",
|
|
64
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
65
|
+
};
|
|
66
|
+
export const apps = {};
|
|
67
|
+
const formatElementName = (elementName) => {
|
|
68
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
44
69
|
};
|
|
45
70
|
class StableBrowser {
|
|
46
|
-
|
|
71
|
+
browser;
|
|
72
|
+
page;
|
|
73
|
+
logger;
|
|
74
|
+
context;
|
|
75
|
+
world;
|
|
76
|
+
project_path = null;
|
|
77
|
+
webLogFile = null;
|
|
78
|
+
networkLogger = null;
|
|
79
|
+
configuration = null;
|
|
80
|
+
appName = "main";
|
|
81
|
+
tags = null;
|
|
82
|
+
isRecording = false;
|
|
83
|
+
initSnapshotTaken = false;
|
|
84
|
+
constructor(browser, page, logger = null, context = null, world = null) {
|
|
47
85
|
this.browser = browser;
|
|
48
86
|
this.page = page;
|
|
49
87
|
this.logger = logger;
|
|
50
88
|
this.context = context;
|
|
51
|
-
this.
|
|
52
|
-
this.webLogFile = null;
|
|
53
|
-
this.configuration = null;
|
|
89
|
+
this.world = world;
|
|
54
90
|
if (!this.logger) {
|
|
55
91
|
this.logger = console;
|
|
56
92
|
}
|
|
@@ -75,17 +111,32 @@ class StableBrowser {
|
|
|
75
111
|
catch (e) {
|
|
76
112
|
this.logger.error("unable to read ai_config.json");
|
|
77
113
|
}
|
|
78
|
-
const logFolder = path.join(this.project_path, "logs", "web");
|
|
79
|
-
this.webLogFile = this.getWebLogFile(logFolder);
|
|
80
|
-
this.registerConsoleLogListener(page, context, this.webLogFile);
|
|
81
|
-
this.registerRequestListener();
|
|
82
|
-
context.pages = [this.page];
|
|
83
114
|
context.pageLoading = { status: false };
|
|
115
|
+
context.pages = [this.page];
|
|
116
|
+
const logFolder = path.join(this.project_path, "logs", "web");
|
|
117
|
+
this.world = world;
|
|
118
|
+
this.registerEventListeners(this.context);
|
|
119
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
120
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
121
|
+
}
|
|
122
|
+
registerEventListeners(context) {
|
|
123
|
+
this.registerConsoleLogListener(this.page, context);
|
|
124
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
125
|
+
if (!context.pageLoading) {
|
|
126
|
+
context.pageLoading = { status: false };
|
|
127
|
+
}
|
|
84
128
|
context.playContext.on("page", async function (page) {
|
|
129
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
130
|
+
console.log("close unexpected popups");
|
|
131
|
+
await page.close();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
85
134
|
context.pageLoading.status = true;
|
|
86
135
|
this.page = page;
|
|
87
136
|
context.page = page;
|
|
88
137
|
context.pages.push(page);
|
|
138
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
139
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
89
140
|
page.on("close", async () => {
|
|
90
141
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
91
142
|
this.context.pages.pop();
|
|
@@ -110,117 +161,164 @@ class StableBrowser {
|
|
|
110
161
|
context.pageLoading.status = false;
|
|
111
162
|
}.bind(this));
|
|
112
163
|
}
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
164
|
+
async switchApp(appName) {
|
|
165
|
+
// check if the current app (this.appName) is the same as the new app
|
|
166
|
+
if (this.appName === appName) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
let newContextCreated = false;
|
|
170
|
+
if (!apps[appName]) {
|
|
171
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
172
|
+
newContextCreated = true;
|
|
173
|
+
apps[appName] = {
|
|
174
|
+
context: newContext,
|
|
175
|
+
browser: newContext.browser,
|
|
176
|
+
page: newContext.page,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const tempContext = {};
|
|
180
|
+
_copyContext(this, tempContext);
|
|
181
|
+
_copyContext(apps[appName], this);
|
|
182
|
+
apps[this.appName] = tempContext;
|
|
183
|
+
this.appName = appName;
|
|
184
|
+
if (newContextCreated) {
|
|
185
|
+
this.registerEventListeners(this.context);
|
|
186
|
+
await this.goto(this.context.environment.baseUrl);
|
|
187
|
+
await this.waitForPageLoad();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async switchTab(tabTitleOrIndex) {
|
|
191
|
+
// first check if the tabNameOrIndex is a number
|
|
192
|
+
let index = parseInt(tabTitleOrIndex);
|
|
193
|
+
if (!isNaN(index)) {
|
|
194
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
195
|
+
this.page = this.context.pages[index];
|
|
196
|
+
this.context.page = this.page;
|
|
197
|
+
await this.page.bringToFront();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
116
200
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
201
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
202
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
203
|
+
let page = this.context.pages[i];
|
|
204
|
+
let title = await page.title();
|
|
205
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
206
|
+
this.page = page;
|
|
207
|
+
this.context.page = this.page;
|
|
208
|
+
await this.page.bringToFront();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
120
211
|
}
|
|
121
|
-
|
|
122
|
-
return path.join(logFolder, fileName);
|
|
212
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
123
213
|
}
|
|
124
|
-
registerConsoleLogListener(page, context
|
|
214
|
+
registerConsoleLogListener(page, context) {
|
|
125
215
|
if (!this.context.webLogger) {
|
|
126
216
|
this.context.webLogger = [];
|
|
127
217
|
}
|
|
128
218
|
page.on("console", async (msg) => {
|
|
129
|
-
|
|
219
|
+
const obj = {
|
|
130
220
|
type: msg.type(),
|
|
131
221
|
text: msg.text(),
|
|
132
222
|
location: msg.location(),
|
|
133
223
|
time: new Date().toISOString(),
|
|
134
|
-
}
|
|
135
|
-
|
|
224
|
+
};
|
|
225
|
+
this.context.webLogger.push(obj);
|
|
226
|
+
if (msg.type() === "error") {
|
|
227
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
228
|
+
}
|
|
136
229
|
});
|
|
137
230
|
}
|
|
138
|
-
registerRequestListener() {
|
|
139
|
-
this.
|
|
231
|
+
registerRequestListener(page, context, logFile) {
|
|
232
|
+
if (!this.context.networkLogger) {
|
|
233
|
+
this.context.networkLogger = [];
|
|
234
|
+
}
|
|
235
|
+
page.on("request", async (data) => {
|
|
236
|
+
const startTime = new Date().getTime();
|
|
140
237
|
try {
|
|
141
|
-
const pageUrl = new URL(
|
|
238
|
+
const pageUrl = new URL(page.url());
|
|
142
239
|
const requestUrl = new URL(data.url());
|
|
143
240
|
if (pageUrl.hostname === requestUrl.hostname) {
|
|
144
241
|
const method = data.method();
|
|
145
|
-
if (
|
|
242
|
+
if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
146
243
|
const token = await data.headerValue("Authorization");
|
|
147
244
|
if (token) {
|
|
148
|
-
|
|
245
|
+
context.authtoken = token;
|
|
149
246
|
}
|
|
150
247
|
}
|
|
151
248
|
}
|
|
249
|
+
const response = await data.response();
|
|
250
|
+
const endTime = new Date().getTime();
|
|
251
|
+
const obj = {
|
|
252
|
+
url: data.url(),
|
|
253
|
+
method: data.method(),
|
|
254
|
+
postData: data.postData(),
|
|
255
|
+
error: data.failure() ? data.failure().errorText : null,
|
|
256
|
+
duration: endTime - startTime,
|
|
257
|
+
startTime,
|
|
258
|
+
};
|
|
259
|
+
context.networkLogger.push(obj);
|
|
260
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
152
261
|
}
|
|
153
262
|
catch (error) {
|
|
154
|
-
console.error("Error in request listener", error);
|
|
263
|
+
// console.error("Error in request listener", error);
|
|
264
|
+
context.networkLogger.push({
|
|
265
|
+
error: "not able to listen",
|
|
266
|
+
message: error.message,
|
|
267
|
+
stack: error.stack,
|
|
268
|
+
time: new Date().toISOString(),
|
|
269
|
+
});
|
|
270
|
+
// await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
|
|
155
271
|
}
|
|
156
272
|
});
|
|
157
273
|
}
|
|
158
274
|
// async closeUnexpectedPopups() {
|
|
159
275
|
// await closeUnexpectedPopups(this.page);
|
|
160
276
|
// }
|
|
161
|
-
async goto(url) {
|
|
277
|
+
async goto(url, world = null) {
|
|
278
|
+
if (!url) {
|
|
279
|
+
throw new Error("url is null, verify that the environment file is correct");
|
|
280
|
+
}
|
|
162
281
|
if (!url.startsWith("http")) {
|
|
163
282
|
url = "https://" + url;
|
|
164
283
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!_params || typeof text !== "string") {
|
|
185
|
-
return text;
|
|
284
|
+
const state = {
|
|
285
|
+
value: url,
|
|
286
|
+
world: world,
|
|
287
|
+
type: Types.NAVIGATE,
|
|
288
|
+
text: `Navigate Page to: ${url}`,
|
|
289
|
+
operation: "goto",
|
|
290
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
291
|
+
info: {},
|
|
292
|
+
locate: false,
|
|
293
|
+
scroll: false,
|
|
294
|
+
screenshot: false,
|
|
295
|
+
highlight: false,
|
|
296
|
+
};
|
|
297
|
+
try {
|
|
298
|
+
await _preCommand(state, this);
|
|
299
|
+
await this.page.goto(url, {
|
|
300
|
+
timeout: 60000,
|
|
301
|
+
});
|
|
302
|
+
await _screenshot(state, this);
|
|
186
303
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// remove the _ prefix
|
|
191
|
-
regValue = key.substring(1);
|
|
192
|
-
}
|
|
193
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
304
|
+
catch (error) {
|
|
305
|
+
console.error("Error on goto", error);
|
|
306
|
+
_commandError(state, error, this);
|
|
194
307
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
198
|
-
// check if not null
|
|
199
|
-
if (!locator) {
|
|
200
|
-
return locator;
|
|
308
|
+
finally {
|
|
309
|
+
await _commandFinally(state, this);
|
|
201
310
|
}
|
|
202
|
-
// clone the locator
|
|
203
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
204
|
-
this.scanAndManipulate(locator, _params);
|
|
205
|
-
return locator;
|
|
206
311
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
else if (this._isObject(currentObj[key])) {
|
|
217
|
-
// Recursively scan nested objects
|
|
218
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
312
|
+
async _getLocator(locator, scope, _params) {
|
|
313
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
314
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
315
|
+
for (let key in locator) {
|
|
316
|
+
if (typeof locator[key] !== "string")
|
|
317
|
+
continue;
|
|
318
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
319
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
219
320
|
}
|
|
220
321
|
}
|
|
221
|
-
}
|
|
222
|
-
_getLocator(locator, scope, _params) {
|
|
223
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
224
322
|
let locatorReturn;
|
|
225
323
|
if (locator.role) {
|
|
226
324
|
if (locator.role[1].nameReg) {
|
|
@@ -228,7 +326,7 @@ class StableBrowser {
|
|
|
228
326
|
delete locator.role[1].nameReg;
|
|
229
327
|
}
|
|
230
328
|
// if (locator.role[1].name) {
|
|
231
|
-
// locator.role[1].name =
|
|
329
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
232
330
|
// }
|
|
233
331
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
234
332
|
}
|
|
@@ -247,7 +345,7 @@ class StableBrowser {
|
|
|
247
345
|
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
248
346
|
}
|
|
249
347
|
}
|
|
250
|
-
if (locator
|
|
348
|
+
if (locator?.engine) {
|
|
251
349
|
if (locator.engine === "css") {
|
|
252
350
|
locatorReturn = scope.locator(locator.selector);
|
|
253
351
|
}
|
|
@@ -268,192 +366,181 @@ class StableBrowser {
|
|
|
268
366
|
return locatorReturn;
|
|
269
367
|
}
|
|
270
368
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
271
|
-
|
|
369
|
+
if (css && css.locator) {
|
|
370
|
+
css = css.locator;
|
|
371
|
+
}
|
|
372
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
272
373
|
if (result.elementCount === 0) {
|
|
273
374
|
return;
|
|
274
375
|
}
|
|
275
|
-
let textElementCss = "[data-blinq-id
|
|
376
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
276
377
|
// css climb to parent element
|
|
277
378
|
const climbArray = [];
|
|
278
379
|
for (let i = 0; i < climb; i++) {
|
|
279
380
|
climbArray.push("..");
|
|
280
381
|
}
|
|
281
382
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
320
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
321
|
-
if (!shadowElement) {
|
|
322
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
326
|
-
elements = elements.concat(shadowElements);
|
|
327
|
-
}
|
|
328
|
-
let randomToken = null;
|
|
329
|
-
const foundElements = [];
|
|
330
|
-
if (regex) {
|
|
331
|
-
let regexpSearch = new RegExp(text, "im");
|
|
332
|
-
for (let i = 0; i < elements.length; i++) {
|
|
333
|
-
const element = elements[i];
|
|
334
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
335
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
336
|
-
foundElements.push(element);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
text = text.trim();
|
|
342
|
-
for (let i = 0; i < elements.length; i++) {
|
|
343
|
-
const element = elements[i];
|
|
344
|
-
if (partial) {
|
|
345
|
-
if ((element.innerText && element.innerText.trim().includes(text)) ||
|
|
346
|
-
(element.value && element.value.includes(text))) {
|
|
347
|
-
foundElements.push(element);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
352
|
-
(element.value && element.value === text)) {
|
|
353
|
-
foundElements.push(element);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
let noChildElements = [];
|
|
359
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
360
|
-
let element = foundElements[i];
|
|
361
|
-
let hasChild = false;
|
|
362
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
363
|
-
if (i === j) {
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (isParent(element, foundElements[j])) {
|
|
367
|
-
hasChild = true;
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (!hasChild) {
|
|
372
|
-
noChildElements.push(element);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
let elementCount = 0;
|
|
376
|
-
if (noChildElements.length > 0) {
|
|
377
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
378
|
-
if (randomToken === null) {
|
|
379
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
380
|
-
}
|
|
381
|
-
let element = noChildElements[i];
|
|
382
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
383
|
-
elementCount++;
|
|
384
|
-
}
|
|
383
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
384
|
+
if (css) {
|
|
385
|
+
resultCss = resultCss + " >> " + css;
|
|
386
|
+
}
|
|
387
|
+
return resultCss;
|
|
388
|
+
}
|
|
389
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
390
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
391
|
+
const locator = scope.locator(query);
|
|
392
|
+
const count = await locator.count();
|
|
393
|
+
if (!tag1) {
|
|
394
|
+
tag1 = "*";
|
|
395
|
+
}
|
|
396
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
397
|
+
let tagCount = 0;
|
|
398
|
+
for (let i = 0; i < count; i++) {
|
|
399
|
+
const element = locator.nth(i);
|
|
400
|
+
// check if the tag matches
|
|
401
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
402
|
+
if (!tag.startsWith("*")) {
|
|
403
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!el.setAttribute) {
|
|
408
|
+
el = el.parentElement;
|
|
409
|
+
}
|
|
410
|
+
// remove any attributes start with data-blinq-id
|
|
411
|
+
// for (let i = 0; i < el.attributes.length; i++) {
|
|
412
|
+
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
413
|
+
// el.removeAttribute(el.attributes[i].name);
|
|
414
|
+
// }
|
|
415
|
+
// }
|
|
416
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
417
|
+
return true;
|
|
418
|
+
}, [tag1, randomToken]))) {
|
|
419
|
+
continue;
|
|
385
420
|
}
|
|
386
|
-
|
|
387
|
-
}
|
|
421
|
+
tagCount++;
|
|
422
|
+
}
|
|
423
|
+
return { elementCount: tagCount, randomToken };
|
|
388
424
|
}
|
|
389
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
425
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
426
|
+
if (!info) {
|
|
427
|
+
info = {};
|
|
428
|
+
}
|
|
429
|
+
if (!info.failCause) {
|
|
430
|
+
info.failCause = {};
|
|
431
|
+
}
|
|
432
|
+
if (!info.log) {
|
|
433
|
+
info.log = "";
|
|
434
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
435
|
+
}
|
|
390
436
|
let locatorSearch = selectorHierarchy[index];
|
|
437
|
+
let originalLocatorSearch = "";
|
|
438
|
+
try {
|
|
439
|
+
originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
|
|
440
|
+
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
console.error(e);
|
|
444
|
+
}
|
|
391
445
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
392
446
|
let locator = null;
|
|
393
447
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
394
|
-
|
|
448
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
449
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
395
450
|
if (!locatorString) {
|
|
451
|
+
info.failCause.textNotFound = true;
|
|
452
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
396
453
|
return;
|
|
397
454
|
}
|
|
398
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
455
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
399
456
|
}
|
|
400
457
|
else if (locatorSearch.text) {
|
|
401
|
-
let
|
|
458
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
459
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
402
460
|
if (result.elementCount === 0) {
|
|
461
|
+
info.failCause.textNotFound = true;
|
|
462
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
403
463
|
return;
|
|
404
464
|
}
|
|
405
|
-
locatorSearch.css = "[data-blinq-id
|
|
465
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
406
466
|
if (locatorSearch.childCss) {
|
|
407
467
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
408
468
|
}
|
|
409
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
469
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
410
470
|
}
|
|
411
471
|
else {
|
|
412
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
472
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
413
473
|
}
|
|
414
474
|
// let cssHref = false;
|
|
415
475
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
416
476
|
// cssHref = true;
|
|
417
477
|
// }
|
|
418
478
|
let count = await locator.count();
|
|
479
|
+
if (count > 0 && !info.failCause.count) {
|
|
480
|
+
info.failCause.count = count;
|
|
481
|
+
}
|
|
419
482
|
//info.log += "total elements found " + count + "\n";
|
|
420
483
|
//let visibleCount = 0;
|
|
421
484
|
let visibleLocator = null;
|
|
422
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
485
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
423
486
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
487
|
+
if (info.locatorLog) {
|
|
488
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
489
|
+
}
|
|
424
490
|
return;
|
|
425
491
|
}
|
|
492
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
493
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
494
|
+
}
|
|
426
495
|
for (let j = 0; j < count; j++) {
|
|
427
496
|
let visible = await locator.nth(j).isVisible();
|
|
428
497
|
const enabled = await locator.nth(j).isEnabled();
|
|
429
498
|
if (!visibleOnly) {
|
|
430
499
|
visible = true;
|
|
431
500
|
}
|
|
432
|
-
if (visible && enabled) {
|
|
501
|
+
if (visible && (allowDisabled || enabled)) {
|
|
433
502
|
foundLocators.push(locator.nth(j));
|
|
503
|
+
if (info.locatorLog) {
|
|
504
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
505
|
+
}
|
|
434
506
|
}
|
|
435
|
-
else {
|
|
507
|
+
else if (logErrors) {
|
|
508
|
+
info.failCause.visible = visible;
|
|
509
|
+
info.failCause.enabled = enabled;
|
|
436
510
|
if (!info.printMessages) {
|
|
437
511
|
info.printMessages = {};
|
|
438
512
|
}
|
|
513
|
+
if (info.locatorLog && !visible) {
|
|
514
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
515
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
516
|
+
}
|
|
517
|
+
if (info.locatorLog && !enabled) {
|
|
518
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
519
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
520
|
+
}
|
|
439
521
|
if (!info.printMessages[j.toString()]) {
|
|
440
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
522
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
441
523
|
info.printMessages[j.toString()] = true;
|
|
442
524
|
}
|
|
443
525
|
}
|
|
444
526
|
}
|
|
445
527
|
}
|
|
446
528
|
async closeUnexpectedPopups(info, _params) {
|
|
529
|
+
if (!info) {
|
|
530
|
+
info = {};
|
|
531
|
+
info.failCause = {};
|
|
532
|
+
info.log = "";
|
|
533
|
+
}
|
|
447
534
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
448
535
|
if (!info) {
|
|
449
536
|
info = {};
|
|
450
537
|
}
|
|
451
|
-
info.log += "scan for popup handlers" + "\n";
|
|
538
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
452
539
|
const handlerGroup = [];
|
|
453
540
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
454
541
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
455
542
|
}
|
|
456
|
-
const scopes =
|
|
543
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
457
544
|
let result = null;
|
|
458
545
|
let scope = null;
|
|
459
546
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -475,53 +562,108 @@ class StableBrowser {
|
|
|
475
562
|
}
|
|
476
563
|
if (result.foundElements.length > 0) {
|
|
477
564
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
478
|
-
|
|
565
|
+
try {
|
|
566
|
+
await scope?.evaluate(() => {
|
|
567
|
+
window.__isClosingPopups = true;
|
|
568
|
+
});
|
|
569
|
+
await dialogCloseLocator.click();
|
|
570
|
+
// wait for the dialog to close
|
|
571
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
await scope?.evaluate(() => {
|
|
577
|
+
window.__isClosingPopups = false;
|
|
578
|
+
});
|
|
579
|
+
}
|
|
479
580
|
return { rerun: true };
|
|
480
581
|
}
|
|
481
582
|
}
|
|
482
583
|
}
|
|
483
584
|
return { rerun: false };
|
|
484
585
|
}
|
|
485
|
-
async _locate(selectors, info, _params, timeout =
|
|
586
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
587
|
+
if (!timeout) {
|
|
588
|
+
timeout = 30000;
|
|
589
|
+
}
|
|
486
590
|
for (let i = 0; i < 3; i++) {
|
|
487
591
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
488
592
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
489
593
|
let selector = selectors.locators[j];
|
|
490
594
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
491
595
|
}
|
|
492
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
596
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
493
597
|
if (!element.rerun) {
|
|
494
|
-
|
|
598
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
599
|
+
element.evaluate((el, randomToken) => {
|
|
600
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
601
|
+
}, randomToken);
|
|
602
|
+
// if (element._frame) {
|
|
603
|
+
// return element;
|
|
604
|
+
// }
|
|
605
|
+
const scope = element._frame ?? element.page();
|
|
606
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
607
|
+
let prefixSelector = "";
|
|
608
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
609
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
610
|
+
if (frameSelectorIndex !== -1) {
|
|
611
|
+
// remove everything after the >> internal:control=enter-frame
|
|
612
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
613
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
614
|
+
}
|
|
615
|
+
// if (element?._frame?._selector) {
|
|
616
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
617
|
+
// }
|
|
618
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
619
|
+
return scope.locator(newSelector);
|
|
495
620
|
}
|
|
496
621
|
}
|
|
497
622
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
498
623
|
}
|
|
499
|
-
async
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
624
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
625
|
+
if (!info) {
|
|
626
|
+
info = {};
|
|
627
|
+
info.failCause = {};
|
|
628
|
+
info.log = "";
|
|
629
|
+
}
|
|
630
|
+
let startTime = Date.now();
|
|
505
631
|
let scope = this.page;
|
|
632
|
+
if (selectors.frame) {
|
|
633
|
+
return selectors.frame;
|
|
634
|
+
}
|
|
506
635
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
507
|
-
const findFrame = (frame, framescope) => {
|
|
636
|
+
const findFrame = async (frame, framescope) => {
|
|
508
637
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
509
638
|
let frameLocator = frame.selectors[i];
|
|
510
639
|
if (frameLocator.css) {
|
|
511
|
-
|
|
512
|
-
|
|
640
|
+
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
641
|
+
if (frameLocator.index) {
|
|
642
|
+
testframescope = framescope.nth(frameLocator.index);
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
await testframescope.owner().evaluateHandle(() => true, null, {
|
|
646
|
+
timeout: 5000,
|
|
647
|
+
});
|
|
648
|
+
framescope = testframescope;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
console.error("frame not found " + frameLocator.css);
|
|
653
|
+
}
|
|
513
654
|
}
|
|
514
655
|
}
|
|
515
656
|
if (frame.children) {
|
|
516
|
-
return findFrame(frame.children, framescope);
|
|
657
|
+
return await findFrame(frame.children, framescope);
|
|
517
658
|
}
|
|
518
659
|
return framescope;
|
|
519
660
|
};
|
|
520
|
-
|
|
661
|
+
let fLocator = null;
|
|
521
662
|
while (true) {
|
|
522
663
|
let frameFound = false;
|
|
523
664
|
if (selectors.nestFrmLoc) {
|
|
524
|
-
|
|
665
|
+
fLocator = selectors.nestFrmLoc;
|
|
666
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
525
667
|
frameFound = true;
|
|
526
668
|
break;
|
|
527
669
|
}
|
|
@@ -529,6 +671,7 @@ class StableBrowser {
|
|
|
529
671
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
530
672
|
let frameLocator = selectors.frameLocators[i];
|
|
531
673
|
if (frameLocator.css) {
|
|
674
|
+
fLocator = frameLocator.css;
|
|
532
675
|
scope = scope.frameLocator(frameLocator.css);
|
|
533
676
|
frameFound = true;
|
|
534
677
|
break;
|
|
@@ -536,20 +679,55 @@ class StableBrowser {
|
|
|
536
679
|
}
|
|
537
680
|
}
|
|
538
681
|
if (!frameFound && selectors.iframe_src) {
|
|
682
|
+
fLocator = selectors.iframe_src;
|
|
539
683
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
540
684
|
}
|
|
541
685
|
if (!scope) {
|
|
542
|
-
info
|
|
543
|
-
|
|
686
|
+
if (info && info.locatorLog) {
|
|
687
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
688
|
+
}
|
|
689
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
690
|
+
if (Date.now() - startTime > timeout) {
|
|
691
|
+
info.failCause.iframeNotFound = true;
|
|
692
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
544
693
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
545
694
|
}
|
|
546
695
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
547
696
|
}
|
|
548
697
|
else {
|
|
698
|
+
if (info && info.locatorLog) {
|
|
699
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
700
|
+
}
|
|
549
701
|
break;
|
|
550
702
|
}
|
|
551
703
|
}
|
|
552
704
|
}
|
|
705
|
+
if (!scope) {
|
|
706
|
+
scope = this.page;
|
|
707
|
+
}
|
|
708
|
+
return scope;
|
|
709
|
+
}
|
|
710
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
711
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
712
|
+
return scope.evaluate(() => {
|
|
713
|
+
var bodyContent = document.body.innerHTML;
|
|
714
|
+
return bodyContent;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
718
|
+
if (!info) {
|
|
719
|
+
info = {};
|
|
720
|
+
info.failCause = {};
|
|
721
|
+
info.log = "";
|
|
722
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
723
|
+
}
|
|
724
|
+
let highPriorityTimeout = 5000;
|
|
725
|
+
let visibleOnlyTimeout = 6000;
|
|
726
|
+
let startTime = Date.now();
|
|
727
|
+
let locatorsCount = 0;
|
|
728
|
+
let lazy_scroll = false;
|
|
729
|
+
//let arrayMode = Array.isArray(selectors);
|
|
730
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
553
731
|
let selectorsLocators = null;
|
|
554
732
|
selectorsLocators = selectors.locators;
|
|
555
733
|
// group selectors by priority
|
|
@@ -585,18 +763,13 @@ class StableBrowser {
|
|
|
585
763
|
}
|
|
586
764
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
587
765
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
588
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
766
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
589
767
|
if (result.foundElements.length === 0) {
|
|
590
768
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
591
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
769
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
592
770
|
}
|
|
593
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
594
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
598
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
599
|
-
}
|
|
771
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
772
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
600
773
|
}
|
|
601
774
|
let foundElements = result.foundElements;
|
|
602
775
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -636,24 +809,43 @@ class StableBrowser {
|
|
|
636
809
|
return maxCountElement.locator;
|
|
637
810
|
}
|
|
638
811
|
}
|
|
639
|
-
if (
|
|
812
|
+
if (Date.now() - startTime > timeout) {
|
|
640
813
|
break;
|
|
641
814
|
}
|
|
642
|
-
if (
|
|
643
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
815
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
816
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
644
817
|
highPriorityOnly = false;
|
|
818
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
819
|
+
lazy_scroll = true;
|
|
820
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
821
|
+
}
|
|
645
822
|
}
|
|
646
|
-
if (
|
|
647
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
823
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
824
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
648
825
|
visibleOnly = false;
|
|
649
826
|
}
|
|
650
827
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
828
|
+
// sheck of more of half of the timeout has passed
|
|
829
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
830
|
+
highPriorityOnly = false;
|
|
831
|
+
visibleOnly = false;
|
|
832
|
+
}
|
|
651
833
|
}
|
|
652
834
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
653
|
-
info.
|
|
835
|
+
// if (info.locatorLog) {
|
|
836
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
837
|
+
// for (let line of lines) {
|
|
838
|
+
// this.logger.debug(line);
|
|
839
|
+
// }
|
|
840
|
+
// }
|
|
841
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
842
|
+
info.failCause.locatorNotFound = true;
|
|
843
|
+
if (!info?.failCause?.lastError) {
|
|
844
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
845
|
+
}
|
|
654
846
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
655
847
|
}
|
|
656
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
848
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
657
849
|
let foundElements = [];
|
|
658
850
|
const result = {
|
|
659
851
|
foundElements: foundElements,
|
|
@@ -661,17 +853,20 @@ class StableBrowser {
|
|
|
661
853
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
662
854
|
let foundLocators = [];
|
|
663
855
|
try {
|
|
664
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
856
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
665
857
|
}
|
|
666
858
|
catch (e) {
|
|
667
|
-
this
|
|
668
|
-
this.logger.debug(
|
|
859
|
+
// this call can fail it the browser is navigating
|
|
860
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
861
|
+
// this.logger.debug(e);
|
|
669
862
|
foundLocators = [];
|
|
670
863
|
try {
|
|
671
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
864
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
672
865
|
}
|
|
673
866
|
catch (e) {
|
|
674
|
-
|
|
867
|
+
if (logErrors) {
|
|
868
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
869
|
+
}
|
|
675
870
|
}
|
|
676
871
|
}
|
|
677
872
|
if (foundLocators.length === 1) {
|
|
@@ -682,90 +877,228 @@ class StableBrowser {
|
|
|
682
877
|
});
|
|
683
878
|
result.locatorIndex = i;
|
|
684
879
|
}
|
|
880
|
+
if (foundLocators.length > 1) {
|
|
881
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
882
|
+
const boxes = [];
|
|
883
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
884
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
885
|
+
}
|
|
886
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
887
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
888
|
+
if (j === k) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
892
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
893
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
894
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
895
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
896
|
+
// as the element is not unique, will remove it
|
|
897
|
+
boxes.splice(k, 1);
|
|
898
|
+
k--;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (boxes.length === 1) {
|
|
903
|
+
result.foundElements.push({
|
|
904
|
+
locator: boxes[0].locator.first(),
|
|
905
|
+
box: boxes[0].box,
|
|
906
|
+
unique: true,
|
|
907
|
+
});
|
|
908
|
+
result.locatorIndex = i;
|
|
909
|
+
}
|
|
910
|
+
else if (logErrors) {
|
|
911
|
+
info.failCause.foundMultiple = true;
|
|
912
|
+
if (info.locatorLog) {
|
|
913
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
685
917
|
}
|
|
686
918
|
return result;
|
|
687
919
|
}
|
|
688
|
-
async
|
|
689
|
-
|
|
920
|
+
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
921
|
+
const state = {
|
|
922
|
+
locate: false,
|
|
923
|
+
scroll: false,
|
|
924
|
+
highlight: false,
|
|
925
|
+
_params,
|
|
926
|
+
options,
|
|
927
|
+
world,
|
|
928
|
+
type: Types.CLICK,
|
|
929
|
+
text: "Click element",
|
|
930
|
+
operation: "simpleClick",
|
|
931
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
932
|
+
};
|
|
933
|
+
_preCommand(state, this);
|
|
690
934
|
const startTime = Date.now();
|
|
691
|
-
|
|
692
|
-
|
|
935
|
+
let timeout = 30000;
|
|
936
|
+
if (options && options.timeout) {
|
|
937
|
+
timeout = options.timeout;
|
|
693
938
|
}
|
|
694
|
-
|
|
695
|
-
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
696
|
-
info.operation = "click";
|
|
697
|
-
info.selectors = selectors;
|
|
698
|
-
let error = null;
|
|
699
|
-
let screenshotId = null;
|
|
700
|
-
let screenshotPath = null;
|
|
701
|
-
try {
|
|
702
|
-
let element = await this._locate(selectors, info, _params);
|
|
703
|
-
await this.scrollIfNeeded(element, info);
|
|
704
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
939
|
+
while (true) {
|
|
705
940
|
try {
|
|
706
|
-
await this.
|
|
707
|
-
|
|
708
|
-
|
|
941
|
+
const result = await locate_element(this.context, elementDescription, "click");
|
|
942
|
+
if (result?.elementNumber >= 0) {
|
|
943
|
+
const selectors = {
|
|
944
|
+
frame: result?.frame,
|
|
945
|
+
locators: [
|
|
946
|
+
{
|
|
947
|
+
css: result?.css,
|
|
948
|
+
},
|
|
949
|
+
],
|
|
950
|
+
};
|
|
951
|
+
await this.click(selectors, _params, options, world);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
709
954
|
}
|
|
710
955
|
catch (e) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
956
|
+
if (performance.now() - startTime > timeout) {
|
|
957
|
+
// throw e;
|
|
958
|
+
try {
|
|
959
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
960
|
+
}
|
|
961
|
+
finally {
|
|
962
|
+
await _commandFinally(state, this);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
970
|
+
const state = {
|
|
971
|
+
locate: false,
|
|
972
|
+
scroll: false,
|
|
973
|
+
highlight: false,
|
|
974
|
+
_params,
|
|
975
|
+
options,
|
|
976
|
+
world,
|
|
977
|
+
type: Types.FILL,
|
|
978
|
+
text: "Fill element",
|
|
979
|
+
operation: "simpleClickType",
|
|
980
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
981
|
+
};
|
|
982
|
+
_preCommand(state, this);
|
|
983
|
+
const startTime = Date.now();
|
|
984
|
+
let timeout = 30000;
|
|
985
|
+
if (options && options.timeout) {
|
|
986
|
+
timeout = options.timeout;
|
|
987
|
+
}
|
|
988
|
+
while (true) {
|
|
989
|
+
try {
|
|
990
|
+
const result = await locate_element(this.context, elementDescription, "fill", value);
|
|
991
|
+
if (result?.elementNumber >= 0) {
|
|
992
|
+
const selectors = {
|
|
993
|
+
frame: result?.frame,
|
|
994
|
+
locators: [
|
|
995
|
+
{
|
|
996
|
+
css: result?.css,
|
|
997
|
+
},
|
|
998
|
+
],
|
|
999
|
+
};
|
|
1000
|
+
await this.clickType(selectors, value, false, _params, options, world);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
catch (e) {
|
|
1005
|
+
if (performance.now() - startTime > timeout) {
|
|
1006
|
+
// throw e;
|
|
1007
|
+
try {
|
|
1008
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1009
|
+
}
|
|
1010
|
+
finally {
|
|
1011
|
+
await _commandFinally(state, this);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
716
1014
|
}
|
|
1015
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async click(selectors, _params, options = {}, world = null) {
|
|
1019
|
+
const state = {
|
|
1020
|
+
selectors,
|
|
1021
|
+
_params,
|
|
1022
|
+
options,
|
|
1023
|
+
world,
|
|
1024
|
+
text: "Click element",
|
|
1025
|
+
_text: "Click on " + selectors.element_name,
|
|
1026
|
+
type: Types.CLICK,
|
|
1027
|
+
operation: "click",
|
|
1028
|
+
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1029
|
+
};
|
|
1030
|
+
try {
|
|
1031
|
+
await _preCommand(state, this);
|
|
1032
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
717
1033
|
await this.waitForPageLoad();
|
|
718
|
-
return info;
|
|
1034
|
+
return state.info;
|
|
719
1035
|
}
|
|
720
1036
|
catch (e) {
|
|
721
|
-
|
|
722
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
723
|
-
info.screenshotPath = screenshotPath;
|
|
724
|
-
Object.assign(e, { info: info });
|
|
725
|
-
error = e;
|
|
726
|
-
throw e;
|
|
1037
|
+
await _commandError(state, e, this);
|
|
727
1038
|
}
|
|
728
1039
|
finally {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1040
|
+
await _commandFinally(state, this);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1044
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1045
|
+
const state = {
|
|
1046
|
+
selectors,
|
|
1047
|
+
_params,
|
|
1048
|
+
options,
|
|
1049
|
+
world,
|
|
1050
|
+
text: "Wait for element",
|
|
1051
|
+
_text: "Wait for " + selectors.element_name,
|
|
1052
|
+
type: Types.WAIT_ELEMENT,
|
|
1053
|
+
operation: "waitForElement",
|
|
1054
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1055
|
+
};
|
|
1056
|
+
let found = false;
|
|
1057
|
+
try {
|
|
1058
|
+
await _preCommand(state, this);
|
|
1059
|
+
// if (state.options && state.options.context) {
|
|
1060
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1061
|
+
// }
|
|
1062
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1063
|
+
found = true;
|
|
1064
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1065
|
+
}
|
|
1066
|
+
catch (e) {
|
|
1067
|
+
console.error("Error on waitForElement", e);
|
|
1068
|
+
// await _commandError(state, e, this);
|
|
1069
|
+
}
|
|
1070
|
+
finally {
|
|
1071
|
+
await _commandFinally(state, this);
|
|
749
1072
|
}
|
|
1073
|
+
return found;
|
|
750
1074
|
}
|
|
751
1075
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1076
|
+
const state = {
|
|
1077
|
+
selectors,
|
|
1078
|
+
_params,
|
|
1079
|
+
options,
|
|
1080
|
+
world,
|
|
1081
|
+
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
1082
|
+
text: checked ? `Check element` : `Uncheck element`,
|
|
1083
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
1084
|
+
operation: "setCheck",
|
|
1085
|
+
log: "***** check " + selectors.element_name + " *****\n",
|
|
1086
|
+
};
|
|
762
1087
|
try {
|
|
763
|
-
|
|
764
|
-
|
|
1088
|
+
await _preCommand(state, this);
|
|
1089
|
+
state.info.checked = checked;
|
|
1090
|
+
// let element = await this._locate(selectors, info, _params);
|
|
1091
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
765
1092
|
try {
|
|
766
|
-
|
|
767
|
-
|
|
1093
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1094
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1095
|
+
await this._highlightElements(state.element);
|
|
1096
|
+
await state.element.setChecked(checked);
|
|
768
1097
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1098
|
+
// await this._unHighlightElements(element);
|
|
1099
|
+
// }
|
|
1100
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1101
|
+
// await this._unHighlightElements(element);
|
|
769
1102
|
}
|
|
770
1103
|
catch (e) {
|
|
771
1104
|
if (e.message && e.message.includes("did not change its state")) {
|
|
@@ -773,179 +1106,102 @@ class StableBrowser {
|
|
|
773
1106
|
}
|
|
774
1107
|
else {
|
|
775
1108
|
//await this.closeUnexpectedPopups();
|
|
776
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
777
|
-
element = await this._locate(selectors, info, _params);
|
|
778
|
-
await element.setChecked(checked, { timeout: 5000, force: true });
|
|
1109
|
+
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1110
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
1111
|
+
await state.element.setChecked(checked, { timeout: 5000, force: true });
|
|
779
1112
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
780
1113
|
}
|
|
781
1114
|
}
|
|
782
1115
|
await this.waitForPageLoad();
|
|
783
|
-
return info;
|
|
1116
|
+
return state.info;
|
|
784
1117
|
}
|
|
785
1118
|
catch (e) {
|
|
786
|
-
|
|
787
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
788
|
-
info.screenshotPath = screenshotPath;
|
|
789
|
-
Object.assign(e, { info: info });
|
|
790
|
-
error = e;
|
|
791
|
-
throw e;
|
|
1119
|
+
await _commandError(state, e, this);
|
|
792
1120
|
}
|
|
793
1121
|
finally {
|
|
794
|
-
|
|
795
|
-
this._reportToWorld(world, {
|
|
796
|
-
element_name: selectors.element_name,
|
|
797
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
798
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
799
|
-
screenshotId,
|
|
800
|
-
result: error
|
|
801
|
-
? {
|
|
802
|
-
status: "FAILED",
|
|
803
|
-
startTime,
|
|
804
|
-
endTime,
|
|
805
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
806
|
-
}
|
|
807
|
-
: {
|
|
808
|
-
status: "PASSED",
|
|
809
|
-
startTime,
|
|
810
|
-
endTime,
|
|
811
|
-
},
|
|
812
|
-
info: info,
|
|
813
|
-
});
|
|
1122
|
+
await _commandFinally(state, this);
|
|
814
1123
|
}
|
|
815
1124
|
}
|
|
816
1125
|
async hover(selectors, _params, options = {}, world = null) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1126
|
+
const state = {
|
|
1127
|
+
selectors,
|
|
1128
|
+
_params,
|
|
1129
|
+
options,
|
|
1130
|
+
world,
|
|
1131
|
+
type: Types.HOVER,
|
|
1132
|
+
text: `Hover element`,
|
|
1133
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1134
|
+
operation: "hover",
|
|
1135
|
+
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1136
|
+
};
|
|
826
1137
|
try {
|
|
827
|
-
|
|
828
|
-
(
|
|
829
|
-
|
|
830
|
-
await this._highlightElements(element);
|
|
831
|
-
await element.hover();
|
|
832
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
833
|
-
}
|
|
834
|
-
catch (e) {
|
|
835
|
-
//await this.closeUnexpectedPopups();
|
|
836
|
-
info.log += "hover failed, will try again" + "\n";
|
|
837
|
-
element = await this._locate(selectors, info, _params);
|
|
838
|
-
await element.hover({ timeout: 10000 });
|
|
839
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
840
|
-
}
|
|
1138
|
+
await _preCommand(state, this);
|
|
1139
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1140
|
+
await _screenshot(state, this);
|
|
841
1141
|
await this.waitForPageLoad();
|
|
842
|
-
return info;
|
|
1142
|
+
return state.info;
|
|
843
1143
|
}
|
|
844
1144
|
catch (e) {
|
|
845
|
-
|
|
846
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
847
|
-
info.screenshotPath = screenshotPath;
|
|
848
|
-
Object.assign(e, { info: info });
|
|
849
|
-
error = e;
|
|
850
|
-
throw e;
|
|
1145
|
+
await _commandError(state, e, this);
|
|
851
1146
|
}
|
|
852
1147
|
finally {
|
|
853
|
-
|
|
854
|
-
this._reportToWorld(world, {
|
|
855
|
-
element_name: selectors.element_name,
|
|
856
|
-
type: Types.HOVER,
|
|
857
|
-
text: `Hover element`,
|
|
858
|
-
screenshotId,
|
|
859
|
-
result: error
|
|
860
|
-
? {
|
|
861
|
-
status: "FAILED",
|
|
862
|
-
startTime,
|
|
863
|
-
endTime,
|
|
864
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
865
|
-
}
|
|
866
|
-
: {
|
|
867
|
-
status: "PASSED",
|
|
868
|
-
startTime,
|
|
869
|
-
endTime,
|
|
870
|
-
},
|
|
871
|
-
info: info,
|
|
872
|
-
});
|
|
1148
|
+
await _commandFinally(state, this);
|
|
873
1149
|
}
|
|
874
1150
|
}
|
|
875
1151
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
876
|
-
this._validateSelectors(selectors);
|
|
877
1152
|
if (!values) {
|
|
878
1153
|
throw new Error("values is null");
|
|
879
1154
|
}
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1155
|
+
const state = {
|
|
1156
|
+
selectors,
|
|
1157
|
+
_params,
|
|
1158
|
+
options,
|
|
1159
|
+
world,
|
|
1160
|
+
value: values.toString(),
|
|
1161
|
+
type: Types.SELECT,
|
|
1162
|
+
text: `Select option: ${values}`,
|
|
1163
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1164
|
+
operation: "selectOption",
|
|
1165
|
+
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1166
|
+
};
|
|
888
1167
|
try {
|
|
889
|
-
|
|
890
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1168
|
+
await _preCommand(state, this);
|
|
891
1169
|
try {
|
|
892
|
-
await
|
|
893
|
-
await element.selectOption(values);
|
|
1170
|
+
await state.element.selectOption(values);
|
|
894
1171
|
}
|
|
895
1172
|
catch (e) {
|
|
896
1173
|
//await this.closeUnexpectedPopups();
|
|
897
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
898
|
-
await element.selectOption(values, { timeout: 10000, force: true });
|
|
1174
|
+
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1175
|
+
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
899
1176
|
}
|
|
900
1177
|
await this.waitForPageLoad();
|
|
901
|
-
return info;
|
|
1178
|
+
return state.info;
|
|
902
1179
|
}
|
|
903
1180
|
catch (e) {
|
|
904
|
-
|
|
905
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
906
|
-
info.screenshotPath = screenshotPath;
|
|
907
|
-
Object.assign(e, { info: info });
|
|
908
|
-
this.logger.info("click failed, will try next selector");
|
|
909
|
-
error = e;
|
|
910
|
-
throw e;
|
|
1181
|
+
await _commandError(state, e, this);
|
|
911
1182
|
}
|
|
912
1183
|
finally {
|
|
913
|
-
|
|
914
|
-
this._reportToWorld(world, {
|
|
915
|
-
element_name: selectors.element_name,
|
|
916
|
-
type: Types.SELECT,
|
|
917
|
-
text: `Select option: ${values}`,
|
|
918
|
-
value: values.toString(),
|
|
919
|
-
screenshotId,
|
|
920
|
-
result: error
|
|
921
|
-
? {
|
|
922
|
-
status: "FAILED",
|
|
923
|
-
startTime,
|
|
924
|
-
endTime,
|
|
925
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
926
|
-
}
|
|
927
|
-
: {
|
|
928
|
-
status: "PASSED",
|
|
929
|
-
startTime,
|
|
930
|
-
endTime,
|
|
931
|
-
},
|
|
932
|
-
info: info,
|
|
933
|
-
});
|
|
1184
|
+
await _commandFinally(state, this);
|
|
934
1185
|
}
|
|
935
1186
|
}
|
|
936
1187
|
async type(_value, _params = null, options = {}, world = null) {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1188
|
+
const state = {
|
|
1189
|
+
value: _value,
|
|
1190
|
+
_params,
|
|
1191
|
+
options,
|
|
1192
|
+
world,
|
|
1193
|
+
locate: false,
|
|
1194
|
+
scroll: false,
|
|
1195
|
+
highlight: false,
|
|
1196
|
+
type: Types.TYPE_PRESS,
|
|
1197
|
+
text: `Type value: ${_value}`,
|
|
1198
|
+
_text: `Type value: ${_value}`,
|
|
1199
|
+
operation: "type",
|
|
1200
|
+
log: "",
|
|
1201
|
+
};
|
|
946
1202
|
try {
|
|
947
|
-
|
|
948
|
-
const valueSegment =
|
|
1203
|
+
await _preCommand(state, this);
|
|
1204
|
+
const valueSegment = state.value.split("&&");
|
|
949
1205
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
950
1206
|
if (i > 0) {
|
|
951
1207
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -965,134 +1221,77 @@ class StableBrowser {
|
|
|
965
1221
|
await this.page.keyboard.type(value);
|
|
966
1222
|
}
|
|
967
1223
|
}
|
|
968
|
-
return info;
|
|
1224
|
+
return state.info;
|
|
969
1225
|
}
|
|
970
1226
|
catch (e) {
|
|
971
|
-
|
|
972
|
-
this.logger.error("type failed " + info.log);
|
|
973
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
974
|
-
info.screenshotPath = screenshotPath;
|
|
975
|
-
Object.assign(e, { info: info });
|
|
976
|
-
error = e;
|
|
977
|
-
throw e;
|
|
1227
|
+
await _commandError(state, e, this);
|
|
978
1228
|
}
|
|
979
1229
|
finally {
|
|
980
|
-
|
|
981
|
-
this._reportToWorld(world, {
|
|
982
|
-
type: Types.TYPE_PRESS,
|
|
983
|
-
screenshotId,
|
|
984
|
-
value: _value,
|
|
985
|
-
text: `type value: ${_value}`,
|
|
986
|
-
result: error
|
|
987
|
-
? {
|
|
988
|
-
status: "FAILED",
|
|
989
|
-
startTime,
|
|
990
|
-
endTime,
|
|
991
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
992
|
-
}
|
|
993
|
-
: {
|
|
994
|
-
status: "PASSED",
|
|
995
|
-
startTime,
|
|
996
|
-
endTime,
|
|
997
|
-
},
|
|
998
|
-
info: info,
|
|
999
|
-
});
|
|
1230
|
+
await _commandFinally(state, this);
|
|
1000
1231
|
}
|
|
1001
1232
|
}
|
|
1002
1233
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
let screenshotPath = null;
|
|
1234
|
+
const state = {
|
|
1235
|
+
selectors,
|
|
1236
|
+
_params,
|
|
1237
|
+
value,
|
|
1238
|
+
options,
|
|
1239
|
+
world,
|
|
1240
|
+
type: Types.SET_INPUT,
|
|
1241
|
+
text: `Set input value`,
|
|
1242
|
+
operation: "setInputValue",
|
|
1243
|
+
log: "***** set input value " + selectors.element_name + " *****\n",
|
|
1244
|
+
};
|
|
1015
1245
|
try {
|
|
1016
|
-
|
|
1017
|
-
let
|
|
1018
|
-
await this.scrollIfNeeded(element, info);
|
|
1019
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1020
|
-
await this._highlightElements(element);
|
|
1246
|
+
await _preCommand(state, this);
|
|
1247
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1021
1248
|
try {
|
|
1022
|
-
await element.evaluateHandle((el, value) => {
|
|
1249
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1023
1250
|
el.value = value;
|
|
1024
1251
|
}, value);
|
|
1025
1252
|
}
|
|
1026
1253
|
catch (error) {
|
|
1027
1254
|
this.logger.error("setInputValue failed, will try again");
|
|
1028
|
-
|
|
1029
|
-
info.
|
|
1030
|
-
|
|
1031
|
-
await element.evaluateHandle((el, value) => {
|
|
1255
|
+
await _screenshot(state, this);
|
|
1256
|
+
Object.assign(error, { info: state.info });
|
|
1257
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1032
1258
|
el.value = value;
|
|
1033
1259
|
});
|
|
1034
1260
|
}
|
|
1035
1261
|
}
|
|
1036
1262
|
catch (e) {
|
|
1037
|
-
|
|
1038
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1039
|
-
info.screenshotPath = screenshotPath;
|
|
1040
|
-
Object.assign(e, { info: info });
|
|
1041
|
-
error = e;
|
|
1042
|
-
throw e;
|
|
1263
|
+
await _commandError(state, e, this);
|
|
1043
1264
|
}
|
|
1044
1265
|
finally {
|
|
1045
|
-
|
|
1046
|
-
this._reportToWorld(world, {
|
|
1047
|
-
element_name: selectors.element_name,
|
|
1048
|
-
type: Types.SET_INPUT,
|
|
1049
|
-
text: `Set input value`,
|
|
1050
|
-
value: value,
|
|
1051
|
-
screenshotId,
|
|
1052
|
-
result: error
|
|
1053
|
-
? {
|
|
1054
|
-
status: "FAILED",
|
|
1055
|
-
startTime,
|
|
1056
|
-
endTime,
|
|
1057
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1058
|
-
}
|
|
1059
|
-
: {
|
|
1060
|
-
status: "PASSED",
|
|
1061
|
-
startTime,
|
|
1062
|
-
endTime,
|
|
1063
|
-
},
|
|
1064
|
-
info: info,
|
|
1065
|
-
});
|
|
1266
|
+
await _commandFinally(state, this);
|
|
1066
1267
|
}
|
|
1067
1268
|
}
|
|
1068
1269
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1270
|
+
const state = {
|
|
1271
|
+
selectors,
|
|
1272
|
+
_params,
|
|
1273
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1274
|
+
options,
|
|
1275
|
+
world,
|
|
1276
|
+
type: Types.SET_DATE_TIME,
|
|
1277
|
+
text: `Set date time value: ${value}`,
|
|
1278
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1279
|
+
operation: "setDateTime",
|
|
1280
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1281
|
+
throwError: false,
|
|
1282
|
+
};
|
|
1079
1283
|
try {
|
|
1080
|
-
|
|
1081
|
-
let element = await this._locate(selectors, info, _params);
|
|
1082
|
-
//insert red border around the element
|
|
1083
|
-
await this.scrollIfNeeded(element, info);
|
|
1084
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1085
|
-
await this._highlightElements(element);
|
|
1284
|
+
await _preCommand(state, this);
|
|
1086
1285
|
try {
|
|
1087
|
-
await element
|
|
1286
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1088
1287
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1089
1288
|
if (format) {
|
|
1090
|
-
value = dayjs(value).format(format);
|
|
1091
|
-
await element.fill(value);
|
|
1289
|
+
state.value = dayjs(state.value).format(format);
|
|
1290
|
+
await state.element.fill(state.value);
|
|
1092
1291
|
}
|
|
1093
1292
|
else {
|
|
1094
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1095
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1293
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1294
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1096
1295
|
el.value = ""; // clear input
|
|
1097
1296
|
el.value = dateTimeValue;
|
|
1098
1297
|
}, dateTimeValue);
|
|
@@ -1105,20 +1304,19 @@ class StableBrowser {
|
|
|
1105
1304
|
}
|
|
1106
1305
|
catch (err) {
|
|
1107
1306
|
//await this.closeUnexpectedPopups();
|
|
1108
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1307
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1109
1308
|
this.logger.info("Trying again");
|
|
1110
|
-
|
|
1111
|
-
info.
|
|
1112
|
-
Object.assign(err, { info: info });
|
|
1309
|
+
await _screenshot(state, this);
|
|
1310
|
+
Object.assign(err, { info: state.info });
|
|
1113
1311
|
await element.click();
|
|
1114
1312
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1115
1313
|
if (format) {
|
|
1116
|
-
value = dayjs(value).format(format);
|
|
1117
|
-
await element.fill(value);
|
|
1314
|
+
state.value = dayjs(state.value).format(format);
|
|
1315
|
+
await state.element.fill(state.value);
|
|
1118
1316
|
}
|
|
1119
1317
|
else {
|
|
1120
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1121
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1318
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1319
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1122
1320
|
el.value = ""; // clear input
|
|
1123
1321
|
el.value = dateTimeValue;
|
|
1124
1322
|
}, dateTimeValue);
|
|
@@ -1131,84 +1329,63 @@ class StableBrowser {
|
|
|
1131
1329
|
}
|
|
1132
1330
|
}
|
|
1133
1331
|
catch (e) {
|
|
1134
|
-
|
|
1135
|
-
throw e;
|
|
1332
|
+
await _commandError(state, e, this);
|
|
1136
1333
|
}
|
|
1137
1334
|
finally {
|
|
1138
|
-
|
|
1139
|
-
this._reportToWorld(world, {
|
|
1140
|
-
element_name: selectors.element_name,
|
|
1141
|
-
type: Types.SET_DATE_TIME,
|
|
1142
|
-
screenshotId,
|
|
1143
|
-
value: value,
|
|
1144
|
-
text: `setDateTime input with value: ${value}`,
|
|
1145
|
-
result: error
|
|
1146
|
-
? {
|
|
1147
|
-
status: "FAILED",
|
|
1148
|
-
startTime,
|
|
1149
|
-
endTime,
|
|
1150
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1151
|
-
}
|
|
1152
|
-
: {
|
|
1153
|
-
status: "PASSED",
|
|
1154
|
-
startTime,
|
|
1155
|
-
endTime,
|
|
1156
|
-
},
|
|
1157
|
-
info: info,
|
|
1158
|
-
});
|
|
1335
|
+
await _commandFinally(state, this);
|
|
1159
1336
|
}
|
|
1160
1337
|
}
|
|
1161
1338
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1162
|
-
|
|
1163
|
-
const startTime = Date.now();
|
|
1164
|
-
let error = null;
|
|
1165
|
-
let screenshotId = null;
|
|
1166
|
-
let screenshotPath = null;
|
|
1167
|
-
const info = {};
|
|
1168
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1169
|
-
info.operation = "clickType";
|
|
1170
|
-
info.selectors = selectors;
|
|
1339
|
+
_value = unEscapeString(_value);
|
|
1171
1340
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1341
|
+
const state = {
|
|
1342
|
+
selectors,
|
|
1343
|
+
_params,
|
|
1344
|
+
value: newValue,
|
|
1345
|
+
originalValue: _value,
|
|
1346
|
+
options,
|
|
1347
|
+
world,
|
|
1348
|
+
type: Types.FILL,
|
|
1349
|
+
text: `Click type input with value: ${_value}`,
|
|
1350
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1351
|
+
operation: "clickType",
|
|
1352
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1353
|
+
};
|
|
1354
|
+
if (!options) {
|
|
1355
|
+
options = {};
|
|
1356
|
+
}
|
|
1172
1357
|
if (newValue !== _value) {
|
|
1173
1358
|
//this.logger.info(_value + "=" + newValue);
|
|
1174
1359
|
_value = newValue;
|
|
1175
1360
|
}
|
|
1176
|
-
info.value = _value;
|
|
1177
1361
|
try {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1182
|
-
await this._highlightElements(element);
|
|
1183
|
-
if (options === null || options === undefined || !options.press) {
|
|
1362
|
+
await _preCommand(state, this);
|
|
1363
|
+
state.info.value = _value;
|
|
1364
|
+
if (!options.press) {
|
|
1184
1365
|
try {
|
|
1185
|
-
let currentValue = await element.inputValue();
|
|
1366
|
+
let currentValue = await state.element.inputValue();
|
|
1186
1367
|
if (currentValue) {
|
|
1187
|
-
await element.fill("");
|
|
1368
|
+
await state.element.fill("");
|
|
1188
1369
|
}
|
|
1189
1370
|
}
|
|
1190
1371
|
catch (e) {
|
|
1191
1372
|
this.logger.info("unable to clear input value");
|
|
1192
1373
|
}
|
|
1193
1374
|
}
|
|
1194
|
-
if (options
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
catch (e) {
|
|
1199
|
-
await element.dispatchEvent("click");
|
|
1200
|
-
}
|
|
1375
|
+
if (options.press) {
|
|
1376
|
+
options.timeout = 5000;
|
|
1377
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1201
1378
|
}
|
|
1202
1379
|
else {
|
|
1203
1380
|
try {
|
|
1204
|
-
await element.focus();
|
|
1381
|
+
await state.element.focus();
|
|
1205
1382
|
}
|
|
1206
1383
|
catch (e) {
|
|
1207
|
-
await element.dispatchEvent("focus");
|
|
1384
|
+
await state.element.dispatchEvent("focus");
|
|
1208
1385
|
}
|
|
1209
1386
|
}
|
|
1210
1387
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1211
|
-
const valueSegment =
|
|
1388
|
+
const valueSegment = state.value.split("&&");
|
|
1212
1389
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1213
1390
|
if (i > 0) {
|
|
1214
1391
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1228,13 +1405,19 @@ class StableBrowser {
|
|
|
1228
1405
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1229
1406
|
}
|
|
1230
1407
|
}
|
|
1408
|
+
await _screenshot(state, this);
|
|
1231
1409
|
if (enter === true) {
|
|
1232
1410
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1233
1411
|
await this.page.keyboard.press("Enter");
|
|
1234
1412
|
await this.waitForPageLoad();
|
|
1235
1413
|
}
|
|
1236
1414
|
else if (enter === false) {
|
|
1237
|
-
|
|
1415
|
+
try {
|
|
1416
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1417
|
+
}
|
|
1418
|
+
catch (e) {
|
|
1419
|
+
// ignore
|
|
1420
|
+
}
|
|
1238
1421
|
//await this.page.keyboard.press("Tab");
|
|
1239
1422
|
}
|
|
1240
1423
|
else {
|
|
@@ -1243,111 +1426,95 @@ class StableBrowser {
|
|
|
1243
1426
|
await this.waitForPageLoad();
|
|
1244
1427
|
}
|
|
1245
1428
|
}
|
|
1246
|
-
return info;
|
|
1429
|
+
return state.info;
|
|
1247
1430
|
}
|
|
1248
1431
|
catch (e) {
|
|
1249
|
-
|
|
1250
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1251
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1252
|
-
info.screenshotPath = screenshotPath;
|
|
1253
|
-
Object.assign(e, { info: info });
|
|
1254
|
-
error = e;
|
|
1255
|
-
throw e;
|
|
1432
|
+
await _commandError(state, e, this);
|
|
1256
1433
|
}
|
|
1257
1434
|
finally {
|
|
1258
|
-
|
|
1259
|
-
this._reportToWorld(world, {
|
|
1260
|
-
element_name: selectors.element_name,
|
|
1261
|
-
type: Types.FILL,
|
|
1262
|
-
screenshotId,
|
|
1263
|
-
value: _value,
|
|
1264
|
-
text: `clickType input with value: ${_value}`,
|
|
1265
|
-
result: error
|
|
1266
|
-
? {
|
|
1267
|
-
status: "FAILED",
|
|
1268
|
-
startTime,
|
|
1269
|
-
endTime,
|
|
1270
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1271
|
-
}
|
|
1272
|
-
: {
|
|
1273
|
-
status: "PASSED",
|
|
1274
|
-
startTime,
|
|
1275
|
-
endTime,
|
|
1276
|
-
},
|
|
1277
|
-
info: info,
|
|
1278
|
-
});
|
|
1435
|
+
await _commandFinally(state, this);
|
|
1279
1436
|
}
|
|
1280
1437
|
}
|
|
1281
1438
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1439
|
+
const state = {
|
|
1440
|
+
selectors,
|
|
1441
|
+
_params,
|
|
1442
|
+
value: unEscapeString(value),
|
|
1443
|
+
options,
|
|
1444
|
+
world,
|
|
1445
|
+
type: Types.FILL,
|
|
1446
|
+
text: `Fill input with value: ${value}`,
|
|
1447
|
+
operation: "fill",
|
|
1448
|
+
log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
|
|
1449
|
+
};
|
|
1292
1450
|
try {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
await
|
|
1296
|
-
await element.fill(value);
|
|
1297
|
-
await element.dispatchEvent("change");
|
|
1451
|
+
await _preCommand(state, this);
|
|
1452
|
+
await state.element.fill(value);
|
|
1453
|
+
await state.element.dispatchEvent("change");
|
|
1298
1454
|
if (enter) {
|
|
1299
1455
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1300
1456
|
await this.page.keyboard.press("Enter");
|
|
1301
1457
|
}
|
|
1302
1458
|
await this.waitForPageLoad();
|
|
1303
|
-
return info;
|
|
1459
|
+
return state.info;
|
|
1304
1460
|
}
|
|
1305
1461
|
catch (e) {
|
|
1306
|
-
|
|
1307
|
-
this.logger.error("fill failed " + info.log);
|
|
1308
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1309
|
-
info.screenshotPath = screenshotPath;
|
|
1310
|
-
Object.assign(e, { info: info });
|
|
1311
|
-
error = e;
|
|
1312
|
-
throw e;
|
|
1462
|
+
await _commandError(state, e, this);
|
|
1313
1463
|
}
|
|
1314
1464
|
finally {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1465
|
+
await _commandFinally(state, this);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1469
|
+
const state = {
|
|
1470
|
+
selectors,
|
|
1471
|
+
_params,
|
|
1472
|
+
files,
|
|
1473
|
+
value: '"' + files.join('", "') + '"',
|
|
1474
|
+
options,
|
|
1475
|
+
world,
|
|
1476
|
+
type: Types.SET_INPUT_FILES,
|
|
1477
|
+
text: `Set input files`,
|
|
1478
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1479
|
+
operation: "setInputFiles",
|
|
1480
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1481
|
+
};
|
|
1482
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1483
|
+
try {
|
|
1484
|
+
await _preCommand(state, this);
|
|
1485
|
+
for (let i = 0; i < files.length; i++) {
|
|
1486
|
+
const file = files[i];
|
|
1487
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1488
|
+
if (!fs.existsSync(filePath)) {
|
|
1489
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1490
|
+
}
|
|
1491
|
+
state.files[i] = filePath;
|
|
1492
|
+
}
|
|
1493
|
+
await state.element.setInputFiles(files);
|
|
1494
|
+
return state.info;
|
|
1495
|
+
}
|
|
1496
|
+
catch (e) {
|
|
1497
|
+
await _commandError(state, e, this);
|
|
1498
|
+
}
|
|
1499
|
+
finally {
|
|
1500
|
+
await _commandFinally(state, this);
|
|
1336
1501
|
}
|
|
1337
1502
|
}
|
|
1338
1503
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1339
1504
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1340
1505
|
}
|
|
1341
1506
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1342
|
-
this.
|
|
1507
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1508
|
+
_validateSelectors(selectors);
|
|
1343
1509
|
let screenshotId = null;
|
|
1344
1510
|
let screenshotPath = null;
|
|
1345
1511
|
if (!info.log) {
|
|
1346
1512
|
info.log = "";
|
|
1513
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1347
1514
|
}
|
|
1348
1515
|
info.operation = "getText";
|
|
1349
1516
|
info.selectors = selectors;
|
|
1350
|
-
let element = await this._locate(selectors, info, _params);
|
|
1517
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1351
1518
|
if (climb > 0) {
|
|
1352
1519
|
const climbArray = [];
|
|
1353
1520
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1366,6 +1533,18 @@ class StableBrowser {
|
|
|
1366
1533
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1367
1534
|
try {
|
|
1368
1535
|
await this._highlightElements(element);
|
|
1536
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1537
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1538
|
+
// this._highlightElements(element)
|
|
1539
|
+
// .then(async () => {
|
|
1540
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1541
|
+
// this._unhighlightElements(element).then(
|
|
1542
|
+
// () => {}
|
|
1543
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1544
|
+
// );
|
|
1545
|
+
// })
|
|
1546
|
+
// .catch(e);
|
|
1547
|
+
// }
|
|
1369
1548
|
const elementText = await element.innerText();
|
|
1370
1549
|
return {
|
|
1371
1550
|
text: elementText,
|
|
@@ -1377,188 +1556,214 @@ class StableBrowser {
|
|
|
1377
1556
|
}
|
|
1378
1557
|
catch (e) {
|
|
1379
1558
|
//await this.closeUnexpectedPopups();
|
|
1380
|
-
this.logger.info("no innerText will use textContent");
|
|
1559
|
+
this.logger.info("no innerText, will use textContent");
|
|
1381
1560
|
const elementText = await element.textContent();
|
|
1382
1561
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1383
1562
|
}
|
|
1384
1563
|
}
|
|
1385
1564
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1386
|
-
var _a;
|
|
1387
|
-
this._validateSelectors(selectors);
|
|
1388
1565
|
if (!pattern) {
|
|
1389
1566
|
throw new Error("pattern is null");
|
|
1390
1567
|
}
|
|
1391
1568
|
if (!text) {
|
|
1392
1569
|
throw new Error("text is null");
|
|
1393
1570
|
}
|
|
1571
|
+
const state = {
|
|
1572
|
+
selectors,
|
|
1573
|
+
_params,
|
|
1574
|
+
pattern,
|
|
1575
|
+
value: pattern,
|
|
1576
|
+
options,
|
|
1577
|
+
world,
|
|
1578
|
+
locate: false,
|
|
1579
|
+
scroll: false,
|
|
1580
|
+
screenshot: false,
|
|
1581
|
+
highlight: false,
|
|
1582
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1583
|
+
text: `Verify element contains pattern: ${pattern}`,
|
|
1584
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1585
|
+
operation: "containsPattern",
|
|
1586
|
+
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1587
|
+
};
|
|
1394
1588
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1395
1589
|
if (newValue !== text) {
|
|
1396
1590
|
this.logger.info(text + "=" + newValue);
|
|
1397
1591
|
text = newValue;
|
|
1398
1592
|
}
|
|
1399
|
-
const startTime = Date.now();
|
|
1400
|
-
let error = null;
|
|
1401
|
-
let screenshotId = null;
|
|
1402
|
-
let screenshotPath = null;
|
|
1403
|
-
const info = {};
|
|
1404
|
-
info.log =
|
|
1405
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1406
|
-
info.operation = "containsPattern";
|
|
1407
|
-
info.selectors = selectors;
|
|
1408
|
-
info.value = text;
|
|
1409
|
-
info.pattern = pattern;
|
|
1410
1593
|
let foundObj = null;
|
|
1411
1594
|
try {
|
|
1412
|
-
|
|
1595
|
+
await _preCommand(state, this);
|
|
1596
|
+
state.info.pattern = pattern;
|
|
1597
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1413
1598
|
if (foundObj && foundObj.element) {
|
|
1414
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1599
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1415
1600
|
}
|
|
1416
|
-
|
|
1601
|
+
await _screenshot(state, this);
|
|
1417
1602
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1418
1603
|
pattern = pattern.replace("{text}", escapedText);
|
|
1419
1604
|
let regex = new RegExp(pattern, "im");
|
|
1420
|
-
if (!regex.test(foundObj
|
|
1421
|
-
info.foundText = foundObj
|
|
1605
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1606
|
+
state.info.foundText = foundObj?.text;
|
|
1422
1607
|
throw new Error("element doesn't contain text " + text);
|
|
1423
1608
|
}
|
|
1424
|
-
return info;
|
|
1609
|
+
return state.info;
|
|
1425
1610
|
}
|
|
1426
1611
|
catch (e) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1430
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1431
|
-
info.screenshotPath = screenshotPath;
|
|
1432
|
-
Object.assign(e, { info: info });
|
|
1433
|
-
error = e;
|
|
1434
|
-
throw e;
|
|
1612
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1613
|
+
await _commandError(state, e, this);
|
|
1435
1614
|
}
|
|
1436
1615
|
finally {
|
|
1437
|
-
|
|
1438
|
-
this._reportToWorld(world, {
|
|
1439
|
-
element_name: selectors.element_name,
|
|
1440
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1441
|
-
value: pattern,
|
|
1442
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1443
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1444
|
-
result: error
|
|
1445
|
-
? {
|
|
1446
|
-
status: "FAILED",
|
|
1447
|
-
startTime,
|
|
1448
|
-
endTime,
|
|
1449
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1450
|
-
}
|
|
1451
|
-
: {
|
|
1452
|
-
status: "PASSED",
|
|
1453
|
-
startTime,
|
|
1454
|
-
endTime,
|
|
1455
|
-
},
|
|
1456
|
-
info: info,
|
|
1457
|
-
});
|
|
1616
|
+
await _commandFinally(state, this);
|
|
1458
1617
|
}
|
|
1459
1618
|
}
|
|
1460
1619
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1620
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1621
|
+
const startTime = Date.now();
|
|
1622
|
+
const state = {
|
|
1623
|
+
selectors,
|
|
1624
|
+
_params,
|
|
1625
|
+
value: text,
|
|
1626
|
+
options,
|
|
1627
|
+
world,
|
|
1628
|
+
locate: false,
|
|
1629
|
+
scroll: false,
|
|
1630
|
+
screenshot: false,
|
|
1631
|
+
highlight: false,
|
|
1632
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1633
|
+
text: `Verify element contains text: ${text}`,
|
|
1634
|
+
operation: "containsText",
|
|
1635
|
+
log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
|
|
1636
|
+
};
|
|
1463
1637
|
if (!text) {
|
|
1464
1638
|
throw new Error("text is null");
|
|
1465
1639
|
}
|
|
1466
|
-
|
|
1467
|
-
let error = null;
|
|
1468
|
-
let screenshotId = null;
|
|
1469
|
-
let screenshotPath = null;
|
|
1470
|
-
const info = {};
|
|
1471
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1472
|
-
info.operation = "containsText";
|
|
1473
|
-
info.selectors = selectors;
|
|
1640
|
+
text = unEscapeString(text);
|
|
1474
1641
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1475
1642
|
if (newValue !== text) {
|
|
1476
1643
|
this.logger.info(text + "=" + newValue);
|
|
1477
1644
|
text = newValue;
|
|
1478
1645
|
}
|
|
1479
|
-
info.value = text;
|
|
1480
1646
|
let foundObj = null;
|
|
1481
1647
|
try {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1648
|
+
while (Date.now() - startTime < timeout) {
|
|
1649
|
+
try {
|
|
1650
|
+
await _preCommand(state, this);
|
|
1651
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1652
|
+
if (foundObj && foundObj.element) {
|
|
1653
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1654
|
+
}
|
|
1655
|
+
await _screenshot(state, this);
|
|
1656
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1657
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1658
|
+
if (dateAlternatives.date) {
|
|
1659
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1660
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1661
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1662
|
+
return state.info;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1494
1665
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1666
|
+
else if (numberAlternatives.number) {
|
|
1667
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1668
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1669
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1670
|
+
return state.info;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1675
|
+
return state.info;
|
|
1503
1676
|
}
|
|
1504
1677
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
throw new Error("element doesn't contain text " + text);
|
|
1678
|
+
catch (e) {
|
|
1679
|
+
// Log error but continue retrying until timeout is reached
|
|
1680
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1681
|
+
}
|
|
1682
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1511
1683
|
}
|
|
1512
|
-
|
|
1684
|
+
state.info.foundText = foundObj?.text;
|
|
1685
|
+
state.info.value = foundObj?.value;
|
|
1686
|
+
throw new Error("element doesn't contain text " + text);
|
|
1513
1687
|
}
|
|
1514
1688
|
catch (e) {
|
|
1515
|
-
|
|
1516
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1517
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1518
|
-
info.screenshotPath = screenshotPath;
|
|
1519
|
-
Object.assign(e, { info: info });
|
|
1520
|
-
error = e;
|
|
1689
|
+
await _commandError(state, e, this);
|
|
1521
1690
|
throw e;
|
|
1522
1691
|
}
|
|
1523
1692
|
finally {
|
|
1524
|
-
|
|
1525
|
-
this._reportToWorld(world, {
|
|
1526
|
-
element_name: selectors.element_name,
|
|
1527
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1528
|
-
text: `Verify element contains text: ${text}`,
|
|
1529
|
-
value: text,
|
|
1530
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1531
|
-
result: error
|
|
1532
|
-
? {
|
|
1533
|
-
status: "FAILED",
|
|
1534
|
-
startTime,
|
|
1535
|
-
endTime,
|
|
1536
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1537
|
-
}
|
|
1538
|
-
: {
|
|
1539
|
-
status: "PASSED",
|
|
1540
|
-
startTime,
|
|
1541
|
-
endTime,
|
|
1542
|
-
},
|
|
1543
|
-
info: info,
|
|
1544
|
-
});
|
|
1693
|
+
await _commandFinally(state, this);
|
|
1545
1694
|
}
|
|
1546
1695
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1696
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1697
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1698
|
+
const startTime = Date.now();
|
|
1699
|
+
const state = {
|
|
1700
|
+
_params,
|
|
1701
|
+
value: referanceSnapshot,
|
|
1702
|
+
options,
|
|
1703
|
+
world,
|
|
1704
|
+
locate: false,
|
|
1705
|
+
scroll: false,
|
|
1706
|
+
screenshot: true,
|
|
1707
|
+
highlight: false,
|
|
1708
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1709
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1710
|
+
operation: "snapshotValidation",
|
|
1711
|
+
log: "***** verify snapshot *****\n",
|
|
1712
|
+
};
|
|
1713
|
+
if (!referanceSnapshot) {
|
|
1714
|
+
throw new Error("referanceSnapshot is null");
|
|
1551
1715
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1716
|
+
let text = null;
|
|
1717
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1718
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1554
1719
|
}
|
|
1555
|
-
else if (this.
|
|
1556
|
-
|
|
1720
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1721
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1722
|
+
}
|
|
1723
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1724
|
+
text = referanceSnapshot.substring(5);
|
|
1557
1725
|
}
|
|
1558
1726
|
else {
|
|
1559
|
-
|
|
1727
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1728
|
+
}
|
|
1729
|
+
state.text = text;
|
|
1730
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1731
|
+
await _preCommand(state, this);
|
|
1732
|
+
let foundObj = null;
|
|
1733
|
+
try {
|
|
1734
|
+
let matchResult = null;
|
|
1735
|
+
while (Date.now() - startTime < timeout) {
|
|
1736
|
+
try {
|
|
1737
|
+
let scope = null;
|
|
1738
|
+
if (!frameSelectors) {
|
|
1739
|
+
scope = this.page;
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1743
|
+
}
|
|
1744
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1745
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1746
|
+
if (matchResult.errorLine !== -1) {
|
|
1747
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1748
|
+
}
|
|
1749
|
+
// highlight and screenshot
|
|
1750
|
+
return state.info;
|
|
1751
|
+
}
|
|
1752
|
+
catch (e) {
|
|
1753
|
+
// Log error but continue retrying until timeout is reached
|
|
1754
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1755
|
+
}
|
|
1756
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1757
|
+
}
|
|
1758
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1759
|
+
}
|
|
1760
|
+
catch (e) {
|
|
1761
|
+
await _commandError(state, e, this);
|
|
1762
|
+
throw e;
|
|
1763
|
+
}
|
|
1764
|
+
finally {
|
|
1765
|
+
await _commandFinally(state, this);
|
|
1560
1766
|
}
|
|
1561
|
-
return dataFile;
|
|
1562
1767
|
}
|
|
1563
1768
|
async waitForUserInput(message, world = null) {
|
|
1564
1769
|
if (!message) {
|
|
@@ -1588,13 +1793,22 @@ class StableBrowser {
|
|
|
1588
1793
|
return;
|
|
1589
1794
|
}
|
|
1590
1795
|
// if data file exists, load it
|
|
1591
|
-
const dataFile =
|
|
1796
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1592
1797
|
let data = this.getTestData(world);
|
|
1593
1798
|
// merge the testData with the existing data
|
|
1594
1799
|
Object.assign(data, testData);
|
|
1595
1800
|
// save the data to the file
|
|
1596
1801
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1597
1802
|
}
|
|
1803
|
+
overwriteTestData(testData, world = null) {
|
|
1804
|
+
if (!testData) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
// if data file exists, load it
|
|
1808
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1809
|
+
// save the data to the file
|
|
1810
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1811
|
+
}
|
|
1598
1812
|
_getDataFilePath(fileName) {
|
|
1599
1813
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1600
1814
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1691,7 +1905,7 @@ class StableBrowser {
|
|
|
1691
1905
|
}
|
|
1692
1906
|
}
|
|
1693
1907
|
getTestData(world = null) {
|
|
1694
|
-
const dataFile =
|
|
1908
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1695
1909
|
let data = {};
|
|
1696
1910
|
if (fs.existsSync(dataFile)) {
|
|
1697
1911
|
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
@@ -1723,11 +1937,9 @@ class StableBrowser {
|
|
|
1723
1937
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1724
1938
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1725
1939
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
1940
|
+
// to make sure the path doesn't start with -
|
|
1941
|
+
const uuidStr = "id_" + randomUUID();
|
|
1942
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1731
1943
|
try {
|
|
1732
1944
|
await this.takeScreenshot(screenshotPath);
|
|
1733
1945
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1737,15 +1949,15 @@ class StableBrowser {
|
|
|
1737
1949
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1738
1950
|
// }
|
|
1739
1951
|
// });
|
|
1952
|
+
result.screenshotId = uuidStr;
|
|
1953
|
+
result.screenshotPath = screenshotPath;
|
|
1954
|
+
if (info && info.box) {
|
|
1955
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1956
|
+
}
|
|
1740
1957
|
}
|
|
1741
1958
|
catch (e) {
|
|
1742
1959
|
this.logger.info("unable to take screenshot, ignored");
|
|
1743
1960
|
}
|
|
1744
|
-
result.screenshotId = nextIndex;
|
|
1745
|
-
result.screenshotPath = screenshotPath;
|
|
1746
|
-
if (info && info.box) {
|
|
1747
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1748
|
-
}
|
|
1749
1961
|
}
|
|
1750
1962
|
else if (options && options.screenshot) {
|
|
1751
1963
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1770,7 +1982,6 @@ class StableBrowser {
|
|
|
1770
1982
|
}
|
|
1771
1983
|
async takeScreenshot(screenshotPath) {
|
|
1772
1984
|
const playContext = this.context.playContext;
|
|
1773
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1774
1985
|
// Using CDP to capture the screenshot
|
|
1775
1986
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1776
1987
|
document.body.scrollWidth,
|
|
@@ -1780,164 +1991,241 @@ class StableBrowser {
|
|
|
1780
1991
|
document.body.clientWidth,
|
|
1781
1992
|
document.documentElement.clientWidth,
|
|
1782
1993
|
])));
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
screenshotBuffer =
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1994
|
+
let screenshotBuffer = null;
|
|
1995
|
+
// if (focusedElement) {
|
|
1996
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
1997
|
+
// await this._unhighlightElements(focusedElement);
|
|
1998
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1999
|
+
// console.log(`Unhighlighted previous element`);
|
|
2000
|
+
// }
|
|
2001
|
+
// if (focusedElement) {
|
|
2002
|
+
// await this._highlightElements(focusedElement);
|
|
2003
|
+
// }
|
|
2004
|
+
if (this.context.browserName === "chromium") {
|
|
2005
|
+
const client = await playContext.newCDPSession(this.page);
|
|
2006
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
2007
|
+
format: "png",
|
|
2008
|
+
// clip: {
|
|
2009
|
+
// x: 0,
|
|
2010
|
+
// y: 0,
|
|
2011
|
+
// width: viewportWidth,
|
|
2012
|
+
// height: viewportHeight,
|
|
2013
|
+
// scale: 1,
|
|
2014
|
+
// },
|
|
2015
|
+
});
|
|
2016
|
+
await client.detach();
|
|
2017
|
+
if (!screenshotPath) {
|
|
2018
|
+
return data;
|
|
2019
|
+
}
|
|
2020
|
+
screenshotBuffer = Buffer.from(data, "base64");
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
screenshotBuffer = await this.page.screenshot();
|
|
2024
|
+
}
|
|
2025
|
+
// if (focusedElement) {
|
|
2026
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2027
|
+
// await this._unhighlightElements(focusedElement);
|
|
2028
|
+
// }
|
|
2029
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
2030
|
+
// Get the image dimensions
|
|
2031
|
+
const { width, height } = image.bitmap;
|
|
2032
|
+
const resizeRatio = viewportWidth / width;
|
|
2033
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
2034
|
+
if (width > viewportWidth) {
|
|
2035
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
2036
|
+
await image.write(screenshotPath);
|
|
2037
|
+
}
|
|
2038
|
+
else {
|
|
2039
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
2040
|
+
}
|
|
2041
|
+
return screenshotBuffer;
|
|
1818
2042
|
}
|
|
1819
2043
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2044
|
+
const state = {
|
|
2045
|
+
selectors,
|
|
2046
|
+
_params,
|
|
2047
|
+
options,
|
|
2048
|
+
world,
|
|
2049
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2050
|
+
text: `Verify element exists in page`,
|
|
2051
|
+
operation: "verifyElementExistInPage",
|
|
2052
|
+
log: "***** verify element " + selectors.element_name + " exists in page *****\n",
|
|
2053
|
+
};
|
|
1825
2054
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1826
|
-
const info = {};
|
|
1827
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1828
|
-
info.operation = "verify";
|
|
1829
|
-
info.selectors = selectors;
|
|
1830
2055
|
try {
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
await this._highlightElements(element);
|
|
1836
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1837
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1838
|
-
return info;
|
|
2056
|
+
await _preCommand(state, this);
|
|
2057
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
2058
|
+
return state.info;
|
|
1839
2059
|
}
|
|
1840
2060
|
catch (e) {
|
|
1841
|
-
|
|
1842
|
-
this.logger.error("verify failed " + info.log);
|
|
1843
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1844
|
-
info.screenshotPath = screenshotPath;
|
|
1845
|
-
Object.assign(e, { info: info });
|
|
1846
|
-
error = e;
|
|
1847
|
-
throw e;
|
|
2061
|
+
await _commandError(state, e, this);
|
|
1848
2062
|
}
|
|
1849
2063
|
finally {
|
|
1850
|
-
|
|
1851
|
-
this._reportToWorld(world, {
|
|
1852
|
-
element_name: selectors.element_name,
|
|
1853
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1854
|
-
text: "Verify element exists in page",
|
|
1855
|
-
screenshotId,
|
|
1856
|
-
result: error
|
|
1857
|
-
? {
|
|
1858
|
-
status: "FAILED",
|
|
1859
|
-
startTime,
|
|
1860
|
-
endTime,
|
|
1861
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1862
|
-
}
|
|
1863
|
-
: {
|
|
1864
|
-
status: "PASSED",
|
|
1865
|
-
startTime,
|
|
1866
|
-
endTime,
|
|
1867
|
-
},
|
|
1868
|
-
info: info,
|
|
1869
|
-
});
|
|
2064
|
+
await _commandFinally(state, this);
|
|
1870
2065
|
}
|
|
1871
2066
|
}
|
|
1872
2067
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2068
|
+
const state = {
|
|
2069
|
+
selectors,
|
|
2070
|
+
_params,
|
|
2071
|
+
attribute,
|
|
2072
|
+
variable,
|
|
2073
|
+
options,
|
|
2074
|
+
world,
|
|
2075
|
+
type: Types.EXTRACT,
|
|
2076
|
+
text: `Extract attribute from element`,
|
|
2077
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
2078
|
+
operation: "extractAttribute",
|
|
2079
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2080
|
+
allowDisabled: true,
|
|
2081
|
+
};
|
|
1878
2082
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1879
|
-
const info = {};
|
|
1880
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1881
|
-
info.operation = "extract";
|
|
1882
|
-
info.selectors = selectors;
|
|
1883
2083
|
try {
|
|
1884
|
-
|
|
1885
|
-
await this._highlightElements(element);
|
|
1886
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2084
|
+
await _preCommand(state, this);
|
|
1887
2085
|
switch (attribute) {
|
|
1888
2086
|
case "inner_text":
|
|
1889
|
-
|
|
2087
|
+
state.value = await state.element.innerText();
|
|
1890
2088
|
break;
|
|
1891
2089
|
case "href":
|
|
1892
|
-
|
|
2090
|
+
state.value = await state.element.getAttribute("href");
|
|
1893
2091
|
break;
|
|
1894
2092
|
case "value":
|
|
1895
|
-
|
|
2093
|
+
state.value = await state.element.inputValue();
|
|
2094
|
+
break;
|
|
2095
|
+
case "text":
|
|
2096
|
+
state.value = await state.element.textContent();
|
|
1896
2097
|
break;
|
|
1897
2098
|
default:
|
|
1898
|
-
|
|
2099
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1899
2100
|
break;
|
|
1900
2101
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2102
|
+
if (options !== null) {
|
|
2103
|
+
if (options.regex && options.regex !== "") {
|
|
2104
|
+
// Construct a regex pattern from the provided string
|
|
2105
|
+
const regex = options.regex.slice(1, -1);
|
|
2106
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2107
|
+
const matches = state.value.match(regexPattern);
|
|
2108
|
+
if (matches) {
|
|
2109
|
+
let newValue = "";
|
|
2110
|
+
for (const match of matches) {
|
|
2111
|
+
newValue += match;
|
|
2112
|
+
}
|
|
2113
|
+
state.value = newValue;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2117
|
+
state.value = state.value.trim();
|
|
2118
|
+
}
|
|
1904
2119
|
}
|
|
1905
|
-
|
|
1906
|
-
this.
|
|
1907
|
-
|
|
2120
|
+
state.info.value = state.value;
|
|
2121
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2122
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2123
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2124
|
+
return state.info;
|
|
1908
2125
|
}
|
|
1909
2126
|
catch (e) {
|
|
1910
|
-
|
|
1911
|
-
this.logger.error("extract failed " + info.log);
|
|
1912
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1913
|
-
info.screenshotPath = screenshotPath;
|
|
1914
|
-
Object.assign(e, { info: info });
|
|
1915
|
-
error = e;
|
|
1916
|
-
throw e;
|
|
2127
|
+
await _commandError(state, e, this);
|
|
1917
2128
|
}
|
|
1918
2129
|
finally {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2130
|
+
await _commandFinally(state, this);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2134
|
+
const state = {
|
|
2135
|
+
selectors,
|
|
2136
|
+
_params,
|
|
2137
|
+
attribute,
|
|
2138
|
+
value,
|
|
2139
|
+
options,
|
|
2140
|
+
world,
|
|
2141
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2142
|
+
highlight: true,
|
|
2143
|
+
screenshot: true,
|
|
2144
|
+
text: `Verify element attribute`,
|
|
2145
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2146
|
+
operation: "verifyAttribute",
|
|
2147
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2148
|
+
allowDisabled: true,
|
|
2149
|
+
};
|
|
2150
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2151
|
+
let val;
|
|
2152
|
+
let expectedValue;
|
|
2153
|
+
try {
|
|
2154
|
+
await _preCommand(state, this);
|
|
2155
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2156
|
+
state.info.expectedValue = expectedValue;
|
|
2157
|
+
switch (attribute) {
|
|
2158
|
+
case "innerText":
|
|
2159
|
+
val = String(await state.element.innerText());
|
|
2160
|
+
break;
|
|
2161
|
+
case "text":
|
|
2162
|
+
val = String(await state.element.textContent());
|
|
2163
|
+
break;
|
|
2164
|
+
case "value":
|
|
2165
|
+
val = String(await state.element.inputValue());
|
|
2166
|
+
break;
|
|
2167
|
+
case "checked":
|
|
2168
|
+
val = String(await state.element.isChecked());
|
|
2169
|
+
break;
|
|
2170
|
+
case "disabled":
|
|
2171
|
+
val = String(await state.element.isDisabled());
|
|
2172
|
+
break;
|
|
2173
|
+
case "readOnly":
|
|
2174
|
+
const isEditable = await state.element.isEditable();
|
|
2175
|
+
val = String(!isEditable);
|
|
2176
|
+
break;
|
|
2177
|
+
default:
|
|
2178
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
state.info.value = val;
|
|
2182
|
+
let regex;
|
|
2183
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2184
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2185
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2186
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2187
|
+
state.info.regex = true;
|
|
2188
|
+
}
|
|
2189
|
+
else {
|
|
2190
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2191
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2192
|
+
}
|
|
2193
|
+
if (attribute === "innerText") {
|
|
2194
|
+
if (state.info.regex) {
|
|
2195
|
+
if (!regex.test(val)) {
|
|
2196
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2197
|
+
state.info.failCause.assertionFailed = true;
|
|
2198
|
+
state.info.failCause.lastError = errorMessage;
|
|
2199
|
+
throw new Error(errorMessage);
|
|
1933
2200
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2201
|
+
}
|
|
2202
|
+
else {
|
|
2203
|
+
const valLines = val.split("\n");
|
|
2204
|
+
const expectedLines = expectedValue.split("\n");
|
|
2205
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2206
|
+
if (!isPart) {
|
|
2207
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2208
|
+
state.info.failCause.assertionFailed = true;
|
|
2209
|
+
state.info.failCause.lastError = errorMessage;
|
|
2210
|
+
throw new Error(errorMessage);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
else {
|
|
2215
|
+
if (!val.match(regex)) {
|
|
2216
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2217
|
+
state.info.failCause.assertionFailed = true;
|
|
2218
|
+
state.info.failCause.lastError = errorMessage;
|
|
2219
|
+
throw new Error(errorMessage);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return state.info;
|
|
2223
|
+
}
|
|
2224
|
+
catch (e) {
|
|
2225
|
+
await _commandError(state, e, this);
|
|
2226
|
+
}
|
|
2227
|
+
finally {
|
|
2228
|
+
await _commandFinally(state, this);
|
|
1941
2229
|
}
|
|
1942
2230
|
}
|
|
1943
2231
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -1958,7 +2246,7 @@ class StableBrowser {
|
|
|
1958
2246
|
if (options && options.timeout) {
|
|
1959
2247
|
timeout = options.timeout;
|
|
1960
2248
|
}
|
|
1961
|
-
const serviceUrl =
|
|
2249
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1962
2250
|
const request = {
|
|
1963
2251
|
method: "POST",
|
|
1964
2252
|
url: serviceUrl,
|
|
@@ -2014,7 +2302,8 @@ class StableBrowser {
|
|
|
2014
2302
|
catch (e) {
|
|
2015
2303
|
errorCount++;
|
|
2016
2304
|
if (errorCount > 3) {
|
|
2017
|
-
throw e;
|
|
2305
|
+
// throw e;
|
|
2306
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2018
2307
|
}
|
|
2019
2308
|
// ignore
|
|
2020
2309
|
}
|
|
@@ -2028,27 +2317,32 @@ class StableBrowser {
|
|
|
2028
2317
|
async _highlightElements(scope, css) {
|
|
2029
2318
|
try {
|
|
2030
2319
|
if (!scope) {
|
|
2320
|
+
// console.log(`Scope is not defined`);
|
|
2031
2321
|
return;
|
|
2032
2322
|
}
|
|
2033
2323
|
if (!css) {
|
|
2034
2324
|
scope
|
|
2035
2325
|
.evaluate((node) => {
|
|
2036
2326
|
if (node && node.style) {
|
|
2037
|
-
let
|
|
2038
|
-
|
|
2327
|
+
let originalOutline = node.style.outline;
|
|
2328
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
2329
|
+
// node.__previousOutline = originalOutline;
|
|
2330
|
+
node.style.outline = "2px solid red";
|
|
2331
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
2039
2332
|
if (window) {
|
|
2040
2333
|
window.addEventListener("beforeunload", function (e) {
|
|
2041
|
-
node.style.
|
|
2334
|
+
node.style.outline = originalOutline;
|
|
2042
2335
|
});
|
|
2043
2336
|
}
|
|
2044
2337
|
setTimeout(function () {
|
|
2045
|
-
node.style.
|
|
2338
|
+
node.style.outline = originalOutline;
|
|
2046
2339
|
}, 2000);
|
|
2047
2340
|
}
|
|
2048
2341
|
})
|
|
2049
2342
|
.then(() => { })
|
|
2050
2343
|
.catch((e) => {
|
|
2051
2344
|
// ignore
|
|
2345
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2052
2346
|
});
|
|
2053
2347
|
}
|
|
2054
2348
|
else {
|
|
@@ -2064,17 +2358,18 @@ class StableBrowser {
|
|
|
2064
2358
|
if (!element.style) {
|
|
2065
2359
|
return;
|
|
2066
2360
|
}
|
|
2067
|
-
|
|
2361
|
+
let originalOutline = element.style.outline;
|
|
2362
|
+
element.__previousOutline = originalOutline;
|
|
2068
2363
|
// Set the new border to be red and 2px solid
|
|
2069
|
-
element.style.
|
|
2364
|
+
element.style.outline = "2px solid red";
|
|
2070
2365
|
if (window) {
|
|
2071
2366
|
window.addEventListener("beforeunload", function (e) {
|
|
2072
|
-
element.style.
|
|
2367
|
+
element.style.outline = originalOutline;
|
|
2073
2368
|
});
|
|
2074
2369
|
}
|
|
2075
2370
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2076
2371
|
setTimeout(function () {
|
|
2077
|
-
element.style.
|
|
2372
|
+
element.style.outline = originalOutline;
|
|
2078
2373
|
}, 2000);
|
|
2079
2374
|
}
|
|
2080
2375
|
return;
|
|
@@ -2082,6 +2377,7 @@ class StableBrowser {
|
|
|
2082
2377
|
.then(() => { })
|
|
2083
2378
|
.catch((e) => {
|
|
2084
2379
|
// ignore
|
|
2380
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2085
2381
|
});
|
|
2086
2382
|
}
|
|
2087
2383
|
}
|
|
@@ -2089,6 +2385,54 @@ class StableBrowser {
|
|
|
2089
2385
|
console.debug(error);
|
|
2090
2386
|
}
|
|
2091
2387
|
}
|
|
2388
|
+
// async _unhighlightElements(scope, css) {
|
|
2389
|
+
// try {
|
|
2390
|
+
// if (!scope) {
|
|
2391
|
+
// return;
|
|
2392
|
+
// }
|
|
2393
|
+
// if (!css) {
|
|
2394
|
+
// scope
|
|
2395
|
+
// .evaluate((node) => {
|
|
2396
|
+
// if (node && node.style) {
|
|
2397
|
+
// if (!node.__previousOutline) {
|
|
2398
|
+
// node.style.outline = "";
|
|
2399
|
+
// } else {
|
|
2400
|
+
// node.style.outline = node.__previousOutline;
|
|
2401
|
+
// }
|
|
2402
|
+
// }
|
|
2403
|
+
// })
|
|
2404
|
+
// .then(() => {})
|
|
2405
|
+
// .catch((e) => {
|
|
2406
|
+
// // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
|
|
2407
|
+
// });
|
|
2408
|
+
// } else {
|
|
2409
|
+
// scope
|
|
2410
|
+
// .evaluate(([css]) => {
|
|
2411
|
+
// if (!css) {
|
|
2412
|
+
// return;
|
|
2413
|
+
// }
|
|
2414
|
+
// let elements = Array.from(document.querySelectorAll(css));
|
|
2415
|
+
// for (i = 0; i < elements.length; i++) {
|
|
2416
|
+
// let element = elements[i];
|
|
2417
|
+
// if (!element.style) {
|
|
2418
|
+
// return;
|
|
2419
|
+
// }
|
|
2420
|
+
// if (!element.__previousOutline) {
|
|
2421
|
+
// element.style.outline = "";
|
|
2422
|
+
// } else {
|
|
2423
|
+
// element.style.outline = element.__previousOutline;
|
|
2424
|
+
// }
|
|
2425
|
+
// }
|
|
2426
|
+
// })
|
|
2427
|
+
// .then(() => {})
|
|
2428
|
+
// .catch((e) => {
|
|
2429
|
+
// // console.error(`Error while unhighlighting element in css: ${e}`);
|
|
2430
|
+
// });
|
|
2431
|
+
// }
|
|
2432
|
+
// } catch (error) {
|
|
2433
|
+
// // console.debug(error);
|
|
2434
|
+
// }
|
|
2435
|
+
// }
|
|
2092
2436
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2093
2437
|
const startTime = Date.now();
|
|
2094
2438
|
let error = null;
|
|
@@ -2125,20 +2469,22 @@ class StableBrowser {
|
|
|
2125
2469
|
info.screenshotPath = screenshotPath;
|
|
2126
2470
|
Object.assign(e, { info: info });
|
|
2127
2471
|
error = e;
|
|
2128
|
-
throw e;
|
|
2472
|
+
// throw e;
|
|
2473
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
|
|
2129
2474
|
}
|
|
2130
2475
|
finally {
|
|
2131
2476
|
const endTime = Date.now();
|
|
2132
|
-
|
|
2477
|
+
_reportToWorld(world, {
|
|
2133
2478
|
type: Types.VERIFY_PAGE_PATH,
|
|
2134
2479
|
text: "Verify page path",
|
|
2480
|
+
_text: "Verify the page path contains " + pathPart,
|
|
2135
2481
|
screenshotId,
|
|
2136
2482
|
result: error
|
|
2137
2483
|
? {
|
|
2138
2484
|
status: "FAILED",
|
|
2139
2485
|
startTime,
|
|
2140
2486
|
endTime,
|
|
2141
|
-
message: error
|
|
2487
|
+
message: error?.message,
|
|
2142
2488
|
}
|
|
2143
2489
|
: {
|
|
2144
2490
|
status: "PASSED",
|
|
@@ -2149,94 +2495,58 @@ class StableBrowser {
|
|
|
2149
2495
|
});
|
|
2150
2496
|
}
|
|
2151
2497
|
}
|
|
2152
|
-
async
|
|
2498
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2153
2499
|
const startTime = Date.now();
|
|
2154
|
-
const timeout = this._getLoadTimeout(options);
|
|
2155
2500
|
let error = null;
|
|
2156
2501
|
let screenshotId = null;
|
|
2157
2502
|
let screenshotPath = null;
|
|
2158
2503
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2159
2504
|
const info = {};
|
|
2160
|
-
info.log = "***** verify
|
|
2161
|
-
info.operation = "
|
|
2162
|
-
const newValue = await this._replaceWithLocalData(
|
|
2163
|
-
if (newValue !==
|
|
2164
|
-
this.logger.info(
|
|
2165
|
-
|
|
2166
|
-
}
|
|
2167
|
-
info.
|
|
2168
|
-
let dateAlternatives = findDateAlternatives(text);
|
|
2169
|
-
let numberAlternatives = findNumberAlternatives(text);
|
|
2505
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2506
|
+
info.operation = "verifyPageTitle";
|
|
2507
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2508
|
+
if (newValue !== title) {
|
|
2509
|
+
this.logger.info(title + "=" + newValue);
|
|
2510
|
+
title = newValue;
|
|
2511
|
+
}
|
|
2512
|
+
info.title = title;
|
|
2170
2513
|
try {
|
|
2171
|
-
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2177
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
|
|
2178
|
-
result.frame = frames[i];
|
|
2179
|
-
results.push(result);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
else if (numberAlternatives.number) {
|
|
2183
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2184
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
|
|
2185
|
-
result.frame = frames[i];
|
|
2186
|
-
results.push(result);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
else {
|
|
2190
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, {});
|
|
2191
|
-
result.frame = frames[i];
|
|
2192
|
-
results.push(result);
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
info.results = results;
|
|
2196
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2197
|
-
if (resultWithElementsFound.length === 0) {
|
|
2198
|
-
if (Date.now() - startTime > timeout) {
|
|
2199
|
-
throw new Error(`Text ${text} not found in page`);
|
|
2514
|
+
for (let i = 0; i < 30; i++) {
|
|
2515
|
+
const foundTitle = await this.page.title();
|
|
2516
|
+
if (!foundTitle.includes(title)) {
|
|
2517
|
+
if (i === 29) {
|
|
2518
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2200
2519
|
}
|
|
2201
2520
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2202
2521
|
continue;
|
|
2203
2522
|
}
|
|
2204
|
-
if (resultWithElementsFound[0].randomToken) {
|
|
2205
|
-
const frame = resultWithElementsFound[0].frame;
|
|
2206
|
-
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
2207
|
-
await this._highlightElements(frame, dataAttribute);
|
|
2208
|
-
const element = await frame.$(dataAttribute);
|
|
2209
|
-
if (element) {
|
|
2210
|
-
await this.scrollIfNeeded(element, info);
|
|
2211
|
-
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
2523
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2215
2524
|
return info;
|
|
2216
2525
|
}
|
|
2217
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2218
2526
|
}
|
|
2219
2527
|
catch (e) {
|
|
2220
2528
|
//await this.closeUnexpectedPopups();
|
|
2221
|
-
this.logger.error("verify
|
|
2529
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2222
2530
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2223
2531
|
info.screenshotPath = screenshotPath;
|
|
2224
2532
|
Object.assign(e, { info: info });
|
|
2225
2533
|
error = e;
|
|
2226
|
-
throw e;
|
|
2534
|
+
// throw e;
|
|
2535
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2227
2536
|
}
|
|
2228
2537
|
finally {
|
|
2229
2538
|
const endTime = Date.now();
|
|
2230
|
-
|
|
2231
|
-
type: Types.
|
|
2232
|
-
text: "Verify
|
|
2539
|
+
_reportToWorld(world, {
|
|
2540
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2541
|
+
text: "Verify page title",
|
|
2542
|
+
_text: "Verify the page title contains " + title,
|
|
2233
2543
|
screenshotId,
|
|
2234
2544
|
result: error
|
|
2235
2545
|
? {
|
|
2236
2546
|
status: "FAILED",
|
|
2237
2547
|
startTime,
|
|
2238
2548
|
endTime,
|
|
2239
|
-
message: error
|
|
2549
|
+
message: error?.message,
|
|
2240
2550
|
}
|
|
2241
2551
|
: {
|
|
2242
2552
|
status: "PASSED",
|
|
@@ -2247,15 +2557,317 @@ class StableBrowser {
|
|
|
2247
2557
|
});
|
|
2248
2558
|
}
|
|
2249
2559
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2560
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2561
|
+
const frames = this.page.frames();
|
|
2562
|
+
let results = [];
|
|
2563
|
+
// let ignoreCase = false;
|
|
2564
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2565
|
+
if (dateAlternatives.date) {
|
|
2566
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2567
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2568
|
+
result.frame = frames[i];
|
|
2569
|
+
results.push(result);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
else if (numberAlternatives.number) {
|
|
2573
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2574
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2575
|
+
result.frame = frames[i];
|
|
2576
|
+
results.push(result);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
else {
|
|
2580
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2581
|
+
result.frame = frames[i];
|
|
2582
|
+
results.push(result);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
state.info.results = results;
|
|
2586
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2587
|
+
return resultWithElementsFound;
|
|
2588
|
+
}
|
|
2589
|
+
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2590
|
+
text = unEscapeString(text);
|
|
2591
|
+
const state = {
|
|
2592
|
+
text_search: text,
|
|
2593
|
+
options,
|
|
2594
|
+
world,
|
|
2595
|
+
locate: false,
|
|
2596
|
+
scroll: false,
|
|
2597
|
+
highlight: false,
|
|
2598
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2599
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2600
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2601
|
+
operation: "verifyTextExistInPage",
|
|
2602
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
2603
|
+
};
|
|
2604
|
+
if (testForRegex(text)) {
|
|
2605
|
+
text = text.replace(/\\"/g, '"');
|
|
2606
|
+
}
|
|
2607
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2608
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2609
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2610
|
+
if (newValue !== text) {
|
|
2611
|
+
this.logger.info(text + "=" + newValue);
|
|
2612
|
+
text = newValue;
|
|
2613
|
+
}
|
|
2614
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2615
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2616
|
+
try {
|
|
2617
|
+
await _preCommand(state, this);
|
|
2618
|
+
state.info.text = text;
|
|
2619
|
+
while (true) {
|
|
2620
|
+
let resultWithElementsFound = {
|
|
2621
|
+
length: 0,
|
|
2622
|
+
};
|
|
2623
|
+
try {
|
|
2624
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2625
|
+
}
|
|
2626
|
+
catch (error) {
|
|
2627
|
+
// ignore
|
|
2628
|
+
}
|
|
2629
|
+
if (resultWithElementsFound.length === 0) {
|
|
2630
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2631
|
+
throw new Error(`Text ${text} not found in page`);
|
|
2632
|
+
}
|
|
2633
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
try {
|
|
2637
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
2638
|
+
const frame = resultWithElementsFound[0].frame;
|
|
2639
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2640
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2641
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2642
|
+
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2643
|
+
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2644
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2645
|
+
// this._unhighlightElements(frame, dataAttribute)
|
|
2646
|
+
// .then(async () => {
|
|
2647
|
+
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2648
|
+
// })
|
|
2649
|
+
// .catch(
|
|
2650
|
+
// (e) => {}
|
|
2651
|
+
// console.error(e)
|
|
2652
|
+
// );
|
|
2653
|
+
// });
|
|
2654
|
+
// }
|
|
2655
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2656
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2657
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2658
|
+
if (element) {
|
|
2659
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2660
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2661
|
+
// await _screenshot(state, this, element);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
await _screenshot(state, this);
|
|
2665
|
+
return state.info;
|
|
2666
|
+
}
|
|
2667
|
+
catch (error) {
|
|
2668
|
+
console.error(error);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2672
|
+
}
|
|
2673
|
+
catch (e) {
|
|
2674
|
+
await _commandError(state, e, this);
|
|
2675
|
+
}
|
|
2676
|
+
finally {
|
|
2677
|
+
await _commandFinally(state, this);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2681
|
+
text = unEscapeString(text);
|
|
2682
|
+
const state = {
|
|
2683
|
+
text_search: text,
|
|
2684
|
+
options,
|
|
2685
|
+
world,
|
|
2686
|
+
locate: false,
|
|
2687
|
+
scroll: false,
|
|
2688
|
+
highlight: false,
|
|
2689
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2690
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2691
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2692
|
+
operation: "verifyTextNotExistInPage",
|
|
2693
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2694
|
+
};
|
|
2695
|
+
if (testForRegex(text)) {
|
|
2696
|
+
text = text.replace(/\\"/g, '"');
|
|
2697
|
+
}
|
|
2698
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2699
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2700
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2701
|
+
if (newValue !== text) {
|
|
2702
|
+
this.logger.info(text + "=" + newValue);
|
|
2703
|
+
text = newValue;
|
|
2704
|
+
}
|
|
2705
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2706
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2707
|
+
try {
|
|
2708
|
+
await _preCommand(state, this);
|
|
2709
|
+
state.info.text = text;
|
|
2710
|
+
let resultWithElementsFound = {
|
|
2711
|
+
length: null, // initial cannot be 0
|
|
2712
|
+
};
|
|
2713
|
+
while (true) {
|
|
2714
|
+
try {
|
|
2715
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2716
|
+
}
|
|
2717
|
+
catch (error) {
|
|
2718
|
+
// ignore
|
|
2719
|
+
}
|
|
2720
|
+
if (resultWithElementsFound.length === 0) {
|
|
2721
|
+
await _screenshot(state, this);
|
|
2722
|
+
return state.info;
|
|
2723
|
+
}
|
|
2724
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2725
|
+
throw new Error(`Text ${text} found in page`);
|
|
2726
|
+
}
|
|
2727
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
catch (e) {
|
|
2731
|
+
await _commandError(state, e, this);
|
|
2732
|
+
}
|
|
2733
|
+
finally {
|
|
2734
|
+
await _commandFinally(state, this);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
2738
|
+
textAnchor = unEscapeString(textAnchor);
|
|
2739
|
+
textToVerify = unEscapeString(textToVerify);
|
|
2740
|
+
const state = {
|
|
2741
|
+
text_search: textToVerify,
|
|
2742
|
+
options,
|
|
2743
|
+
world,
|
|
2744
|
+
locate: false,
|
|
2745
|
+
scroll: false,
|
|
2746
|
+
highlight: false,
|
|
2747
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2748
|
+
text: `Verify text with relation to another text`,
|
|
2749
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
2750
|
+
operation: "verify_text_with_relation",
|
|
2751
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2752
|
+
};
|
|
2753
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2754
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2755
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2756
|
+
if (newValue !== textAnchor) {
|
|
2757
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
2758
|
+
textAnchor = newValue;
|
|
2759
|
+
}
|
|
2760
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
2761
|
+
if (newValue !== textToVerify) {
|
|
2762
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
2763
|
+
textToVerify = newValue;
|
|
2764
|
+
}
|
|
2765
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
2766
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
2767
|
+
let foundAncore = false;
|
|
2768
|
+
try {
|
|
2769
|
+
await _preCommand(state, this);
|
|
2770
|
+
state.info.text = textToVerify;
|
|
2771
|
+
let resultWithElementsFound = {
|
|
2772
|
+
length: 0,
|
|
2773
|
+
};
|
|
2774
|
+
while (true) {
|
|
2775
|
+
try {
|
|
2776
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2777
|
+
}
|
|
2778
|
+
catch (error) {
|
|
2779
|
+
// ignore
|
|
2780
|
+
}
|
|
2781
|
+
if (resultWithElementsFound.length === 0) {
|
|
2782
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2783
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2784
|
+
}
|
|
2785
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
try {
|
|
2789
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2790
|
+
foundAncore = true;
|
|
2791
|
+
const result = resultWithElementsFound[i];
|
|
2792
|
+
const token = result.randomToken;
|
|
2793
|
+
const frame = result.frame;
|
|
2794
|
+
let css = `[data-blinq-id-${token}]`;
|
|
2795
|
+
const climbArray1 = [];
|
|
2796
|
+
for (let i = 0; i < climb; i++) {
|
|
2797
|
+
climbArray1.push("..");
|
|
2798
|
+
}
|
|
2799
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
2800
|
+
css = css + " >> " + climbXpath;
|
|
2801
|
+
const count = await frame.locator(css).count();
|
|
2802
|
+
for (let j = 0; j < count; j++) {
|
|
2803
|
+
const continer = await frame.locator(css).nth(j);
|
|
2804
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2805
|
+
if (result.elementCount > 0) {
|
|
2806
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2807
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2808
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2809
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2810
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
2811
|
+
// this._highlightElements(frame, dataAttribute)
|
|
2812
|
+
// .then(async () => {
|
|
2813
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2814
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
2815
|
+
// () => {}
|
|
2816
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
2817
|
+
// );
|
|
2818
|
+
// })
|
|
2819
|
+
// .catch(e);
|
|
2820
|
+
// }
|
|
2821
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
2822
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2823
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2824
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2825
|
+
if (element) {
|
|
2826
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2827
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2828
|
+
}
|
|
2829
|
+
await _screenshot(state, this);
|
|
2830
|
+
return state.info;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
catch (error) {
|
|
2836
|
+
console.error(error);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2254
2840
|
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2841
|
+
catch (e) {
|
|
2842
|
+
await _commandError(state, e, this);
|
|
2843
|
+
}
|
|
2844
|
+
finally {
|
|
2845
|
+
await _commandFinally(state, this);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2849
|
+
const frames = this.page.frames();
|
|
2850
|
+
let results = [];
|
|
2851
|
+
let ignoreCase = false;
|
|
2852
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2853
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2854
|
+
result.frame = frames[i];
|
|
2855
|
+
const climbArray = [];
|
|
2856
|
+
for (let i = 0; i < climb; i++) {
|
|
2857
|
+
climbArray.push("..");
|
|
2858
|
+
}
|
|
2859
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2860
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2861
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2862
|
+
if (count > 0) {
|
|
2863
|
+
result.elementCount = count;
|
|
2864
|
+
result.locator = newLocator;
|
|
2865
|
+
results.push(result);
|
|
2866
|
+
}
|
|
2257
2867
|
}
|
|
2258
|
-
|
|
2868
|
+
// state.info.results = results;
|
|
2869
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2870
|
+
return resultWithElementsFound;
|
|
2259
2871
|
}
|
|
2260
2872
|
async visualVerification(text, options = {}, world = null) {
|
|
2261
2873
|
const startTime = Date.now();
|
|
@@ -2271,14 +2883,17 @@ class StableBrowser {
|
|
|
2271
2883
|
throw new Error("TOKEN is not set");
|
|
2272
2884
|
}
|
|
2273
2885
|
try {
|
|
2274
|
-
let serviceUrl =
|
|
2886
|
+
let serviceUrl = _getServerUrl();
|
|
2275
2887
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2276
2888
|
info.screenshotPath = screenshotPath;
|
|
2277
2889
|
const screenshot = await this.takeScreenshot();
|
|
2278
|
-
|
|
2279
|
-
method: "
|
|
2890
|
+
let request = {
|
|
2891
|
+
method: "post",
|
|
2892
|
+
maxBodyLength: Infinity,
|
|
2280
2893
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2281
2894
|
headers: {
|
|
2895
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
2896
|
+
"x-source": "aaa",
|
|
2282
2897
|
"Content-Type": "application/json",
|
|
2283
2898
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2284
2899
|
},
|
|
@@ -2287,7 +2902,7 @@ class StableBrowser {
|
|
|
2287
2902
|
screenshot: screenshot,
|
|
2288
2903
|
}),
|
|
2289
2904
|
};
|
|
2290
|
-
|
|
2905
|
+
const result = await axios.request(request);
|
|
2291
2906
|
if (result.data.status !== true) {
|
|
2292
2907
|
throw new Error("Visual validation failed");
|
|
2293
2908
|
}
|
|
@@ -2307,20 +2922,22 @@ class StableBrowser {
|
|
|
2307
2922
|
info.screenshotPath = screenshotPath;
|
|
2308
2923
|
Object.assign(e, { info: info });
|
|
2309
2924
|
error = e;
|
|
2310
|
-
throw e;
|
|
2925
|
+
// throw e;
|
|
2926
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2311
2927
|
}
|
|
2312
2928
|
finally {
|
|
2313
2929
|
const endTime = Date.now();
|
|
2314
|
-
|
|
2930
|
+
_reportToWorld(world, {
|
|
2315
2931
|
type: Types.VERIFY_VISUAL,
|
|
2316
2932
|
text: "Visual verification",
|
|
2933
|
+
_text: "Visual verification of " + text,
|
|
2317
2934
|
screenshotId,
|
|
2318
2935
|
result: error
|
|
2319
2936
|
? {
|
|
2320
2937
|
status: "FAILED",
|
|
2321
2938
|
startTime,
|
|
2322
2939
|
endTime,
|
|
2323
|
-
message: error
|
|
2940
|
+
message: error?.message,
|
|
2324
2941
|
}
|
|
2325
2942
|
: {
|
|
2326
2943
|
status: "PASSED",
|
|
@@ -2352,13 +2969,14 @@ class StableBrowser {
|
|
|
2352
2969
|
this.logger.info("Table data verified");
|
|
2353
2970
|
}
|
|
2354
2971
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2355
|
-
|
|
2972
|
+
_validateSelectors(selectors);
|
|
2356
2973
|
const startTime = Date.now();
|
|
2357
2974
|
let error = null;
|
|
2358
2975
|
let screenshotId = null;
|
|
2359
2976
|
let screenshotPath = null;
|
|
2360
2977
|
const info = {};
|
|
2361
2978
|
info.log = "";
|
|
2979
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2362
2980
|
info.operation = "getTableData";
|
|
2363
2981
|
info.selectors = selectors;
|
|
2364
2982
|
try {
|
|
@@ -2374,11 +2992,12 @@ class StableBrowser {
|
|
|
2374
2992
|
info.screenshotPath = screenshotPath;
|
|
2375
2993
|
Object.assign(e, { info: info });
|
|
2376
2994
|
error = e;
|
|
2377
|
-
throw e;
|
|
2995
|
+
// throw e;
|
|
2996
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2378
2997
|
}
|
|
2379
2998
|
finally {
|
|
2380
2999
|
const endTime = Date.now();
|
|
2381
|
-
|
|
3000
|
+
_reportToWorld(world, {
|
|
2382
3001
|
element_name: selectors.element_name,
|
|
2383
3002
|
type: Types.GET_TABLE_DATA,
|
|
2384
3003
|
text: "Get table data",
|
|
@@ -2388,7 +3007,7 @@ class StableBrowser {
|
|
|
2388
3007
|
status: "FAILED",
|
|
2389
3008
|
startTime,
|
|
2390
3009
|
endTime,
|
|
2391
|
-
message: error
|
|
3010
|
+
message: error?.message,
|
|
2392
3011
|
}
|
|
2393
3012
|
: {
|
|
2394
3013
|
status: "PASSED",
|
|
@@ -2400,7 +3019,7 @@ class StableBrowser {
|
|
|
2400
3019
|
}
|
|
2401
3020
|
}
|
|
2402
3021
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2403
|
-
|
|
3022
|
+
_validateSelectors(selectors);
|
|
2404
3023
|
if (!query) {
|
|
2405
3024
|
throw new Error("query is null");
|
|
2406
3025
|
}
|
|
@@ -2433,7 +3052,7 @@ class StableBrowser {
|
|
|
2433
3052
|
info.operation = "analyzeTable";
|
|
2434
3053
|
info.selectors = selectors;
|
|
2435
3054
|
info.query = query;
|
|
2436
|
-
query =
|
|
3055
|
+
query = _fixUsingParams(query, _params);
|
|
2437
3056
|
info.query_fixed = query;
|
|
2438
3057
|
info.operator = operator;
|
|
2439
3058
|
info.value = value;
|
|
@@ -2539,11 +3158,12 @@ class StableBrowser {
|
|
|
2539
3158
|
info.screenshotPath = screenshotPath;
|
|
2540
3159
|
Object.assign(e, { info: info });
|
|
2541
3160
|
error = e;
|
|
2542
|
-
throw e;
|
|
3161
|
+
// throw e;
|
|
3162
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2543
3163
|
}
|
|
2544
3164
|
finally {
|
|
2545
3165
|
const endTime = Date.now();
|
|
2546
|
-
|
|
3166
|
+
_reportToWorld(world, {
|
|
2547
3167
|
element_name: selectors.element_name,
|
|
2548
3168
|
type: Types.ANALYZE_TABLE,
|
|
2549
3169
|
text: "Analyze table",
|
|
@@ -2553,7 +3173,7 @@ class StableBrowser {
|
|
|
2553
3173
|
status: "FAILED",
|
|
2554
3174
|
startTime,
|
|
2555
3175
|
endTime,
|
|
2556
|
-
message: error
|
|
3176
|
+
message: error?.message,
|
|
2557
3177
|
}
|
|
2558
3178
|
: {
|
|
2559
3179
|
status: "PASSED",
|
|
@@ -2565,27 +3185,13 @@ class StableBrowser {
|
|
|
2565
3185
|
}
|
|
2566
3186
|
}
|
|
2567
3187
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2568
|
-
|
|
2569
|
-
return value;
|
|
2570
|
-
}
|
|
2571
|
-
// find all the accurance of {{(.*?)}} and replace with the value
|
|
2572
|
-
let regex = /{{(.*?)}}/g;
|
|
2573
|
-
let matches = value.match(regex);
|
|
2574
|
-
if (matches) {
|
|
2575
|
-
const testData = this.getTestData(world);
|
|
2576
|
-
for (let i = 0; i < matches.length; i++) {
|
|
2577
|
-
let match = matches[i];
|
|
2578
|
-
let key = match.substring(2, match.length - 2);
|
|
2579
|
-
let newValue = objectPath.get(testData, key, null);
|
|
2580
|
-
if (newValue !== null) {
|
|
2581
|
-
value = value.replace(match, newValue);
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
3188
|
+
try {
|
|
3189
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2584
3190
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
3191
|
+
catch (error) {
|
|
3192
|
+
this.logger.debug(error);
|
|
3193
|
+
throw error;
|
|
2587
3194
|
}
|
|
2588
|
-
return value;
|
|
2589
3195
|
}
|
|
2590
3196
|
_getLoadTimeout(options) {
|
|
2591
3197
|
let timeout = 15000;
|
|
@@ -2597,6 +3203,35 @@ class StableBrowser {
|
|
|
2597
3203
|
}
|
|
2598
3204
|
return timeout;
|
|
2599
3205
|
}
|
|
3206
|
+
_getFindElementTimeout(options) {
|
|
3207
|
+
if (options && options.timeout) {
|
|
3208
|
+
return options.timeout;
|
|
3209
|
+
}
|
|
3210
|
+
if (this.configuration.find_element_timeout) {
|
|
3211
|
+
return this.configuration.find_element_timeout;
|
|
3212
|
+
}
|
|
3213
|
+
return 30000;
|
|
3214
|
+
}
|
|
3215
|
+
async saveStoreState(path = null, world = null) {
|
|
3216
|
+
const storageState = await this.page.context().storageState();
|
|
3217
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
3218
|
+
if (path) {
|
|
3219
|
+
// save { storageState: storageState } into the path
|
|
3220
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
3221
|
+
}
|
|
3222
|
+
else {
|
|
3223
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
async restoreSaveState(path = null, world = null) {
|
|
3227
|
+
await refreshBrowser(this, path, world);
|
|
3228
|
+
this.registerEventListeners(this.context);
|
|
3229
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
3230
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
3231
|
+
if (this.onRestoreSaveState) {
|
|
3232
|
+
this.onRestoreSaveState(path);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
2600
3235
|
async waitForPageLoad(options = {}, world = null) {
|
|
2601
3236
|
let timeout = this._getLoadTimeout(options);
|
|
2602
3237
|
const promiseArray = [];
|
|
@@ -2636,7 +3271,7 @@ class StableBrowser {
|
|
|
2636
3271
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2637
3272
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2638
3273
|
const endTime = Date.now();
|
|
2639
|
-
|
|
3274
|
+
_reportToWorld(world, {
|
|
2640
3275
|
type: Types.GET_PAGE_STATUS,
|
|
2641
3276
|
text: "Wait for page load",
|
|
2642
3277
|
screenshotId,
|
|
@@ -2645,7 +3280,7 @@ class StableBrowser {
|
|
|
2645
3280
|
status: "FAILED",
|
|
2646
3281
|
startTime,
|
|
2647
3282
|
endTime,
|
|
2648
|
-
message: error
|
|
3283
|
+
message: error?.message,
|
|
2649
3284
|
}
|
|
2650
3285
|
: {
|
|
2651
3286
|
status: "PASSED",
|
|
@@ -2656,41 +3291,123 @@ class StableBrowser {
|
|
|
2656
3291
|
}
|
|
2657
3292
|
}
|
|
2658
3293
|
async closePage(options = {}, world = null) {
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
3294
|
+
const state = {
|
|
3295
|
+
options,
|
|
3296
|
+
world,
|
|
3297
|
+
locate: false,
|
|
3298
|
+
scroll: false,
|
|
3299
|
+
highlight: false,
|
|
3300
|
+
type: Types.CLOSE_PAGE,
|
|
3301
|
+
text: `Close page`,
|
|
3302
|
+
_text: `Close the page`,
|
|
3303
|
+
operation: "closePage",
|
|
3304
|
+
log: "***** close page *****\n",
|
|
3305
|
+
throwError: false,
|
|
3306
|
+
};
|
|
2664
3307
|
try {
|
|
3308
|
+
await _preCommand(state, this);
|
|
2665
3309
|
await this.page.close();
|
|
2666
3310
|
}
|
|
2667
3311
|
catch (e) {
|
|
2668
3312
|
console.log(".");
|
|
3313
|
+
await _commandError(state, e, this);
|
|
2669
3314
|
}
|
|
2670
3315
|
finally {
|
|
2671
|
-
await
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3316
|
+
await _commandFinally(state, this);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3320
|
+
let operation = null;
|
|
3321
|
+
if (!options || !options.operation) {
|
|
3322
|
+
throw new Error("operation is not defined");
|
|
3323
|
+
}
|
|
3324
|
+
operation = options.operation;
|
|
3325
|
+
// validate operation is one of the supported operations
|
|
3326
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3327
|
+
throw new Error("operation is not supported");
|
|
3328
|
+
}
|
|
3329
|
+
const state = {
|
|
3330
|
+
options,
|
|
3331
|
+
world,
|
|
3332
|
+
locate: false,
|
|
3333
|
+
scroll: false,
|
|
3334
|
+
highlight: false,
|
|
3335
|
+
type: Types.TABLE_OPERATION,
|
|
3336
|
+
text: `Table operation`,
|
|
3337
|
+
_text: `Table ${operation} operation`,
|
|
3338
|
+
operation: operation,
|
|
3339
|
+
log: "***** Table operation *****\n",
|
|
3340
|
+
};
|
|
3341
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3342
|
+
try {
|
|
3343
|
+
await _preCommand(state, this);
|
|
3344
|
+
const start = Date.now();
|
|
3345
|
+
let cellArea = null;
|
|
3346
|
+
while (true) {
|
|
3347
|
+
try {
|
|
3348
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3349
|
+
if (cellArea) {
|
|
3350
|
+
break;
|
|
2684
3351
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
3352
|
+
}
|
|
3353
|
+
catch (e) {
|
|
3354
|
+
// ignore
|
|
3355
|
+
}
|
|
3356
|
+
if (Date.now() - start > timeout) {
|
|
3357
|
+
throw new Error(`Cell not found in table`);
|
|
3358
|
+
}
|
|
3359
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3360
|
+
}
|
|
3361
|
+
switch (operation) {
|
|
3362
|
+
case "click":
|
|
3363
|
+
if (!options.css) {
|
|
3364
|
+
// will click in the center of the cell
|
|
3365
|
+
let xOffset = 0;
|
|
3366
|
+
let yOffset = 0;
|
|
3367
|
+
if (options.xOffset) {
|
|
3368
|
+
xOffset = options.xOffset;
|
|
3369
|
+
}
|
|
3370
|
+
if (options.yOffset) {
|
|
3371
|
+
yOffset = options.yOffset;
|
|
3372
|
+
}
|
|
3373
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3374
|
+
}
|
|
3375
|
+
else {
|
|
3376
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3377
|
+
if (results.length === 0) {
|
|
3378
|
+
throw new Error(`Element not found in cell area`);
|
|
3379
|
+
}
|
|
3380
|
+
state.element = results[0];
|
|
3381
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3382
|
+
}
|
|
3383
|
+
break;
|
|
3384
|
+
case "hover+click":
|
|
3385
|
+
if (!options.css) {
|
|
3386
|
+
throw new Error("css is not defined");
|
|
3387
|
+
}
|
|
3388
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3389
|
+
if (results.length === 0) {
|
|
3390
|
+
throw new Error(`Element not found in cell area`);
|
|
3391
|
+
}
|
|
3392
|
+
state.element = results[0];
|
|
3393
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3394
|
+
break;
|
|
3395
|
+
default:
|
|
3396
|
+
throw new Error("operation is not supported");
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
catch (e) {
|
|
3400
|
+
await _commandError(state, e, this);
|
|
3401
|
+
}
|
|
3402
|
+
finally {
|
|
3403
|
+
await _commandFinally(state, this);
|
|
2692
3404
|
}
|
|
2693
3405
|
}
|
|
3406
|
+
saveTestDataAsGlobal(options, world) {
|
|
3407
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
3408
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
3409
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
3410
|
+
}
|
|
2694
3411
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2695
3412
|
const startTime = Date.now();
|
|
2696
3413
|
let error = null;
|
|
@@ -2708,21 +3425,23 @@ class StableBrowser {
|
|
|
2708
3425
|
}
|
|
2709
3426
|
catch (e) {
|
|
2710
3427
|
console.log(".");
|
|
3428
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2711
3429
|
}
|
|
2712
3430
|
finally {
|
|
2713
3431
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2714
3432
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2715
3433
|
const endTime = Date.now();
|
|
2716
|
-
|
|
3434
|
+
_reportToWorld(world, {
|
|
2717
3435
|
type: Types.SET_VIEWPORT,
|
|
2718
3436
|
text: "set viewport size to " + width + "x" + hight,
|
|
3437
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2719
3438
|
screenshotId,
|
|
2720
3439
|
result: error
|
|
2721
3440
|
? {
|
|
2722
3441
|
status: "FAILED",
|
|
2723
3442
|
startTime,
|
|
2724
3443
|
endTime,
|
|
2725
|
-
message: error
|
|
3444
|
+
message: error?.message,
|
|
2726
3445
|
}
|
|
2727
3446
|
: {
|
|
2728
3447
|
status: "PASSED",
|
|
@@ -2744,12 +3463,13 @@ class StableBrowser {
|
|
|
2744
3463
|
}
|
|
2745
3464
|
catch (e) {
|
|
2746
3465
|
console.log(".");
|
|
3466
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2747
3467
|
}
|
|
2748
3468
|
finally {
|
|
2749
3469
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2750
3470
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2751
3471
|
const endTime = Date.now();
|
|
2752
|
-
|
|
3472
|
+
_reportToWorld(world, {
|
|
2753
3473
|
type: Types.GET_PAGE_STATUS,
|
|
2754
3474
|
text: "page relaod",
|
|
2755
3475
|
screenshotId,
|
|
@@ -2758,7 +3478,7 @@ class StableBrowser {
|
|
|
2758
3478
|
status: "FAILED",
|
|
2759
3479
|
startTime,
|
|
2760
3480
|
endTime,
|
|
2761
|
-
message: error
|
|
3481
|
+
message: error?.message,
|
|
2762
3482
|
}
|
|
2763
3483
|
: {
|
|
2764
3484
|
status: "PASSED",
|
|
@@ -2785,11 +3505,129 @@ class StableBrowser {
|
|
|
2785
3505
|
console.log("#-#");
|
|
2786
3506
|
}
|
|
2787
3507
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3508
|
+
async beforeScenario(world, scenario) {
|
|
3509
|
+
this.beforeScenarioCalled = true;
|
|
3510
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3511
|
+
this.scenarioName = scenario.pickle.name;
|
|
3512
|
+
}
|
|
3513
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3514
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3515
|
+
}
|
|
3516
|
+
if (this.context) {
|
|
3517
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3518
|
+
}
|
|
3519
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3520
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3521
|
+
// check if @global_test_data tag is present
|
|
3522
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3523
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
// update test data based on feature/scenario
|
|
3527
|
+
let envName = null;
|
|
3528
|
+
if (this.context && this.context.environment) {
|
|
3529
|
+
envName = this.context.environment.name;
|
|
3530
|
+
}
|
|
3531
|
+
if (!process.env.TEMP_RUN) {
|
|
3532
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3533
|
+
}
|
|
3534
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3535
|
+
}
|
|
3536
|
+
async afterScenario(world, scenario) { }
|
|
3537
|
+
async beforeStep(world, step) {
|
|
3538
|
+
if (!this.beforeScenarioCalled) {
|
|
3539
|
+
this.beforeScenario(world, step);
|
|
3540
|
+
}
|
|
3541
|
+
if (this.stepIndex === undefined) {
|
|
3542
|
+
this.stepIndex = 0;
|
|
3543
|
+
}
|
|
3544
|
+
else {
|
|
3545
|
+
this.stepIndex++;
|
|
3546
|
+
}
|
|
3547
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3548
|
+
this.stepName = step.pickleStep.text;
|
|
3549
|
+
this.logger.info("step: " + this.stepName);
|
|
3550
|
+
}
|
|
3551
|
+
else if (step && step.text) {
|
|
3552
|
+
this.stepName = step.text;
|
|
3553
|
+
}
|
|
3554
|
+
else {
|
|
3555
|
+
this.stepName = "step " + this.stepIndex;
|
|
3556
|
+
}
|
|
3557
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3558
|
+
if (this.context.browserObject.context) {
|
|
3559
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (this.initSnapshotTaken === false) {
|
|
3563
|
+
this.initSnapshotTaken = true;
|
|
3564
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3565
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3566
|
+
if (snapshot) {
|
|
3567
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
async getAriaSnapshot() {
|
|
3573
|
+
try {
|
|
3574
|
+
// find the page url
|
|
3575
|
+
const url = await this.page.url();
|
|
3576
|
+
// extract the path from the url
|
|
3577
|
+
const path = new URL(url).pathname;
|
|
3578
|
+
// get the page title
|
|
3579
|
+
const title = await this.page.title();
|
|
3580
|
+
// go over other frams
|
|
3581
|
+
const frames = this.page.frames();
|
|
3582
|
+
const snapshots = [];
|
|
3583
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3584
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3585
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3586
|
+
const frame = frames[i];
|
|
3587
|
+
try {
|
|
3588
|
+
// Ensure frame is attached and has body
|
|
3589
|
+
const body = frame.locator("body");
|
|
3590
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3591
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3592
|
+
content.push(`- frame: ${i}`);
|
|
3593
|
+
content.push(snapshot);
|
|
3594
|
+
}
|
|
3595
|
+
catch (innerErr) { }
|
|
3596
|
+
}
|
|
3597
|
+
return content.join("\n");
|
|
3598
|
+
}
|
|
3599
|
+
catch (e) {
|
|
3600
|
+
console.log("Error in getAriaSnapshot");
|
|
3601
|
+
//console.debug(e);
|
|
3602
|
+
}
|
|
3603
|
+
return null;
|
|
3604
|
+
}
|
|
3605
|
+
async afterStep(world, step) {
|
|
3606
|
+
this.stepName = null;
|
|
3607
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3608
|
+
if (this.context.browserObject.context) {
|
|
3609
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
3610
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3611
|
+
});
|
|
3612
|
+
if (world && world.attach) {
|
|
3613
|
+
await world.attach(JSON.stringify({
|
|
3614
|
+
type: "trace",
|
|
3615
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3616
|
+
}), "application/json+trace");
|
|
3617
|
+
}
|
|
3618
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
if (this.context) {
|
|
3622
|
+
this.context.examplesRow = null;
|
|
3623
|
+
}
|
|
3624
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3625
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3626
|
+
if (snapshot) {
|
|
3627
|
+
const obj = {};
|
|
3628
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3629
|
+
}
|
|
2791
3630
|
}
|
|
2792
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2793
3631
|
}
|
|
2794
3632
|
}
|
|
2795
3633
|
function createTimedPromise(promise, label) {
|
|
@@ -2797,151 +3635,5 @@ function createTimedPromise(promise, label) {
|
|
|
2797
3635
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2798
3636
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2799
3637
|
}
|
|
2800
|
-
const KEYBOARD_EVENTS = [
|
|
2801
|
-
"ALT",
|
|
2802
|
-
"AltGraph",
|
|
2803
|
-
"CapsLock",
|
|
2804
|
-
"Control",
|
|
2805
|
-
"Fn",
|
|
2806
|
-
"FnLock",
|
|
2807
|
-
"Hyper",
|
|
2808
|
-
"Meta",
|
|
2809
|
-
"NumLock",
|
|
2810
|
-
"ScrollLock",
|
|
2811
|
-
"Shift",
|
|
2812
|
-
"Super",
|
|
2813
|
-
"Symbol",
|
|
2814
|
-
"SymbolLock",
|
|
2815
|
-
"Enter",
|
|
2816
|
-
"Tab",
|
|
2817
|
-
"ArrowDown",
|
|
2818
|
-
"ArrowLeft",
|
|
2819
|
-
"ArrowRight",
|
|
2820
|
-
"ArrowUp",
|
|
2821
|
-
"End",
|
|
2822
|
-
"Home",
|
|
2823
|
-
"PageDown",
|
|
2824
|
-
"PageUp",
|
|
2825
|
-
"Backspace",
|
|
2826
|
-
"Clear",
|
|
2827
|
-
"Copy",
|
|
2828
|
-
"CrSel",
|
|
2829
|
-
"Cut",
|
|
2830
|
-
"Delete",
|
|
2831
|
-
"EraseEof",
|
|
2832
|
-
"ExSel",
|
|
2833
|
-
"Insert",
|
|
2834
|
-
"Paste",
|
|
2835
|
-
"Redo",
|
|
2836
|
-
"Undo",
|
|
2837
|
-
"Accept",
|
|
2838
|
-
"Again",
|
|
2839
|
-
"Attn",
|
|
2840
|
-
"Cancel",
|
|
2841
|
-
"ContextMenu",
|
|
2842
|
-
"Escape",
|
|
2843
|
-
"Execute",
|
|
2844
|
-
"Find",
|
|
2845
|
-
"Finish",
|
|
2846
|
-
"Help",
|
|
2847
|
-
"Pause",
|
|
2848
|
-
"Play",
|
|
2849
|
-
"Props",
|
|
2850
|
-
"Select",
|
|
2851
|
-
"ZoomIn",
|
|
2852
|
-
"ZoomOut",
|
|
2853
|
-
"BrightnessDown",
|
|
2854
|
-
"BrightnessUp",
|
|
2855
|
-
"Eject",
|
|
2856
|
-
"LogOff",
|
|
2857
|
-
"Power",
|
|
2858
|
-
"PowerOff",
|
|
2859
|
-
"PrintScreen",
|
|
2860
|
-
"Hibernate",
|
|
2861
|
-
"Standby",
|
|
2862
|
-
"WakeUp",
|
|
2863
|
-
"AllCandidates",
|
|
2864
|
-
"Alphanumeric",
|
|
2865
|
-
"CodeInput",
|
|
2866
|
-
"Compose",
|
|
2867
|
-
"Convert",
|
|
2868
|
-
"Dead",
|
|
2869
|
-
"FinalMode",
|
|
2870
|
-
"GroupFirst",
|
|
2871
|
-
"GroupLast",
|
|
2872
|
-
"GroupNext",
|
|
2873
|
-
"GroupPrevious",
|
|
2874
|
-
"ModeChange",
|
|
2875
|
-
"NextCandidate",
|
|
2876
|
-
"NonConvert",
|
|
2877
|
-
"PreviousCandidate",
|
|
2878
|
-
"Process",
|
|
2879
|
-
"SingleCandidate",
|
|
2880
|
-
"HangulMode",
|
|
2881
|
-
"HanjaMode",
|
|
2882
|
-
"JunjaMode",
|
|
2883
|
-
"Eisu",
|
|
2884
|
-
"Hankaku",
|
|
2885
|
-
"Hiragana",
|
|
2886
|
-
"HiraganaKatakana",
|
|
2887
|
-
"KanaMode",
|
|
2888
|
-
"KanjiMode",
|
|
2889
|
-
"Katakana",
|
|
2890
|
-
"Romaji",
|
|
2891
|
-
"Zenkaku",
|
|
2892
|
-
"ZenkakuHanaku",
|
|
2893
|
-
"F1",
|
|
2894
|
-
"F2",
|
|
2895
|
-
"F3",
|
|
2896
|
-
"F4",
|
|
2897
|
-
"F5",
|
|
2898
|
-
"F6",
|
|
2899
|
-
"F7",
|
|
2900
|
-
"F8",
|
|
2901
|
-
"F9",
|
|
2902
|
-
"F10",
|
|
2903
|
-
"F11",
|
|
2904
|
-
"F12",
|
|
2905
|
-
"Soft1",
|
|
2906
|
-
"Soft2",
|
|
2907
|
-
"Soft3",
|
|
2908
|
-
"Soft4",
|
|
2909
|
-
"ChannelDown",
|
|
2910
|
-
"ChannelUp",
|
|
2911
|
-
"Close",
|
|
2912
|
-
"MailForward",
|
|
2913
|
-
"MailReply",
|
|
2914
|
-
"MailSend",
|
|
2915
|
-
"MediaFastForward",
|
|
2916
|
-
"MediaPause",
|
|
2917
|
-
"MediaPlay",
|
|
2918
|
-
"MediaPlayPause",
|
|
2919
|
-
"MediaRecord",
|
|
2920
|
-
"MediaRewind",
|
|
2921
|
-
"MediaStop",
|
|
2922
|
-
"MediaTrackNext",
|
|
2923
|
-
"MediaTrackPrevious",
|
|
2924
|
-
"AudioBalanceLeft",
|
|
2925
|
-
"AudioBalanceRight",
|
|
2926
|
-
"AudioBassBoostDown",
|
|
2927
|
-
"AudioBassBoostToggle",
|
|
2928
|
-
"AudioBassBoostUp",
|
|
2929
|
-
"AudioFaderFront",
|
|
2930
|
-
"AudioFaderRear",
|
|
2931
|
-
"AudioSurroundModeNext",
|
|
2932
|
-
"AudioTrebleDown",
|
|
2933
|
-
"AudioTrebleUp",
|
|
2934
|
-
"AudioVolumeDown",
|
|
2935
|
-
"AudioVolumeMute",
|
|
2936
|
-
"AudioVolumeUp",
|
|
2937
|
-
"MicrophoneToggle",
|
|
2938
|
-
"MicrophoneVolumeDown",
|
|
2939
|
-
"MicrophoneVolumeMute",
|
|
2940
|
-
"MicrophoneVolumeUp",
|
|
2941
|
-
"TV",
|
|
2942
|
-
"TV3DMode",
|
|
2943
|
-
"TVAntennaCable",
|
|
2944
|
-
"TVAudioDescription",
|
|
2945
|
-
];
|
|
2946
3638
|
export { StableBrowser };
|
|
2947
3639
|
//# sourceMappingURL=stable_browser.js.map
|