automation_model 1.0.438-dev → 1.0.438
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 -44
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +5 -2
- package/lib/auto_page.js +197 -50
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +7 -3
- package/lib/browser_manager.js +156 -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 +3 -0
- package/lib/environment.js +5 -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 +124 -7
- 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/stable_browser.d.ts +106 -36
- package/lib/stable_browser.js +1821 -1245
- 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 +6 -0
- package/lib/test_context.js +14 -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 +14 -10
package/lib/stable_browser.js
CHANGED
|
@@ -2,21 +2,31 @@
|
|
|
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
|
+
export const Types = {
|
|
19
28
|
CLICK: "click_element",
|
|
29
|
+
WAIT_ELEMENT: "wait_element",
|
|
20
30
|
NAVIGATE: "navigate",
|
|
21
31
|
FILL: "fill_element",
|
|
22
32
|
EXECUTE: "execute_page_method",
|
|
@@ -26,6 +36,8 @@ const Types = {
|
|
|
26
36
|
GET_PAGE_STATUS: "get_page_status",
|
|
27
37
|
CLICK_ROW_ACTION: "click_row_action",
|
|
28
38
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
39
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
40
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
29
41
|
ANALYZE_TABLE: "analyze_table",
|
|
30
42
|
SELECT: "select_combobox",
|
|
31
43
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
@@ -36,21 +48,43 @@ const Types = {
|
|
|
36
48
|
UNCHECK: "uncheck_element",
|
|
37
49
|
EXTRACT: "extract_attribute",
|
|
38
50
|
CLOSE_PAGE: "close_page",
|
|
51
|
+
TABLE_OPERATION: "table_operation",
|
|
39
52
|
SET_DATE_TIME: "set_date_time",
|
|
40
53
|
SET_VIEWPORT: "set_viewport",
|
|
41
54
|
VERIFY_VISUAL: "verify_visual",
|
|
42
55
|
LOAD_DATA: "load_data",
|
|
43
56
|
SET_INPUT: "set_input",
|
|
57
|
+
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
58
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
59
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
60
|
+
BRUNO: "bruno",
|
|
61
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
62
|
+
SET_INPUT_FILES: "set_input_files",
|
|
63
|
+
};
|
|
64
|
+
export const apps = {};
|
|
65
|
+
const formatElementName = (elementName) => {
|
|
66
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
44
67
|
};
|
|
45
68
|
class StableBrowser {
|
|
46
|
-
|
|
69
|
+
browser;
|
|
70
|
+
page;
|
|
71
|
+
logger;
|
|
72
|
+
context;
|
|
73
|
+
world;
|
|
74
|
+
project_path = null;
|
|
75
|
+
webLogFile = null;
|
|
76
|
+
networkLogger = null;
|
|
77
|
+
configuration = null;
|
|
78
|
+
appName = "main";
|
|
79
|
+
tags = null;
|
|
80
|
+
isRecording = false;
|
|
81
|
+
initSnapshotTaken = false;
|
|
82
|
+
constructor(browser, page, logger = null, context = null, world = null) {
|
|
47
83
|
this.browser = browser;
|
|
48
84
|
this.page = page;
|
|
49
85
|
this.logger = logger;
|
|
50
86
|
this.context = context;
|
|
51
|
-
this.
|
|
52
|
-
this.webLogFile = null;
|
|
53
|
-
this.configuration = null;
|
|
87
|
+
this.world = world;
|
|
54
88
|
if (!this.logger) {
|
|
55
89
|
this.logger = console;
|
|
56
90
|
}
|
|
@@ -75,17 +109,32 @@ class StableBrowser {
|
|
|
75
109
|
catch (e) {
|
|
76
110
|
this.logger.error("unable to read ai_config.json");
|
|
77
111
|
}
|
|
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
112
|
context.pageLoading = { status: false };
|
|
113
|
+
context.pages = [this.page];
|
|
114
|
+
const logFolder = path.join(this.project_path, "logs", "web");
|
|
115
|
+
this.world = world;
|
|
116
|
+
this.registerEventListeners(this.context);
|
|
117
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
118
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
119
|
+
}
|
|
120
|
+
registerEventListeners(context) {
|
|
121
|
+
this.registerConsoleLogListener(this.page, context);
|
|
122
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
123
|
+
if (!context.pageLoading) {
|
|
124
|
+
context.pageLoading = { status: false };
|
|
125
|
+
}
|
|
84
126
|
context.playContext.on("page", async function (page) {
|
|
127
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
128
|
+
console.log("close unexpected popups");
|
|
129
|
+
await page.close();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
85
132
|
context.pageLoading.status = true;
|
|
86
133
|
this.page = page;
|
|
87
134
|
context.page = page;
|
|
88
135
|
context.pages.push(page);
|
|
136
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
137
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
89
138
|
page.on("close", async () => {
|
|
90
139
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
91
140
|
this.context.pages.pop();
|
|
@@ -110,117 +159,164 @@ class StableBrowser {
|
|
|
110
159
|
context.pageLoading.status = false;
|
|
111
160
|
}.bind(this));
|
|
112
161
|
}
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
162
|
+
async switchApp(appName) {
|
|
163
|
+
// check if the current app (this.appName) is the same as the new app
|
|
164
|
+
if (this.appName === appName) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let newContextCreated = false;
|
|
168
|
+
if (!apps[appName]) {
|
|
169
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
170
|
+
newContextCreated = true;
|
|
171
|
+
apps[appName] = {
|
|
172
|
+
context: newContext,
|
|
173
|
+
browser: newContext.browser,
|
|
174
|
+
page: newContext.page,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const tempContext = {};
|
|
178
|
+
_copyContext(this, tempContext);
|
|
179
|
+
_copyContext(apps[appName], this);
|
|
180
|
+
apps[this.appName] = tempContext;
|
|
181
|
+
this.appName = appName;
|
|
182
|
+
if (newContextCreated) {
|
|
183
|
+
this.registerEventListeners(this.context);
|
|
184
|
+
await this.goto(this.context.environment.baseUrl);
|
|
185
|
+
await this.waitForPageLoad();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async switchTab(tabTitleOrIndex) {
|
|
189
|
+
// first check if the tabNameOrIndex is a number
|
|
190
|
+
let index = parseInt(tabTitleOrIndex);
|
|
191
|
+
if (!isNaN(index)) {
|
|
192
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
193
|
+
this.page = this.context.pages[index];
|
|
194
|
+
this.context.page = this.page;
|
|
195
|
+
await this.page.bringToFront();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
116
198
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
199
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
200
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
201
|
+
let page = this.context.pages[i];
|
|
202
|
+
let title = await page.title();
|
|
203
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
204
|
+
this.page = page;
|
|
205
|
+
this.context.page = this.page;
|
|
206
|
+
await this.page.bringToFront();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
120
209
|
}
|
|
121
|
-
|
|
122
|
-
return path.join(logFolder, fileName);
|
|
210
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
123
211
|
}
|
|
124
|
-
registerConsoleLogListener(page, context
|
|
212
|
+
registerConsoleLogListener(page, context) {
|
|
125
213
|
if (!this.context.webLogger) {
|
|
126
214
|
this.context.webLogger = [];
|
|
127
215
|
}
|
|
128
216
|
page.on("console", async (msg) => {
|
|
129
|
-
|
|
217
|
+
const obj = {
|
|
130
218
|
type: msg.type(),
|
|
131
219
|
text: msg.text(),
|
|
132
220
|
location: msg.location(),
|
|
133
221
|
time: new Date().toISOString(),
|
|
134
|
-
}
|
|
135
|
-
|
|
222
|
+
};
|
|
223
|
+
this.context.webLogger.push(obj);
|
|
224
|
+
if (msg.type() === "error") {
|
|
225
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
226
|
+
}
|
|
136
227
|
});
|
|
137
228
|
}
|
|
138
|
-
registerRequestListener() {
|
|
139
|
-
this.
|
|
229
|
+
registerRequestListener(page, context, logFile) {
|
|
230
|
+
if (!this.context.networkLogger) {
|
|
231
|
+
this.context.networkLogger = [];
|
|
232
|
+
}
|
|
233
|
+
page.on("request", async (data) => {
|
|
234
|
+
const startTime = new Date().getTime();
|
|
140
235
|
try {
|
|
141
|
-
const pageUrl = new URL(
|
|
236
|
+
const pageUrl = new URL(page.url());
|
|
142
237
|
const requestUrl = new URL(data.url());
|
|
143
238
|
if (pageUrl.hostname === requestUrl.hostname) {
|
|
144
239
|
const method = data.method();
|
|
145
|
-
if (
|
|
240
|
+
if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
146
241
|
const token = await data.headerValue("Authorization");
|
|
147
242
|
if (token) {
|
|
148
|
-
|
|
243
|
+
context.authtoken = token;
|
|
149
244
|
}
|
|
150
245
|
}
|
|
151
246
|
}
|
|
247
|
+
const response = await data.response();
|
|
248
|
+
const endTime = new Date().getTime();
|
|
249
|
+
const obj = {
|
|
250
|
+
url: data.url(),
|
|
251
|
+
method: data.method(),
|
|
252
|
+
postData: data.postData(),
|
|
253
|
+
error: data.failure() ? data.failure().errorText : null,
|
|
254
|
+
duration: endTime - startTime,
|
|
255
|
+
startTime,
|
|
256
|
+
};
|
|
257
|
+
context.networkLogger.push(obj);
|
|
258
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
152
259
|
}
|
|
153
260
|
catch (error) {
|
|
154
|
-
console.error("Error in request listener", error);
|
|
261
|
+
// console.error("Error in request listener", error);
|
|
262
|
+
context.networkLogger.push({
|
|
263
|
+
error: "not able to listen",
|
|
264
|
+
message: error.message,
|
|
265
|
+
stack: error.stack,
|
|
266
|
+
time: new Date().toISOString(),
|
|
267
|
+
});
|
|
268
|
+
// await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
|
|
155
269
|
}
|
|
156
270
|
});
|
|
157
271
|
}
|
|
158
272
|
// async closeUnexpectedPopups() {
|
|
159
273
|
// await closeUnexpectedPopups(this.page);
|
|
160
274
|
// }
|
|
161
|
-
async goto(url) {
|
|
275
|
+
async goto(url, world = null) {
|
|
276
|
+
if (!url) {
|
|
277
|
+
throw new Error("url is null, verify that the environment file is correct");
|
|
278
|
+
}
|
|
162
279
|
if (!url.startsWith("http")) {
|
|
163
280
|
url = "https://" + url;
|
|
164
281
|
}
|
|
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;
|
|
282
|
+
const state = {
|
|
283
|
+
value: url,
|
|
284
|
+
world: world,
|
|
285
|
+
type: Types.NAVIGATE,
|
|
286
|
+
text: `Navigate Page to: ${url}`,
|
|
287
|
+
operation: "goto",
|
|
288
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
289
|
+
info: {},
|
|
290
|
+
locate: false,
|
|
291
|
+
scroll: false,
|
|
292
|
+
screenshot: false,
|
|
293
|
+
highlight: false,
|
|
294
|
+
};
|
|
295
|
+
try {
|
|
296
|
+
await _preCommand(state, this);
|
|
297
|
+
await this.page.goto(url, {
|
|
298
|
+
timeout: 60000,
|
|
299
|
+
});
|
|
300
|
+
await _screenshot(state, this);
|
|
186
301
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// remove the _ prefix
|
|
191
|
-
regValue = key.substring(1);
|
|
192
|
-
}
|
|
193
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
302
|
+
catch (error) {
|
|
303
|
+
console.error("Error on goto", error);
|
|
304
|
+
_commandError(state, error, this);
|
|
194
305
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
198
|
-
// check if not null
|
|
199
|
-
if (!locator) {
|
|
200
|
-
return locator;
|
|
306
|
+
finally {
|
|
307
|
+
await _commandFinally(state, this);
|
|
201
308
|
}
|
|
202
|
-
// clone the locator
|
|
203
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
204
|
-
this.scanAndManipulate(locator, _params);
|
|
205
|
-
return locator;
|
|
206
309
|
}
|
|
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);
|
|
310
|
+
async _getLocator(locator, scope, _params) {
|
|
311
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
312
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
313
|
+
for (let key in locator) {
|
|
314
|
+
if (typeof locator[key] !== "string")
|
|
315
|
+
continue;
|
|
316
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
317
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
219
318
|
}
|
|
220
319
|
}
|
|
221
|
-
}
|
|
222
|
-
_getLocator(locator, scope, _params) {
|
|
223
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
224
320
|
let locatorReturn;
|
|
225
321
|
if (locator.role) {
|
|
226
322
|
if (locator.role[1].nameReg) {
|
|
@@ -228,7 +324,7 @@ class StableBrowser {
|
|
|
228
324
|
delete locator.role[1].nameReg;
|
|
229
325
|
}
|
|
230
326
|
// if (locator.role[1].name) {
|
|
231
|
-
// locator.role[1].name =
|
|
327
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
232
328
|
// }
|
|
233
329
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
234
330
|
}
|
|
@@ -247,7 +343,7 @@ class StableBrowser {
|
|
|
247
343
|
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
248
344
|
}
|
|
249
345
|
}
|
|
250
|
-
if (locator
|
|
346
|
+
if (locator?.engine) {
|
|
251
347
|
if (locator.engine === "css") {
|
|
252
348
|
locatorReturn = scope.locator(locator.selector);
|
|
253
349
|
}
|
|
@@ -268,192 +364,181 @@ class StableBrowser {
|
|
|
268
364
|
return locatorReturn;
|
|
269
365
|
}
|
|
270
366
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
271
|
-
|
|
367
|
+
if (css && css.locator) {
|
|
368
|
+
css = css.locator;
|
|
369
|
+
}
|
|
370
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
272
371
|
if (result.elementCount === 0) {
|
|
273
372
|
return;
|
|
274
373
|
}
|
|
275
|
-
let textElementCss = "[data-blinq-id
|
|
374
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
276
375
|
// css climb to parent element
|
|
277
376
|
const climbArray = [];
|
|
278
377
|
for (let i = 0; i < climb; i++) {
|
|
279
378
|
climbArray.push("..");
|
|
280
379
|
}
|
|
281
380
|
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
|
-
}
|
|
381
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
382
|
+
if (css) {
|
|
383
|
+
resultCss = resultCss + " >> " + css;
|
|
384
|
+
}
|
|
385
|
+
return resultCss;
|
|
386
|
+
}
|
|
387
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
388
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
389
|
+
const locator = scope.locator(query);
|
|
390
|
+
const count = await locator.count();
|
|
391
|
+
if (!tag1) {
|
|
392
|
+
tag1 = "*";
|
|
393
|
+
}
|
|
394
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
395
|
+
let tagCount = 0;
|
|
396
|
+
for (let i = 0; i < count; i++) {
|
|
397
|
+
const element = locator.nth(i);
|
|
398
|
+
// check if the tag matches
|
|
399
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
400
|
+
if (!tag.startsWith("*")) {
|
|
401
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!el.setAttribute) {
|
|
406
|
+
el = el.parentElement;
|
|
407
|
+
}
|
|
408
|
+
// remove any attributes start with data-blinq-id
|
|
409
|
+
// for (let i = 0; i < el.attributes.length; i++) {
|
|
410
|
+
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
411
|
+
// el.removeAttribute(el.attributes[i].name);
|
|
412
|
+
// }
|
|
413
|
+
// }
|
|
414
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
415
|
+
return true;
|
|
416
|
+
}, [tag1, randomToken]))) {
|
|
417
|
+
continue;
|
|
385
418
|
}
|
|
386
|
-
|
|
387
|
-
}
|
|
419
|
+
tagCount++;
|
|
420
|
+
}
|
|
421
|
+
return { elementCount: tagCount, randomToken };
|
|
388
422
|
}
|
|
389
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
423
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
424
|
+
if (!info) {
|
|
425
|
+
info = {};
|
|
426
|
+
}
|
|
427
|
+
if (!info.failCause) {
|
|
428
|
+
info.failCause = {};
|
|
429
|
+
}
|
|
430
|
+
if (!info.log) {
|
|
431
|
+
info.log = "";
|
|
432
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
433
|
+
}
|
|
390
434
|
let locatorSearch = selectorHierarchy[index];
|
|
435
|
+
let originalLocatorSearch = "";
|
|
436
|
+
try {
|
|
437
|
+
originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
|
|
438
|
+
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
console.error(e);
|
|
442
|
+
}
|
|
391
443
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
392
444
|
let locator = null;
|
|
393
445
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
394
|
-
|
|
446
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
447
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
395
448
|
if (!locatorString) {
|
|
449
|
+
info.failCause.textNotFound = true;
|
|
450
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
396
451
|
return;
|
|
397
452
|
}
|
|
398
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
453
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
399
454
|
}
|
|
400
455
|
else if (locatorSearch.text) {
|
|
401
|
-
let
|
|
456
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
457
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
402
458
|
if (result.elementCount === 0) {
|
|
459
|
+
info.failCause.textNotFound = true;
|
|
460
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
403
461
|
return;
|
|
404
462
|
}
|
|
405
|
-
locatorSearch.css = "[data-blinq-id
|
|
463
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
406
464
|
if (locatorSearch.childCss) {
|
|
407
465
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
408
466
|
}
|
|
409
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
467
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
410
468
|
}
|
|
411
469
|
else {
|
|
412
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
470
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
413
471
|
}
|
|
414
472
|
// let cssHref = false;
|
|
415
473
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
416
474
|
// cssHref = true;
|
|
417
475
|
// }
|
|
418
476
|
let count = await locator.count();
|
|
477
|
+
if (count > 0 && !info.failCause.count) {
|
|
478
|
+
info.failCause.count = count;
|
|
479
|
+
}
|
|
419
480
|
//info.log += "total elements found " + count + "\n";
|
|
420
481
|
//let visibleCount = 0;
|
|
421
482
|
let visibleLocator = null;
|
|
422
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
483
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
423
484
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
485
|
+
if (info.locatorLog) {
|
|
486
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
487
|
+
}
|
|
424
488
|
return;
|
|
425
489
|
}
|
|
490
|
+
if (info.locatorLog && count === 0) {
|
|
491
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
492
|
+
}
|
|
426
493
|
for (let j = 0; j < count; j++) {
|
|
427
494
|
let visible = await locator.nth(j).isVisible();
|
|
428
495
|
const enabled = await locator.nth(j).isEnabled();
|
|
429
496
|
if (!visibleOnly) {
|
|
430
497
|
visible = true;
|
|
431
498
|
}
|
|
432
|
-
if (visible && enabled) {
|
|
499
|
+
if (visible && (allowDisabled || enabled)) {
|
|
433
500
|
foundLocators.push(locator.nth(j));
|
|
501
|
+
if (info.locatorLog) {
|
|
502
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
503
|
+
}
|
|
434
504
|
}
|
|
435
505
|
else {
|
|
506
|
+
info.failCause.visible = visible;
|
|
507
|
+
info.failCause.enabled = enabled;
|
|
436
508
|
if (!info.printMessages) {
|
|
437
509
|
info.printMessages = {};
|
|
438
510
|
}
|
|
511
|
+
if (info.locatorLog && !visible) {
|
|
512
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
513
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
514
|
+
}
|
|
515
|
+
if (info.locatorLog && !enabled) {
|
|
516
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
517
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
518
|
+
}
|
|
439
519
|
if (!info.printMessages[j.toString()]) {
|
|
440
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
520
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
441
521
|
info.printMessages[j.toString()] = true;
|
|
442
522
|
}
|
|
443
523
|
}
|
|
444
524
|
}
|
|
445
525
|
}
|
|
446
526
|
async closeUnexpectedPopups(info, _params) {
|
|
527
|
+
if (!info) {
|
|
528
|
+
info = {};
|
|
529
|
+
info.failCause = {};
|
|
530
|
+
info.log = "";
|
|
531
|
+
}
|
|
447
532
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
448
533
|
if (!info) {
|
|
449
534
|
info = {};
|
|
450
535
|
}
|
|
451
|
-
info.log += "scan for popup handlers" + "\n";
|
|
536
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
452
537
|
const handlerGroup = [];
|
|
453
538
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
454
539
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
455
540
|
}
|
|
456
|
-
const scopes =
|
|
541
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
457
542
|
let result = null;
|
|
458
543
|
let scope = null;
|
|
459
544
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -475,53 +560,108 @@ class StableBrowser {
|
|
|
475
560
|
}
|
|
476
561
|
if (result.foundElements.length > 0) {
|
|
477
562
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
478
|
-
|
|
563
|
+
try {
|
|
564
|
+
await scope?.evaluate(() => {
|
|
565
|
+
window.__isClosingPopups = true;
|
|
566
|
+
});
|
|
567
|
+
await dialogCloseLocator.click();
|
|
568
|
+
// wait for the dialog to close
|
|
569
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
570
|
+
}
|
|
571
|
+
catch (e) {
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
await scope?.evaluate(() => {
|
|
575
|
+
window.__isClosingPopups = false;
|
|
576
|
+
});
|
|
577
|
+
}
|
|
479
578
|
return { rerun: true };
|
|
480
579
|
}
|
|
481
580
|
}
|
|
482
581
|
}
|
|
483
582
|
return { rerun: false };
|
|
484
583
|
}
|
|
485
|
-
async _locate(selectors, info, _params, timeout =
|
|
584
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
585
|
+
if (!timeout) {
|
|
586
|
+
timeout = 30000;
|
|
587
|
+
}
|
|
486
588
|
for (let i = 0; i < 3; i++) {
|
|
487
589
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
488
590
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
489
591
|
let selector = selectors.locators[j];
|
|
490
592
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
491
593
|
}
|
|
492
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
594
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
493
595
|
if (!element.rerun) {
|
|
494
|
-
|
|
596
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
597
|
+
element.evaluate((el, randomToken) => {
|
|
598
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
599
|
+
}, randomToken);
|
|
600
|
+
// if (element._frame) {
|
|
601
|
+
// return element;
|
|
602
|
+
// }
|
|
603
|
+
const scope = element._frame ?? element.page();
|
|
604
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
605
|
+
let prefixSelector = "";
|
|
606
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
607
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
608
|
+
if (frameSelectorIndex !== -1) {
|
|
609
|
+
// remove everything after the >> internal:control=enter-frame
|
|
610
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
611
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
612
|
+
}
|
|
613
|
+
// if (element?._frame?._selector) {
|
|
614
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
615
|
+
// }
|
|
616
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
617
|
+
return scope.locator(newSelector);
|
|
495
618
|
}
|
|
496
619
|
}
|
|
497
620
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
498
621
|
}
|
|
499
|
-
async
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
622
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
623
|
+
if (!info) {
|
|
624
|
+
info = {};
|
|
625
|
+
info.failCause = {};
|
|
626
|
+
info.log = "";
|
|
627
|
+
}
|
|
628
|
+
let startTime = Date.now();
|
|
505
629
|
let scope = this.page;
|
|
630
|
+
if (selectors.frame) {
|
|
631
|
+
return selectors.frame;
|
|
632
|
+
}
|
|
506
633
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
507
|
-
const findFrame = (frame, framescope) => {
|
|
634
|
+
const findFrame = async (frame, framescope) => {
|
|
508
635
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
509
636
|
let frameLocator = frame.selectors[i];
|
|
510
637
|
if (frameLocator.css) {
|
|
511
|
-
|
|
512
|
-
|
|
638
|
+
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
639
|
+
if (frameLocator.index) {
|
|
640
|
+
testframescope = framescope.nth(frameLocator.index);
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
await testframescope.owner().evaluateHandle(() => true, null, {
|
|
644
|
+
timeout: 5000,
|
|
645
|
+
});
|
|
646
|
+
framescope = testframescope;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
console.error("frame not found " + frameLocator.css);
|
|
651
|
+
}
|
|
513
652
|
}
|
|
514
653
|
}
|
|
515
654
|
if (frame.children) {
|
|
516
|
-
return findFrame(frame.children, framescope);
|
|
655
|
+
return await findFrame(frame.children, framescope);
|
|
517
656
|
}
|
|
518
657
|
return framescope;
|
|
519
658
|
};
|
|
520
|
-
|
|
659
|
+
let fLocator = null;
|
|
521
660
|
while (true) {
|
|
522
661
|
let frameFound = false;
|
|
523
662
|
if (selectors.nestFrmLoc) {
|
|
524
|
-
|
|
663
|
+
fLocator = selectors.nestFrmLoc;
|
|
664
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
525
665
|
frameFound = true;
|
|
526
666
|
break;
|
|
527
667
|
}
|
|
@@ -529,6 +669,7 @@ class StableBrowser {
|
|
|
529
669
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
530
670
|
let frameLocator = selectors.frameLocators[i];
|
|
531
671
|
if (frameLocator.css) {
|
|
672
|
+
fLocator = frameLocator.css;
|
|
532
673
|
scope = scope.frameLocator(frameLocator.css);
|
|
533
674
|
frameFound = true;
|
|
534
675
|
break;
|
|
@@ -536,20 +677,55 @@ class StableBrowser {
|
|
|
536
677
|
}
|
|
537
678
|
}
|
|
538
679
|
if (!frameFound && selectors.iframe_src) {
|
|
680
|
+
fLocator = selectors.iframe_src;
|
|
539
681
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
540
682
|
}
|
|
541
683
|
if (!scope) {
|
|
542
|
-
info
|
|
543
|
-
|
|
684
|
+
if (info && info.locatorLog) {
|
|
685
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
686
|
+
}
|
|
687
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
688
|
+
if (Date.now() - startTime > timeout) {
|
|
689
|
+
info.failCause.iframeNotFound = true;
|
|
690
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
544
691
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
545
692
|
}
|
|
546
693
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
547
694
|
}
|
|
548
695
|
else {
|
|
696
|
+
if (info && info.locatorLog) {
|
|
697
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
698
|
+
}
|
|
549
699
|
break;
|
|
550
700
|
}
|
|
551
701
|
}
|
|
552
702
|
}
|
|
703
|
+
if (!scope) {
|
|
704
|
+
scope = this.page;
|
|
705
|
+
}
|
|
706
|
+
return scope;
|
|
707
|
+
}
|
|
708
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
709
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
710
|
+
return scope.evaluate(() => {
|
|
711
|
+
var bodyContent = document.body.innerHTML;
|
|
712
|
+
return bodyContent;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
716
|
+
if (!info) {
|
|
717
|
+
info = {};
|
|
718
|
+
info.failCause = {};
|
|
719
|
+
info.log = "";
|
|
720
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
721
|
+
}
|
|
722
|
+
let highPriorityTimeout = 5000;
|
|
723
|
+
let visibleOnlyTimeout = 6000;
|
|
724
|
+
let startTime = Date.now();
|
|
725
|
+
let locatorsCount = 0;
|
|
726
|
+
let lazy_scroll = false;
|
|
727
|
+
//let arrayMode = Array.isArray(selectors);
|
|
728
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
553
729
|
let selectorsLocators = null;
|
|
554
730
|
selectorsLocators = selectors.locators;
|
|
555
731
|
// group selectors by priority
|
|
@@ -585,18 +761,13 @@ class StableBrowser {
|
|
|
585
761
|
}
|
|
586
762
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
587
763
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
588
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
764
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
589
765
|
if (result.foundElements.length === 0) {
|
|
590
766
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
591
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
592
|
-
}
|
|
593
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
594
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
767
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
595
768
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
599
|
-
}
|
|
769
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
770
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
600
771
|
}
|
|
601
772
|
let foundElements = result.foundElements;
|
|
602
773
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -636,24 +807,43 @@ class StableBrowser {
|
|
|
636
807
|
return maxCountElement.locator;
|
|
637
808
|
}
|
|
638
809
|
}
|
|
639
|
-
if (
|
|
810
|
+
if (Date.now() - startTime > timeout) {
|
|
640
811
|
break;
|
|
641
812
|
}
|
|
642
|
-
if (
|
|
643
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
813
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
814
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
644
815
|
highPriorityOnly = false;
|
|
816
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
817
|
+
lazy_scroll = true;
|
|
818
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
819
|
+
}
|
|
645
820
|
}
|
|
646
|
-
if (
|
|
647
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
821
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
822
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
648
823
|
visibleOnly = false;
|
|
649
824
|
}
|
|
650
825
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
826
|
+
// sheck of more of half of the timeout has passed
|
|
827
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
828
|
+
highPriorityOnly = false;
|
|
829
|
+
visibleOnly = false;
|
|
830
|
+
}
|
|
651
831
|
}
|
|
652
832
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
653
|
-
info.
|
|
833
|
+
// if (info.locatorLog) {
|
|
834
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
835
|
+
// for (let line of lines) {
|
|
836
|
+
// this.logger.debug(line);
|
|
837
|
+
// }
|
|
838
|
+
// }
|
|
839
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
840
|
+
info.failCause.locatorNotFound = true;
|
|
841
|
+
if (!info?.failCause?.lastError) {
|
|
842
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
843
|
+
}
|
|
654
844
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
655
845
|
}
|
|
656
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
846
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
657
847
|
let foundElements = [];
|
|
658
848
|
const result = {
|
|
659
849
|
foundElements: foundElements,
|
|
@@ -661,14 +851,15 @@ class StableBrowser {
|
|
|
661
851
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
662
852
|
let foundLocators = [];
|
|
663
853
|
try {
|
|
664
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
854
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
665
855
|
}
|
|
666
856
|
catch (e) {
|
|
667
|
-
this
|
|
668
|
-
this.logger.debug(
|
|
857
|
+
// this call can fail it the browser is navigating
|
|
858
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
859
|
+
// this.logger.debug(e);
|
|
669
860
|
foundLocators = [];
|
|
670
861
|
try {
|
|
671
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
862
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
672
863
|
}
|
|
673
864
|
catch (e) {
|
|
674
865
|
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
@@ -682,90 +873,228 @@ class StableBrowser {
|
|
|
682
873
|
});
|
|
683
874
|
result.locatorIndex = i;
|
|
684
875
|
}
|
|
876
|
+
if (foundLocators.length > 1) {
|
|
877
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
878
|
+
const boxes = [];
|
|
879
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
880
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
881
|
+
}
|
|
882
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
883
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
884
|
+
if (j === k) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
888
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
889
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
890
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
891
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
892
|
+
// as the element is not unique, will remove it
|
|
893
|
+
boxes.splice(k, 1);
|
|
894
|
+
k--;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (boxes.length === 1) {
|
|
899
|
+
result.foundElements.push({
|
|
900
|
+
locator: boxes[0].locator.first(),
|
|
901
|
+
box: boxes[0].box,
|
|
902
|
+
unique: true,
|
|
903
|
+
});
|
|
904
|
+
result.locatorIndex = i;
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
info.failCause.foundMultiple = true;
|
|
908
|
+
if (info.locatorLog) {
|
|
909
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
685
913
|
}
|
|
686
914
|
return result;
|
|
687
915
|
}
|
|
688
|
-
async
|
|
689
|
-
|
|
916
|
+
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
917
|
+
const state = {
|
|
918
|
+
locate: false,
|
|
919
|
+
scroll: false,
|
|
920
|
+
highlight: false,
|
|
921
|
+
_params,
|
|
922
|
+
options,
|
|
923
|
+
world,
|
|
924
|
+
type: Types.CLICK,
|
|
925
|
+
text: "Click element",
|
|
926
|
+
operation: "simpleClick",
|
|
927
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
928
|
+
};
|
|
929
|
+
_preCommand(state, this);
|
|
690
930
|
const startTime = Date.now();
|
|
691
|
-
|
|
692
|
-
|
|
931
|
+
let timeout = 30000;
|
|
932
|
+
if (options && options.timeout) {
|
|
933
|
+
timeout = options.timeout;
|
|
693
934
|
}
|
|
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));
|
|
935
|
+
while (true) {
|
|
705
936
|
try {
|
|
706
|
-
await this.
|
|
707
|
-
|
|
708
|
-
|
|
937
|
+
const result = await locate_element(this.context, elementDescription, "click");
|
|
938
|
+
if (result?.elementNumber >= 0) {
|
|
939
|
+
const selectors = {
|
|
940
|
+
frame: result?.frame,
|
|
941
|
+
locators: [
|
|
942
|
+
{
|
|
943
|
+
css: result?.css,
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
};
|
|
947
|
+
await this.click(selectors, _params, options, world);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
709
950
|
}
|
|
710
951
|
catch (e) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
952
|
+
if (performance.now() - startTime > timeout) {
|
|
953
|
+
// throw e;
|
|
954
|
+
try {
|
|
955
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
956
|
+
}
|
|
957
|
+
finally {
|
|
958
|
+
await _commandFinally(state, this);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
716
961
|
}
|
|
962
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
966
|
+
const state = {
|
|
967
|
+
locate: false,
|
|
968
|
+
scroll: false,
|
|
969
|
+
highlight: false,
|
|
970
|
+
_params,
|
|
971
|
+
options,
|
|
972
|
+
world,
|
|
973
|
+
type: Types.FILL,
|
|
974
|
+
text: "Fill element",
|
|
975
|
+
operation: "simpleClickType",
|
|
976
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
977
|
+
};
|
|
978
|
+
_preCommand(state, this);
|
|
979
|
+
const startTime = Date.now();
|
|
980
|
+
let timeout = 30000;
|
|
981
|
+
if (options && options.timeout) {
|
|
982
|
+
timeout = options.timeout;
|
|
983
|
+
}
|
|
984
|
+
while (true) {
|
|
985
|
+
try {
|
|
986
|
+
const result = await locate_element(this.context, elementDescription, "fill", value);
|
|
987
|
+
if (result?.elementNumber >= 0) {
|
|
988
|
+
const selectors = {
|
|
989
|
+
frame: result?.frame,
|
|
990
|
+
locators: [
|
|
991
|
+
{
|
|
992
|
+
css: result?.css,
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
};
|
|
996
|
+
await this.clickType(selectors, value, false, _params, options, world);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
catch (e) {
|
|
1001
|
+
if (performance.now() - startTime > timeout) {
|
|
1002
|
+
// throw e;
|
|
1003
|
+
try {
|
|
1004
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
await _commandFinally(state, this);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async click(selectors, _params, options = {}, world = null) {
|
|
1015
|
+
const state = {
|
|
1016
|
+
selectors,
|
|
1017
|
+
_params,
|
|
1018
|
+
options,
|
|
1019
|
+
world,
|
|
1020
|
+
text: "Click element",
|
|
1021
|
+
_text: "Click on " + selectors.element_name,
|
|
1022
|
+
type: Types.CLICK,
|
|
1023
|
+
operation: "click",
|
|
1024
|
+
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1025
|
+
};
|
|
1026
|
+
try {
|
|
1027
|
+
await _preCommand(state, this);
|
|
1028
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
717
1029
|
await this.waitForPageLoad();
|
|
718
|
-
return info;
|
|
1030
|
+
return state.info;
|
|
719
1031
|
}
|
|
720
1032
|
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;
|
|
1033
|
+
await _commandError(state, e, this);
|
|
727
1034
|
}
|
|
728
1035
|
finally {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1036
|
+
await _commandFinally(state, this);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1040
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1041
|
+
const state = {
|
|
1042
|
+
selectors,
|
|
1043
|
+
_params,
|
|
1044
|
+
options,
|
|
1045
|
+
world,
|
|
1046
|
+
text: "Wait for element",
|
|
1047
|
+
_text: "Wait for " + selectors.element_name,
|
|
1048
|
+
type: Types.WAIT_ELEMENT,
|
|
1049
|
+
operation: "waitForElement",
|
|
1050
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1051
|
+
};
|
|
1052
|
+
let found = false;
|
|
1053
|
+
try {
|
|
1054
|
+
await _preCommand(state, this);
|
|
1055
|
+
// if (state.options && state.options.context) {
|
|
1056
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1057
|
+
// }
|
|
1058
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1059
|
+
found = true;
|
|
1060
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1061
|
+
}
|
|
1062
|
+
catch (e) {
|
|
1063
|
+
console.error("Error on waitForElement", e);
|
|
1064
|
+
// await _commandError(state, e, this);
|
|
749
1065
|
}
|
|
1066
|
+
finally {
|
|
1067
|
+
await _commandFinally(state, this);
|
|
1068
|
+
}
|
|
1069
|
+
return found;
|
|
750
1070
|
}
|
|
751
1071
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1072
|
+
const state = {
|
|
1073
|
+
selectors,
|
|
1074
|
+
_params,
|
|
1075
|
+
options,
|
|
1076
|
+
world,
|
|
1077
|
+
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
1078
|
+
text: checked ? `Check element` : `Uncheck element`,
|
|
1079
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
1080
|
+
operation: "setCheck",
|
|
1081
|
+
log: "***** check " + selectors.element_name + " *****\n",
|
|
1082
|
+
};
|
|
762
1083
|
try {
|
|
763
|
-
|
|
764
|
-
|
|
1084
|
+
await _preCommand(state, this);
|
|
1085
|
+
state.info.checked = checked;
|
|
1086
|
+
// let element = await this._locate(selectors, info, _params);
|
|
1087
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
765
1088
|
try {
|
|
766
|
-
|
|
767
|
-
|
|
1089
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1090
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1091
|
+
await this._highlightElements(state.element);
|
|
1092
|
+
await state.element.setChecked(checked);
|
|
768
1093
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1094
|
+
// await this._unHighlightElements(element);
|
|
1095
|
+
// }
|
|
1096
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1097
|
+
// await this._unHighlightElements(element);
|
|
769
1098
|
}
|
|
770
1099
|
catch (e) {
|
|
771
1100
|
if (e.message && e.message.includes("did not change its state")) {
|
|
@@ -773,179 +1102,102 @@ class StableBrowser {
|
|
|
773
1102
|
}
|
|
774
1103
|
else {
|
|
775
1104
|
//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 });
|
|
1105
|
+
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1106
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
1107
|
+
await state.element.setChecked(checked, { timeout: 5000, force: true });
|
|
779
1108
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
780
1109
|
}
|
|
781
1110
|
}
|
|
782
1111
|
await this.waitForPageLoad();
|
|
783
|
-
return info;
|
|
1112
|
+
return state.info;
|
|
784
1113
|
}
|
|
785
1114
|
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;
|
|
1115
|
+
await _commandError(state, e, this);
|
|
792
1116
|
}
|
|
793
1117
|
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
|
-
});
|
|
1118
|
+
await _commandFinally(state, this);
|
|
814
1119
|
}
|
|
815
1120
|
}
|
|
816
1121
|
async hover(selectors, _params, options = {}, world = null) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1122
|
+
const state = {
|
|
1123
|
+
selectors,
|
|
1124
|
+
_params,
|
|
1125
|
+
options,
|
|
1126
|
+
world,
|
|
1127
|
+
type: Types.HOVER,
|
|
1128
|
+
text: `Hover element`,
|
|
1129
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1130
|
+
operation: "hover",
|
|
1131
|
+
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1132
|
+
};
|
|
826
1133
|
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
|
-
}
|
|
1134
|
+
await _preCommand(state, this);
|
|
1135
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1136
|
+
await _screenshot(state, this);
|
|
841
1137
|
await this.waitForPageLoad();
|
|
842
|
-
return info;
|
|
1138
|
+
return state.info;
|
|
843
1139
|
}
|
|
844
1140
|
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;
|
|
1141
|
+
await _commandError(state, e, this);
|
|
851
1142
|
}
|
|
852
1143
|
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
|
-
});
|
|
1144
|
+
await _commandFinally(state, this);
|
|
873
1145
|
}
|
|
874
1146
|
}
|
|
875
1147
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
876
|
-
this._validateSelectors(selectors);
|
|
877
1148
|
if (!values) {
|
|
878
1149
|
throw new Error("values is null");
|
|
879
1150
|
}
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1151
|
+
const state = {
|
|
1152
|
+
selectors,
|
|
1153
|
+
_params,
|
|
1154
|
+
options,
|
|
1155
|
+
world,
|
|
1156
|
+
value: values.toString(),
|
|
1157
|
+
type: Types.SELECT,
|
|
1158
|
+
text: `Select option: ${values}`,
|
|
1159
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1160
|
+
operation: "selectOption",
|
|
1161
|
+
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1162
|
+
};
|
|
888
1163
|
try {
|
|
889
|
-
|
|
890
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1164
|
+
await _preCommand(state, this);
|
|
891
1165
|
try {
|
|
892
|
-
await
|
|
893
|
-
await element.selectOption(values);
|
|
1166
|
+
await state.element.selectOption(values);
|
|
894
1167
|
}
|
|
895
1168
|
catch (e) {
|
|
896
1169
|
//await this.closeUnexpectedPopups();
|
|
897
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
898
|
-
await element.selectOption(values, { timeout: 10000, force: true });
|
|
1170
|
+
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1171
|
+
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
899
1172
|
}
|
|
900
1173
|
await this.waitForPageLoad();
|
|
901
|
-
return info;
|
|
1174
|
+
return state.info;
|
|
902
1175
|
}
|
|
903
1176
|
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;
|
|
1177
|
+
await _commandError(state, e, this);
|
|
911
1178
|
}
|
|
912
1179
|
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
|
-
});
|
|
1180
|
+
await _commandFinally(state, this);
|
|
934
1181
|
}
|
|
935
1182
|
}
|
|
936
1183
|
async type(_value, _params = null, options = {}, world = null) {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1184
|
+
const state = {
|
|
1185
|
+
value: _value,
|
|
1186
|
+
_params,
|
|
1187
|
+
options,
|
|
1188
|
+
world,
|
|
1189
|
+
locate: false,
|
|
1190
|
+
scroll: false,
|
|
1191
|
+
highlight: false,
|
|
1192
|
+
type: Types.TYPE_PRESS,
|
|
1193
|
+
text: `Type value: ${_value}`,
|
|
1194
|
+
_text: `Type value: ${_value}`,
|
|
1195
|
+
operation: "type",
|
|
1196
|
+
log: "",
|
|
1197
|
+
};
|
|
946
1198
|
try {
|
|
947
|
-
|
|
948
|
-
const valueSegment =
|
|
1199
|
+
await _preCommand(state, this);
|
|
1200
|
+
const valueSegment = state.value.split("&&");
|
|
949
1201
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
950
1202
|
if (i > 0) {
|
|
951
1203
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -965,134 +1217,77 @@ class StableBrowser {
|
|
|
965
1217
|
await this.page.keyboard.type(value);
|
|
966
1218
|
}
|
|
967
1219
|
}
|
|
968
|
-
return info;
|
|
1220
|
+
return state.info;
|
|
969
1221
|
}
|
|
970
1222
|
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;
|
|
1223
|
+
await _commandError(state, e, this);
|
|
978
1224
|
}
|
|
979
1225
|
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
|
-
});
|
|
1226
|
+
await _commandFinally(state, this);
|
|
1000
1227
|
}
|
|
1001
1228
|
}
|
|
1002
1229
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
let screenshotPath = null;
|
|
1230
|
+
const state = {
|
|
1231
|
+
selectors,
|
|
1232
|
+
_params,
|
|
1233
|
+
value,
|
|
1234
|
+
options,
|
|
1235
|
+
world,
|
|
1236
|
+
type: Types.SET_INPUT,
|
|
1237
|
+
text: `Set input value`,
|
|
1238
|
+
operation: "setInputValue",
|
|
1239
|
+
log: "***** set input value " + selectors.element_name + " *****\n",
|
|
1240
|
+
};
|
|
1015
1241
|
try {
|
|
1016
|
-
|
|
1017
|
-
let
|
|
1018
|
-
await this.scrollIfNeeded(element, info);
|
|
1019
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1020
|
-
await this._highlightElements(element);
|
|
1242
|
+
await _preCommand(state, this);
|
|
1243
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1021
1244
|
try {
|
|
1022
|
-
await element.evaluateHandle((el, value) => {
|
|
1245
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1023
1246
|
el.value = value;
|
|
1024
1247
|
}, value);
|
|
1025
1248
|
}
|
|
1026
1249
|
catch (error) {
|
|
1027
1250
|
this.logger.error("setInputValue failed, will try again");
|
|
1028
|
-
|
|
1029
|
-
info.
|
|
1030
|
-
|
|
1031
|
-
await element.evaluateHandle((el, value) => {
|
|
1251
|
+
await _screenshot(state, this);
|
|
1252
|
+
Object.assign(error, { info: state.info });
|
|
1253
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1032
1254
|
el.value = value;
|
|
1033
1255
|
});
|
|
1034
1256
|
}
|
|
1035
1257
|
}
|
|
1036
1258
|
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;
|
|
1259
|
+
await _commandError(state, e, this);
|
|
1043
1260
|
}
|
|
1044
1261
|
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
|
-
});
|
|
1262
|
+
await _commandFinally(state, this);
|
|
1066
1263
|
}
|
|
1067
1264
|
}
|
|
1068
1265
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1266
|
+
const state = {
|
|
1267
|
+
selectors,
|
|
1268
|
+
_params,
|
|
1269
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1270
|
+
options,
|
|
1271
|
+
world,
|
|
1272
|
+
type: Types.SET_DATE_TIME,
|
|
1273
|
+
text: `Set date time value: ${value}`,
|
|
1274
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1275
|
+
operation: "setDateTime",
|
|
1276
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1277
|
+
throwError: false,
|
|
1278
|
+
};
|
|
1079
1279
|
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);
|
|
1280
|
+
await _preCommand(state, this);
|
|
1086
1281
|
try {
|
|
1087
|
-
await element
|
|
1282
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1088
1283
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1089
1284
|
if (format) {
|
|
1090
|
-
value = dayjs(value).format(format);
|
|
1091
|
-
await element.fill(value);
|
|
1285
|
+
state.value = dayjs(state.value).format(format);
|
|
1286
|
+
await state.element.fill(state.value);
|
|
1092
1287
|
}
|
|
1093
1288
|
else {
|
|
1094
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1095
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1289
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1290
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1096
1291
|
el.value = ""; // clear input
|
|
1097
1292
|
el.value = dateTimeValue;
|
|
1098
1293
|
}, dateTimeValue);
|
|
@@ -1105,20 +1300,19 @@ class StableBrowser {
|
|
|
1105
1300
|
}
|
|
1106
1301
|
catch (err) {
|
|
1107
1302
|
//await this.closeUnexpectedPopups();
|
|
1108
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1303
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1109
1304
|
this.logger.info("Trying again");
|
|
1110
|
-
|
|
1111
|
-
info.
|
|
1112
|
-
Object.assign(err, { info: info });
|
|
1305
|
+
await _screenshot(state, this);
|
|
1306
|
+
Object.assign(err, { info: state.info });
|
|
1113
1307
|
await element.click();
|
|
1114
1308
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1115
1309
|
if (format) {
|
|
1116
|
-
value = dayjs(value).format(format);
|
|
1117
|
-
await element.fill(value);
|
|
1310
|
+
state.value = dayjs(state.value).format(format);
|
|
1311
|
+
await state.element.fill(state.value);
|
|
1118
1312
|
}
|
|
1119
1313
|
else {
|
|
1120
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1121
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1314
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1315
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1122
1316
|
el.value = ""; // clear input
|
|
1123
1317
|
el.value = dateTimeValue;
|
|
1124
1318
|
}, dateTimeValue);
|
|
@@ -1131,84 +1325,63 @@ class StableBrowser {
|
|
|
1131
1325
|
}
|
|
1132
1326
|
}
|
|
1133
1327
|
catch (e) {
|
|
1134
|
-
|
|
1135
|
-
throw e;
|
|
1328
|
+
await _commandError(state, e, this);
|
|
1136
1329
|
}
|
|
1137
1330
|
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
|
-
});
|
|
1331
|
+
await _commandFinally(state, this);
|
|
1159
1332
|
}
|
|
1160
1333
|
}
|
|
1161
1334
|
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;
|
|
1335
|
+
_value = unEscapeString(_value);
|
|
1171
1336
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1337
|
+
const state = {
|
|
1338
|
+
selectors,
|
|
1339
|
+
_params,
|
|
1340
|
+
value: newValue,
|
|
1341
|
+
originalValue: _value,
|
|
1342
|
+
options,
|
|
1343
|
+
world,
|
|
1344
|
+
type: Types.FILL,
|
|
1345
|
+
text: `Click type input with value: ${_value}`,
|
|
1346
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1347
|
+
operation: "clickType",
|
|
1348
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1349
|
+
};
|
|
1350
|
+
if (!options) {
|
|
1351
|
+
options = {};
|
|
1352
|
+
}
|
|
1172
1353
|
if (newValue !== _value) {
|
|
1173
1354
|
//this.logger.info(_value + "=" + newValue);
|
|
1174
1355
|
_value = newValue;
|
|
1175
1356
|
}
|
|
1176
|
-
info.value = _value;
|
|
1177
1357
|
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) {
|
|
1358
|
+
await _preCommand(state, this);
|
|
1359
|
+
state.info.value = _value;
|
|
1360
|
+
if (!options.press) {
|
|
1184
1361
|
try {
|
|
1185
|
-
let currentValue = await element.inputValue();
|
|
1362
|
+
let currentValue = await state.element.inputValue();
|
|
1186
1363
|
if (currentValue) {
|
|
1187
|
-
await element.fill("");
|
|
1364
|
+
await state.element.fill("");
|
|
1188
1365
|
}
|
|
1189
1366
|
}
|
|
1190
1367
|
catch (e) {
|
|
1191
1368
|
this.logger.info("unable to clear input value");
|
|
1192
1369
|
}
|
|
1193
1370
|
}
|
|
1194
|
-
if (options
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
catch (e) {
|
|
1199
|
-
await element.dispatchEvent("click");
|
|
1200
|
-
}
|
|
1371
|
+
if (options.press) {
|
|
1372
|
+
options.timeout = 5000;
|
|
1373
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1201
1374
|
}
|
|
1202
1375
|
else {
|
|
1203
1376
|
try {
|
|
1204
|
-
await element.focus();
|
|
1377
|
+
await state.element.focus();
|
|
1205
1378
|
}
|
|
1206
1379
|
catch (e) {
|
|
1207
|
-
await element.dispatchEvent("focus");
|
|
1380
|
+
await state.element.dispatchEvent("focus");
|
|
1208
1381
|
}
|
|
1209
1382
|
}
|
|
1210
1383
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1211
|
-
const valueSegment =
|
|
1384
|
+
const valueSegment = state.value.split("&&");
|
|
1212
1385
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1213
1386
|
if (i > 0) {
|
|
1214
1387
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1228,13 +1401,19 @@ class StableBrowser {
|
|
|
1228
1401
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1229
1402
|
}
|
|
1230
1403
|
}
|
|
1404
|
+
await _screenshot(state, this);
|
|
1231
1405
|
if (enter === true) {
|
|
1232
1406
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1233
1407
|
await this.page.keyboard.press("Enter");
|
|
1234
1408
|
await this.waitForPageLoad();
|
|
1235
1409
|
}
|
|
1236
1410
|
else if (enter === false) {
|
|
1237
|
-
|
|
1411
|
+
try {
|
|
1412
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1413
|
+
}
|
|
1414
|
+
catch (e) {
|
|
1415
|
+
// ignore
|
|
1416
|
+
}
|
|
1238
1417
|
//await this.page.keyboard.press("Tab");
|
|
1239
1418
|
}
|
|
1240
1419
|
else {
|
|
@@ -1243,111 +1422,95 @@ class StableBrowser {
|
|
|
1243
1422
|
await this.waitForPageLoad();
|
|
1244
1423
|
}
|
|
1245
1424
|
}
|
|
1246
|
-
return info;
|
|
1425
|
+
return state.info;
|
|
1247
1426
|
}
|
|
1248
1427
|
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;
|
|
1428
|
+
await _commandError(state, e, this);
|
|
1256
1429
|
}
|
|
1257
1430
|
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
|
-
});
|
|
1431
|
+
await _commandFinally(state, this);
|
|
1279
1432
|
}
|
|
1280
1433
|
}
|
|
1281
1434
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1435
|
+
const state = {
|
|
1436
|
+
selectors,
|
|
1437
|
+
_params,
|
|
1438
|
+
value: unEscapeString(value),
|
|
1439
|
+
options,
|
|
1440
|
+
world,
|
|
1441
|
+
type: Types.FILL,
|
|
1442
|
+
text: `Fill input with value: ${value}`,
|
|
1443
|
+
operation: "fill",
|
|
1444
|
+
log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
|
|
1445
|
+
};
|
|
1292
1446
|
try {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
await
|
|
1296
|
-
await element.fill(value);
|
|
1297
|
-
await element.dispatchEvent("change");
|
|
1447
|
+
await _preCommand(state, this);
|
|
1448
|
+
await state.element.fill(value);
|
|
1449
|
+
await state.element.dispatchEvent("change");
|
|
1298
1450
|
if (enter) {
|
|
1299
1451
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1300
1452
|
await this.page.keyboard.press("Enter");
|
|
1301
1453
|
}
|
|
1302
1454
|
await this.waitForPageLoad();
|
|
1303
|
-
return info;
|
|
1455
|
+
return state.info;
|
|
1304
1456
|
}
|
|
1305
1457
|
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;
|
|
1458
|
+
await _commandError(state, e, this);
|
|
1313
1459
|
}
|
|
1314
1460
|
finally {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1461
|
+
await _commandFinally(state, this);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1465
|
+
const state = {
|
|
1466
|
+
selectors,
|
|
1467
|
+
_params,
|
|
1468
|
+
files,
|
|
1469
|
+
value: '"' + files.join('", "') + '"',
|
|
1470
|
+
options,
|
|
1471
|
+
world,
|
|
1472
|
+
type: Types.SET_INPUT_FILES,
|
|
1473
|
+
text: `Set input files`,
|
|
1474
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1475
|
+
operation: "setInputFiles",
|
|
1476
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1477
|
+
};
|
|
1478
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1479
|
+
try {
|
|
1480
|
+
await _preCommand(state, this);
|
|
1481
|
+
for (let i = 0; i < files.length; i++) {
|
|
1482
|
+
const file = files[i];
|
|
1483
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1484
|
+
if (!fs.existsSync(filePath)) {
|
|
1485
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1486
|
+
}
|
|
1487
|
+
state.files[i] = filePath;
|
|
1488
|
+
}
|
|
1489
|
+
await state.element.setInputFiles(files);
|
|
1490
|
+
return state.info;
|
|
1491
|
+
}
|
|
1492
|
+
catch (e) {
|
|
1493
|
+
await _commandError(state, e, this);
|
|
1494
|
+
}
|
|
1495
|
+
finally {
|
|
1496
|
+
await _commandFinally(state, this);
|
|
1336
1497
|
}
|
|
1337
1498
|
}
|
|
1338
1499
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1339
1500
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1340
1501
|
}
|
|
1341
1502
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1342
|
-
this.
|
|
1503
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1504
|
+
_validateSelectors(selectors);
|
|
1343
1505
|
let screenshotId = null;
|
|
1344
1506
|
let screenshotPath = null;
|
|
1345
1507
|
if (!info.log) {
|
|
1346
1508
|
info.log = "";
|
|
1509
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1347
1510
|
}
|
|
1348
1511
|
info.operation = "getText";
|
|
1349
1512
|
info.selectors = selectors;
|
|
1350
|
-
let element = await this._locate(selectors, info, _params);
|
|
1513
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1351
1514
|
if (climb > 0) {
|
|
1352
1515
|
const climbArray = [];
|
|
1353
1516
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1366,6 +1529,18 @@ class StableBrowser {
|
|
|
1366
1529
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1367
1530
|
try {
|
|
1368
1531
|
await this._highlightElements(element);
|
|
1532
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1533
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1534
|
+
// this._highlightElements(element)
|
|
1535
|
+
// .then(async () => {
|
|
1536
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1537
|
+
// this._unhighlightElements(element).then(
|
|
1538
|
+
// () => {}
|
|
1539
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1540
|
+
// );
|
|
1541
|
+
// })
|
|
1542
|
+
// .catch(e);
|
|
1543
|
+
// }
|
|
1369
1544
|
const elementText = await element.innerText();
|
|
1370
1545
|
return {
|
|
1371
1546
|
text: elementText,
|
|
@@ -1377,188 +1552,142 @@ class StableBrowser {
|
|
|
1377
1552
|
}
|
|
1378
1553
|
catch (e) {
|
|
1379
1554
|
//await this.closeUnexpectedPopups();
|
|
1380
|
-
this.logger.info("no innerText will use textContent");
|
|
1555
|
+
this.logger.info("no innerText, will use textContent");
|
|
1381
1556
|
const elementText = await element.textContent();
|
|
1382
1557
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1383
1558
|
}
|
|
1384
1559
|
}
|
|
1385
1560
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1386
|
-
var _a;
|
|
1387
|
-
this._validateSelectors(selectors);
|
|
1388
1561
|
if (!pattern) {
|
|
1389
1562
|
throw new Error("pattern is null");
|
|
1390
1563
|
}
|
|
1391
1564
|
if (!text) {
|
|
1392
1565
|
throw new Error("text is null");
|
|
1393
1566
|
}
|
|
1567
|
+
const state = {
|
|
1568
|
+
selectors,
|
|
1569
|
+
_params,
|
|
1570
|
+
pattern,
|
|
1571
|
+
value: pattern,
|
|
1572
|
+
options,
|
|
1573
|
+
world,
|
|
1574
|
+
locate: false,
|
|
1575
|
+
scroll: false,
|
|
1576
|
+
screenshot: false,
|
|
1577
|
+
highlight: false,
|
|
1578
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1579
|
+
text: `Verify element contains pattern: ${pattern}`,
|
|
1580
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1581
|
+
operation: "containsPattern",
|
|
1582
|
+
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1583
|
+
};
|
|
1394
1584
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1395
1585
|
if (newValue !== text) {
|
|
1396
1586
|
this.logger.info(text + "=" + newValue);
|
|
1397
1587
|
text = newValue;
|
|
1398
1588
|
}
|
|
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
1589
|
let foundObj = null;
|
|
1411
1590
|
try {
|
|
1412
|
-
|
|
1591
|
+
await _preCommand(state, this);
|
|
1592
|
+
state.info.pattern = pattern;
|
|
1593
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1413
1594
|
if (foundObj && foundObj.element) {
|
|
1414
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1595
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1415
1596
|
}
|
|
1416
|
-
|
|
1597
|
+
await _screenshot(state, this);
|
|
1417
1598
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1418
1599
|
pattern = pattern.replace("{text}", escapedText);
|
|
1419
1600
|
let regex = new RegExp(pattern, "im");
|
|
1420
|
-
if (!regex.test(foundObj
|
|
1421
|
-
info.foundText = foundObj
|
|
1601
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1602
|
+
state.info.foundText = foundObj?.text;
|
|
1422
1603
|
throw new Error("element doesn't contain text " + text);
|
|
1423
1604
|
}
|
|
1424
|
-
return info;
|
|
1605
|
+
return state.info;
|
|
1425
1606
|
}
|
|
1426
1607
|
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;
|
|
1608
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1609
|
+
await _commandError(state, e, this);
|
|
1435
1610
|
}
|
|
1436
1611
|
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
|
-
});
|
|
1612
|
+
await _commandFinally(state, this);
|
|
1458
1613
|
}
|
|
1459
1614
|
}
|
|
1460
1615
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1616
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1617
|
+
const startTime = Date.now();
|
|
1618
|
+
const state = {
|
|
1619
|
+
selectors,
|
|
1620
|
+
_params,
|
|
1621
|
+
value: text,
|
|
1622
|
+
options,
|
|
1623
|
+
world,
|
|
1624
|
+
locate: false,
|
|
1625
|
+
scroll: false,
|
|
1626
|
+
screenshot: false,
|
|
1627
|
+
highlight: false,
|
|
1628
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1629
|
+
text: `Verify element contains text: ${text}`,
|
|
1630
|
+
operation: "containsText",
|
|
1631
|
+
log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
|
|
1632
|
+
};
|
|
1463
1633
|
if (!text) {
|
|
1464
1634
|
throw new Error("text is null");
|
|
1465
1635
|
}
|
|
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;
|
|
1636
|
+
text = unEscapeString(text);
|
|
1474
1637
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1475
1638
|
if (newValue !== text) {
|
|
1476
1639
|
this.logger.info(text + "=" + newValue);
|
|
1477
1640
|
text = newValue;
|
|
1478
1641
|
}
|
|
1479
|
-
info.value = text;
|
|
1480
1642
|
let foundObj = null;
|
|
1481
1643
|
try {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1644
|
+
while (Date.now() - startTime < timeout) {
|
|
1645
|
+
try {
|
|
1646
|
+
await _preCommand(state, this);
|
|
1647
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1648
|
+
if (foundObj && foundObj.element) {
|
|
1649
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1650
|
+
}
|
|
1651
|
+
await _screenshot(state, this);
|
|
1652
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1653
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1654
|
+
if (dateAlternatives.date) {
|
|
1655
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1656
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1657
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1658
|
+
return state.info;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1494
1661
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1662
|
+
else if (numberAlternatives.number) {
|
|
1663
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1664
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1665
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1666
|
+
return state.info;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1671
|
+
return state.info;
|
|
1503
1672
|
}
|
|
1504
1673
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
throw new Error("element doesn't contain text " + text);
|
|
1674
|
+
catch (e) {
|
|
1675
|
+
// Log error but continue retrying until timeout is reached
|
|
1676
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1677
|
+
}
|
|
1678
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1511
1679
|
}
|
|
1512
|
-
|
|
1680
|
+
state.info.foundText = foundObj?.text;
|
|
1681
|
+
state.info.value = foundObj?.value;
|
|
1682
|
+
throw new Error("element doesn't contain text " + text);
|
|
1513
1683
|
}
|
|
1514
1684
|
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;
|
|
1685
|
+
await _commandError(state, e, this);
|
|
1521
1686
|
throw e;
|
|
1522
1687
|
}
|
|
1523
1688
|
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
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
_getDataFile(world = null) {
|
|
1548
|
-
let dataFile = null;
|
|
1549
|
-
if (world && world.reportFolder) {
|
|
1550
|
-
dataFile = path.join(world.reportFolder, "data.json");
|
|
1551
|
-
}
|
|
1552
|
-
else if (this.reportFolder) {
|
|
1553
|
-
dataFile = path.join(this.reportFolder, "data.json");
|
|
1554
|
-
}
|
|
1555
|
-
else if (this.context && this.context.reportFolder) {
|
|
1556
|
-
dataFile = path.join(this.context.reportFolder, "data.json");
|
|
1689
|
+
await _commandFinally(state, this);
|
|
1557
1690
|
}
|
|
1558
|
-
else {
|
|
1559
|
-
dataFile = "data.json";
|
|
1560
|
-
}
|
|
1561
|
-
return dataFile;
|
|
1562
1691
|
}
|
|
1563
1692
|
async waitForUserInput(message, world = null) {
|
|
1564
1693
|
if (!message) {
|
|
@@ -1588,13 +1717,22 @@ class StableBrowser {
|
|
|
1588
1717
|
return;
|
|
1589
1718
|
}
|
|
1590
1719
|
// if data file exists, load it
|
|
1591
|
-
const dataFile =
|
|
1720
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1592
1721
|
let data = this.getTestData(world);
|
|
1593
1722
|
// merge the testData with the existing data
|
|
1594
1723
|
Object.assign(data, testData);
|
|
1595
1724
|
// save the data to the file
|
|
1596
1725
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1597
1726
|
}
|
|
1727
|
+
overwriteTestData(testData, world = null) {
|
|
1728
|
+
if (!testData) {
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
// if data file exists, load it
|
|
1732
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1733
|
+
// save the data to the file
|
|
1734
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1735
|
+
}
|
|
1598
1736
|
_getDataFilePath(fileName) {
|
|
1599
1737
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1600
1738
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1691,7 +1829,7 @@ class StableBrowser {
|
|
|
1691
1829
|
}
|
|
1692
1830
|
}
|
|
1693
1831
|
getTestData(world = null) {
|
|
1694
|
-
const dataFile =
|
|
1832
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1695
1833
|
let data = {};
|
|
1696
1834
|
if (fs.existsSync(dataFile)) {
|
|
1697
1835
|
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
@@ -1723,11 +1861,9 @@ class StableBrowser {
|
|
|
1723
1861
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1724
1862
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1725
1863
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
1864
|
+
// to make sure the path doesn't start with -
|
|
1865
|
+
const uuidStr = "id_" + randomUUID();
|
|
1866
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1731
1867
|
try {
|
|
1732
1868
|
await this.takeScreenshot(screenshotPath);
|
|
1733
1869
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1737,15 +1873,15 @@ class StableBrowser {
|
|
|
1737
1873
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1738
1874
|
// }
|
|
1739
1875
|
// });
|
|
1876
|
+
result.screenshotId = uuidStr;
|
|
1877
|
+
result.screenshotPath = screenshotPath;
|
|
1878
|
+
if (info && info.box) {
|
|
1879
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1880
|
+
}
|
|
1740
1881
|
}
|
|
1741
1882
|
catch (e) {
|
|
1742
1883
|
this.logger.info("unable to take screenshot, ignored");
|
|
1743
1884
|
}
|
|
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
1885
|
}
|
|
1750
1886
|
else if (options && options.screenshot) {
|
|
1751
1887
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1770,7 +1906,6 @@ class StableBrowser {
|
|
|
1770
1906
|
}
|
|
1771
1907
|
async takeScreenshot(screenshotPath) {
|
|
1772
1908
|
const playContext = this.context.playContext;
|
|
1773
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1774
1909
|
// Using CDP to capture the screenshot
|
|
1775
1910
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1776
1911
|
document.body.scrollWidth,
|
|
@@ -1780,164 +1915,216 @@ class StableBrowser {
|
|
|
1780
1915
|
document.body.clientWidth,
|
|
1781
1916
|
document.documentElement.clientWidth,
|
|
1782
1917
|
])));
|
|
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
|
-
|
|
1918
|
+
let screenshotBuffer = null;
|
|
1919
|
+
// if (focusedElement) {
|
|
1920
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
1921
|
+
// await this._unhighlightElements(focusedElement);
|
|
1922
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1923
|
+
// console.log(`Unhighlighted previous element`);
|
|
1924
|
+
// }
|
|
1925
|
+
// if (focusedElement) {
|
|
1926
|
+
// await this._highlightElements(focusedElement);
|
|
1927
|
+
// }
|
|
1928
|
+
if (this.context.browserName === "chromium") {
|
|
1929
|
+
const client = await playContext.newCDPSession(this.page);
|
|
1930
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
1931
|
+
format: "png",
|
|
1932
|
+
// clip: {
|
|
1933
|
+
// x: 0,
|
|
1934
|
+
// y: 0,
|
|
1935
|
+
// width: viewportWidth,
|
|
1936
|
+
// height: viewportHeight,
|
|
1937
|
+
// scale: 1,
|
|
1938
|
+
// },
|
|
1939
|
+
});
|
|
1940
|
+
await client.detach();
|
|
1941
|
+
if (!screenshotPath) {
|
|
1942
|
+
return data;
|
|
1943
|
+
}
|
|
1944
|
+
screenshotBuffer = Buffer.from(data, "base64");
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
screenshotBuffer = await this.page.screenshot();
|
|
1948
|
+
}
|
|
1949
|
+
// if (focusedElement) {
|
|
1950
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
1951
|
+
// await this._unhighlightElements(focusedElement);
|
|
1952
|
+
// }
|
|
1953
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
1954
|
+
// Get the image dimensions
|
|
1955
|
+
const { width, height } = image.bitmap;
|
|
1956
|
+
const resizeRatio = viewportWidth / width;
|
|
1957
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
1958
|
+
if (width > viewportWidth) {
|
|
1959
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
1960
|
+
await image.write(screenshotPath);
|
|
1961
|
+
}
|
|
1962
|
+
else {
|
|
1963
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1964
|
+
}
|
|
1965
|
+
return screenshotBuffer;
|
|
1818
1966
|
}
|
|
1819
1967
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1968
|
+
const state = {
|
|
1969
|
+
selectors,
|
|
1970
|
+
_params,
|
|
1971
|
+
options,
|
|
1972
|
+
world,
|
|
1973
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1974
|
+
text: `Verify element exists in page`,
|
|
1975
|
+
operation: "verifyElementExistInPage",
|
|
1976
|
+
log: "***** verify element " + selectors.element_name + " exists in page *****\n",
|
|
1977
|
+
};
|
|
1825
1978
|
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
1979
|
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;
|
|
1980
|
+
await _preCommand(state, this);
|
|
1981
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
1982
|
+
return state.info;
|
|
1839
1983
|
}
|
|
1840
1984
|
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;
|
|
1985
|
+
await _commandError(state, e, this);
|
|
1848
1986
|
}
|
|
1849
1987
|
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
|
-
});
|
|
1988
|
+
await _commandFinally(state, this);
|
|
1870
1989
|
}
|
|
1871
1990
|
}
|
|
1872
1991
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1992
|
+
const state = {
|
|
1993
|
+
selectors,
|
|
1994
|
+
_params,
|
|
1995
|
+
attribute,
|
|
1996
|
+
variable,
|
|
1997
|
+
options,
|
|
1998
|
+
world,
|
|
1999
|
+
type: Types.EXTRACT,
|
|
2000
|
+
text: `Extract attribute from element`,
|
|
2001
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
2002
|
+
operation: "extractAttribute",
|
|
2003
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2004
|
+
allowDisabled: true,
|
|
2005
|
+
};
|
|
1878
2006
|
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
2007
|
try {
|
|
1884
|
-
|
|
1885
|
-
await this._highlightElements(element);
|
|
1886
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2008
|
+
await _preCommand(state, this);
|
|
1887
2009
|
switch (attribute) {
|
|
1888
2010
|
case "inner_text":
|
|
1889
|
-
|
|
2011
|
+
state.value = await state.element.innerText();
|
|
1890
2012
|
break;
|
|
1891
2013
|
case "href":
|
|
1892
|
-
|
|
2014
|
+
state.value = await state.element.getAttribute("href");
|
|
1893
2015
|
break;
|
|
1894
2016
|
case "value":
|
|
1895
|
-
|
|
2017
|
+
state.value = await state.element.inputValue();
|
|
2018
|
+
break;
|
|
2019
|
+
case "text":
|
|
2020
|
+
state.value = await state.element.textContent();
|
|
1896
2021
|
break;
|
|
1897
2022
|
default:
|
|
1898
|
-
|
|
2023
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1899
2024
|
break;
|
|
1900
2025
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2026
|
+
if (options !== null) {
|
|
2027
|
+
if (options.regex && options.regex !== "") {
|
|
2028
|
+
// Construct a regex pattern from the provided string
|
|
2029
|
+
const regex = options.regex.slice(1, -1);
|
|
2030
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2031
|
+
const matches = state.value.match(regexPattern);
|
|
2032
|
+
if (matches) {
|
|
2033
|
+
let newValue = "";
|
|
2034
|
+
for (const match of matches) {
|
|
2035
|
+
newValue += match;
|
|
2036
|
+
}
|
|
2037
|
+
state.value = newValue;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2041
|
+
state.value = state.value.trim();
|
|
2042
|
+
}
|
|
1904
2043
|
}
|
|
1905
|
-
|
|
1906
|
-
this.
|
|
1907
|
-
|
|
2044
|
+
state.info.value = state.value;
|
|
2045
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2046
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2047
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2048
|
+
return state.info;
|
|
1908
2049
|
}
|
|
1909
2050
|
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;
|
|
2051
|
+
await _commandError(state, e, this);
|
|
1917
2052
|
}
|
|
1918
2053
|
finally {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2054
|
+
await _commandFinally(state, this);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2058
|
+
const state = {
|
|
2059
|
+
selectors,
|
|
2060
|
+
_params,
|
|
2061
|
+
attribute,
|
|
2062
|
+
value,
|
|
2063
|
+
options,
|
|
2064
|
+
world,
|
|
2065
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2066
|
+
highlight: true,
|
|
2067
|
+
screenshot: true,
|
|
2068
|
+
text: `Verify element attribute`,
|
|
2069
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2070
|
+
operation: "verifyAttribute",
|
|
2071
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2072
|
+
allowDisabled: true,
|
|
2073
|
+
};
|
|
2074
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2075
|
+
let val;
|
|
2076
|
+
let expectedValue;
|
|
2077
|
+
try {
|
|
2078
|
+
await _preCommand(state, this);
|
|
2079
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2080
|
+
state.info.expectedValue = expectedValue;
|
|
2081
|
+
switch (attribute) {
|
|
2082
|
+
case "innerText":
|
|
2083
|
+
val = String(await state.element.innerText());
|
|
2084
|
+
break;
|
|
2085
|
+
case "text":
|
|
2086
|
+
val = String(await state.element.textContent());
|
|
2087
|
+
break;
|
|
2088
|
+
case "value":
|
|
2089
|
+
val = String(await state.element.inputValue());
|
|
2090
|
+
break;
|
|
2091
|
+
case "checked":
|
|
2092
|
+
val = String(await state.element.isChecked());
|
|
2093
|
+
break;
|
|
2094
|
+
case "disabled":
|
|
2095
|
+
val = String(await state.element.isDisabled());
|
|
2096
|
+
break;
|
|
2097
|
+
case "readOnly":
|
|
2098
|
+
const isEditable = await state.element.isEditable();
|
|
2099
|
+
val = String(!isEditable);
|
|
2100
|
+
break;
|
|
2101
|
+
default:
|
|
2102
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2103
|
+
break;
|
|
2104
|
+
}
|
|
2105
|
+
state.info.value = val;
|
|
2106
|
+
let regex;
|
|
2107
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2108
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2109
|
+
regex = new RegExp(patternBody, "g");
|
|
2110
|
+
}
|
|
2111
|
+
else {
|
|
2112
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2113
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2114
|
+
}
|
|
2115
|
+
if (!val.match(regex)) {
|
|
2116
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2117
|
+
state.info.failCause.assertionFailed = true;
|
|
2118
|
+
state.info.failCause.lastError = errorMessage;
|
|
2119
|
+
throw new Error(errorMessage);
|
|
2120
|
+
}
|
|
2121
|
+
return state.info;
|
|
2122
|
+
}
|
|
2123
|
+
catch (e) {
|
|
2124
|
+
await _commandError(state, e, this);
|
|
2125
|
+
}
|
|
2126
|
+
finally {
|
|
2127
|
+
await _commandFinally(state, this);
|
|
1941
2128
|
}
|
|
1942
2129
|
}
|
|
1943
2130
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -1958,7 +2145,7 @@ class StableBrowser {
|
|
|
1958
2145
|
if (options && options.timeout) {
|
|
1959
2146
|
timeout = options.timeout;
|
|
1960
2147
|
}
|
|
1961
|
-
const serviceUrl =
|
|
2148
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1962
2149
|
const request = {
|
|
1963
2150
|
method: "POST",
|
|
1964
2151
|
url: serviceUrl,
|
|
@@ -2014,7 +2201,8 @@ class StableBrowser {
|
|
|
2014
2201
|
catch (e) {
|
|
2015
2202
|
errorCount++;
|
|
2016
2203
|
if (errorCount > 3) {
|
|
2017
|
-
throw e;
|
|
2204
|
+
// throw e;
|
|
2205
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2018
2206
|
}
|
|
2019
2207
|
// ignore
|
|
2020
2208
|
}
|
|
@@ -2028,27 +2216,32 @@ class StableBrowser {
|
|
|
2028
2216
|
async _highlightElements(scope, css) {
|
|
2029
2217
|
try {
|
|
2030
2218
|
if (!scope) {
|
|
2219
|
+
// console.log(`Scope is not defined`);
|
|
2031
2220
|
return;
|
|
2032
2221
|
}
|
|
2033
2222
|
if (!css) {
|
|
2034
2223
|
scope
|
|
2035
2224
|
.evaluate((node) => {
|
|
2036
2225
|
if (node && node.style) {
|
|
2037
|
-
let
|
|
2038
|
-
|
|
2226
|
+
let originalOutline = node.style.outline;
|
|
2227
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
2228
|
+
// node.__previousOutline = originalOutline;
|
|
2229
|
+
node.style.outline = "2px solid red";
|
|
2230
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
2039
2231
|
if (window) {
|
|
2040
2232
|
window.addEventListener("beforeunload", function (e) {
|
|
2041
|
-
node.style.
|
|
2233
|
+
node.style.outline = originalOutline;
|
|
2042
2234
|
});
|
|
2043
2235
|
}
|
|
2044
2236
|
setTimeout(function () {
|
|
2045
|
-
node.style.
|
|
2237
|
+
node.style.outline = originalOutline;
|
|
2046
2238
|
}, 2000);
|
|
2047
2239
|
}
|
|
2048
2240
|
})
|
|
2049
2241
|
.then(() => { })
|
|
2050
2242
|
.catch((e) => {
|
|
2051
2243
|
// ignore
|
|
2244
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2052
2245
|
});
|
|
2053
2246
|
}
|
|
2054
2247
|
else {
|
|
@@ -2064,17 +2257,18 @@ class StableBrowser {
|
|
|
2064
2257
|
if (!element.style) {
|
|
2065
2258
|
return;
|
|
2066
2259
|
}
|
|
2067
|
-
|
|
2260
|
+
let originalOutline = element.style.outline;
|
|
2261
|
+
element.__previousOutline = originalOutline;
|
|
2068
2262
|
// Set the new border to be red and 2px solid
|
|
2069
|
-
element.style.
|
|
2263
|
+
element.style.outline = "2px solid red";
|
|
2070
2264
|
if (window) {
|
|
2071
2265
|
window.addEventListener("beforeunload", function (e) {
|
|
2072
|
-
element.style.
|
|
2266
|
+
element.style.outline = originalOutline;
|
|
2073
2267
|
});
|
|
2074
2268
|
}
|
|
2075
2269
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2076
2270
|
setTimeout(function () {
|
|
2077
|
-
element.style.
|
|
2271
|
+
element.style.outline = originalOutline;
|
|
2078
2272
|
}, 2000);
|
|
2079
2273
|
}
|
|
2080
2274
|
return;
|
|
@@ -2082,6 +2276,7 @@ class StableBrowser {
|
|
|
2082
2276
|
.then(() => { })
|
|
2083
2277
|
.catch((e) => {
|
|
2084
2278
|
// ignore
|
|
2279
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2085
2280
|
});
|
|
2086
2281
|
}
|
|
2087
2282
|
}
|
|
@@ -2089,6 +2284,54 @@ class StableBrowser {
|
|
|
2089
2284
|
console.debug(error);
|
|
2090
2285
|
}
|
|
2091
2286
|
}
|
|
2287
|
+
// async _unhighlightElements(scope, css) {
|
|
2288
|
+
// try {
|
|
2289
|
+
// if (!scope) {
|
|
2290
|
+
// return;
|
|
2291
|
+
// }
|
|
2292
|
+
// if (!css) {
|
|
2293
|
+
// scope
|
|
2294
|
+
// .evaluate((node) => {
|
|
2295
|
+
// if (node && node.style) {
|
|
2296
|
+
// if (!node.__previousOutline) {
|
|
2297
|
+
// node.style.outline = "";
|
|
2298
|
+
// } else {
|
|
2299
|
+
// node.style.outline = node.__previousOutline;
|
|
2300
|
+
// }
|
|
2301
|
+
// }
|
|
2302
|
+
// })
|
|
2303
|
+
// .then(() => {})
|
|
2304
|
+
// .catch((e) => {
|
|
2305
|
+
// // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
|
|
2306
|
+
// });
|
|
2307
|
+
// } else {
|
|
2308
|
+
// scope
|
|
2309
|
+
// .evaluate(([css]) => {
|
|
2310
|
+
// if (!css) {
|
|
2311
|
+
// return;
|
|
2312
|
+
// }
|
|
2313
|
+
// let elements = Array.from(document.querySelectorAll(css));
|
|
2314
|
+
// for (i = 0; i < elements.length; i++) {
|
|
2315
|
+
// let element = elements[i];
|
|
2316
|
+
// if (!element.style) {
|
|
2317
|
+
// return;
|
|
2318
|
+
// }
|
|
2319
|
+
// if (!element.__previousOutline) {
|
|
2320
|
+
// element.style.outline = "";
|
|
2321
|
+
// } else {
|
|
2322
|
+
// element.style.outline = element.__previousOutline;
|
|
2323
|
+
// }
|
|
2324
|
+
// }
|
|
2325
|
+
// })
|
|
2326
|
+
// .then(() => {})
|
|
2327
|
+
// .catch((e) => {
|
|
2328
|
+
// // console.error(`Error while unhighlighting element in css: ${e}`);
|
|
2329
|
+
// });
|
|
2330
|
+
// }
|
|
2331
|
+
// } catch (error) {
|
|
2332
|
+
// // console.debug(error);
|
|
2333
|
+
// }
|
|
2334
|
+
// }
|
|
2092
2335
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2093
2336
|
const startTime = Date.now();
|
|
2094
2337
|
let error = null;
|
|
@@ -2125,20 +2368,22 @@ class StableBrowser {
|
|
|
2125
2368
|
info.screenshotPath = screenshotPath;
|
|
2126
2369
|
Object.assign(e, { info: info });
|
|
2127
2370
|
error = e;
|
|
2128
|
-
throw e;
|
|
2371
|
+
// throw e;
|
|
2372
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2129
2373
|
}
|
|
2130
2374
|
finally {
|
|
2131
2375
|
const endTime = Date.now();
|
|
2132
|
-
|
|
2376
|
+
_reportToWorld(world, {
|
|
2133
2377
|
type: Types.VERIFY_PAGE_PATH,
|
|
2134
2378
|
text: "Verify page path",
|
|
2379
|
+
_text: "Verify the page path contains " + pathPart,
|
|
2135
2380
|
screenshotId,
|
|
2136
2381
|
result: error
|
|
2137
2382
|
? {
|
|
2138
2383
|
status: "FAILED",
|
|
2139
2384
|
startTime,
|
|
2140
2385
|
endTime,
|
|
2141
|
-
message: error
|
|
2386
|
+
message: error?.message,
|
|
2142
2387
|
}
|
|
2143
2388
|
: {
|
|
2144
2389
|
status: "PASSED",
|
|
@@ -2149,94 +2394,58 @@ class StableBrowser {
|
|
|
2149
2394
|
});
|
|
2150
2395
|
}
|
|
2151
2396
|
}
|
|
2152
|
-
async
|
|
2397
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2153
2398
|
const startTime = Date.now();
|
|
2154
|
-
const timeout = this._getLoadTimeout(options);
|
|
2155
2399
|
let error = null;
|
|
2156
2400
|
let screenshotId = null;
|
|
2157
2401
|
let screenshotPath = null;
|
|
2158
2402
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2159
2403
|
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);
|
|
2404
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2405
|
+
info.operation = "verifyPageTitle";
|
|
2406
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2407
|
+
if (newValue !== title) {
|
|
2408
|
+
this.logger.info(title + "=" + newValue);
|
|
2409
|
+
title = newValue;
|
|
2410
|
+
}
|
|
2411
|
+
info.title = title;
|
|
2170
2412
|
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`);
|
|
2413
|
+
for (let i = 0; i < 30; i++) {
|
|
2414
|
+
const foundTitle = await this.page.title();
|
|
2415
|
+
if (!foundTitle.includes(title)) {
|
|
2416
|
+
if (i === 29) {
|
|
2417
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2200
2418
|
}
|
|
2201
2419
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2202
2420
|
continue;
|
|
2203
2421
|
}
|
|
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
2422
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2215
2423
|
return info;
|
|
2216
2424
|
}
|
|
2217
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2218
2425
|
}
|
|
2219
2426
|
catch (e) {
|
|
2220
2427
|
//await this.closeUnexpectedPopups();
|
|
2221
|
-
this.logger.error("verify
|
|
2428
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2222
2429
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2223
2430
|
info.screenshotPath = screenshotPath;
|
|
2224
2431
|
Object.assign(e, { info: info });
|
|
2225
2432
|
error = e;
|
|
2226
|
-
throw e;
|
|
2433
|
+
// throw e;
|
|
2434
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2227
2435
|
}
|
|
2228
2436
|
finally {
|
|
2229
2437
|
const endTime = Date.now();
|
|
2230
|
-
|
|
2231
|
-
type: Types.
|
|
2232
|
-
text: "Verify
|
|
2438
|
+
_reportToWorld(world, {
|
|
2439
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2440
|
+
text: "Verify page title",
|
|
2441
|
+
_text: "Verify the page title contains " + title,
|
|
2233
2442
|
screenshotId,
|
|
2234
2443
|
result: error
|
|
2235
2444
|
? {
|
|
2236
2445
|
status: "FAILED",
|
|
2237
2446
|
startTime,
|
|
2238
2447
|
endTime,
|
|
2239
|
-
message: error
|
|
2448
|
+
message: error?.message,
|
|
2240
2449
|
}
|
|
2241
2450
|
: {
|
|
2242
2451
|
status: "PASSED",
|
|
@@ -2247,15 +2456,317 @@ class StableBrowser {
|
|
|
2247
2456
|
});
|
|
2248
2457
|
}
|
|
2249
2458
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2459
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2460
|
+
const frames = this.page.frames();
|
|
2461
|
+
let results = [];
|
|
2462
|
+
// let ignoreCase = false;
|
|
2463
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2464
|
+
if (dateAlternatives.date) {
|
|
2465
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2466
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2467
|
+
result.frame = frames[i];
|
|
2468
|
+
results.push(result);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
else if (numberAlternatives.number) {
|
|
2472
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2473
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2474
|
+
result.frame = frames[i];
|
|
2475
|
+
results.push(result);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
else {
|
|
2479
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2480
|
+
result.frame = frames[i];
|
|
2481
|
+
results.push(result);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
state.info.results = results;
|
|
2485
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2486
|
+
return resultWithElementsFound;
|
|
2487
|
+
}
|
|
2488
|
+
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2489
|
+
text = unEscapeString(text);
|
|
2490
|
+
const state = {
|
|
2491
|
+
text_search: text,
|
|
2492
|
+
options,
|
|
2493
|
+
world,
|
|
2494
|
+
locate: false,
|
|
2495
|
+
scroll: false,
|
|
2496
|
+
highlight: false,
|
|
2497
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2498
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2499
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2500
|
+
operation: "verifyTextExistInPage",
|
|
2501
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
2502
|
+
};
|
|
2503
|
+
if (testForRegex(text)) {
|
|
2504
|
+
text = text.replace(/\\"/g, '"');
|
|
2505
|
+
}
|
|
2506
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2507
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2508
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2509
|
+
if (newValue !== text) {
|
|
2510
|
+
this.logger.info(text + "=" + newValue);
|
|
2511
|
+
text = newValue;
|
|
2512
|
+
}
|
|
2513
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2514
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2515
|
+
try {
|
|
2516
|
+
await _preCommand(state, this);
|
|
2517
|
+
state.info.text = text;
|
|
2518
|
+
while (true) {
|
|
2519
|
+
let resultWithElementsFound = {
|
|
2520
|
+
length: 0,
|
|
2521
|
+
};
|
|
2522
|
+
try {
|
|
2523
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2524
|
+
}
|
|
2525
|
+
catch (error) {
|
|
2526
|
+
// ignore
|
|
2527
|
+
}
|
|
2528
|
+
if (resultWithElementsFound.length === 0) {
|
|
2529
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2530
|
+
throw new Error(`Text ${text} not found in page`);
|
|
2531
|
+
}
|
|
2532
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
try {
|
|
2536
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
2537
|
+
const frame = resultWithElementsFound[0].frame;
|
|
2538
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2539
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2540
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2541
|
+
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2542
|
+
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2543
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2544
|
+
// this._unhighlightElements(frame, dataAttribute)
|
|
2545
|
+
// .then(async () => {
|
|
2546
|
+
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2547
|
+
// })
|
|
2548
|
+
// .catch(
|
|
2549
|
+
// (e) => {}
|
|
2550
|
+
// console.error(e)
|
|
2551
|
+
// );
|
|
2552
|
+
// });
|
|
2553
|
+
// }
|
|
2554
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2555
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2556
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2557
|
+
if (element) {
|
|
2558
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2559
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2560
|
+
// await _screenshot(state, this, element);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
await _screenshot(state, this);
|
|
2564
|
+
return state.info;
|
|
2565
|
+
}
|
|
2566
|
+
catch (error) {
|
|
2567
|
+
console.error(error);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2571
|
+
}
|
|
2572
|
+
catch (e) {
|
|
2573
|
+
await _commandError(state, e, this);
|
|
2574
|
+
}
|
|
2575
|
+
finally {
|
|
2576
|
+
await _commandFinally(state, this);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2580
|
+
text = unEscapeString(text);
|
|
2581
|
+
const state = {
|
|
2582
|
+
text_search: text,
|
|
2583
|
+
options,
|
|
2584
|
+
world,
|
|
2585
|
+
locate: false,
|
|
2586
|
+
scroll: false,
|
|
2587
|
+
highlight: false,
|
|
2588
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2589
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2590
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2591
|
+
operation: "verifyTextNotExistInPage",
|
|
2592
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2593
|
+
};
|
|
2594
|
+
if (testForRegex(text)) {
|
|
2595
|
+
text = text.replace(/\\"/g, '"');
|
|
2596
|
+
}
|
|
2597
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2598
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2599
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2600
|
+
if (newValue !== text) {
|
|
2601
|
+
this.logger.info(text + "=" + newValue);
|
|
2602
|
+
text = newValue;
|
|
2603
|
+
}
|
|
2604
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2605
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2606
|
+
try {
|
|
2607
|
+
await _preCommand(state, this);
|
|
2608
|
+
state.info.text = text;
|
|
2609
|
+
let resultWithElementsFound = {
|
|
2610
|
+
length: null, // initial cannot be 0
|
|
2611
|
+
};
|
|
2612
|
+
while (true) {
|
|
2613
|
+
try {
|
|
2614
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2615
|
+
}
|
|
2616
|
+
catch (error) {
|
|
2617
|
+
// ignore
|
|
2618
|
+
}
|
|
2619
|
+
if (resultWithElementsFound.length === 0) {
|
|
2620
|
+
await _screenshot(state, this);
|
|
2621
|
+
return state.info;
|
|
2622
|
+
}
|
|
2623
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2624
|
+
throw new Error(`Text ${text} found in page`);
|
|
2625
|
+
}
|
|
2626
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
catch (e) {
|
|
2630
|
+
await _commandError(state, e, this);
|
|
2631
|
+
}
|
|
2632
|
+
finally {
|
|
2633
|
+
await _commandFinally(state, this);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
2637
|
+
textAnchor = unEscapeString(textAnchor);
|
|
2638
|
+
textToVerify = unEscapeString(textToVerify);
|
|
2639
|
+
const state = {
|
|
2640
|
+
text_search: textToVerify,
|
|
2641
|
+
options,
|
|
2642
|
+
world,
|
|
2643
|
+
locate: false,
|
|
2644
|
+
scroll: false,
|
|
2645
|
+
highlight: false,
|
|
2646
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2647
|
+
text: `Verify text with relation to another text`,
|
|
2648
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
2649
|
+
operation: "verify_text_with_relation",
|
|
2650
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2651
|
+
};
|
|
2652
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2653
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2654
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2655
|
+
if (newValue !== textAnchor) {
|
|
2656
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
2657
|
+
textAnchor = newValue;
|
|
2658
|
+
}
|
|
2659
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
2660
|
+
if (newValue !== textToVerify) {
|
|
2661
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
2662
|
+
textToVerify = newValue;
|
|
2663
|
+
}
|
|
2664
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
2665
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
2666
|
+
let foundAncore = false;
|
|
2667
|
+
try {
|
|
2668
|
+
await _preCommand(state, this);
|
|
2669
|
+
state.info.text = textToVerify;
|
|
2670
|
+
let resultWithElementsFound = {
|
|
2671
|
+
length: 0,
|
|
2672
|
+
};
|
|
2673
|
+
while (true) {
|
|
2674
|
+
try {
|
|
2675
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2676
|
+
}
|
|
2677
|
+
catch (error) {
|
|
2678
|
+
// ignore
|
|
2679
|
+
}
|
|
2680
|
+
if (resultWithElementsFound.length === 0) {
|
|
2681
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2682
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2683
|
+
}
|
|
2684
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
try {
|
|
2688
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2689
|
+
foundAncore = true;
|
|
2690
|
+
const result = resultWithElementsFound[i];
|
|
2691
|
+
const token = result.randomToken;
|
|
2692
|
+
const frame = result.frame;
|
|
2693
|
+
let css = `[data-blinq-id-${token}]`;
|
|
2694
|
+
const climbArray1 = [];
|
|
2695
|
+
for (let i = 0; i < climb; i++) {
|
|
2696
|
+
climbArray1.push("..");
|
|
2697
|
+
}
|
|
2698
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
2699
|
+
css = css + " >> " + climbXpath;
|
|
2700
|
+
const count = await frame.locator(css).count();
|
|
2701
|
+
for (let j = 0; j < count; j++) {
|
|
2702
|
+
const continer = await frame.locator(css).nth(j);
|
|
2703
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2704
|
+
if (result.elementCount > 0) {
|
|
2705
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2706
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2707
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2708
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2709
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
2710
|
+
// this._highlightElements(frame, dataAttribute)
|
|
2711
|
+
// .then(async () => {
|
|
2712
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2713
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
2714
|
+
// () => {}
|
|
2715
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
2716
|
+
// );
|
|
2717
|
+
// })
|
|
2718
|
+
// .catch(e);
|
|
2719
|
+
// }
|
|
2720
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
2721
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2722
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2723
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2724
|
+
if (element) {
|
|
2725
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2726
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2727
|
+
}
|
|
2728
|
+
await _screenshot(state, this);
|
|
2729
|
+
return state.info;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
catch (error) {
|
|
2735
|
+
console.error(error);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2739
|
+
}
|
|
2740
|
+
catch (e) {
|
|
2741
|
+
await _commandError(state, e, this);
|
|
2742
|
+
}
|
|
2743
|
+
finally {
|
|
2744
|
+
await _commandFinally(state, this);
|
|
2254
2745
|
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2746
|
+
}
|
|
2747
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2748
|
+
const frames = this.page.frames();
|
|
2749
|
+
let results = [];
|
|
2750
|
+
let ignoreCase = false;
|
|
2751
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2752
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2753
|
+
result.frame = frames[i];
|
|
2754
|
+
const climbArray = [];
|
|
2755
|
+
for (let i = 0; i < climb; i++) {
|
|
2756
|
+
climbArray.push("..");
|
|
2757
|
+
}
|
|
2758
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2759
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2760
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2761
|
+
if (count > 0) {
|
|
2762
|
+
result.elementCount = count;
|
|
2763
|
+
result.locator = newLocator;
|
|
2764
|
+
results.push(result);
|
|
2765
|
+
}
|
|
2257
2766
|
}
|
|
2258
|
-
|
|
2767
|
+
// state.info.results = results;
|
|
2768
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2769
|
+
return resultWithElementsFound;
|
|
2259
2770
|
}
|
|
2260
2771
|
async visualVerification(text, options = {}, world = null) {
|
|
2261
2772
|
const startTime = Date.now();
|
|
@@ -2271,14 +2782,17 @@ class StableBrowser {
|
|
|
2271
2782
|
throw new Error("TOKEN is not set");
|
|
2272
2783
|
}
|
|
2273
2784
|
try {
|
|
2274
|
-
let serviceUrl =
|
|
2785
|
+
let serviceUrl = _getServerUrl();
|
|
2275
2786
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2276
2787
|
info.screenshotPath = screenshotPath;
|
|
2277
2788
|
const screenshot = await this.takeScreenshot();
|
|
2278
|
-
|
|
2279
|
-
method: "
|
|
2789
|
+
let request = {
|
|
2790
|
+
method: "post",
|
|
2791
|
+
maxBodyLength: Infinity,
|
|
2280
2792
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2281
2793
|
headers: {
|
|
2794
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
2795
|
+
"x-source": "aaa",
|
|
2282
2796
|
"Content-Type": "application/json",
|
|
2283
2797
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2284
2798
|
},
|
|
@@ -2287,7 +2801,7 @@ class StableBrowser {
|
|
|
2287
2801
|
screenshot: screenshot,
|
|
2288
2802
|
}),
|
|
2289
2803
|
};
|
|
2290
|
-
|
|
2804
|
+
const result = await axios.request(request);
|
|
2291
2805
|
if (result.data.status !== true) {
|
|
2292
2806
|
throw new Error("Visual validation failed");
|
|
2293
2807
|
}
|
|
@@ -2307,20 +2821,22 @@ class StableBrowser {
|
|
|
2307
2821
|
info.screenshotPath = screenshotPath;
|
|
2308
2822
|
Object.assign(e, { info: info });
|
|
2309
2823
|
error = e;
|
|
2310
|
-
throw e;
|
|
2824
|
+
// throw e;
|
|
2825
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2311
2826
|
}
|
|
2312
2827
|
finally {
|
|
2313
2828
|
const endTime = Date.now();
|
|
2314
|
-
|
|
2829
|
+
_reportToWorld(world, {
|
|
2315
2830
|
type: Types.VERIFY_VISUAL,
|
|
2316
2831
|
text: "Visual verification",
|
|
2832
|
+
_text: "Visual verification of " + text,
|
|
2317
2833
|
screenshotId,
|
|
2318
2834
|
result: error
|
|
2319
2835
|
? {
|
|
2320
2836
|
status: "FAILED",
|
|
2321
2837
|
startTime,
|
|
2322
2838
|
endTime,
|
|
2323
|
-
message: error
|
|
2839
|
+
message: error?.message,
|
|
2324
2840
|
}
|
|
2325
2841
|
: {
|
|
2326
2842
|
status: "PASSED",
|
|
@@ -2352,13 +2868,14 @@ class StableBrowser {
|
|
|
2352
2868
|
this.logger.info("Table data verified");
|
|
2353
2869
|
}
|
|
2354
2870
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2355
|
-
|
|
2871
|
+
_validateSelectors(selectors);
|
|
2356
2872
|
const startTime = Date.now();
|
|
2357
2873
|
let error = null;
|
|
2358
2874
|
let screenshotId = null;
|
|
2359
2875
|
let screenshotPath = null;
|
|
2360
2876
|
const info = {};
|
|
2361
2877
|
info.log = "";
|
|
2878
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2362
2879
|
info.operation = "getTableData";
|
|
2363
2880
|
info.selectors = selectors;
|
|
2364
2881
|
try {
|
|
@@ -2374,11 +2891,12 @@ class StableBrowser {
|
|
|
2374
2891
|
info.screenshotPath = screenshotPath;
|
|
2375
2892
|
Object.assign(e, { info: info });
|
|
2376
2893
|
error = e;
|
|
2377
|
-
throw e;
|
|
2894
|
+
// throw e;
|
|
2895
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2378
2896
|
}
|
|
2379
2897
|
finally {
|
|
2380
2898
|
const endTime = Date.now();
|
|
2381
|
-
|
|
2899
|
+
_reportToWorld(world, {
|
|
2382
2900
|
element_name: selectors.element_name,
|
|
2383
2901
|
type: Types.GET_TABLE_DATA,
|
|
2384
2902
|
text: "Get table data",
|
|
@@ -2388,7 +2906,7 @@ class StableBrowser {
|
|
|
2388
2906
|
status: "FAILED",
|
|
2389
2907
|
startTime,
|
|
2390
2908
|
endTime,
|
|
2391
|
-
message: error
|
|
2909
|
+
message: error?.message,
|
|
2392
2910
|
}
|
|
2393
2911
|
: {
|
|
2394
2912
|
status: "PASSED",
|
|
@@ -2400,7 +2918,7 @@ class StableBrowser {
|
|
|
2400
2918
|
}
|
|
2401
2919
|
}
|
|
2402
2920
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2403
|
-
|
|
2921
|
+
_validateSelectors(selectors);
|
|
2404
2922
|
if (!query) {
|
|
2405
2923
|
throw new Error("query is null");
|
|
2406
2924
|
}
|
|
@@ -2433,7 +2951,7 @@ class StableBrowser {
|
|
|
2433
2951
|
info.operation = "analyzeTable";
|
|
2434
2952
|
info.selectors = selectors;
|
|
2435
2953
|
info.query = query;
|
|
2436
|
-
query =
|
|
2954
|
+
query = _fixUsingParams(query, _params);
|
|
2437
2955
|
info.query_fixed = query;
|
|
2438
2956
|
info.operator = operator;
|
|
2439
2957
|
info.value = value;
|
|
@@ -2539,11 +3057,12 @@ class StableBrowser {
|
|
|
2539
3057
|
info.screenshotPath = screenshotPath;
|
|
2540
3058
|
Object.assign(e, { info: info });
|
|
2541
3059
|
error = e;
|
|
2542
|
-
throw e;
|
|
3060
|
+
// throw e;
|
|
3061
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2543
3062
|
}
|
|
2544
3063
|
finally {
|
|
2545
3064
|
const endTime = Date.now();
|
|
2546
|
-
|
|
3065
|
+
_reportToWorld(world, {
|
|
2547
3066
|
element_name: selectors.element_name,
|
|
2548
3067
|
type: Types.ANALYZE_TABLE,
|
|
2549
3068
|
text: "Analyze table",
|
|
@@ -2553,7 +3072,7 @@ class StableBrowser {
|
|
|
2553
3072
|
status: "FAILED",
|
|
2554
3073
|
startTime,
|
|
2555
3074
|
endTime,
|
|
2556
|
-
message: error
|
|
3075
|
+
message: error?.message,
|
|
2557
3076
|
}
|
|
2558
3077
|
: {
|
|
2559
3078
|
status: "PASSED",
|
|
@@ -2565,27 +3084,7 @@ class StableBrowser {
|
|
|
2565
3084
|
}
|
|
2566
3085
|
}
|
|
2567
3086
|
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
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
|
|
2586
|
-
return await decrypt(value, null, totpWait);
|
|
2587
|
-
}
|
|
2588
|
-
return value;
|
|
3087
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2589
3088
|
}
|
|
2590
3089
|
_getLoadTimeout(options) {
|
|
2591
3090
|
let timeout = 15000;
|
|
@@ -2597,6 +3096,32 @@ class StableBrowser {
|
|
|
2597
3096
|
}
|
|
2598
3097
|
return timeout;
|
|
2599
3098
|
}
|
|
3099
|
+
_getFindElementTimeout(options) {
|
|
3100
|
+
if (options && options.timeout) {
|
|
3101
|
+
return options.timeout;
|
|
3102
|
+
}
|
|
3103
|
+
if (this.configuration.find_element_timeout) {
|
|
3104
|
+
return this.configuration.find_element_timeout;
|
|
3105
|
+
}
|
|
3106
|
+
return 30000;
|
|
3107
|
+
}
|
|
3108
|
+
async saveStoreState(path = null, world = null) {
|
|
3109
|
+
const storageState = await this.page.context().storageState();
|
|
3110
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
3111
|
+
if (path) {
|
|
3112
|
+
// save { storageState: storageState } into the path
|
|
3113
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
3114
|
+
}
|
|
3115
|
+
else {
|
|
3116
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
async restoreSaveState(path = null, world = null) {
|
|
3120
|
+
await refreshBrowser(this, path, world);
|
|
3121
|
+
this.registerEventListeners(this.context);
|
|
3122
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
3123
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
3124
|
+
}
|
|
2600
3125
|
async waitForPageLoad(options = {}, world = null) {
|
|
2601
3126
|
let timeout = this._getLoadTimeout(options);
|
|
2602
3127
|
const promiseArray = [];
|
|
@@ -2636,7 +3161,7 @@ class StableBrowser {
|
|
|
2636
3161
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2637
3162
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2638
3163
|
const endTime = Date.now();
|
|
2639
|
-
|
|
3164
|
+
_reportToWorld(world, {
|
|
2640
3165
|
type: Types.GET_PAGE_STATUS,
|
|
2641
3166
|
text: "Wait for page load",
|
|
2642
3167
|
screenshotId,
|
|
@@ -2645,7 +3170,7 @@ class StableBrowser {
|
|
|
2645
3170
|
status: "FAILED",
|
|
2646
3171
|
startTime,
|
|
2647
3172
|
endTime,
|
|
2648
|
-
message: error
|
|
3173
|
+
message: error?.message,
|
|
2649
3174
|
}
|
|
2650
3175
|
: {
|
|
2651
3176
|
status: "PASSED",
|
|
@@ -2656,41 +3181,123 @@ class StableBrowser {
|
|
|
2656
3181
|
}
|
|
2657
3182
|
}
|
|
2658
3183
|
async closePage(options = {}, world = null) {
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
3184
|
+
const state = {
|
|
3185
|
+
options,
|
|
3186
|
+
world,
|
|
3187
|
+
locate: false,
|
|
3188
|
+
scroll: false,
|
|
3189
|
+
highlight: false,
|
|
3190
|
+
type: Types.CLOSE_PAGE,
|
|
3191
|
+
text: `Close page`,
|
|
3192
|
+
_text: `Close the page`,
|
|
3193
|
+
operation: "closePage",
|
|
3194
|
+
log: "***** close page *****\n",
|
|
3195
|
+
throwError: false,
|
|
3196
|
+
};
|
|
2664
3197
|
try {
|
|
3198
|
+
await _preCommand(state, this);
|
|
2665
3199
|
await this.page.close();
|
|
2666
3200
|
}
|
|
2667
3201
|
catch (e) {
|
|
2668
3202
|
console.log(".");
|
|
3203
|
+
await _commandError(state, e, this);
|
|
2669
3204
|
}
|
|
2670
3205
|
finally {
|
|
2671
|
-
await
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3206
|
+
await _commandFinally(state, this);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3210
|
+
let operation = null;
|
|
3211
|
+
if (!options || !options.operation) {
|
|
3212
|
+
throw new Error("operation is not defined");
|
|
3213
|
+
}
|
|
3214
|
+
operation = options.operation;
|
|
3215
|
+
// validate operation is one of the supported operations
|
|
3216
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3217
|
+
throw new Error("operation is not supported");
|
|
3218
|
+
}
|
|
3219
|
+
const state = {
|
|
3220
|
+
options,
|
|
3221
|
+
world,
|
|
3222
|
+
locate: false,
|
|
3223
|
+
scroll: false,
|
|
3224
|
+
highlight: false,
|
|
3225
|
+
type: Types.TABLE_OPERATION,
|
|
3226
|
+
text: `Table operation`,
|
|
3227
|
+
_text: `Table ${operation} operation`,
|
|
3228
|
+
operation: operation,
|
|
3229
|
+
log: "***** Table operation *****\n",
|
|
3230
|
+
};
|
|
3231
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3232
|
+
try {
|
|
3233
|
+
await _preCommand(state, this);
|
|
3234
|
+
const start = Date.now();
|
|
3235
|
+
let cellArea = null;
|
|
3236
|
+
while (true) {
|
|
3237
|
+
try {
|
|
3238
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3239
|
+
if (cellArea) {
|
|
3240
|
+
break;
|
|
2684
3241
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
3242
|
+
}
|
|
3243
|
+
catch (e) {
|
|
3244
|
+
// ignore
|
|
3245
|
+
}
|
|
3246
|
+
if (Date.now() - start > timeout) {
|
|
3247
|
+
throw new Error(`Cell not found in table`);
|
|
3248
|
+
}
|
|
3249
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3250
|
+
}
|
|
3251
|
+
switch (operation) {
|
|
3252
|
+
case "click":
|
|
3253
|
+
if (!options.css) {
|
|
3254
|
+
// will click in the center of the cell
|
|
3255
|
+
let xOffset = 0;
|
|
3256
|
+
let yOffset = 0;
|
|
3257
|
+
if (options.xOffset) {
|
|
3258
|
+
xOffset = options.xOffset;
|
|
3259
|
+
}
|
|
3260
|
+
if (options.yOffset) {
|
|
3261
|
+
yOffset = options.yOffset;
|
|
3262
|
+
}
|
|
3263
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3264
|
+
}
|
|
3265
|
+
else {
|
|
3266
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3267
|
+
if (results.length === 0) {
|
|
3268
|
+
throw new Error(`Element not found in cell area`);
|
|
3269
|
+
}
|
|
3270
|
+
state.element = results[0];
|
|
3271
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3272
|
+
}
|
|
3273
|
+
break;
|
|
3274
|
+
case "hover+click":
|
|
3275
|
+
if (!options.css) {
|
|
3276
|
+
throw new Error("css is not defined");
|
|
3277
|
+
}
|
|
3278
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3279
|
+
if (results.length === 0) {
|
|
3280
|
+
throw new Error(`Element not found in cell area`);
|
|
3281
|
+
}
|
|
3282
|
+
state.element = results[0];
|
|
3283
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3284
|
+
break;
|
|
3285
|
+
default:
|
|
3286
|
+
throw new Error("operation is not supported");
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
catch (e) {
|
|
3290
|
+
await _commandError(state, e, this);
|
|
3291
|
+
}
|
|
3292
|
+
finally {
|
|
3293
|
+
await _commandFinally(state, this);
|
|
2692
3294
|
}
|
|
2693
3295
|
}
|
|
3296
|
+
saveTestDataAsGlobal(options, world) {
|
|
3297
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
3298
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
3299
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
3300
|
+
}
|
|
2694
3301
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2695
3302
|
const startTime = Date.now();
|
|
2696
3303
|
let error = null;
|
|
@@ -2708,21 +3315,23 @@ class StableBrowser {
|
|
|
2708
3315
|
}
|
|
2709
3316
|
catch (e) {
|
|
2710
3317
|
console.log(".");
|
|
3318
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2711
3319
|
}
|
|
2712
3320
|
finally {
|
|
2713
3321
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2714
3322
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2715
3323
|
const endTime = Date.now();
|
|
2716
|
-
|
|
3324
|
+
_reportToWorld(world, {
|
|
2717
3325
|
type: Types.SET_VIEWPORT,
|
|
2718
3326
|
text: "set viewport size to " + width + "x" + hight,
|
|
3327
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2719
3328
|
screenshotId,
|
|
2720
3329
|
result: error
|
|
2721
3330
|
? {
|
|
2722
3331
|
status: "FAILED",
|
|
2723
3332
|
startTime,
|
|
2724
3333
|
endTime,
|
|
2725
|
-
message: error
|
|
3334
|
+
message: error?.message,
|
|
2726
3335
|
}
|
|
2727
3336
|
: {
|
|
2728
3337
|
status: "PASSED",
|
|
@@ -2744,12 +3353,13 @@ class StableBrowser {
|
|
|
2744
3353
|
}
|
|
2745
3354
|
catch (e) {
|
|
2746
3355
|
console.log(".");
|
|
3356
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2747
3357
|
}
|
|
2748
3358
|
finally {
|
|
2749
3359
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2750
3360
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2751
3361
|
const endTime = Date.now();
|
|
2752
|
-
|
|
3362
|
+
_reportToWorld(world, {
|
|
2753
3363
|
type: Types.GET_PAGE_STATUS,
|
|
2754
3364
|
text: "page relaod",
|
|
2755
3365
|
screenshotId,
|
|
@@ -2758,7 +3368,7 @@ class StableBrowser {
|
|
|
2758
3368
|
status: "FAILED",
|
|
2759
3369
|
startTime,
|
|
2760
3370
|
endTime,
|
|
2761
|
-
message: error
|
|
3371
|
+
message: error?.message,
|
|
2762
3372
|
}
|
|
2763
3373
|
: {
|
|
2764
3374
|
status: "PASSED",
|
|
@@ -2785,11 +3395,123 @@ class StableBrowser {
|
|
|
2785
3395
|
console.log("#-#");
|
|
2786
3396
|
}
|
|
2787
3397
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3398
|
+
async beforeScenario(world, scenario) {
|
|
3399
|
+
this.beforeScenarioCalled = true;
|
|
3400
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3401
|
+
this.scenarioName = scenario.pickle.name;
|
|
3402
|
+
}
|
|
3403
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3404
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3405
|
+
}
|
|
3406
|
+
if (this.context) {
|
|
3407
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3408
|
+
}
|
|
3409
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3410
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3411
|
+
// check if @global_test_data tag is present
|
|
3412
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3413
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
// update test data based on feature/scenario
|
|
3417
|
+
let envName = null;
|
|
3418
|
+
if (this.context && this.context.environment) {
|
|
3419
|
+
envName = this.context.environment.name;
|
|
3420
|
+
}
|
|
3421
|
+
if (!process.env.TEMP_RUN) {
|
|
3422
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3423
|
+
}
|
|
3424
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3425
|
+
}
|
|
3426
|
+
async afterScenario(world, scenario) { }
|
|
3427
|
+
async beforeStep(world, step) {
|
|
3428
|
+
if (!this.beforeScenarioCalled) {
|
|
3429
|
+
this.beforeScenario(world, step);
|
|
3430
|
+
}
|
|
3431
|
+
if (this.stepIndex === undefined) {
|
|
3432
|
+
this.stepIndex = 0;
|
|
3433
|
+
}
|
|
3434
|
+
else {
|
|
3435
|
+
this.stepIndex++;
|
|
3436
|
+
}
|
|
3437
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3438
|
+
this.stepName = step.pickleStep.text;
|
|
3439
|
+
this.logger.info("step: " + this.stepName);
|
|
3440
|
+
}
|
|
3441
|
+
else if (step && step.text) {
|
|
3442
|
+
this.stepName = step.text;
|
|
3443
|
+
}
|
|
3444
|
+
else {
|
|
3445
|
+
this.stepName = "step " + this.stepIndex;
|
|
3446
|
+
}
|
|
3447
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3448
|
+
if (this.context.browserObject.context) {
|
|
3449
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
if (this.initSnapshotTaken === false) {
|
|
3453
|
+
this.initSnapshotTaken = true;
|
|
3454
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3455
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3456
|
+
if (snapshot) {
|
|
3457
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
async getAriaSnapshot() {
|
|
3463
|
+
try {
|
|
3464
|
+
// find the page url
|
|
3465
|
+
const url = await this.page.url();
|
|
3466
|
+
// extract the path from the url
|
|
3467
|
+
const path = new URL(url).pathname;
|
|
3468
|
+
// get the page title
|
|
3469
|
+
const title = await this.page.title();
|
|
3470
|
+
// go over other frams
|
|
3471
|
+
const frames = this.page.frames();
|
|
3472
|
+
const snapshots = [];
|
|
3473
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3474
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3475
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3476
|
+
content.push(`- frame: ${i}`);
|
|
3477
|
+
const frame = frames[i];
|
|
3478
|
+
const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
|
|
3479
|
+
content.push(snapshot);
|
|
3480
|
+
}
|
|
3481
|
+
return content.join("\n");
|
|
3482
|
+
}
|
|
3483
|
+
catch (e) {
|
|
3484
|
+
console.log("Error in getAriaSnapshot");
|
|
3485
|
+
console.debug(e);
|
|
3486
|
+
}
|
|
3487
|
+
return null;
|
|
3488
|
+
}
|
|
3489
|
+
async afterStep(world, step) {
|
|
3490
|
+
this.stepName = null;
|
|
3491
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3492
|
+
if (this.context.browserObject.context) {
|
|
3493
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
3494
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3495
|
+
});
|
|
3496
|
+
if (world && world.attach) {
|
|
3497
|
+
await world.attach(JSON.stringify({
|
|
3498
|
+
type: "trace",
|
|
3499
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3500
|
+
}), "application/json+trace");
|
|
3501
|
+
}
|
|
3502
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
if (this.context) {
|
|
3506
|
+
this.context.examplesRow = null;
|
|
3507
|
+
}
|
|
3508
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3509
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3510
|
+
if (snapshot) {
|
|
3511
|
+
const obj = {};
|
|
3512
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3513
|
+
}
|
|
2791
3514
|
}
|
|
2792
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2793
3515
|
}
|
|
2794
3516
|
}
|
|
2795
3517
|
function createTimedPromise(promise, label) {
|
|
@@ -2797,151 +3519,5 @@ function createTimedPromise(promise, label) {
|
|
|
2797
3519
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2798
3520
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2799
3521
|
}
|
|
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
3522
|
export { StableBrowser };
|
|
2947
3523
|
//# sourceMappingURL=stable_browser.js.map
|