automation_model 1.0.445-dev → 1.0.445
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/analyze_helper.js.map +1 -1
- package/lib/api.d.ts +43 -2
- package/lib/api.js +239 -49
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +5 -2
- package/lib/auto_page.js +231 -49
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +7 -3
- package/lib/browser_manager.js +172 -48
- 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 +202 -0
- package/lib/command_common.js.map +1 -0
- package/lib/date_time.js.map +1 -1
- package/lib/drawRect.js.map +1 -1
- package/lib/environment.d.ts +4 -0
- package/lib/environment.js +6 -2
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.d.ts +6 -0
- package/lib/error-messages.js +206 -0
- package/lib/error-messages.js.map +1 -0
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +61 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/find_function.js.map +1 -1
- package/lib/generation_scripts.d.ts +4 -0
- package/lib/generation_scripts.js +2 -0
- package/lib/generation_scripts.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +5 -2
- package/lib/init_browser.js +128 -11
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.d.ts +7 -0
- package/lib/locate_element.js +215 -0
- package/lib/locate_element.js.map +1 -0
- package/lib/locator.d.ts +37 -0
- package/lib/locator.js +172 -0
- package/lib/locator.js.map +1 -1
- package/lib/locator_log.d.ts +26 -0
- package/lib/locator_log.js +69 -0
- package/lib/locator_log.js.map +1 -0
- package/lib/network.d.ts +3 -0
- package/lib/network.js +183 -0
- package/lib/network.js.map +1 -0
- package/lib/scripts/axe.mini.js +12 -0
- package/lib/snapshot_validation.d.ts +37 -0
- package/lib/snapshot_validation.js +357 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +152 -56
- package/lib/stable_browser.js +2416 -1303
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +15 -0
- package/lib/table.js +257 -0
- package/lib/table.js.map +1 -0
- package/lib/table_analyze.js.map +1 -1
- package/lib/table_helper.d.ts +19 -0
- package/lib/table_helper.js +116 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +7 -0
- package/lib/test_context.js +15 -10
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +22 -2
- package/lib/utils.js +678 -11
- package/lib/utils.js.map +1 -1
- package/package.json +18 -10
package/lib/stable_browser.js
CHANGED
|
@@ -2,33 +2,47 @@
|
|
|
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 { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
27
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
28
|
+
export const Types = {
|
|
19
29
|
CLICK: "click_element",
|
|
20
|
-
|
|
30
|
+
WAIT_ELEMENT: "wait_element",
|
|
31
|
+
NAVIGATE: "navigate", ///
|
|
21
32
|
FILL: "fill_element",
|
|
22
|
-
EXECUTE: "execute_page_method",
|
|
23
|
-
OPEN: "open_environment",
|
|
33
|
+
EXECUTE: "execute_page_method", //
|
|
34
|
+
OPEN: "open_environment", //
|
|
24
35
|
COMPLETE: "step_complete",
|
|
25
36
|
ASK: "information_needed",
|
|
26
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
27
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
37
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
38
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
28
39
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
40
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
41
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
29
42
|
ANALYZE_TABLE: "analyze_table",
|
|
30
|
-
SELECT: "select_combobox",
|
|
43
|
+
SELECT: "select_combobox", //
|
|
31
44
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
45
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
32
46
|
TYPE_PRESS: "type_press",
|
|
33
47
|
PRESS: "press_key",
|
|
34
48
|
HOVER: "hover_element",
|
|
@@ -36,21 +50,49 @@ const Types = {
|
|
|
36
50
|
UNCHECK: "uncheck_element",
|
|
37
51
|
EXTRACT: "extract_attribute",
|
|
38
52
|
CLOSE_PAGE: "close_page",
|
|
53
|
+
TABLE_OPERATION: "table_operation",
|
|
39
54
|
SET_DATE_TIME: "set_date_time",
|
|
40
55
|
SET_VIEWPORT: "set_viewport",
|
|
41
56
|
VERIFY_VISUAL: "verify_visual",
|
|
42
57
|
LOAD_DATA: "load_data",
|
|
43
58
|
SET_INPUT: "set_input",
|
|
59
|
+
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
60
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
61
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
62
|
+
BRUNO: "bruno",
|
|
63
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
64
|
+
SET_INPUT_FILES: "set_input_files",
|
|
65
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
66
|
+
REPORT_COMMAND: "report_command",
|
|
67
|
+
STEP_COMPLETE: "step_complete",
|
|
68
|
+
SLEEP: "sleep",
|
|
69
|
+
};
|
|
70
|
+
export const apps = {};
|
|
71
|
+
const formatElementName = (elementName) => {
|
|
72
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
44
73
|
};
|
|
45
74
|
class StableBrowser {
|
|
46
|
-
|
|
75
|
+
browser;
|
|
76
|
+
page;
|
|
77
|
+
logger;
|
|
78
|
+
context;
|
|
79
|
+
world;
|
|
80
|
+
fastMode;
|
|
81
|
+
project_path = null;
|
|
82
|
+
webLogFile = null;
|
|
83
|
+
networkLogger = null;
|
|
84
|
+
configuration = null;
|
|
85
|
+
appName = "main";
|
|
86
|
+
tags = null;
|
|
87
|
+
isRecording = false;
|
|
88
|
+
initSnapshotTaken = false;
|
|
89
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
|
|
47
90
|
this.browser = browser;
|
|
48
91
|
this.page = page;
|
|
49
92
|
this.logger = logger;
|
|
50
93
|
this.context = context;
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
this.configuration = null;
|
|
94
|
+
this.world = world;
|
|
95
|
+
this.fastMode = fastMode;
|
|
54
96
|
if (!this.logger) {
|
|
55
97
|
this.logger = console;
|
|
56
98
|
}
|
|
@@ -75,17 +117,49 @@ class StableBrowser {
|
|
|
75
117
|
catch (e) {
|
|
76
118
|
this.logger.error("unable to read ai_config.json");
|
|
77
119
|
}
|
|
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
120
|
context.pageLoading = { status: false };
|
|
121
|
+
context.pages = [this.page];
|
|
122
|
+
const logFolder = path.join(this.project_path, "logs", "web");
|
|
123
|
+
this.world = world;
|
|
124
|
+
if (process.env.FAST_MODE === "true") {
|
|
125
|
+
this.fastMode = true;
|
|
126
|
+
}
|
|
127
|
+
if (this.context) {
|
|
128
|
+
this.context.fastMode = this.fastMode;
|
|
129
|
+
}
|
|
130
|
+
this.registerEventListeners(this.context);
|
|
131
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
132
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
133
|
+
}
|
|
134
|
+
registerEventListeners(context) {
|
|
135
|
+
this.registerConsoleLogListener(this.page, context);
|
|
136
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
137
|
+
if (!context.pageLoading) {
|
|
138
|
+
context.pageLoading = { status: false };
|
|
139
|
+
}
|
|
140
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
141
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
142
|
+
}
|
|
84
143
|
context.playContext.on("page", async function (page) {
|
|
144
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
145
|
+
console.log("close unexpected popups");
|
|
146
|
+
await page.close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
85
149
|
context.pageLoading.status = true;
|
|
86
150
|
this.page = page;
|
|
151
|
+
try {
|
|
152
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
153
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error("Error on dialog accept registration", error);
|
|
158
|
+
}
|
|
87
159
|
context.page = page;
|
|
88
160
|
context.pages.push(page);
|
|
161
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
162
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
89
163
|
page.on("close", async () => {
|
|
90
164
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
91
165
|
this.context.pages.pop();
|
|
@@ -110,117 +184,167 @@ class StableBrowser {
|
|
|
110
184
|
context.pageLoading.status = false;
|
|
111
185
|
}.bind(this));
|
|
112
186
|
}
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
187
|
+
async switchApp(appName) {
|
|
188
|
+
// check if the current app (this.appName) is the same as the new app
|
|
189
|
+
if (this.appName === appName) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
let newContextCreated = false;
|
|
193
|
+
if (!apps[appName]) {
|
|
194
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
195
|
+
newContextCreated = true;
|
|
196
|
+
apps[appName] = {
|
|
197
|
+
context: newContext,
|
|
198
|
+
browser: newContext.browser,
|
|
199
|
+
page: newContext.page,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const tempContext = {};
|
|
203
|
+
_copyContext(this, tempContext);
|
|
204
|
+
_copyContext(apps[appName], this);
|
|
205
|
+
apps[this.appName] = tempContext;
|
|
206
|
+
this.appName = appName;
|
|
207
|
+
if (newContextCreated) {
|
|
208
|
+
this.registerEventListeners(this.context);
|
|
209
|
+
await this.goto(this.context.environment.baseUrl);
|
|
210
|
+
if (!this.fastMode) {
|
|
211
|
+
await this.waitForPageLoad();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async switchTab(tabTitleOrIndex) {
|
|
216
|
+
// first check if the tabNameOrIndex is a number
|
|
217
|
+
let index = parseInt(tabTitleOrIndex);
|
|
218
|
+
if (!isNaN(index)) {
|
|
219
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
220
|
+
this.page = this.context.pages[index];
|
|
221
|
+
this.context.page = this.page;
|
|
222
|
+
await this.page.bringToFront();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
116
225
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
226
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
227
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
228
|
+
let page = this.context.pages[i];
|
|
229
|
+
let title = await page.title();
|
|
230
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
231
|
+
this.page = page;
|
|
232
|
+
this.context.page = this.page;
|
|
233
|
+
await this.page.bringToFront();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
120
236
|
}
|
|
121
|
-
|
|
122
|
-
return path.join(logFolder, fileName);
|
|
237
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
123
238
|
}
|
|
124
|
-
registerConsoleLogListener(page, context
|
|
239
|
+
registerConsoleLogListener(page, context) {
|
|
125
240
|
if (!this.context.webLogger) {
|
|
126
241
|
this.context.webLogger = [];
|
|
127
242
|
}
|
|
128
243
|
page.on("console", async (msg) => {
|
|
129
|
-
|
|
244
|
+
const obj = {
|
|
130
245
|
type: msg.type(),
|
|
131
246
|
text: msg.text(),
|
|
132
247
|
location: msg.location(),
|
|
133
248
|
time: new Date().toISOString(),
|
|
134
|
-
}
|
|
135
|
-
|
|
249
|
+
};
|
|
250
|
+
this.context.webLogger.push(obj);
|
|
251
|
+
if (msg.type() === "error") {
|
|
252
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
253
|
+
}
|
|
136
254
|
});
|
|
137
255
|
}
|
|
138
|
-
registerRequestListener() {
|
|
139
|
-
this.
|
|
256
|
+
registerRequestListener(page, context, logFile) {
|
|
257
|
+
if (!this.context.networkLogger) {
|
|
258
|
+
this.context.networkLogger = [];
|
|
259
|
+
}
|
|
260
|
+
page.on("request", async (data) => {
|
|
261
|
+
const startTime = new Date().getTime();
|
|
140
262
|
try {
|
|
141
|
-
const pageUrl = new URL(
|
|
263
|
+
const pageUrl = new URL(page.url());
|
|
142
264
|
const requestUrl = new URL(data.url());
|
|
143
265
|
if (pageUrl.hostname === requestUrl.hostname) {
|
|
144
266
|
const method = data.method();
|
|
145
|
-
if (
|
|
267
|
+
if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
146
268
|
const token = await data.headerValue("Authorization");
|
|
147
269
|
if (token) {
|
|
148
|
-
|
|
270
|
+
context.authtoken = token;
|
|
149
271
|
}
|
|
150
272
|
}
|
|
151
273
|
}
|
|
274
|
+
const response = await data.response();
|
|
275
|
+
const endTime = new Date().getTime();
|
|
276
|
+
const obj = {
|
|
277
|
+
url: data.url(),
|
|
278
|
+
method: data.method(),
|
|
279
|
+
postData: data.postData(),
|
|
280
|
+
error: data.failure() ? data.failure().errorText : null,
|
|
281
|
+
duration: endTime - startTime,
|
|
282
|
+
startTime,
|
|
283
|
+
};
|
|
284
|
+
context.networkLogger.push(obj);
|
|
285
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
152
286
|
}
|
|
153
287
|
catch (error) {
|
|
154
|
-
console.error("Error in request listener", error);
|
|
288
|
+
// console.error("Error in request listener", error);
|
|
289
|
+
context.networkLogger.push({
|
|
290
|
+
error: "not able to listen",
|
|
291
|
+
message: error.message,
|
|
292
|
+
stack: error.stack,
|
|
293
|
+
time: new Date().toISOString(),
|
|
294
|
+
});
|
|
295
|
+
// await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
|
|
155
296
|
}
|
|
156
297
|
});
|
|
157
298
|
}
|
|
158
299
|
// async closeUnexpectedPopups() {
|
|
159
300
|
// await closeUnexpectedPopups(this.page);
|
|
160
301
|
// }
|
|
161
|
-
async goto(url) {
|
|
302
|
+
async goto(url, world = null) {
|
|
303
|
+
if (!url) {
|
|
304
|
+
throw new Error("url is null, verify that the environment file is correct");
|
|
305
|
+
}
|
|
306
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
162
307
|
if (!url.startsWith("http")) {
|
|
163
308
|
url = "https://" + url;
|
|
164
309
|
}
|
|
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;
|
|
310
|
+
const state = {
|
|
311
|
+
value: url,
|
|
312
|
+
world: world,
|
|
313
|
+
type: Types.NAVIGATE,
|
|
314
|
+
text: `Navigate Page to: ${url}`,
|
|
315
|
+
operation: "goto",
|
|
316
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
317
|
+
info: {},
|
|
318
|
+
locate: false,
|
|
319
|
+
scroll: false,
|
|
320
|
+
screenshot: false,
|
|
321
|
+
highlight: false,
|
|
322
|
+
};
|
|
323
|
+
try {
|
|
324
|
+
await _preCommand(state, this);
|
|
325
|
+
await this.page.goto(url, {
|
|
326
|
+
timeout: 60000,
|
|
327
|
+
});
|
|
328
|
+
await _screenshot(state, this);
|
|
186
329
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// remove the _ prefix
|
|
191
|
-
regValue = key.substring(1);
|
|
192
|
-
}
|
|
193
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
330
|
+
catch (error) {
|
|
331
|
+
console.error("Error on goto", error);
|
|
332
|
+
_commandError(state, error, this);
|
|
194
333
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
198
|
-
// check if not null
|
|
199
|
-
if (!locator) {
|
|
200
|
-
return locator;
|
|
334
|
+
finally {
|
|
335
|
+
await _commandFinally(state, this);
|
|
201
336
|
}
|
|
202
|
-
// clone the locator
|
|
203
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
204
|
-
this.scanAndManipulate(locator, _params);
|
|
205
|
-
return locator;
|
|
206
|
-
}
|
|
207
|
-
_isObject(value) {
|
|
208
|
-
return value && typeof value === "object" && value.constructor === Object;
|
|
209
337
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
338
|
+
async _getLocator(locator, scope, _params) {
|
|
339
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
340
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
341
|
+
for (let key in locator) {
|
|
342
|
+
if (typeof locator[key] !== "string")
|
|
343
|
+
continue;
|
|
344
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
345
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
219
346
|
}
|
|
220
347
|
}
|
|
221
|
-
}
|
|
222
|
-
_getLocator(locator, scope, _params) {
|
|
223
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
224
348
|
let locatorReturn;
|
|
225
349
|
if (locator.role) {
|
|
226
350
|
if (locator.role[1].nameReg) {
|
|
@@ -228,7 +352,7 @@ class StableBrowser {
|
|
|
228
352
|
delete locator.role[1].nameReg;
|
|
229
353
|
}
|
|
230
354
|
// if (locator.role[1].name) {
|
|
231
|
-
// locator.role[1].name =
|
|
355
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
232
356
|
// }
|
|
233
357
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
234
358
|
}
|
|
@@ -247,7 +371,7 @@ class StableBrowser {
|
|
|
247
371
|
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
248
372
|
}
|
|
249
373
|
}
|
|
250
|
-
if (locator
|
|
374
|
+
if (locator?.engine) {
|
|
251
375
|
if (locator.engine === "css") {
|
|
252
376
|
locatorReturn = scope.locator(locator.selector);
|
|
253
377
|
}
|
|
@@ -268,192 +392,181 @@ class StableBrowser {
|
|
|
268
392
|
return locatorReturn;
|
|
269
393
|
}
|
|
270
394
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
271
|
-
|
|
395
|
+
if (css && css.locator) {
|
|
396
|
+
css = css.locator;
|
|
397
|
+
}
|
|
398
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
272
399
|
if (result.elementCount === 0) {
|
|
273
400
|
return;
|
|
274
401
|
}
|
|
275
|
-
let textElementCss = "[data-blinq-id
|
|
402
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
276
403
|
// css climb to parent element
|
|
277
404
|
const climbArray = [];
|
|
278
405
|
for (let i = 0; i < climb; i++) {
|
|
279
406
|
climbArray.push("..");
|
|
280
407
|
}
|
|
281
408
|
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
|
-
}
|
|
409
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
410
|
+
if (css) {
|
|
411
|
+
resultCss = resultCss + " >> " + css;
|
|
412
|
+
}
|
|
413
|
+
return resultCss;
|
|
414
|
+
}
|
|
415
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
416
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
417
|
+
const locator = scope.locator(query);
|
|
418
|
+
const count = await locator.count();
|
|
419
|
+
if (!tag1) {
|
|
420
|
+
tag1 = "*";
|
|
421
|
+
}
|
|
422
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
423
|
+
let tagCount = 0;
|
|
424
|
+
for (let i = 0; i < count; i++) {
|
|
425
|
+
const element = locator.nth(i);
|
|
426
|
+
// check if the tag matches
|
|
427
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
428
|
+
if (!tag.startsWith("*")) {
|
|
429
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!el.setAttribute) {
|
|
434
|
+
el = el.parentElement;
|
|
435
|
+
}
|
|
436
|
+
// remove any attributes start with data-blinq-id
|
|
437
|
+
// for (let i = 0; i < el.attributes.length; i++) {
|
|
438
|
+
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
439
|
+
// el.removeAttribute(el.attributes[i].name);
|
|
440
|
+
// }
|
|
441
|
+
// }
|
|
442
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
443
|
+
return true;
|
|
444
|
+
}, [tag1, randomToken]))) {
|
|
445
|
+
continue;
|
|
385
446
|
}
|
|
386
|
-
|
|
387
|
-
}
|
|
447
|
+
tagCount++;
|
|
448
|
+
}
|
|
449
|
+
return { elementCount: tagCount, randomToken };
|
|
388
450
|
}
|
|
389
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
451
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
452
|
+
if (!info) {
|
|
453
|
+
info = {};
|
|
454
|
+
}
|
|
455
|
+
if (!info.failCause) {
|
|
456
|
+
info.failCause = {};
|
|
457
|
+
}
|
|
458
|
+
if (!info.log) {
|
|
459
|
+
info.log = "";
|
|
460
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
461
|
+
}
|
|
390
462
|
let locatorSearch = selectorHierarchy[index];
|
|
463
|
+
let originalLocatorSearch = "";
|
|
464
|
+
try {
|
|
465
|
+
originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
|
|
466
|
+
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
console.error(e);
|
|
470
|
+
}
|
|
391
471
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
392
472
|
let locator = null;
|
|
393
473
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
394
|
-
|
|
474
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
475
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
395
476
|
if (!locatorString) {
|
|
477
|
+
info.failCause.textNotFound = true;
|
|
478
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
396
479
|
return;
|
|
397
480
|
}
|
|
398
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
481
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
399
482
|
}
|
|
400
483
|
else if (locatorSearch.text) {
|
|
401
|
-
let
|
|
484
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
485
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
402
486
|
if (result.elementCount === 0) {
|
|
487
|
+
info.failCause.textNotFound = true;
|
|
488
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
403
489
|
return;
|
|
404
490
|
}
|
|
405
|
-
locatorSearch.css = "[data-blinq-id
|
|
491
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
406
492
|
if (locatorSearch.childCss) {
|
|
407
493
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
408
494
|
}
|
|
409
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
495
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
410
496
|
}
|
|
411
497
|
else {
|
|
412
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
498
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
413
499
|
}
|
|
414
500
|
// let cssHref = false;
|
|
415
501
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
416
502
|
// cssHref = true;
|
|
417
503
|
// }
|
|
418
504
|
let count = await locator.count();
|
|
505
|
+
if (count > 0 && !info.failCause.count) {
|
|
506
|
+
info.failCause.count = count;
|
|
507
|
+
}
|
|
419
508
|
//info.log += "total elements found " + count + "\n";
|
|
420
509
|
//let visibleCount = 0;
|
|
421
510
|
let visibleLocator = null;
|
|
422
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
511
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
423
512
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
513
|
+
if (info.locatorLog) {
|
|
514
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
515
|
+
}
|
|
424
516
|
return;
|
|
425
517
|
}
|
|
518
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
519
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
520
|
+
}
|
|
426
521
|
for (let j = 0; j < count; j++) {
|
|
427
522
|
let visible = await locator.nth(j).isVisible();
|
|
428
523
|
const enabled = await locator.nth(j).isEnabled();
|
|
429
524
|
if (!visibleOnly) {
|
|
430
525
|
visible = true;
|
|
431
526
|
}
|
|
432
|
-
if (visible && enabled) {
|
|
527
|
+
if (visible && (allowDisabled || enabled)) {
|
|
433
528
|
foundLocators.push(locator.nth(j));
|
|
529
|
+
if (info.locatorLog) {
|
|
530
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
531
|
+
}
|
|
434
532
|
}
|
|
435
|
-
else {
|
|
533
|
+
else if (logErrors) {
|
|
534
|
+
info.failCause.visible = visible;
|
|
535
|
+
info.failCause.enabled = enabled;
|
|
436
536
|
if (!info.printMessages) {
|
|
437
537
|
info.printMessages = {};
|
|
438
538
|
}
|
|
539
|
+
if (info.locatorLog && !visible) {
|
|
540
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
541
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
542
|
+
}
|
|
543
|
+
if (info.locatorLog && !enabled) {
|
|
544
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
545
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
546
|
+
}
|
|
439
547
|
if (!info.printMessages[j.toString()]) {
|
|
440
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
548
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
441
549
|
info.printMessages[j.toString()] = true;
|
|
442
550
|
}
|
|
443
551
|
}
|
|
444
552
|
}
|
|
445
553
|
}
|
|
446
554
|
async closeUnexpectedPopups(info, _params) {
|
|
555
|
+
if (!info) {
|
|
556
|
+
info = {};
|
|
557
|
+
info.failCause = {};
|
|
558
|
+
info.log = "";
|
|
559
|
+
}
|
|
447
560
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
448
561
|
if (!info) {
|
|
449
562
|
info = {};
|
|
450
563
|
}
|
|
451
|
-
info.log += "scan for popup handlers" + "\n";
|
|
564
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
452
565
|
const handlerGroup = [];
|
|
453
566
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
454
567
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
455
568
|
}
|
|
456
|
-
const scopes =
|
|
569
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
457
570
|
let result = null;
|
|
458
571
|
let scope = null;
|
|
459
572
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -475,55 +588,108 @@ class StableBrowser {
|
|
|
475
588
|
}
|
|
476
589
|
if (result.foundElements.length > 0) {
|
|
477
590
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
591
|
+
try {
|
|
592
|
+
await scope?.evaluate(() => {
|
|
593
|
+
window.__isClosingPopups = true;
|
|
594
|
+
});
|
|
595
|
+
await dialogCloseLocator.click();
|
|
596
|
+
// wait for the dialog to close
|
|
597
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
await scope?.evaluate(() => {
|
|
603
|
+
window.__isClosingPopups = false;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
481
606
|
return { rerun: true };
|
|
482
607
|
}
|
|
483
608
|
}
|
|
484
609
|
}
|
|
485
610
|
return { rerun: false };
|
|
486
611
|
}
|
|
487
|
-
async _locate(selectors, info, _params, timeout =
|
|
612
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
613
|
+
if (!timeout) {
|
|
614
|
+
timeout = 30000;
|
|
615
|
+
}
|
|
488
616
|
for (let i = 0; i < 3; i++) {
|
|
489
617
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
490
618
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
491
619
|
let selector = selectors.locators[j];
|
|
492
620
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
493
621
|
}
|
|
494
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
622
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
495
623
|
if (!element.rerun) {
|
|
496
|
-
|
|
624
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
625
|
+
await element.evaluate((el, randomToken) => {
|
|
626
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
627
|
+
}, randomToken);
|
|
628
|
+
// if (element._frame) {
|
|
629
|
+
// return element;
|
|
630
|
+
// }
|
|
631
|
+
const scope = element._frame ?? element.page();
|
|
632
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
633
|
+
let prefixSelector = "";
|
|
634
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
635
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
636
|
+
if (frameSelectorIndex !== -1) {
|
|
637
|
+
// remove everything after the >> internal:control=enter-frame
|
|
638
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
639
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
640
|
+
}
|
|
641
|
+
// if (element?._frame?._selector) {
|
|
642
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
643
|
+
// }
|
|
644
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
645
|
+
return scope.locator(newSelector);
|
|
497
646
|
}
|
|
498
647
|
}
|
|
499
648
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
500
649
|
}
|
|
501
|
-
async
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
650
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
651
|
+
if (!info) {
|
|
652
|
+
info = {};
|
|
653
|
+
info.failCause = {};
|
|
654
|
+
info.log = "";
|
|
655
|
+
}
|
|
656
|
+
let startTime = Date.now();
|
|
507
657
|
let scope = this.page;
|
|
658
|
+
if (selectors.frame) {
|
|
659
|
+
return selectors.frame;
|
|
660
|
+
}
|
|
508
661
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
509
|
-
const findFrame = (frame, framescope) => {
|
|
662
|
+
const findFrame = async (frame, framescope) => {
|
|
510
663
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
511
664
|
let frameLocator = frame.selectors[i];
|
|
512
665
|
if (frameLocator.css) {
|
|
513
|
-
|
|
514
|
-
|
|
666
|
+
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
667
|
+
if (frameLocator.index) {
|
|
668
|
+
testframescope = framescope.nth(frameLocator.index);
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
await testframescope.owner().evaluateHandle(() => true, null, {
|
|
672
|
+
timeout: 5000,
|
|
673
|
+
});
|
|
674
|
+
framescope = testframescope;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
console.error("frame not found " + frameLocator.css);
|
|
679
|
+
}
|
|
515
680
|
}
|
|
516
681
|
}
|
|
517
682
|
if (frame.children) {
|
|
518
|
-
return findFrame(frame.children, framescope);
|
|
683
|
+
return await findFrame(frame.children, framescope);
|
|
519
684
|
}
|
|
520
685
|
return framescope;
|
|
521
686
|
};
|
|
522
|
-
|
|
687
|
+
let fLocator = null;
|
|
523
688
|
while (true) {
|
|
524
689
|
let frameFound = false;
|
|
525
690
|
if (selectors.nestFrmLoc) {
|
|
526
|
-
|
|
691
|
+
fLocator = selectors.nestFrmLoc;
|
|
692
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
527
693
|
frameFound = true;
|
|
528
694
|
break;
|
|
529
695
|
}
|
|
@@ -531,6 +697,7 @@ class StableBrowser {
|
|
|
531
697
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
532
698
|
let frameLocator = selectors.frameLocators[i];
|
|
533
699
|
if (frameLocator.css) {
|
|
700
|
+
fLocator = frameLocator.css;
|
|
534
701
|
scope = scope.frameLocator(frameLocator.css);
|
|
535
702
|
frameFound = true;
|
|
536
703
|
break;
|
|
@@ -538,20 +705,55 @@ class StableBrowser {
|
|
|
538
705
|
}
|
|
539
706
|
}
|
|
540
707
|
if (!frameFound && selectors.iframe_src) {
|
|
708
|
+
fLocator = selectors.iframe_src;
|
|
541
709
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
542
710
|
}
|
|
543
711
|
if (!scope) {
|
|
544
|
-
info
|
|
545
|
-
|
|
712
|
+
if (info && info.locatorLog) {
|
|
713
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
714
|
+
}
|
|
715
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
716
|
+
if (Date.now() - startTime > timeout) {
|
|
717
|
+
info.failCause.iframeNotFound = true;
|
|
718
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
546
719
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
547
720
|
}
|
|
548
721
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
549
722
|
}
|
|
550
723
|
else {
|
|
724
|
+
if (info && info.locatorLog) {
|
|
725
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
726
|
+
}
|
|
551
727
|
break;
|
|
552
728
|
}
|
|
553
729
|
}
|
|
554
730
|
}
|
|
731
|
+
if (!scope) {
|
|
732
|
+
scope = this.page;
|
|
733
|
+
}
|
|
734
|
+
return scope;
|
|
735
|
+
}
|
|
736
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
737
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
738
|
+
return scope.evaluate(() => {
|
|
739
|
+
var bodyContent = document.body.innerHTML;
|
|
740
|
+
return bodyContent;
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
744
|
+
if (!info) {
|
|
745
|
+
info = {};
|
|
746
|
+
info.failCause = {};
|
|
747
|
+
info.log = "";
|
|
748
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
749
|
+
}
|
|
750
|
+
let highPriorityTimeout = 5000;
|
|
751
|
+
let visibleOnlyTimeout = 6000;
|
|
752
|
+
let startTime = Date.now();
|
|
753
|
+
let locatorsCount = 0;
|
|
754
|
+
let lazy_scroll = false;
|
|
755
|
+
//let arrayMode = Array.isArray(selectors);
|
|
756
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
555
757
|
let selectorsLocators = null;
|
|
556
758
|
selectorsLocators = selectors.locators;
|
|
557
759
|
// group selectors by priority
|
|
@@ -587,18 +789,13 @@ class StableBrowser {
|
|
|
587
789
|
}
|
|
588
790
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
589
791
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
590
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
792
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
591
793
|
if (result.foundElements.length === 0) {
|
|
592
794
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
593
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
594
|
-
}
|
|
595
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
596
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
795
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
597
796
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
601
|
-
}
|
|
797
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
798
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
602
799
|
}
|
|
603
800
|
let foundElements = result.foundElements;
|
|
604
801
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -638,24 +835,43 @@ class StableBrowser {
|
|
|
638
835
|
return maxCountElement.locator;
|
|
639
836
|
}
|
|
640
837
|
}
|
|
641
|
-
if (
|
|
838
|
+
if (Date.now() - startTime > timeout) {
|
|
642
839
|
break;
|
|
643
840
|
}
|
|
644
|
-
if (
|
|
645
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
841
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
842
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
646
843
|
highPriorityOnly = false;
|
|
844
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
845
|
+
lazy_scroll = true;
|
|
846
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
847
|
+
}
|
|
647
848
|
}
|
|
648
|
-
if (
|
|
649
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
849
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
850
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
650
851
|
visibleOnly = false;
|
|
651
852
|
}
|
|
652
853
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
854
|
+
// sheck of more of half of the timeout has passed
|
|
855
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
856
|
+
highPriorityOnly = false;
|
|
857
|
+
visibleOnly = false;
|
|
858
|
+
}
|
|
653
859
|
}
|
|
654
860
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
655
|
-
info.
|
|
861
|
+
// if (info.locatorLog) {
|
|
862
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
863
|
+
// for (let line of lines) {
|
|
864
|
+
// this.logger.debug(line);
|
|
865
|
+
// }
|
|
866
|
+
// }
|
|
867
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
868
|
+
info.failCause.locatorNotFound = true;
|
|
869
|
+
if (!info?.failCause?.lastError) {
|
|
870
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
871
|
+
}
|
|
656
872
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
657
873
|
}
|
|
658
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
874
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
659
875
|
let foundElements = [];
|
|
660
876
|
const result = {
|
|
661
877
|
foundElements: foundElements,
|
|
@@ -663,17 +879,20 @@ class StableBrowser {
|
|
|
663
879
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
664
880
|
let foundLocators = [];
|
|
665
881
|
try {
|
|
666
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
882
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
667
883
|
}
|
|
668
884
|
catch (e) {
|
|
669
|
-
this
|
|
670
|
-
this.logger.debug(
|
|
885
|
+
// this call can fail it the browser is navigating
|
|
886
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
887
|
+
// this.logger.debug(e);
|
|
671
888
|
foundLocators = [];
|
|
672
889
|
try {
|
|
673
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
890
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
674
891
|
}
|
|
675
892
|
catch (e) {
|
|
676
|
-
|
|
893
|
+
if (logErrors) {
|
|
894
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
895
|
+
}
|
|
677
896
|
}
|
|
678
897
|
}
|
|
679
898
|
if (foundLocators.length === 1) {
|
|
@@ -684,270 +903,350 @@ class StableBrowser {
|
|
|
684
903
|
});
|
|
685
904
|
result.locatorIndex = i;
|
|
686
905
|
}
|
|
906
|
+
if (foundLocators.length > 1) {
|
|
907
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
908
|
+
const boxes = [];
|
|
909
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
910
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
911
|
+
}
|
|
912
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
913
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
914
|
+
if (j === k) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
918
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
919
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
920
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
921
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
922
|
+
// as the element is not unique, will remove it
|
|
923
|
+
boxes.splice(k, 1);
|
|
924
|
+
k--;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (boxes.length === 1) {
|
|
929
|
+
result.foundElements.push({
|
|
930
|
+
locator: boxes[0].locator.first(),
|
|
931
|
+
box: boxes[0].box,
|
|
932
|
+
unique: true,
|
|
933
|
+
});
|
|
934
|
+
result.locatorIndex = i;
|
|
935
|
+
}
|
|
936
|
+
else if (logErrors) {
|
|
937
|
+
info.failCause.foundMultiple = true;
|
|
938
|
+
if (info.locatorLog) {
|
|
939
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
687
943
|
}
|
|
688
944
|
return result;
|
|
689
945
|
}
|
|
690
|
-
async
|
|
691
|
-
|
|
946
|
+
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
947
|
+
const state = {
|
|
948
|
+
locate: false,
|
|
949
|
+
scroll: false,
|
|
950
|
+
highlight: false,
|
|
951
|
+
_params,
|
|
952
|
+
options,
|
|
953
|
+
world,
|
|
954
|
+
type: Types.CLICK,
|
|
955
|
+
text: "Click element",
|
|
956
|
+
operation: "simpleClick",
|
|
957
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
958
|
+
};
|
|
959
|
+
_preCommand(state, this);
|
|
692
960
|
const startTime = Date.now();
|
|
693
|
-
|
|
694
|
-
|
|
961
|
+
let timeout = 30000;
|
|
962
|
+
if (options && options.timeout) {
|
|
963
|
+
timeout = options.timeout;
|
|
695
964
|
}
|
|
696
|
-
|
|
697
|
-
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
698
|
-
info.operation = "click";
|
|
699
|
-
info.selectors = selectors;
|
|
700
|
-
let error = null;
|
|
701
|
-
let screenshotId = null;
|
|
702
|
-
let screenshotPath = null;
|
|
703
|
-
try {
|
|
704
|
-
let element = await this._locate(selectors, info, _params);
|
|
705
|
-
await this.scrollIfNeeded(element, info);
|
|
706
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
965
|
+
while (true) {
|
|
707
966
|
try {
|
|
708
|
-
await this.
|
|
709
|
-
|
|
710
|
-
|
|
967
|
+
const result = await locate_element(this.context, elementDescription, "click");
|
|
968
|
+
if (result?.elementNumber >= 0) {
|
|
969
|
+
const selectors = {
|
|
970
|
+
frame: result?.frame,
|
|
971
|
+
locators: [
|
|
972
|
+
{
|
|
973
|
+
css: result?.css,
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
};
|
|
977
|
+
await this.click(selectors, _params, options, world);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
711
980
|
}
|
|
712
981
|
catch (e) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
982
|
+
if (performance.now() - startTime > timeout) {
|
|
983
|
+
// throw e;
|
|
984
|
+
try {
|
|
985
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
await _commandFinally(state, this);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
718
991
|
}
|
|
719
|
-
await
|
|
720
|
-
|
|
992
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
996
|
+
const state = {
|
|
997
|
+
locate: false,
|
|
998
|
+
scroll: false,
|
|
999
|
+
highlight: false,
|
|
1000
|
+
_params,
|
|
1001
|
+
options,
|
|
1002
|
+
world,
|
|
1003
|
+
type: Types.FILL,
|
|
1004
|
+
text: "Fill element",
|
|
1005
|
+
operation: "simpleClickType",
|
|
1006
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
1007
|
+
};
|
|
1008
|
+
_preCommand(state, this);
|
|
1009
|
+
const startTime = Date.now();
|
|
1010
|
+
let timeout = 30000;
|
|
1011
|
+
if (options && options.timeout) {
|
|
1012
|
+
timeout = options.timeout;
|
|
1013
|
+
}
|
|
1014
|
+
while (true) {
|
|
1015
|
+
try {
|
|
1016
|
+
const result = await locate_element(this.context, elementDescription, "fill", value);
|
|
1017
|
+
if (result?.elementNumber >= 0) {
|
|
1018
|
+
const selectors = {
|
|
1019
|
+
frame: result?.frame,
|
|
1020
|
+
locators: [
|
|
1021
|
+
{
|
|
1022
|
+
css: result?.css,
|
|
1023
|
+
},
|
|
1024
|
+
],
|
|
1025
|
+
};
|
|
1026
|
+
await this.clickType(selectors, value, false, _params, options, world);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
catch (e) {
|
|
1031
|
+
if (performance.now() - startTime > timeout) {
|
|
1032
|
+
// throw e;
|
|
1033
|
+
try {
|
|
1034
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1035
|
+
}
|
|
1036
|
+
finally {
|
|
1037
|
+
await _commandFinally(state, this);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async click(selectors, _params, options = {}, world = null) {
|
|
1045
|
+
const state = {
|
|
1046
|
+
selectors,
|
|
1047
|
+
_params,
|
|
1048
|
+
options,
|
|
1049
|
+
world,
|
|
1050
|
+
text: "Click element",
|
|
1051
|
+
_text: "Click on " + selectors.element_name,
|
|
1052
|
+
type: Types.CLICK,
|
|
1053
|
+
operation: "click",
|
|
1054
|
+
log: "***** click on " + selectors.element_name + " *****\n",
|
|
1055
|
+
};
|
|
1056
|
+
try {
|
|
1057
|
+
await _preCommand(state, this);
|
|
1058
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1059
|
+
if (!this.fastMode) {
|
|
1060
|
+
await this.waitForPageLoad();
|
|
1061
|
+
}
|
|
1062
|
+
return state.info;
|
|
721
1063
|
}
|
|
722
1064
|
catch (e) {
|
|
723
|
-
|
|
724
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
725
|
-
info.screenshotPath = screenshotPath;
|
|
726
|
-
Object.assign(e, { info: info });
|
|
727
|
-
error = e;
|
|
728
|
-
throw e;
|
|
1065
|
+
await _commandError(state, e, this);
|
|
729
1066
|
}
|
|
730
1067
|
finally {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1068
|
+
await _commandFinally(state, this);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1072
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1073
|
+
const state = {
|
|
1074
|
+
selectors,
|
|
1075
|
+
_params,
|
|
1076
|
+
options,
|
|
1077
|
+
world,
|
|
1078
|
+
text: "Wait for element",
|
|
1079
|
+
_text: "Wait for " + selectors.element_name,
|
|
1080
|
+
type: Types.WAIT_ELEMENT,
|
|
1081
|
+
operation: "waitForElement",
|
|
1082
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1083
|
+
};
|
|
1084
|
+
let found = false;
|
|
1085
|
+
try {
|
|
1086
|
+
await _preCommand(state, this);
|
|
1087
|
+
// if (state.options && state.options.context) {
|
|
1088
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1089
|
+
// }
|
|
1090
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1091
|
+
found = true;
|
|
1092
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1093
|
+
}
|
|
1094
|
+
catch (e) {
|
|
1095
|
+
console.error("Error on waitForElement", e);
|
|
1096
|
+
// await _commandError(state, e, this);
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
await _commandFinally(state, this);
|
|
751
1100
|
}
|
|
1101
|
+
return found;
|
|
752
1102
|
}
|
|
753
1103
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1104
|
+
const state = {
|
|
1105
|
+
selectors,
|
|
1106
|
+
_params,
|
|
1107
|
+
options,
|
|
1108
|
+
world,
|
|
1109
|
+
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
1110
|
+
text: checked ? `Check element` : `Uncheck element`,
|
|
1111
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
1112
|
+
operation: "setCheck",
|
|
1113
|
+
log: "***** check " + selectors.element_name + " *****\n",
|
|
1114
|
+
};
|
|
764
1115
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
1116
|
+
await _preCommand(state, this);
|
|
1117
|
+
state.info.checked = checked;
|
|
1118
|
+
// let element = await this._locate(selectors, info, _params);
|
|
1119
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
767
1120
|
try {
|
|
768
|
-
|
|
769
|
-
|
|
1121
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1122
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1123
|
+
await this._highlightElements(state.element);
|
|
1124
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
770
1125
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1126
|
+
// await this._unHighlightElements(element);
|
|
1127
|
+
// }
|
|
1128
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1129
|
+
// await this._unHighlightElements(element);
|
|
771
1130
|
}
|
|
772
1131
|
catch (e) {
|
|
773
1132
|
if (e.message && e.message.includes("did not change its state")) {
|
|
774
1133
|
this.logger.info("element did not change its state, ignoring...");
|
|
775
1134
|
}
|
|
776
1135
|
else {
|
|
777
|
-
//await this.closeUnexpectedPopups();
|
|
778
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
779
|
-
element = await this._locate(selectors, info, _params);
|
|
780
|
-
await element.setChecked(checked, { timeout: 5000, force: true });
|
|
781
1136
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1137
|
+
//await this.closeUnexpectedPopups();
|
|
1138
|
+
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1139
|
+
state.element_found = false;
|
|
1140
|
+
try {
|
|
1141
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1142
|
+
state.element_found = true;
|
|
1143
|
+
// check the check state
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
// element dismissed
|
|
1147
|
+
}
|
|
1148
|
+
if (state.element_found) {
|
|
1149
|
+
const isChecked = await state.element.isChecked();
|
|
1150
|
+
if (isChecked !== checked) {
|
|
1151
|
+
// perform click
|
|
1152
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
782
1158
|
}
|
|
783
1159
|
}
|
|
784
1160
|
await this.waitForPageLoad();
|
|
785
|
-
return info;
|
|
1161
|
+
return state.info;
|
|
786
1162
|
}
|
|
787
1163
|
catch (e) {
|
|
788
|
-
|
|
789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
790
|
-
info.screenshotPath = screenshotPath;
|
|
791
|
-
Object.assign(e, { info: info });
|
|
792
|
-
error = e;
|
|
793
|
-
throw e;
|
|
1164
|
+
await _commandError(state, e, this);
|
|
794
1165
|
}
|
|
795
1166
|
finally {
|
|
796
|
-
|
|
797
|
-
this._reportToWorld(world, {
|
|
798
|
-
element_name: selectors.element_name,
|
|
799
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
800
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
801
|
-
screenshotId,
|
|
802
|
-
result: error
|
|
803
|
-
? {
|
|
804
|
-
status: "FAILED",
|
|
805
|
-
startTime,
|
|
806
|
-
endTime,
|
|
807
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
808
|
-
}
|
|
809
|
-
: {
|
|
810
|
-
status: "PASSED",
|
|
811
|
-
startTime,
|
|
812
|
-
endTime,
|
|
813
|
-
},
|
|
814
|
-
info: info,
|
|
815
|
-
});
|
|
1167
|
+
await _commandFinally(state, this);
|
|
816
1168
|
}
|
|
817
1169
|
}
|
|
818
1170
|
async hover(selectors, _params, options = {}, world = null) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1171
|
+
const state = {
|
|
1172
|
+
selectors,
|
|
1173
|
+
_params,
|
|
1174
|
+
options,
|
|
1175
|
+
world,
|
|
1176
|
+
type: Types.HOVER,
|
|
1177
|
+
text: `Hover element`,
|
|
1178
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1179
|
+
operation: "hover",
|
|
1180
|
+
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1181
|
+
};
|
|
828
1182
|
try {
|
|
829
|
-
|
|
830
|
-
(
|
|
831
|
-
|
|
832
|
-
await this._highlightElements(element);
|
|
833
|
-
await element.hover();
|
|
834
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
835
|
-
}
|
|
836
|
-
catch (e) {
|
|
837
|
-
//await this.closeUnexpectedPopups();
|
|
838
|
-
info.log += "hover failed, will try again" + "\n";
|
|
839
|
-
element = await this._locate(selectors, info, _params);
|
|
840
|
-
await element.hover({ timeout: 10000 });
|
|
841
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
842
|
-
}
|
|
1183
|
+
await _preCommand(state, this);
|
|
1184
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1185
|
+
await _screenshot(state, this);
|
|
843
1186
|
await this.waitForPageLoad();
|
|
844
|
-
return info;
|
|
1187
|
+
return state.info;
|
|
845
1188
|
}
|
|
846
1189
|
catch (e) {
|
|
847
|
-
|
|
848
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
849
|
-
info.screenshotPath = screenshotPath;
|
|
850
|
-
Object.assign(e, { info: info });
|
|
851
|
-
error = e;
|
|
852
|
-
throw e;
|
|
1190
|
+
await _commandError(state, e, this);
|
|
853
1191
|
}
|
|
854
1192
|
finally {
|
|
855
|
-
|
|
856
|
-
this._reportToWorld(world, {
|
|
857
|
-
element_name: selectors.element_name,
|
|
858
|
-
type: Types.HOVER,
|
|
859
|
-
text: `Hover element`,
|
|
860
|
-
screenshotId,
|
|
861
|
-
result: error
|
|
862
|
-
? {
|
|
863
|
-
status: "FAILED",
|
|
864
|
-
startTime,
|
|
865
|
-
endTime,
|
|
866
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
867
|
-
}
|
|
868
|
-
: {
|
|
869
|
-
status: "PASSED",
|
|
870
|
-
startTime,
|
|
871
|
-
endTime,
|
|
872
|
-
},
|
|
873
|
-
info: info,
|
|
874
|
-
});
|
|
1193
|
+
await _commandFinally(state, this);
|
|
875
1194
|
}
|
|
876
1195
|
}
|
|
877
1196
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
878
|
-
this._validateSelectors(selectors);
|
|
879
1197
|
if (!values) {
|
|
880
1198
|
throw new Error("values is null");
|
|
881
1199
|
}
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1200
|
+
const state = {
|
|
1201
|
+
selectors,
|
|
1202
|
+
_params,
|
|
1203
|
+
options,
|
|
1204
|
+
world,
|
|
1205
|
+
value: values.toString(),
|
|
1206
|
+
type: Types.SELECT,
|
|
1207
|
+
text: `Select option: ${values}`,
|
|
1208
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1209
|
+
operation: "selectOption",
|
|
1210
|
+
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1211
|
+
};
|
|
890
1212
|
try {
|
|
891
|
-
|
|
892
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1213
|
+
await _preCommand(state, this);
|
|
893
1214
|
try {
|
|
894
|
-
await
|
|
895
|
-
await element.selectOption(values);
|
|
1215
|
+
await state.element.selectOption(values);
|
|
896
1216
|
}
|
|
897
1217
|
catch (e) {
|
|
898
1218
|
//await this.closeUnexpectedPopups();
|
|
899
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
900
|
-
await element.selectOption(values, { timeout: 10000, force: true });
|
|
1219
|
+
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1220
|
+
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
901
1221
|
}
|
|
902
1222
|
await this.waitForPageLoad();
|
|
903
|
-
return info;
|
|
1223
|
+
return state.info;
|
|
904
1224
|
}
|
|
905
1225
|
catch (e) {
|
|
906
|
-
|
|
907
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
908
|
-
info.screenshotPath = screenshotPath;
|
|
909
|
-
Object.assign(e, { info: info });
|
|
910
|
-
this.logger.info("click failed, will try next selector");
|
|
911
|
-
error = e;
|
|
912
|
-
throw e;
|
|
1226
|
+
await _commandError(state, e, this);
|
|
913
1227
|
}
|
|
914
1228
|
finally {
|
|
915
|
-
|
|
916
|
-
this._reportToWorld(world, {
|
|
917
|
-
element_name: selectors.element_name,
|
|
918
|
-
type: Types.SELECT,
|
|
919
|
-
text: `Select option: ${values}`,
|
|
920
|
-
value: values.toString(),
|
|
921
|
-
screenshotId,
|
|
922
|
-
result: error
|
|
923
|
-
? {
|
|
924
|
-
status: "FAILED",
|
|
925
|
-
startTime,
|
|
926
|
-
endTime,
|
|
927
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
928
|
-
}
|
|
929
|
-
: {
|
|
930
|
-
status: "PASSED",
|
|
931
|
-
startTime,
|
|
932
|
-
endTime,
|
|
933
|
-
},
|
|
934
|
-
info: info,
|
|
935
|
-
});
|
|
1229
|
+
await _commandFinally(state, this);
|
|
936
1230
|
}
|
|
937
1231
|
}
|
|
938
1232
|
async type(_value, _params = null, options = {}, world = null) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1233
|
+
const state = {
|
|
1234
|
+
value: _value,
|
|
1235
|
+
_params,
|
|
1236
|
+
options,
|
|
1237
|
+
world,
|
|
1238
|
+
locate: false,
|
|
1239
|
+
scroll: false,
|
|
1240
|
+
highlight: false,
|
|
1241
|
+
type: Types.TYPE_PRESS,
|
|
1242
|
+
text: `Type value: ${_value}`,
|
|
1243
|
+
_text: `Type value: ${_value}`,
|
|
1244
|
+
operation: "type",
|
|
1245
|
+
log: "",
|
|
1246
|
+
};
|
|
948
1247
|
try {
|
|
949
|
-
|
|
950
|
-
const valueSegment =
|
|
1248
|
+
await _preCommand(state, this);
|
|
1249
|
+
const valueSegment = state.value.split("&&");
|
|
951
1250
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
952
1251
|
if (i > 0) {
|
|
953
1252
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -967,134 +1266,77 @@ class StableBrowser {
|
|
|
967
1266
|
await this.page.keyboard.type(value);
|
|
968
1267
|
}
|
|
969
1268
|
}
|
|
970
|
-
return info;
|
|
1269
|
+
return state.info;
|
|
971
1270
|
}
|
|
972
1271
|
catch (e) {
|
|
973
|
-
|
|
974
|
-
this.logger.error("type failed " + info.log);
|
|
975
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
976
|
-
info.screenshotPath = screenshotPath;
|
|
977
|
-
Object.assign(e, { info: info });
|
|
978
|
-
error = e;
|
|
979
|
-
throw e;
|
|
1272
|
+
await _commandError(state, e, this);
|
|
980
1273
|
}
|
|
981
1274
|
finally {
|
|
982
|
-
|
|
983
|
-
this._reportToWorld(world, {
|
|
984
|
-
type: Types.TYPE_PRESS,
|
|
985
|
-
screenshotId,
|
|
986
|
-
value: _value,
|
|
987
|
-
text: `type value: ${_value}`,
|
|
988
|
-
result: error
|
|
989
|
-
? {
|
|
990
|
-
status: "FAILED",
|
|
991
|
-
startTime,
|
|
992
|
-
endTime,
|
|
993
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
994
|
-
}
|
|
995
|
-
: {
|
|
996
|
-
status: "PASSED",
|
|
997
|
-
startTime,
|
|
998
|
-
endTime,
|
|
999
|
-
},
|
|
1000
|
-
info: info,
|
|
1001
|
-
});
|
|
1275
|
+
await _commandFinally(state, this);
|
|
1002
1276
|
}
|
|
1003
1277
|
}
|
|
1004
1278
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
let screenshotPath = null;
|
|
1279
|
+
const state = {
|
|
1280
|
+
selectors,
|
|
1281
|
+
_params,
|
|
1282
|
+
value,
|
|
1283
|
+
options,
|
|
1284
|
+
world,
|
|
1285
|
+
type: Types.SET_INPUT,
|
|
1286
|
+
text: `Set input value`,
|
|
1287
|
+
operation: "setInputValue",
|
|
1288
|
+
log: "***** set input value " + selectors.element_name + " *****\n",
|
|
1289
|
+
};
|
|
1017
1290
|
try {
|
|
1018
|
-
|
|
1019
|
-
let
|
|
1020
|
-
await this.scrollIfNeeded(element, info);
|
|
1021
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1022
|
-
await this._highlightElements(element);
|
|
1291
|
+
await _preCommand(state, this);
|
|
1292
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1023
1293
|
try {
|
|
1024
|
-
await element.evaluateHandle((el, value) => {
|
|
1294
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1025
1295
|
el.value = value;
|
|
1026
1296
|
}, value);
|
|
1027
1297
|
}
|
|
1028
1298
|
catch (error) {
|
|
1029
1299
|
this.logger.error("setInputValue failed, will try again");
|
|
1030
|
-
|
|
1031
|
-
info.
|
|
1032
|
-
|
|
1033
|
-
await element.evaluateHandle((el, value) => {
|
|
1300
|
+
await _screenshot(state, this);
|
|
1301
|
+
Object.assign(error, { info: state.info });
|
|
1302
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1034
1303
|
el.value = value;
|
|
1035
1304
|
});
|
|
1036
1305
|
}
|
|
1037
1306
|
}
|
|
1038
1307
|
catch (e) {
|
|
1039
|
-
|
|
1040
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1041
|
-
info.screenshotPath = screenshotPath;
|
|
1042
|
-
Object.assign(e, { info: info });
|
|
1043
|
-
error = e;
|
|
1044
|
-
throw e;
|
|
1308
|
+
await _commandError(state, e, this);
|
|
1045
1309
|
}
|
|
1046
1310
|
finally {
|
|
1047
|
-
|
|
1048
|
-
this._reportToWorld(world, {
|
|
1049
|
-
element_name: selectors.element_name,
|
|
1050
|
-
type: Types.SET_INPUT,
|
|
1051
|
-
text: `Set input value`,
|
|
1052
|
-
value: value,
|
|
1053
|
-
screenshotId,
|
|
1054
|
-
result: error
|
|
1055
|
-
? {
|
|
1056
|
-
status: "FAILED",
|
|
1057
|
-
startTime,
|
|
1058
|
-
endTime,
|
|
1059
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1060
|
-
}
|
|
1061
|
-
: {
|
|
1062
|
-
status: "PASSED",
|
|
1063
|
-
startTime,
|
|
1064
|
-
endTime,
|
|
1065
|
-
},
|
|
1066
|
-
info: info,
|
|
1067
|
-
});
|
|
1311
|
+
await _commandFinally(state, this);
|
|
1068
1312
|
}
|
|
1069
1313
|
}
|
|
1070
1314
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1315
|
+
const state = {
|
|
1316
|
+
selectors,
|
|
1317
|
+
_params,
|
|
1318
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1319
|
+
options,
|
|
1320
|
+
world,
|
|
1321
|
+
type: Types.SET_DATE_TIME,
|
|
1322
|
+
text: `Set date time value: ${value}`,
|
|
1323
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1324
|
+
operation: "setDateTime",
|
|
1325
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1326
|
+
throwError: false,
|
|
1327
|
+
};
|
|
1081
1328
|
try {
|
|
1082
|
-
|
|
1083
|
-
let element = await this._locate(selectors, info, _params);
|
|
1084
|
-
//insert red border around the element
|
|
1085
|
-
await this.scrollIfNeeded(element, info);
|
|
1086
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1087
|
-
await this._highlightElements(element);
|
|
1329
|
+
await _preCommand(state, this);
|
|
1088
1330
|
try {
|
|
1089
|
-
await element
|
|
1331
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1090
1332
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1091
1333
|
if (format) {
|
|
1092
|
-
value = dayjs(value).format(format);
|
|
1093
|
-
await element.fill(value);
|
|
1334
|
+
state.value = dayjs(state.value).format(format);
|
|
1335
|
+
await state.element.fill(state.value);
|
|
1094
1336
|
}
|
|
1095
1337
|
else {
|
|
1096
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1097
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1338
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1339
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1098
1340
|
el.value = ""; // clear input
|
|
1099
1341
|
el.value = dateTimeValue;
|
|
1100
1342
|
}, dateTimeValue);
|
|
@@ -1107,20 +1349,19 @@ class StableBrowser {
|
|
|
1107
1349
|
}
|
|
1108
1350
|
catch (err) {
|
|
1109
1351
|
//await this.closeUnexpectedPopups();
|
|
1110
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1352
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1111
1353
|
this.logger.info("Trying again");
|
|
1112
|
-
|
|
1113
|
-
info.
|
|
1114
|
-
Object.assign(err, { info: info });
|
|
1354
|
+
await _screenshot(state, this);
|
|
1355
|
+
Object.assign(err, { info: state.info });
|
|
1115
1356
|
await element.click();
|
|
1116
1357
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1117
1358
|
if (format) {
|
|
1118
|
-
value = dayjs(value).format(format);
|
|
1119
|
-
await element.fill(value);
|
|
1359
|
+
state.value = dayjs(state.value).format(format);
|
|
1360
|
+
await state.element.fill(state.value);
|
|
1120
1361
|
}
|
|
1121
1362
|
else {
|
|
1122
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1123
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1363
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1364
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1124
1365
|
el.value = ""; // clear input
|
|
1125
1366
|
el.value = dateTimeValue;
|
|
1126
1367
|
}, dateTimeValue);
|
|
@@ -1133,84 +1374,63 @@ class StableBrowser {
|
|
|
1133
1374
|
}
|
|
1134
1375
|
}
|
|
1135
1376
|
catch (e) {
|
|
1136
|
-
|
|
1137
|
-
throw e;
|
|
1377
|
+
await _commandError(state, e, this);
|
|
1138
1378
|
}
|
|
1139
1379
|
finally {
|
|
1140
|
-
|
|
1141
|
-
this._reportToWorld(world, {
|
|
1142
|
-
element_name: selectors.element_name,
|
|
1143
|
-
type: Types.SET_DATE_TIME,
|
|
1144
|
-
screenshotId,
|
|
1145
|
-
value: value,
|
|
1146
|
-
text: `setDateTime input with value: ${value}`,
|
|
1147
|
-
result: error
|
|
1148
|
-
? {
|
|
1149
|
-
status: "FAILED",
|
|
1150
|
-
startTime,
|
|
1151
|
-
endTime,
|
|
1152
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1153
|
-
}
|
|
1154
|
-
: {
|
|
1155
|
-
status: "PASSED",
|
|
1156
|
-
startTime,
|
|
1157
|
-
endTime,
|
|
1158
|
-
},
|
|
1159
|
-
info: info,
|
|
1160
|
-
});
|
|
1380
|
+
await _commandFinally(state, this);
|
|
1161
1381
|
}
|
|
1162
1382
|
}
|
|
1163
1383
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1164
|
-
|
|
1165
|
-
const startTime = Date.now();
|
|
1166
|
-
let error = null;
|
|
1167
|
-
let screenshotId = null;
|
|
1168
|
-
let screenshotPath = null;
|
|
1169
|
-
const info = {};
|
|
1170
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1171
|
-
info.operation = "clickType";
|
|
1172
|
-
info.selectors = selectors;
|
|
1384
|
+
_value = unEscapeString(_value);
|
|
1173
1385
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1386
|
+
const state = {
|
|
1387
|
+
selectors,
|
|
1388
|
+
_params,
|
|
1389
|
+
value: newValue,
|
|
1390
|
+
originalValue: _value,
|
|
1391
|
+
options,
|
|
1392
|
+
world,
|
|
1393
|
+
type: Types.FILL,
|
|
1394
|
+
text: `Click type input with value: ${_value}`,
|
|
1395
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1396
|
+
operation: "clickType",
|
|
1397
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1398
|
+
};
|
|
1399
|
+
if (!options) {
|
|
1400
|
+
options = {};
|
|
1401
|
+
}
|
|
1174
1402
|
if (newValue !== _value) {
|
|
1175
1403
|
//this.logger.info(_value + "=" + newValue);
|
|
1176
1404
|
_value = newValue;
|
|
1177
1405
|
}
|
|
1178
|
-
info.value = _value;
|
|
1179
1406
|
try {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1184
|
-
await this._highlightElements(element);
|
|
1185
|
-
if (options === null || options === undefined || !options.press) {
|
|
1407
|
+
await _preCommand(state, this);
|
|
1408
|
+
state.info.value = _value;
|
|
1409
|
+
if (!options.press) {
|
|
1186
1410
|
try {
|
|
1187
|
-
let currentValue = await element.inputValue();
|
|
1411
|
+
let currentValue = await state.element.inputValue();
|
|
1188
1412
|
if (currentValue) {
|
|
1189
|
-
await element.fill("");
|
|
1413
|
+
await state.element.fill("");
|
|
1190
1414
|
}
|
|
1191
1415
|
}
|
|
1192
1416
|
catch (e) {
|
|
1193
1417
|
this.logger.info("unable to clear input value");
|
|
1194
1418
|
}
|
|
1195
1419
|
}
|
|
1196
|
-
if (options
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1200
|
-
catch (e) {
|
|
1201
|
-
await element.dispatchEvent("click");
|
|
1202
|
-
}
|
|
1420
|
+
if (options.press) {
|
|
1421
|
+
options.timeout = 5000;
|
|
1422
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1203
1423
|
}
|
|
1204
1424
|
else {
|
|
1205
1425
|
try {
|
|
1206
|
-
await element.focus();
|
|
1426
|
+
await state.element.focus();
|
|
1207
1427
|
}
|
|
1208
1428
|
catch (e) {
|
|
1209
|
-
await element.dispatchEvent("focus");
|
|
1429
|
+
await state.element.dispatchEvent("focus");
|
|
1210
1430
|
}
|
|
1211
1431
|
}
|
|
1212
1432
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1213
|
-
const valueSegment =
|
|
1433
|
+
const valueSegment = state.value.split("&&");
|
|
1214
1434
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1215
1435
|
if (i > 0) {
|
|
1216
1436
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1230,13 +1450,21 @@ class StableBrowser {
|
|
|
1230
1450
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1231
1451
|
}
|
|
1232
1452
|
}
|
|
1453
|
+
//if (!this.fastMode) {
|
|
1454
|
+
await _screenshot(state, this);
|
|
1455
|
+
//}
|
|
1233
1456
|
if (enter === true) {
|
|
1234
1457
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1235
1458
|
await this.page.keyboard.press("Enter");
|
|
1236
1459
|
await this.waitForPageLoad();
|
|
1237
1460
|
}
|
|
1238
1461
|
else if (enter === false) {
|
|
1239
|
-
|
|
1462
|
+
try {
|
|
1463
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1464
|
+
}
|
|
1465
|
+
catch (e) {
|
|
1466
|
+
// ignore
|
|
1467
|
+
}
|
|
1240
1468
|
//await this.page.keyboard.press("Tab");
|
|
1241
1469
|
}
|
|
1242
1470
|
else {
|
|
@@ -1245,111 +1473,95 @@ class StableBrowser {
|
|
|
1245
1473
|
await this.waitForPageLoad();
|
|
1246
1474
|
}
|
|
1247
1475
|
}
|
|
1248
|
-
return info;
|
|
1476
|
+
return state.info;
|
|
1249
1477
|
}
|
|
1250
1478
|
catch (e) {
|
|
1251
|
-
|
|
1252
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1253
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1254
|
-
info.screenshotPath = screenshotPath;
|
|
1255
|
-
Object.assign(e, { info: info });
|
|
1256
|
-
error = e;
|
|
1257
|
-
throw e;
|
|
1479
|
+
await _commandError(state, e, this);
|
|
1258
1480
|
}
|
|
1259
1481
|
finally {
|
|
1260
|
-
|
|
1261
|
-
this._reportToWorld(world, {
|
|
1262
|
-
element_name: selectors.element_name,
|
|
1263
|
-
type: Types.FILL,
|
|
1264
|
-
screenshotId,
|
|
1265
|
-
value: _value,
|
|
1266
|
-
text: `clickType input with value: ${_value}`,
|
|
1267
|
-
result: error
|
|
1268
|
-
? {
|
|
1269
|
-
status: "FAILED",
|
|
1270
|
-
startTime,
|
|
1271
|
-
endTime,
|
|
1272
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1273
|
-
}
|
|
1274
|
-
: {
|
|
1275
|
-
status: "PASSED",
|
|
1276
|
-
startTime,
|
|
1277
|
-
endTime,
|
|
1278
|
-
},
|
|
1279
|
-
info: info,
|
|
1280
|
-
});
|
|
1482
|
+
await _commandFinally(state, this);
|
|
1281
1483
|
}
|
|
1282
1484
|
}
|
|
1283
1485
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1486
|
+
const state = {
|
|
1487
|
+
selectors,
|
|
1488
|
+
_params,
|
|
1489
|
+
value: unEscapeString(value),
|
|
1490
|
+
options,
|
|
1491
|
+
world,
|
|
1492
|
+
type: Types.FILL,
|
|
1493
|
+
text: `Fill input with value: ${value}`,
|
|
1494
|
+
operation: "fill",
|
|
1495
|
+
log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
|
|
1496
|
+
};
|
|
1294
1497
|
try {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
await
|
|
1298
|
-
await element.fill(value);
|
|
1299
|
-
await element.dispatchEvent("change");
|
|
1498
|
+
await _preCommand(state, this);
|
|
1499
|
+
await state.element.fill(value);
|
|
1500
|
+
await state.element.dispatchEvent("change");
|
|
1300
1501
|
if (enter) {
|
|
1301
1502
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1302
1503
|
await this.page.keyboard.press("Enter");
|
|
1303
1504
|
}
|
|
1304
1505
|
await this.waitForPageLoad();
|
|
1305
|
-
return info;
|
|
1506
|
+
return state.info;
|
|
1306
1507
|
}
|
|
1307
1508
|
catch (e) {
|
|
1308
|
-
|
|
1309
|
-
this.logger.error("fill failed " + info.log);
|
|
1310
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1311
|
-
info.screenshotPath = screenshotPath;
|
|
1312
|
-
Object.assign(e, { info: info });
|
|
1313
|
-
error = e;
|
|
1314
|
-
throw e;
|
|
1509
|
+
await _commandError(state, e, this);
|
|
1315
1510
|
}
|
|
1316
1511
|
finally {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1512
|
+
await _commandFinally(state, this);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1516
|
+
const state = {
|
|
1517
|
+
selectors,
|
|
1518
|
+
_params,
|
|
1519
|
+
files,
|
|
1520
|
+
value: '"' + files.join('", "') + '"',
|
|
1521
|
+
options,
|
|
1522
|
+
world,
|
|
1523
|
+
type: Types.SET_INPUT_FILES,
|
|
1524
|
+
text: `Set input files`,
|
|
1525
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1526
|
+
operation: "setInputFiles",
|
|
1527
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1528
|
+
};
|
|
1529
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1530
|
+
try {
|
|
1531
|
+
await _preCommand(state, this);
|
|
1532
|
+
for (let i = 0; i < files.length; i++) {
|
|
1533
|
+
const file = files[i];
|
|
1534
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1535
|
+
if (!fs.existsSync(filePath)) {
|
|
1536
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1537
|
+
}
|
|
1538
|
+
state.files[i] = filePath;
|
|
1539
|
+
}
|
|
1540
|
+
await state.element.setInputFiles(files);
|
|
1541
|
+
return state.info;
|
|
1542
|
+
}
|
|
1543
|
+
catch (e) {
|
|
1544
|
+
await _commandError(state, e, this);
|
|
1545
|
+
}
|
|
1546
|
+
finally {
|
|
1547
|
+
await _commandFinally(state, this);
|
|
1338
1548
|
}
|
|
1339
1549
|
}
|
|
1340
1550
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1341
1551
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1342
1552
|
}
|
|
1343
1553
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1344
|
-
this.
|
|
1554
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1555
|
+
_validateSelectors(selectors);
|
|
1345
1556
|
let screenshotId = null;
|
|
1346
1557
|
let screenshotPath = null;
|
|
1347
1558
|
if (!info.log) {
|
|
1348
1559
|
info.log = "";
|
|
1560
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1349
1561
|
}
|
|
1350
1562
|
info.operation = "getText";
|
|
1351
1563
|
info.selectors = selectors;
|
|
1352
|
-
let element = await this._locate(selectors, info, _params);
|
|
1564
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1353
1565
|
if (climb > 0) {
|
|
1354
1566
|
const climbArray = [];
|
|
1355
1567
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1368,6 +1580,18 @@ class StableBrowser {
|
|
|
1368
1580
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1369
1581
|
try {
|
|
1370
1582
|
await this._highlightElements(element);
|
|
1583
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1584
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1585
|
+
// this._highlightElements(element)
|
|
1586
|
+
// .then(async () => {
|
|
1587
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1588
|
+
// this._unhighlightElements(element).then(
|
|
1589
|
+
// () => {}
|
|
1590
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1591
|
+
// );
|
|
1592
|
+
// })
|
|
1593
|
+
// .catch(e);
|
|
1594
|
+
// }
|
|
1371
1595
|
const elementText = await element.innerText();
|
|
1372
1596
|
return {
|
|
1373
1597
|
text: elementText,
|
|
@@ -1379,188 +1603,219 @@ class StableBrowser {
|
|
|
1379
1603
|
}
|
|
1380
1604
|
catch (e) {
|
|
1381
1605
|
//await this.closeUnexpectedPopups();
|
|
1382
|
-
this.logger.info("no innerText will use textContent");
|
|
1606
|
+
this.logger.info("no innerText, will use textContent");
|
|
1383
1607
|
const elementText = await element.textContent();
|
|
1384
1608
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1385
1609
|
}
|
|
1386
1610
|
}
|
|
1387
1611
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1388
|
-
var _a;
|
|
1389
|
-
this._validateSelectors(selectors);
|
|
1390
1612
|
if (!pattern) {
|
|
1391
1613
|
throw new Error("pattern is null");
|
|
1392
1614
|
}
|
|
1393
1615
|
if (!text) {
|
|
1394
1616
|
throw new Error("text is null");
|
|
1395
1617
|
}
|
|
1618
|
+
const state = {
|
|
1619
|
+
selectors,
|
|
1620
|
+
_params,
|
|
1621
|
+
pattern,
|
|
1622
|
+
value: pattern,
|
|
1623
|
+
options,
|
|
1624
|
+
world,
|
|
1625
|
+
locate: false,
|
|
1626
|
+
scroll: false,
|
|
1627
|
+
screenshot: false,
|
|
1628
|
+
highlight: false,
|
|
1629
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1630
|
+
text: `Verify element contains pattern: ${pattern}`,
|
|
1631
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1632
|
+
operation: "containsPattern",
|
|
1633
|
+
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1634
|
+
};
|
|
1396
1635
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1397
1636
|
if (newValue !== text) {
|
|
1398
1637
|
this.logger.info(text + "=" + newValue);
|
|
1399
1638
|
text = newValue;
|
|
1400
1639
|
}
|
|
1401
|
-
const startTime = Date.now();
|
|
1402
|
-
let error = null;
|
|
1403
|
-
let screenshotId = null;
|
|
1404
|
-
let screenshotPath = null;
|
|
1405
|
-
const info = {};
|
|
1406
|
-
info.log =
|
|
1407
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1408
|
-
info.operation = "containsPattern";
|
|
1409
|
-
info.selectors = selectors;
|
|
1410
|
-
info.value = text;
|
|
1411
|
-
info.pattern = pattern;
|
|
1412
1640
|
let foundObj = null;
|
|
1413
1641
|
try {
|
|
1414
|
-
|
|
1642
|
+
await _preCommand(state, this);
|
|
1643
|
+
state.info.pattern = pattern;
|
|
1644
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1415
1645
|
if (foundObj && foundObj.element) {
|
|
1416
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1646
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1417
1647
|
}
|
|
1418
|
-
|
|
1648
|
+
await _screenshot(state, this);
|
|
1419
1649
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1420
1650
|
pattern = pattern.replace("{text}", escapedText);
|
|
1421
1651
|
let regex = new RegExp(pattern, "im");
|
|
1422
|
-
if (!regex.test(foundObj
|
|
1423
|
-
info.foundText = foundObj
|
|
1652
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1653
|
+
state.info.foundText = foundObj?.text;
|
|
1424
1654
|
throw new Error("element doesn't contain text " + text);
|
|
1425
1655
|
}
|
|
1426
|
-
return info;
|
|
1656
|
+
return state.info;
|
|
1427
1657
|
}
|
|
1428
1658
|
catch (e) {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1432
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1433
|
-
info.screenshotPath = screenshotPath;
|
|
1434
|
-
Object.assign(e, { info: info });
|
|
1435
|
-
error = e;
|
|
1436
|
-
throw e;
|
|
1659
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1660
|
+
await _commandError(state, e, this);
|
|
1437
1661
|
}
|
|
1438
1662
|
finally {
|
|
1439
|
-
|
|
1440
|
-
this._reportToWorld(world, {
|
|
1441
|
-
element_name: selectors.element_name,
|
|
1442
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1443
|
-
value: pattern,
|
|
1444
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1445
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1446
|
-
result: error
|
|
1447
|
-
? {
|
|
1448
|
-
status: "FAILED",
|
|
1449
|
-
startTime,
|
|
1450
|
-
endTime,
|
|
1451
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1452
|
-
}
|
|
1453
|
-
: {
|
|
1454
|
-
status: "PASSED",
|
|
1455
|
-
startTime,
|
|
1456
|
-
endTime,
|
|
1457
|
-
},
|
|
1458
|
-
info: info,
|
|
1459
|
-
});
|
|
1663
|
+
await _commandFinally(state, this);
|
|
1460
1664
|
}
|
|
1461
1665
|
}
|
|
1462
1666
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1667
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1668
|
+
const startTime = Date.now();
|
|
1669
|
+
const state = {
|
|
1670
|
+
selectors,
|
|
1671
|
+
_params,
|
|
1672
|
+
value: text,
|
|
1673
|
+
options,
|
|
1674
|
+
world,
|
|
1675
|
+
locate: false,
|
|
1676
|
+
scroll: false,
|
|
1677
|
+
screenshot: false,
|
|
1678
|
+
highlight: false,
|
|
1679
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1680
|
+
text: `Verify element contains text: ${text}`,
|
|
1681
|
+
operation: "containsText",
|
|
1682
|
+
log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
|
|
1683
|
+
};
|
|
1465
1684
|
if (!text) {
|
|
1466
1685
|
throw new Error("text is null");
|
|
1467
1686
|
}
|
|
1468
|
-
|
|
1469
|
-
let error = null;
|
|
1470
|
-
let screenshotId = null;
|
|
1471
|
-
let screenshotPath = null;
|
|
1472
|
-
const info = {};
|
|
1473
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1474
|
-
info.operation = "containsText";
|
|
1475
|
-
info.selectors = selectors;
|
|
1687
|
+
text = unEscapeString(text);
|
|
1476
1688
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1477
1689
|
if (newValue !== text) {
|
|
1478
1690
|
this.logger.info(text + "=" + newValue);
|
|
1479
1691
|
text = newValue;
|
|
1480
1692
|
}
|
|
1481
|
-
info.value = text;
|
|
1482
1693
|
let foundObj = null;
|
|
1483
1694
|
try {
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1695
|
+
while (Date.now() - startTime < timeout) {
|
|
1696
|
+
try {
|
|
1697
|
+
await _preCommand(state, this);
|
|
1698
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1699
|
+
if (foundObj && foundObj.element) {
|
|
1700
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1701
|
+
}
|
|
1702
|
+
await _screenshot(state, this);
|
|
1703
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1704
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1705
|
+
if (dateAlternatives.date) {
|
|
1706
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1707
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1708
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1709
|
+
return state.info;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1496
1712
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1713
|
+
else if (numberAlternatives.number) {
|
|
1714
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1715
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1716
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1717
|
+
return state.info;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1722
|
+
return state.info;
|
|
1505
1723
|
}
|
|
1506
1724
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
throw new Error("element doesn't contain text " + text);
|
|
1725
|
+
catch (e) {
|
|
1726
|
+
// Log error but continue retrying until timeout is reached
|
|
1727
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1728
|
+
}
|
|
1729
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1513
1730
|
}
|
|
1514
|
-
|
|
1731
|
+
state.info.foundText = foundObj?.text;
|
|
1732
|
+
state.info.value = foundObj?.value;
|
|
1733
|
+
throw new Error("element doesn't contain text " + text);
|
|
1515
1734
|
}
|
|
1516
1735
|
catch (e) {
|
|
1517
|
-
|
|
1518
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1519
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1520
|
-
info.screenshotPath = screenshotPath;
|
|
1521
|
-
Object.assign(e, { info: info });
|
|
1522
|
-
error = e;
|
|
1736
|
+
await _commandError(state, e, this);
|
|
1523
1737
|
throw e;
|
|
1524
1738
|
}
|
|
1525
1739
|
finally {
|
|
1526
|
-
|
|
1527
|
-
this._reportToWorld(world, {
|
|
1528
|
-
element_name: selectors.element_name,
|
|
1529
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1530
|
-
text: `Verify element contains text: ${text}`,
|
|
1531
|
-
value: text,
|
|
1532
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1533
|
-
result: error
|
|
1534
|
-
? {
|
|
1535
|
-
status: "FAILED",
|
|
1536
|
-
startTime,
|
|
1537
|
-
endTime,
|
|
1538
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1539
|
-
}
|
|
1540
|
-
: {
|
|
1541
|
-
status: "PASSED",
|
|
1542
|
-
startTime,
|
|
1543
|
-
endTime,
|
|
1544
|
-
},
|
|
1545
|
-
info: info,
|
|
1546
|
-
});
|
|
1740
|
+
await _commandFinally(state, this);
|
|
1547
1741
|
}
|
|
1548
1742
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1743
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1744
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1745
|
+
const startTime = Date.now();
|
|
1746
|
+
const state = {
|
|
1747
|
+
_params,
|
|
1748
|
+
value: referanceSnapshot,
|
|
1749
|
+
options,
|
|
1750
|
+
world,
|
|
1751
|
+
locate: false,
|
|
1752
|
+
scroll: false,
|
|
1753
|
+
screenshot: true,
|
|
1754
|
+
highlight: false,
|
|
1755
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1756
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1757
|
+
operation: "snapshotValidation",
|
|
1758
|
+
log: "***** verify snapshot *****\n",
|
|
1759
|
+
};
|
|
1760
|
+
if (!referanceSnapshot) {
|
|
1761
|
+
throw new Error("referanceSnapshot is null");
|
|
1553
1762
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1763
|
+
let text = null;
|
|
1764
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1765
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1556
1766
|
}
|
|
1557
|
-
else if (this.
|
|
1558
|
-
|
|
1767
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1768
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1769
|
+
}
|
|
1770
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1771
|
+
text = referanceSnapshot.substring(5);
|
|
1559
1772
|
}
|
|
1560
1773
|
else {
|
|
1561
|
-
|
|
1774
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1775
|
+
}
|
|
1776
|
+
state.text = text;
|
|
1777
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1778
|
+
await _preCommand(state, this);
|
|
1779
|
+
let foundObj = null;
|
|
1780
|
+
try {
|
|
1781
|
+
let matchResult = null;
|
|
1782
|
+
while (Date.now() - startTime < timeout) {
|
|
1783
|
+
try {
|
|
1784
|
+
let scope = null;
|
|
1785
|
+
if (!frameSelectors) {
|
|
1786
|
+
scope = this.page;
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1790
|
+
}
|
|
1791
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1792
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1793
|
+
if (matchResult.errorLine !== -1) {
|
|
1794
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1795
|
+
}
|
|
1796
|
+
// highlight and screenshot
|
|
1797
|
+
try {
|
|
1798
|
+
await await highlightSnapshot(newValue, scope);
|
|
1799
|
+
await _screenshot(state, this);
|
|
1800
|
+
}
|
|
1801
|
+
catch (e) { }
|
|
1802
|
+
return state.info;
|
|
1803
|
+
}
|
|
1804
|
+
catch (e) {
|
|
1805
|
+
// Log error but continue retrying until timeout is reached
|
|
1806
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1807
|
+
}
|
|
1808
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1809
|
+
}
|
|
1810
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1811
|
+
}
|
|
1812
|
+
catch (e) {
|
|
1813
|
+
await _commandError(state, e, this);
|
|
1814
|
+
throw e;
|
|
1815
|
+
}
|
|
1816
|
+
finally {
|
|
1817
|
+
await _commandFinally(state, this);
|
|
1562
1818
|
}
|
|
1563
|
-
return dataFile;
|
|
1564
1819
|
}
|
|
1565
1820
|
async waitForUserInput(message, world = null) {
|
|
1566
1821
|
if (!message) {
|
|
@@ -1590,12 +1845,21 @@ class StableBrowser {
|
|
|
1590
1845
|
return;
|
|
1591
1846
|
}
|
|
1592
1847
|
// if data file exists, load it
|
|
1593
|
-
const dataFile =
|
|
1848
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1594
1849
|
let data = this.getTestData(world);
|
|
1595
1850
|
// merge the testData with the existing data
|
|
1596
1851
|
Object.assign(data, testData);
|
|
1597
1852
|
// save the data to the file
|
|
1598
|
-
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1853
|
+
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1854
|
+
}
|
|
1855
|
+
overwriteTestData(testData, world = null) {
|
|
1856
|
+
if (!testData) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
// if data file exists, load it
|
|
1860
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1861
|
+
// save the data to the file
|
|
1862
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1599
1863
|
}
|
|
1600
1864
|
_getDataFilePath(fileName) {
|
|
1601
1865
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
@@ -1693,7 +1957,7 @@ class StableBrowser {
|
|
|
1693
1957
|
}
|
|
1694
1958
|
}
|
|
1695
1959
|
getTestData(world = null) {
|
|
1696
|
-
const dataFile =
|
|
1960
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1697
1961
|
let data = {};
|
|
1698
1962
|
if (fs.existsSync(dataFile)) {
|
|
1699
1963
|
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
@@ -1725,11 +1989,9 @@ class StableBrowser {
|
|
|
1725
1989
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1726
1990
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1727
1991
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
}
|
|
1732
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
1992
|
+
// to make sure the path doesn't start with -
|
|
1993
|
+
const uuidStr = "id_" + randomUUID();
|
|
1994
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1733
1995
|
try {
|
|
1734
1996
|
await this.takeScreenshot(screenshotPath);
|
|
1735
1997
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1739,15 +2001,15 @@ class StableBrowser {
|
|
|
1739
2001
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1740
2002
|
// }
|
|
1741
2003
|
// });
|
|
2004
|
+
result.screenshotId = uuidStr;
|
|
2005
|
+
result.screenshotPath = screenshotPath;
|
|
2006
|
+
if (info && info.box) {
|
|
2007
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
2008
|
+
}
|
|
1742
2009
|
}
|
|
1743
2010
|
catch (e) {
|
|
1744
2011
|
this.logger.info("unable to take screenshot, ignored");
|
|
1745
2012
|
}
|
|
1746
|
-
result.screenshotId = nextIndex;
|
|
1747
|
-
result.screenshotPath = screenshotPath;
|
|
1748
|
-
if (info && info.box) {
|
|
1749
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1750
|
-
}
|
|
1751
2013
|
}
|
|
1752
2014
|
else if (options && options.screenshot) {
|
|
1753
2015
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1772,7 +2034,6 @@ class StableBrowser {
|
|
|
1772
2034
|
}
|
|
1773
2035
|
async takeScreenshot(screenshotPath) {
|
|
1774
2036
|
const playContext = this.context.playContext;
|
|
1775
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1776
2037
|
// Using CDP to capture the screenshot
|
|
1777
2038
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1778
2039
|
document.body.scrollWidth,
|
|
@@ -1782,164 +2043,433 @@ class StableBrowser {
|
|
|
1782
2043
|
document.body.clientWidth,
|
|
1783
2044
|
document.documentElement.clientWidth,
|
|
1784
2045
|
])));
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
screenshotBuffer =
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
|
|
2046
|
+
let screenshotBuffer = null;
|
|
2047
|
+
// if (focusedElement) {
|
|
2048
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2049
|
+
// await this._unhighlightElements(focusedElement);
|
|
2050
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2051
|
+
// console.log(`Unhighlighted previous element`);
|
|
2052
|
+
// }
|
|
2053
|
+
// if (focusedElement) {
|
|
2054
|
+
// await this._highlightElements(focusedElement);
|
|
2055
|
+
// }
|
|
2056
|
+
if (this.context.browserName === "chromium") {
|
|
2057
|
+
const client = await playContext.newCDPSession(this.page);
|
|
2058
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
2059
|
+
format: "png",
|
|
2060
|
+
// clip: {
|
|
2061
|
+
// x: 0,
|
|
2062
|
+
// y: 0,
|
|
2063
|
+
// width: viewportWidth,
|
|
2064
|
+
// height: viewportHeight,
|
|
2065
|
+
// scale: 1,
|
|
2066
|
+
// },
|
|
2067
|
+
});
|
|
2068
|
+
await client.detach();
|
|
2069
|
+
if (!screenshotPath) {
|
|
2070
|
+
return data;
|
|
2071
|
+
}
|
|
2072
|
+
screenshotBuffer = Buffer.from(data, "base64");
|
|
2073
|
+
}
|
|
2074
|
+
else {
|
|
2075
|
+
screenshotBuffer = await this.page.screenshot();
|
|
2076
|
+
}
|
|
2077
|
+
// if (focusedElement) {
|
|
2078
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2079
|
+
// await this._unhighlightElements(focusedElement);
|
|
2080
|
+
// }
|
|
2081
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
2082
|
+
// Get the image dimensions
|
|
2083
|
+
const { width, height } = image.bitmap;
|
|
2084
|
+
const resizeRatio = viewportWidth / width;
|
|
2085
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
2086
|
+
if (width > viewportWidth) {
|
|
2087
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
2088
|
+
await image.write(screenshotPath);
|
|
2089
|
+
}
|
|
2090
|
+
else {
|
|
2091
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
2092
|
+
}
|
|
2093
|
+
return screenshotBuffer;
|
|
1820
2094
|
}
|
|
1821
2095
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
2096
|
+
const state = {
|
|
2097
|
+
selectors,
|
|
2098
|
+
_params,
|
|
2099
|
+
options,
|
|
2100
|
+
world,
|
|
2101
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2102
|
+
text: `Verify element exists in page`,
|
|
2103
|
+
operation: "verifyElementExistInPage",
|
|
2104
|
+
log: "***** verify element " + selectors.element_name + " exists in page *****\n",
|
|
2105
|
+
};
|
|
1827
2106
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1828
|
-
const info = {};
|
|
1829
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1830
|
-
info.operation = "verify";
|
|
1831
|
-
info.selectors = selectors;
|
|
1832
2107
|
try {
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1837
|
-
await this._highlightElements(element);
|
|
1838
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1839
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1840
|
-
return info;
|
|
2108
|
+
await _preCommand(state, this);
|
|
2109
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
2110
|
+
return state.info;
|
|
1841
2111
|
}
|
|
1842
2112
|
catch (e) {
|
|
1843
|
-
|
|
1844
|
-
this.logger.error("verify failed " + info.log);
|
|
1845
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1846
|
-
info.screenshotPath = screenshotPath;
|
|
1847
|
-
Object.assign(e, { info: info });
|
|
1848
|
-
error = e;
|
|
1849
|
-
throw e;
|
|
2113
|
+
await _commandError(state, e, this);
|
|
1850
2114
|
}
|
|
1851
2115
|
finally {
|
|
1852
|
-
|
|
1853
|
-
this._reportToWorld(world, {
|
|
1854
|
-
element_name: selectors.element_name,
|
|
1855
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1856
|
-
text: "Verify element exists in page",
|
|
1857
|
-
screenshotId,
|
|
1858
|
-
result: error
|
|
1859
|
-
? {
|
|
1860
|
-
status: "FAILED",
|
|
1861
|
-
startTime,
|
|
1862
|
-
endTime,
|
|
1863
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1864
|
-
}
|
|
1865
|
-
: {
|
|
1866
|
-
status: "PASSED",
|
|
1867
|
-
startTime,
|
|
1868
|
-
endTime,
|
|
1869
|
-
},
|
|
1870
|
-
info: info,
|
|
1871
|
-
});
|
|
2116
|
+
await _commandFinally(state, this);
|
|
1872
2117
|
}
|
|
1873
2118
|
}
|
|
1874
2119
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2120
|
+
const state = {
|
|
2121
|
+
selectors,
|
|
2122
|
+
_params,
|
|
2123
|
+
attribute,
|
|
2124
|
+
variable,
|
|
2125
|
+
options,
|
|
2126
|
+
world,
|
|
2127
|
+
type: Types.EXTRACT,
|
|
2128
|
+
text: `Extract attribute from element`,
|
|
2129
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
2130
|
+
operation: "extractAttribute",
|
|
2131
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2132
|
+
allowDisabled: true,
|
|
2133
|
+
};
|
|
1880
2134
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1881
|
-
const info = {};
|
|
1882
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1883
|
-
info.operation = "extract";
|
|
1884
|
-
info.selectors = selectors;
|
|
1885
2135
|
try {
|
|
1886
|
-
|
|
1887
|
-
await this._highlightElements(element);
|
|
1888
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2136
|
+
await _preCommand(state, this);
|
|
1889
2137
|
switch (attribute) {
|
|
1890
2138
|
case "inner_text":
|
|
1891
|
-
|
|
2139
|
+
state.value = await state.element.innerText();
|
|
1892
2140
|
break;
|
|
1893
2141
|
case "href":
|
|
1894
|
-
|
|
2142
|
+
state.value = await state.element.getAttribute("href");
|
|
1895
2143
|
break;
|
|
1896
2144
|
case "value":
|
|
1897
|
-
|
|
2145
|
+
state.value = await state.element.inputValue();
|
|
2146
|
+
break;
|
|
2147
|
+
case "text":
|
|
2148
|
+
state.value = await state.element.textContent();
|
|
1898
2149
|
break;
|
|
1899
2150
|
default:
|
|
1900
|
-
|
|
2151
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1901
2152
|
break;
|
|
1902
2153
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2154
|
+
if (options !== null) {
|
|
2155
|
+
if (options.regex && options.regex !== "") {
|
|
2156
|
+
// Construct a regex pattern from the provided string
|
|
2157
|
+
const regex = options.regex.slice(1, -1);
|
|
2158
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2159
|
+
const matches = state.value.match(regexPattern);
|
|
2160
|
+
if (matches) {
|
|
2161
|
+
let newValue = "";
|
|
2162
|
+
for (const match of matches) {
|
|
2163
|
+
newValue += match;
|
|
2164
|
+
}
|
|
2165
|
+
state.value = newValue;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2169
|
+
state.value = state.value.trim();
|
|
2170
|
+
}
|
|
1906
2171
|
}
|
|
1907
|
-
|
|
1908
|
-
this.
|
|
1909
|
-
|
|
2172
|
+
state.info.value = state.value;
|
|
2173
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2174
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2175
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2176
|
+
return state.info;
|
|
1910
2177
|
}
|
|
1911
2178
|
catch (e) {
|
|
1912
|
-
|
|
1913
|
-
this.logger.error("extract failed " + info.log);
|
|
1914
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1915
|
-
info.screenshotPath = screenshotPath;
|
|
1916
|
-
Object.assign(e, { info: info });
|
|
1917
|
-
error = e;
|
|
1918
|
-
throw e;
|
|
2179
|
+
await _commandError(state, e, this);
|
|
1919
2180
|
}
|
|
1920
2181
|
finally {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2182
|
+
await _commandFinally(state, this);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2186
|
+
const state = {
|
|
2187
|
+
selectors,
|
|
2188
|
+
_params,
|
|
2189
|
+
property,
|
|
2190
|
+
variable,
|
|
2191
|
+
options,
|
|
2192
|
+
world,
|
|
2193
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2194
|
+
text: `Extract property from element`,
|
|
2195
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2196
|
+
operation: "extractProperty",
|
|
2197
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2198
|
+
allowDisabled: true,
|
|
2199
|
+
};
|
|
2200
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2201
|
+
try {
|
|
2202
|
+
await _preCommand(state, this);
|
|
2203
|
+
switch (property) {
|
|
2204
|
+
case "inner_text":
|
|
2205
|
+
state.value = await state.element.innerText();
|
|
2206
|
+
break;
|
|
2207
|
+
case "href":
|
|
2208
|
+
state.value = await state.element.getAttribute("href");
|
|
2209
|
+
break;
|
|
2210
|
+
case "value":
|
|
2211
|
+
state.value = await state.element.inputValue();
|
|
2212
|
+
break;
|
|
2213
|
+
case "text":
|
|
2214
|
+
state.value = await state.element.textContent();
|
|
2215
|
+
break;
|
|
2216
|
+
default:
|
|
2217
|
+
if (property.startsWith("dataset.")) {
|
|
2218
|
+
const dataAttribute = property.substring(8);
|
|
2219
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
1935
2220
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2221
|
+
else {
|
|
2222
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
if (options !== null) {
|
|
2226
|
+
if (options.regex && options.regex !== "") {
|
|
2227
|
+
// Construct a regex pattern from the provided string
|
|
2228
|
+
const regex = options.regex.slice(1, -1);
|
|
2229
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2230
|
+
const matches = state.value.match(regexPattern);
|
|
2231
|
+
if (matches) {
|
|
2232
|
+
let newValue = "";
|
|
2233
|
+
for (const match of matches) {
|
|
2234
|
+
newValue += match;
|
|
2235
|
+
}
|
|
2236
|
+
state.value = newValue;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2240
|
+
state.value = state.value.trim();
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
state.info.value = state.value;
|
|
2244
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2245
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2246
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2247
|
+
return state.info;
|
|
2248
|
+
}
|
|
2249
|
+
catch (e) {
|
|
2250
|
+
await _commandError(state, e, this);
|
|
2251
|
+
}
|
|
2252
|
+
finally {
|
|
2253
|
+
await _commandFinally(state, this);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2257
|
+
const state = {
|
|
2258
|
+
selectors,
|
|
2259
|
+
_params,
|
|
2260
|
+
attribute,
|
|
2261
|
+
value,
|
|
2262
|
+
options,
|
|
2263
|
+
world,
|
|
2264
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2265
|
+
highlight: true,
|
|
2266
|
+
screenshot: true,
|
|
2267
|
+
text: `Verify element attribute`,
|
|
2268
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2269
|
+
operation: "verifyAttribute",
|
|
2270
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2271
|
+
allowDisabled: true,
|
|
2272
|
+
};
|
|
2273
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2274
|
+
let val;
|
|
2275
|
+
let expectedValue;
|
|
2276
|
+
try {
|
|
2277
|
+
await _preCommand(state, this);
|
|
2278
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2279
|
+
state.info.expectedValue = expectedValue;
|
|
2280
|
+
switch (attribute) {
|
|
2281
|
+
case "innerText":
|
|
2282
|
+
val = String(await state.element.innerText());
|
|
2283
|
+
break;
|
|
2284
|
+
case "text":
|
|
2285
|
+
val = String(await state.element.textContent());
|
|
2286
|
+
break;
|
|
2287
|
+
case "value":
|
|
2288
|
+
val = String(await state.element.inputValue());
|
|
2289
|
+
break;
|
|
2290
|
+
case "checked":
|
|
2291
|
+
val = String(await state.element.isChecked());
|
|
2292
|
+
break;
|
|
2293
|
+
case "disabled":
|
|
2294
|
+
val = String(await state.element.isDisabled());
|
|
2295
|
+
break;
|
|
2296
|
+
case "readOnly":
|
|
2297
|
+
const isEditable = await state.element.isEditable();
|
|
2298
|
+
val = String(!isEditable);
|
|
2299
|
+
break;
|
|
2300
|
+
default:
|
|
2301
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2302
|
+
break;
|
|
2303
|
+
}
|
|
2304
|
+
state.info.value = val;
|
|
2305
|
+
let regex;
|
|
2306
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2307
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2308
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2309
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2310
|
+
state.info.regex = true;
|
|
2311
|
+
}
|
|
2312
|
+
else {
|
|
2313
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2314
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2315
|
+
}
|
|
2316
|
+
if (attribute === "innerText") {
|
|
2317
|
+
if (state.info.regex) {
|
|
2318
|
+
if (!regex.test(val)) {
|
|
2319
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2320
|
+
state.info.failCause.assertionFailed = true;
|
|
2321
|
+
state.info.failCause.lastError = errorMessage;
|
|
2322
|
+
throw new Error(errorMessage);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
else {
|
|
2326
|
+
const valLines = val.split("\n");
|
|
2327
|
+
const expectedLines = expectedValue.split("\n");
|
|
2328
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2329
|
+
if (!isPart) {
|
|
2330
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2331
|
+
state.info.failCause.assertionFailed = true;
|
|
2332
|
+
state.info.failCause.lastError = errorMessage;
|
|
2333
|
+
throw new Error(errorMessage);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
else {
|
|
2338
|
+
if (!val.match(regex)) {
|
|
2339
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2340
|
+
state.info.failCause.assertionFailed = true;
|
|
2341
|
+
state.info.failCause.lastError = errorMessage;
|
|
2342
|
+
throw new Error(errorMessage);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return state.info;
|
|
2346
|
+
}
|
|
2347
|
+
catch (e) {
|
|
2348
|
+
await _commandError(state, e, this);
|
|
2349
|
+
}
|
|
2350
|
+
finally {
|
|
2351
|
+
await _commandFinally(state, this);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2355
|
+
const state = {
|
|
2356
|
+
selectors,
|
|
2357
|
+
_params,
|
|
2358
|
+
property,
|
|
2359
|
+
value,
|
|
2360
|
+
options,
|
|
2361
|
+
world,
|
|
2362
|
+
type: Types.VERIFY_PROPERTY,
|
|
2363
|
+
highlight: true,
|
|
2364
|
+
screenshot: true,
|
|
2365
|
+
text: `Verify element property`,
|
|
2366
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2367
|
+
operation: "verifyProperty",
|
|
2368
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2369
|
+
allowDisabled: true,
|
|
2370
|
+
};
|
|
2371
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2372
|
+
let val;
|
|
2373
|
+
let expectedValue;
|
|
2374
|
+
try {
|
|
2375
|
+
await _preCommand(state, this);
|
|
2376
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2377
|
+
state.info.expectedValue = expectedValue;
|
|
2378
|
+
switch (property) {
|
|
2379
|
+
case "innerText":
|
|
2380
|
+
val = String(await state.element.innerText());
|
|
2381
|
+
break;
|
|
2382
|
+
case "text":
|
|
2383
|
+
val = String(await state.element.textContent());
|
|
2384
|
+
break;
|
|
2385
|
+
case "value":
|
|
2386
|
+
val = String(await state.element.inputValue());
|
|
2387
|
+
break;
|
|
2388
|
+
case "checked":
|
|
2389
|
+
val = String(await state.element.isChecked());
|
|
2390
|
+
break;
|
|
2391
|
+
case "disabled":
|
|
2392
|
+
val = String(await state.element.isDisabled());
|
|
2393
|
+
break;
|
|
2394
|
+
case "readOnly":
|
|
2395
|
+
const isEditable = await state.element.isEditable();
|
|
2396
|
+
val = String(!isEditable);
|
|
2397
|
+
break;
|
|
2398
|
+
case "innerHTML":
|
|
2399
|
+
val = String(await state.element.innerHTML());
|
|
2400
|
+
break;
|
|
2401
|
+
case "outerHTML":
|
|
2402
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2403
|
+
break;
|
|
2404
|
+
default:
|
|
2405
|
+
if (property.startsWith("dataset.")) {
|
|
2406
|
+
const dataAttribute = property.substring(8);
|
|
2407
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2408
|
+
}
|
|
2409
|
+
else {
|
|
2410
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
// Helper function to remove all style="" attributes
|
|
2414
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2415
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, '');
|
|
2416
|
+
};
|
|
2417
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2418
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2419
|
+
val = removeStyleAttributes(val);
|
|
2420
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2421
|
+
}
|
|
2422
|
+
state.info.value = val;
|
|
2423
|
+
let regex;
|
|
2424
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2425
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2426
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2427
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2428
|
+
state.info.regex = true;
|
|
2429
|
+
}
|
|
2430
|
+
else {
|
|
2431
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2432
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2433
|
+
}
|
|
2434
|
+
if (property === "innerText") {
|
|
2435
|
+
if (state.info.regex) {
|
|
2436
|
+
if (!regex.test(val)) {
|
|
2437
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2438
|
+
state.info.failCause.assertionFailed = true;
|
|
2439
|
+
state.info.failCause.lastError = errorMessage;
|
|
2440
|
+
throw new Error(errorMessage);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
else {
|
|
2444
|
+
// Fix: Replace escaped newlines with actual newlines before splitting
|
|
2445
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, '\n');
|
|
2446
|
+
const valLines = val.split("\n");
|
|
2447
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2448
|
+
// Check if all expected lines are present in the actual lines
|
|
2449
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2450
|
+
if (!isPart) {
|
|
2451
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2452
|
+
state.info.failCause.assertionFailed = true;
|
|
2453
|
+
state.info.failCause.lastError = errorMessage;
|
|
2454
|
+
throw new Error(errorMessage);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
else {
|
|
2459
|
+
if (!val.match(regex)) {
|
|
2460
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2461
|
+
state.info.failCause.assertionFailed = true;
|
|
2462
|
+
state.info.failCause.lastError = errorMessage;
|
|
2463
|
+
throw new Error(errorMessage);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return state.info;
|
|
2467
|
+
}
|
|
2468
|
+
catch (e) {
|
|
2469
|
+
await _commandError(state, e, this);
|
|
2470
|
+
}
|
|
2471
|
+
finally {
|
|
2472
|
+
await _commandFinally(state, this);
|
|
1943
2473
|
}
|
|
1944
2474
|
}
|
|
1945
2475
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -1960,7 +2490,7 @@ class StableBrowser {
|
|
|
1960
2490
|
if (options && options.timeout) {
|
|
1961
2491
|
timeout = options.timeout;
|
|
1962
2492
|
}
|
|
1963
|
-
const serviceUrl =
|
|
2493
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1964
2494
|
const request = {
|
|
1965
2495
|
method: "POST",
|
|
1966
2496
|
url: serviceUrl,
|
|
@@ -2016,7 +2546,8 @@ class StableBrowser {
|
|
|
2016
2546
|
catch (e) {
|
|
2017
2547
|
errorCount++;
|
|
2018
2548
|
if (errorCount > 3) {
|
|
2019
|
-
throw e;
|
|
2549
|
+
// throw e;
|
|
2550
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2020
2551
|
}
|
|
2021
2552
|
// ignore
|
|
2022
2553
|
}
|
|
@@ -2030,27 +2561,32 @@ class StableBrowser {
|
|
|
2030
2561
|
async _highlightElements(scope, css) {
|
|
2031
2562
|
try {
|
|
2032
2563
|
if (!scope) {
|
|
2564
|
+
// console.log(`Scope is not defined`);
|
|
2033
2565
|
return;
|
|
2034
2566
|
}
|
|
2035
2567
|
if (!css) {
|
|
2036
2568
|
scope
|
|
2037
2569
|
.evaluate((node) => {
|
|
2038
2570
|
if (node && node.style) {
|
|
2039
|
-
let
|
|
2040
|
-
|
|
2571
|
+
let originalOutline = node.style.outline;
|
|
2572
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
2573
|
+
// node.__previousOutline = originalOutline;
|
|
2574
|
+
node.style.outline = "2px solid red";
|
|
2575
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
2041
2576
|
if (window) {
|
|
2042
2577
|
window.addEventListener("beforeunload", function (e) {
|
|
2043
|
-
node.style.
|
|
2578
|
+
node.style.outline = originalOutline;
|
|
2044
2579
|
});
|
|
2045
2580
|
}
|
|
2046
2581
|
setTimeout(function () {
|
|
2047
|
-
node.style.
|
|
2582
|
+
node.style.outline = originalOutline;
|
|
2048
2583
|
}, 2000);
|
|
2049
2584
|
}
|
|
2050
2585
|
})
|
|
2051
2586
|
.then(() => { })
|
|
2052
2587
|
.catch((e) => {
|
|
2053
2588
|
// ignore
|
|
2589
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2054
2590
|
});
|
|
2055
2591
|
}
|
|
2056
2592
|
else {
|
|
@@ -2066,17 +2602,18 @@ class StableBrowser {
|
|
|
2066
2602
|
if (!element.style) {
|
|
2067
2603
|
return;
|
|
2068
2604
|
}
|
|
2069
|
-
|
|
2605
|
+
let originalOutline = element.style.outline;
|
|
2606
|
+
element.__previousOutline = originalOutline;
|
|
2070
2607
|
// Set the new border to be red and 2px solid
|
|
2071
|
-
element.style.
|
|
2608
|
+
element.style.outline = "2px solid red";
|
|
2072
2609
|
if (window) {
|
|
2073
2610
|
window.addEventListener("beforeunload", function (e) {
|
|
2074
|
-
element.style.
|
|
2611
|
+
element.style.outline = originalOutline;
|
|
2075
2612
|
});
|
|
2076
2613
|
}
|
|
2077
2614
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2078
2615
|
setTimeout(function () {
|
|
2079
|
-
element.style.
|
|
2616
|
+
element.style.outline = originalOutline;
|
|
2080
2617
|
}, 2000);
|
|
2081
2618
|
}
|
|
2082
2619
|
return;
|
|
@@ -2084,6 +2621,7 @@ class StableBrowser {
|
|
|
2084
2621
|
.then(() => { })
|
|
2085
2622
|
.catch((e) => {
|
|
2086
2623
|
// ignore
|
|
2624
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2087
2625
|
});
|
|
2088
2626
|
}
|
|
2089
2627
|
}
|
|
@@ -2091,173 +2629,563 @@ class StableBrowser {
|
|
|
2091
2629
|
console.debug(error);
|
|
2092
2630
|
}
|
|
2093
2631
|
}
|
|
2632
|
+
_matcher(text) {
|
|
2633
|
+
if (!text) {
|
|
2634
|
+
return { matcher: "contains", queryText: "" };
|
|
2635
|
+
}
|
|
2636
|
+
if (text.length < 2) {
|
|
2637
|
+
return { matcher: "contains", queryText: text };
|
|
2638
|
+
}
|
|
2639
|
+
const split = text.split(":");
|
|
2640
|
+
const matcher = split[0].toLowerCase();
|
|
2641
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2642
|
+
return { matcher, queryText };
|
|
2643
|
+
}
|
|
2644
|
+
_getDomain(url) {
|
|
2645
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2646
|
+
return "";
|
|
2647
|
+
}
|
|
2648
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2649
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2650
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2651
|
+
}
|
|
2652
|
+
let n = hostnameFragments.length;
|
|
2653
|
+
let fragments = [...hostnameFragments];
|
|
2654
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2655
|
+
hostnameFragments.pop();
|
|
2656
|
+
n = hostnameFragments.length;
|
|
2657
|
+
}
|
|
2658
|
+
if (n == 0) {
|
|
2659
|
+
if (fragments[0] === "www")
|
|
2660
|
+
fragments = fragments.slice(1);
|
|
2661
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2662
|
+
}
|
|
2663
|
+
if (hostnameFragments[0] === "www")
|
|
2664
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2665
|
+
return hostnameFragments.join(".");
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Verify the page path matches the given path.
|
|
2669
|
+
* @param {string} pathPart - The path to verify.
|
|
2670
|
+
* @param {object} options - Options for verification.
|
|
2671
|
+
* @param {object} world - The world context.
|
|
2672
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2673
|
+
*/
|
|
2094
2674
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2095
|
-
const startTime = Date.now();
|
|
2096
2675
|
let error = null;
|
|
2097
2676
|
let screenshotId = null;
|
|
2098
2677
|
let screenshotPath = null;
|
|
2099
2678
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2100
|
-
const info = {};
|
|
2101
|
-
info.log = "***** verify page path " + pathPart + " *****\n";
|
|
2102
|
-
info.operation = "verifyPagePath";
|
|
2103
|
-
const newValue = await this._replaceWithLocalData(pathPart, world);
|
|
2104
|
-
if (newValue !== pathPart) {
|
|
2105
|
-
this.logger.info(pathPart + "=" + newValue);
|
|
2106
|
-
pathPart = newValue;
|
|
2679
|
+
const info = {};
|
|
2680
|
+
info.log = "***** verify page path " + pathPart + " *****\n";
|
|
2681
|
+
info.operation = "verifyPagePath";
|
|
2682
|
+
const newValue = await this._replaceWithLocalData(pathPart, world);
|
|
2683
|
+
if (newValue !== pathPart) {
|
|
2684
|
+
this.logger.info(pathPart + "=" + newValue);
|
|
2685
|
+
pathPart = newValue;
|
|
2686
|
+
}
|
|
2687
|
+
info.pathPart = pathPart;
|
|
2688
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2689
|
+
const state = {
|
|
2690
|
+
text_search: queryText,
|
|
2691
|
+
options,
|
|
2692
|
+
world,
|
|
2693
|
+
locate: false,
|
|
2694
|
+
scroll: false,
|
|
2695
|
+
highlight: false,
|
|
2696
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2697
|
+
text: `Verify the page url is ${queryText}`,
|
|
2698
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2699
|
+
operation: "verifyPagePath",
|
|
2700
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2701
|
+
};
|
|
2702
|
+
try {
|
|
2703
|
+
await _preCommand(state, this);
|
|
2704
|
+
state.info.text = queryText;
|
|
2705
|
+
for (let i = 0; i < 30; i++) {
|
|
2706
|
+
const url = await this.page.url();
|
|
2707
|
+
switch (matcher) {
|
|
2708
|
+
case "exact":
|
|
2709
|
+
if (url !== queryText) {
|
|
2710
|
+
if (i === 29) {
|
|
2711
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2712
|
+
}
|
|
2713
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
break;
|
|
2717
|
+
case "contains":
|
|
2718
|
+
if (!url.includes(queryText)) {
|
|
2719
|
+
if (i === 29) {
|
|
2720
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2721
|
+
}
|
|
2722
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
break;
|
|
2726
|
+
case "starts-with":
|
|
2727
|
+
{
|
|
2728
|
+
const domain = this._getDomain(url);
|
|
2729
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2730
|
+
if (i === 29) {
|
|
2731
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2732
|
+
}
|
|
2733
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
break;
|
|
2738
|
+
case "ends-with":
|
|
2739
|
+
{
|
|
2740
|
+
const urlObj = new URL(url);
|
|
2741
|
+
let route = "/";
|
|
2742
|
+
if (urlObj.pathname !== "/") {
|
|
2743
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2744
|
+
}
|
|
2745
|
+
else {
|
|
2746
|
+
route = "/";
|
|
2747
|
+
}
|
|
2748
|
+
if (route !== queryText) {
|
|
2749
|
+
if (i === 29) {
|
|
2750
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2751
|
+
}
|
|
2752
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
break;
|
|
2757
|
+
case "regex":
|
|
2758
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2759
|
+
if (!regex.test(url)) {
|
|
2760
|
+
if (i === 29) {
|
|
2761
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2762
|
+
}
|
|
2763
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
break;
|
|
2767
|
+
default:
|
|
2768
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2769
|
+
if (!url.includes(pathPart)) {
|
|
2770
|
+
if (i === 29) {
|
|
2771
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2772
|
+
}
|
|
2773
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
await _screenshot(state, this);
|
|
2778
|
+
return state.info;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
catch (e) {
|
|
2782
|
+
state.info.failCause.lastError = e.message;
|
|
2783
|
+
state.info.failCause.assertionFailed = true;
|
|
2784
|
+
await _commandError(state, e, this);
|
|
2785
|
+
}
|
|
2786
|
+
finally {
|
|
2787
|
+
await _commandFinally(state, this);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Verify the page title matches the given title.
|
|
2792
|
+
* @param {string} title - The title to verify.
|
|
2793
|
+
* @param {object} options - Options for verification.
|
|
2794
|
+
* @param {object} world - The world context.
|
|
2795
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2796
|
+
*/
|
|
2797
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2798
|
+
let error = null;
|
|
2799
|
+
let screenshotId = null;
|
|
2800
|
+
let screenshotPath = null;
|
|
2801
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2802
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2803
|
+
if (newValue !== title) {
|
|
2804
|
+
this.logger.info(title + "=" + newValue);
|
|
2805
|
+
title = newValue;
|
|
2806
|
+
}
|
|
2807
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2808
|
+
const state = {
|
|
2809
|
+
text_search: queryText,
|
|
2810
|
+
options,
|
|
2811
|
+
world,
|
|
2812
|
+
locate: false,
|
|
2813
|
+
scroll: false,
|
|
2814
|
+
highlight: false,
|
|
2815
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2816
|
+
text: `Verify the page title is ${queryText}`,
|
|
2817
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2818
|
+
operation: "verifyPageTitle",
|
|
2819
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2820
|
+
};
|
|
2821
|
+
try {
|
|
2822
|
+
await _preCommand(state, this);
|
|
2823
|
+
state.info.text = queryText;
|
|
2824
|
+
for (let i = 0; i < 30; i++) {
|
|
2825
|
+
const foundTitle = await this.page.title();
|
|
2826
|
+
switch (matcher) {
|
|
2827
|
+
case "exact":
|
|
2828
|
+
if (foundTitle !== queryText) {
|
|
2829
|
+
if (i === 29) {
|
|
2830
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2831
|
+
}
|
|
2832
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
break;
|
|
2836
|
+
case "contains":
|
|
2837
|
+
if (!foundTitle.includes(queryText)) {
|
|
2838
|
+
if (i === 29) {
|
|
2839
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2840
|
+
}
|
|
2841
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
break;
|
|
2845
|
+
case "starts-with":
|
|
2846
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2847
|
+
if (i === 29) {
|
|
2848
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2849
|
+
}
|
|
2850
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
break;
|
|
2854
|
+
case "ends-with":
|
|
2855
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2856
|
+
if (i === 29) {
|
|
2857
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2858
|
+
}
|
|
2859
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
break;
|
|
2863
|
+
case "regex":
|
|
2864
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2865
|
+
if (!regex.test(foundTitle)) {
|
|
2866
|
+
if (i === 29) {
|
|
2867
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2868
|
+
}
|
|
2869
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
break;
|
|
2873
|
+
default:
|
|
2874
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2875
|
+
if (!foundTitle.includes(title)) {
|
|
2876
|
+
if (i === 29) {
|
|
2877
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2878
|
+
}
|
|
2879
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
await _screenshot(state, this);
|
|
2884
|
+
return state.info;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
catch (e) {
|
|
2888
|
+
state.info.failCause.lastError = e.message;
|
|
2889
|
+
state.info.failCause.assertionFailed = true;
|
|
2890
|
+
await _commandError(state, e, this);
|
|
2891
|
+
}
|
|
2892
|
+
finally {
|
|
2893
|
+
await _commandFinally(state, this);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2897
|
+
const frames = this.page.frames();
|
|
2898
|
+
let results = [];
|
|
2899
|
+
// let ignoreCase = false;
|
|
2900
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2901
|
+
if (dateAlternatives.date) {
|
|
2902
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2903
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2904
|
+
result.frame = frames[i];
|
|
2905
|
+
results.push(result);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
else if (numberAlternatives.number) {
|
|
2909
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2910
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2911
|
+
result.frame = frames[i];
|
|
2912
|
+
results.push(result);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
else {
|
|
2916
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2917
|
+
result.frame = frames[i];
|
|
2918
|
+
results.push(result);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
state.info.results = results;
|
|
2922
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2923
|
+
return resultWithElementsFound;
|
|
2924
|
+
}
|
|
2925
|
+
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2926
|
+
text = unEscapeString(text);
|
|
2927
|
+
const state = {
|
|
2928
|
+
text_search: text,
|
|
2929
|
+
options,
|
|
2930
|
+
world,
|
|
2931
|
+
locate: false,
|
|
2932
|
+
scroll: false,
|
|
2933
|
+
highlight: false,
|
|
2934
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2935
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2936
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2937
|
+
operation: "verifyTextExistInPage",
|
|
2938
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
2939
|
+
};
|
|
2940
|
+
if (testForRegex(text)) {
|
|
2941
|
+
text = text.replace(/\\"/g, '"');
|
|
2942
|
+
}
|
|
2943
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2944
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2945
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2946
|
+
if (newValue !== text) {
|
|
2947
|
+
this.logger.info(text + "=" + newValue);
|
|
2948
|
+
text = newValue;
|
|
2107
2949
|
}
|
|
2108
|
-
|
|
2950
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2951
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2109
2952
|
try {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2953
|
+
await _preCommand(state, this);
|
|
2954
|
+
state.info.text = text;
|
|
2955
|
+
while (true) {
|
|
2956
|
+
let resultWithElementsFound = {
|
|
2957
|
+
length: 0,
|
|
2958
|
+
};
|
|
2959
|
+
try {
|
|
2960
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2961
|
+
}
|
|
2962
|
+
catch (error) {
|
|
2963
|
+
// ignore
|
|
2964
|
+
}
|
|
2965
|
+
if (resultWithElementsFound.length === 0) {
|
|
2966
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2967
|
+
throw new Error(`Text ${text} not found in page`);
|
|
2115
2968
|
}
|
|
2116
2969
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2117
2970
|
continue;
|
|
2118
2971
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2972
|
+
try {
|
|
2973
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
2974
|
+
const frame = resultWithElementsFound[0].frame;
|
|
2975
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2976
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2977
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2978
|
+
if (element) {
|
|
2979
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2980
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
await _screenshot(state, this);
|
|
2984
|
+
return state.info;
|
|
2985
|
+
}
|
|
2986
|
+
catch (error) {
|
|
2987
|
+
console.error(error);
|
|
2988
|
+
}
|
|
2121
2989
|
}
|
|
2122
2990
|
}
|
|
2123
2991
|
catch (e) {
|
|
2124
|
-
|
|
2125
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2126
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2127
|
-
info.screenshotPath = screenshotPath;
|
|
2128
|
-
Object.assign(e, { info: info });
|
|
2129
|
-
error = e;
|
|
2130
|
-
throw e;
|
|
2992
|
+
await _commandError(state, e, this);
|
|
2131
2993
|
}
|
|
2132
2994
|
finally {
|
|
2133
|
-
|
|
2134
|
-
this._reportToWorld(world, {
|
|
2135
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2136
|
-
text: "Verify page path",
|
|
2137
|
-
screenshotId,
|
|
2138
|
-
result: error
|
|
2139
|
-
? {
|
|
2140
|
-
status: "FAILED",
|
|
2141
|
-
startTime,
|
|
2142
|
-
endTime,
|
|
2143
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2144
|
-
}
|
|
2145
|
-
: {
|
|
2146
|
-
status: "PASSED",
|
|
2147
|
-
startTime,
|
|
2148
|
-
endTime,
|
|
2149
|
-
},
|
|
2150
|
-
info: info,
|
|
2151
|
-
});
|
|
2995
|
+
await _commandFinally(state, this);
|
|
2152
2996
|
}
|
|
2153
2997
|
}
|
|
2154
|
-
async
|
|
2155
|
-
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2998
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2999
|
+
text = unEscapeString(text);
|
|
3000
|
+
const state = {
|
|
3001
|
+
text_search: text,
|
|
3002
|
+
options,
|
|
3003
|
+
world,
|
|
3004
|
+
locate: false,
|
|
3005
|
+
scroll: false,
|
|
3006
|
+
highlight: false,
|
|
3007
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
3008
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
3009
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
3010
|
+
operation: "verifyTextNotExistInPage",
|
|
3011
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
3012
|
+
};
|
|
3013
|
+
if (testForRegex(text)) {
|
|
3014
|
+
text = text.replace(/\\"/g, '"');
|
|
3015
|
+
}
|
|
3016
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2160
3017
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2161
|
-
const info = {};
|
|
2162
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2163
|
-
info.operation = "verifyTextExistInPage";
|
|
2164
3018
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2165
3019
|
if (newValue !== text) {
|
|
2166
3020
|
this.logger.info(text + "=" + newValue);
|
|
2167
3021
|
text = newValue;
|
|
2168
3022
|
}
|
|
2169
|
-
info.text = text;
|
|
2170
3023
|
let dateAlternatives = findDateAlternatives(text);
|
|
2171
3024
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2172
3025
|
try {
|
|
3026
|
+
await _preCommand(state, this);
|
|
3027
|
+
state.info.text = text;
|
|
3028
|
+
let resultWithElementsFound = {
|
|
3029
|
+
length: null, // initial cannot be 0
|
|
3030
|
+
};
|
|
2173
3031
|
while (true) {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
|
|
2180
|
-
result.frame = frames[i];
|
|
2181
|
-
results.push(result);
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
else if (numberAlternatives.number) {
|
|
2185
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2186
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
|
|
2187
|
-
result.frame = frames[i];
|
|
2188
|
-
results.push(result);
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
else {
|
|
2192
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, {});
|
|
2193
|
-
result.frame = frames[i];
|
|
2194
|
-
results.push(result);
|
|
2195
|
-
}
|
|
3032
|
+
try {
|
|
3033
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3034
|
+
}
|
|
3035
|
+
catch (error) {
|
|
3036
|
+
// ignore
|
|
2196
3037
|
}
|
|
2197
|
-
info.results = results;
|
|
2198
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2199
3038
|
if (resultWithElementsFound.length === 0) {
|
|
2200
|
-
|
|
2201
|
-
|
|
3039
|
+
await _screenshot(state, this);
|
|
3040
|
+
return state.info;
|
|
3041
|
+
}
|
|
3042
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3043
|
+
throw new Error(`Text ${text} found in page`);
|
|
3044
|
+
}
|
|
3045
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
catch (e) {
|
|
3049
|
+
await _commandError(state, e, this);
|
|
3050
|
+
}
|
|
3051
|
+
finally {
|
|
3052
|
+
await _commandFinally(state, this);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
3056
|
+
textAnchor = unEscapeString(textAnchor);
|
|
3057
|
+
textToVerify = unEscapeString(textToVerify);
|
|
3058
|
+
const state = {
|
|
3059
|
+
text_search: textToVerify,
|
|
3060
|
+
options,
|
|
3061
|
+
world,
|
|
3062
|
+
locate: false,
|
|
3063
|
+
scroll: false,
|
|
3064
|
+
highlight: false,
|
|
3065
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
3066
|
+
text: `Verify text with relation to another text`,
|
|
3067
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
3068
|
+
operation: "verify_text_with_relation",
|
|
3069
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3070
|
+
};
|
|
3071
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3072
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3073
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
3074
|
+
if (newValue !== textAnchor) {
|
|
3075
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
3076
|
+
textAnchor = newValue;
|
|
3077
|
+
}
|
|
3078
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
3079
|
+
if (newValue !== textToVerify) {
|
|
3080
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
3081
|
+
textToVerify = newValue;
|
|
3082
|
+
}
|
|
3083
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
3084
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
3085
|
+
let foundAncore = false;
|
|
3086
|
+
try {
|
|
3087
|
+
await _preCommand(state, this);
|
|
3088
|
+
state.info.text = textToVerify;
|
|
3089
|
+
let resultWithElementsFound = {
|
|
3090
|
+
length: 0,
|
|
3091
|
+
};
|
|
3092
|
+
while (true) {
|
|
3093
|
+
try {
|
|
3094
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3095
|
+
}
|
|
3096
|
+
catch (error) {
|
|
3097
|
+
// ignore
|
|
3098
|
+
}
|
|
3099
|
+
if (resultWithElementsFound.length === 0) {
|
|
3100
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3101
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2202
3102
|
}
|
|
2203
3103
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2204
3104
|
continue;
|
|
2205
3105
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
3106
|
+
try {
|
|
3107
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3108
|
+
foundAncore = true;
|
|
3109
|
+
const result = resultWithElementsFound[i];
|
|
3110
|
+
const token = result.randomToken;
|
|
3111
|
+
const frame = result.frame;
|
|
3112
|
+
let css = `[data-blinq-id-${token}]`;
|
|
3113
|
+
const climbArray1 = [];
|
|
3114
|
+
for (let i = 0; i < climb; i++) {
|
|
3115
|
+
climbArray1.push("..");
|
|
3116
|
+
}
|
|
3117
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
3118
|
+
css = css + " >> " + climbXpath;
|
|
3119
|
+
const count = await frame.locator(css).count();
|
|
3120
|
+
for (let j = 0; j < count; j++) {
|
|
3121
|
+
const continer = await frame.locator(css).nth(j);
|
|
3122
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
3123
|
+
if (result.elementCount > 0) {
|
|
3124
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
3125
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3126
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
3127
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
3128
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
3129
|
+
// this._highlightElements(frame, dataAttribute)
|
|
3130
|
+
// .then(async () => {
|
|
3131
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3132
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
3133
|
+
// () => {}
|
|
3134
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
3135
|
+
// );
|
|
3136
|
+
// })
|
|
3137
|
+
// .catch(e);
|
|
3138
|
+
// }
|
|
3139
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
3140
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3141
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3142
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
3143
|
+
if (element) {
|
|
3144
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3145
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3146
|
+
}
|
|
3147
|
+
await _screenshot(state, this);
|
|
3148
|
+
return state.info;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
2214
3151
|
}
|
|
2215
3152
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
3153
|
+
catch (error) {
|
|
3154
|
+
console.error(error);
|
|
3155
|
+
}
|
|
2218
3156
|
}
|
|
2219
3157
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2220
3158
|
}
|
|
2221
3159
|
catch (e) {
|
|
2222
|
-
|
|
2223
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2224
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2225
|
-
info.screenshotPath = screenshotPath;
|
|
2226
|
-
Object.assign(e, { info: info });
|
|
2227
|
-
error = e;
|
|
2228
|
-
throw e;
|
|
3160
|
+
await _commandError(state, e, this);
|
|
2229
3161
|
}
|
|
2230
3162
|
finally {
|
|
2231
|
-
|
|
2232
|
-
this._reportToWorld(world, {
|
|
2233
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2234
|
-
text: "Verify text exists in page",
|
|
2235
|
-
screenshotId,
|
|
2236
|
-
result: error
|
|
2237
|
-
? {
|
|
2238
|
-
status: "FAILED",
|
|
2239
|
-
startTime,
|
|
2240
|
-
endTime,
|
|
2241
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2242
|
-
}
|
|
2243
|
-
: {
|
|
2244
|
-
status: "PASSED",
|
|
2245
|
-
startTime,
|
|
2246
|
-
endTime,
|
|
2247
|
-
},
|
|
2248
|
-
info: info,
|
|
2249
|
-
});
|
|
3163
|
+
await _commandFinally(state, this);
|
|
2250
3164
|
}
|
|
2251
3165
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
3166
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
3167
|
+
const frames = this.page.frames();
|
|
3168
|
+
let results = [];
|
|
3169
|
+
let ignoreCase = false;
|
|
3170
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3171
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
3172
|
+
result.frame = frames[i];
|
|
3173
|
+
const climbArray = [];
|
|
3174
|
+
for (let i = 0; i < climb; i++) {
|
|
3175
|
+
climbArray.push("..");
|
|
3176
|
+
}
|
|
3177
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
3178
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
3179
|
+
const count = await frames[i].locator(newLocator).count();
|
|
3180
|
+
if (count > 0) {
|
|
3181
|
+
result.elementCount = count;
|
|
3182
|
+
result.locator = newLocator;
|
|
3183
|
+
results.push(result);
|
|
3184
|
+
}
|
|
2259
3185
|
}
|
|
2260
|
-
|
|
3186
|
+
// state.info.results = results;
|
|
3187
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3188
|
+
return resultWithElementsFound;
|
|
2261
3189
|
}
|
|
2262
3190
|
async visualVerification(text, options = {}, world = null) {
|
|
2263
3191
|
const startTime = Date.now();
|
|
@@ -2273,14 +3201,17 @@ class StableBrowser {
|
|
|
2273
3201
|
throw new Error("TOKEN is not set");
|
|
2274
3202
|
}
|
|
2275
3203
|
try {
|
|
2276
|
-
let serviceUrl =
|
|
3204
|
+
let serviceUrl = _getServerUrl();
|
|
2277
3205
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2278
3206
|
info.screenshotPath = screenshotPath;
|
|
2279
3207
|
const screenshot = await this.takeScreenshot();
|
|
2280
|
-
|
|
2281
|
-
method: "
|
|
3208
|
+
let request = {
|
|
3209
|
+
method: "post",
|
|
3210
|
+
maxBodyLength: Infinity,
|
|
2282
3211
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2283
3212
|
headers: {
|
|
3213
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3214
|
+
"x-source": "aaa",
|
|
2284
3215
|
"Content-Type": "application/json",
|
|
2285
3216
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2286
3217
|
},
|
|
@@ -2289,7 +3220,7 @@ class StableBrowser {
|
|
|
2289
3220
|
screenshot: screenshot,
|
|
2290
3221
|
}),
|
|
2291
3222
|
};
|
|
2292
|
-
|
|
3223
|
+
const result = await axios.request(request);
|
|
2293
3224
|
if (result.data.status !== true) {
|
|
2294
3225
|
throw new Error("Visual validation failed");
|
|
2295
3226
|
}
|
|
@@ -2309,20 +3240,22 @@ class StableBrowser {
|
|
|
2309
3240
|
info.screenshotPath = screenshotPath;
|
|
2310
3241
|
Object.assign(e, { info: info });
|
|
2311
3242
|
error = e;
|
|
2312
|
-
throw e;
|
|
3243
|
+
// throw e;
|
|
3244
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2313
3245
|
}
|
|
2314
3246
|
finally {
|
|
2315
3247
|
const endTime = Date.now();
|
|
2316
|
-
|
|
3248
|
+
_reportToWorld(world, {
|
|
2317
3249
|
type: Types.VERIFY_VISUAL,
|
|
2318
3250
|
text: "Visual verification",
|
|
3251
|
+
_text: "Visual verification of " + text,
|
|
2319
3252
|
screenshotId,
|
|
2320
3253
|
result: error
|
|
2321
3254
|
? {
|
|
2322
3255
|
status: "FAILED",
|
|
2323
3256
|
startTime,
|
|
2324
3257
|
endTime,
|
|
2325
|
-
message: error
|
|
3258
|
+
message: error?.message,
|
|
2326
3259
|
}
|
|
2327
3260
|
: {
|
|
2328
3261
|
status: "PASSED",
|
|
@@ -2354,13 +3287,14 @@ class StableBrowser {
|
|
|
2354
3287
|
this.logger.info("Table data verified");
|
|
2355
3288
|
}
|
|
2356
3289
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2357
|
-
|
|
3290
|
+
_validateSelectors(selectors);
|
|
2358
3291
|
const startTime = Date.now();
|
|
2359
3292
|
let error = null;
|
|
2360
3293
|
let screenshotId = null;
|
|
2361
3294
|
let screenshotPath = null;
|
|
2362
3295
|
const info = {};
|
|
2363
3296
|
info.log = "";
|
|
3297
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2364
3298
|
info.operation = "getTableData";
|
|
2365
3299
|
info.selectors = selectors;
|
|
2366
3300
|
try {
|
|
@@ -2376,11 +3310,12 @@ class StableBrowser {
|
|
|
2376
3310
|
info.screenshotPath = screenshotPath;
|
|
2377
3311
|
Object.assign(e, { info: info });
|
|
2378
3312
|
error = e;
|
|
2379
|
-
throw e;
|
|
3313
|
+
// throw e;
|
|
3314
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2380
3315
|
}
|
|
2381
3316
|
finally {
|
|
2382
3317
|
const endTime = Date.now();
|
|
2383
|
-
|
|
3318
|
+
_reportToWorld(world, {
|
|
2384
3319
|
element_name: selectors.element_name,
|
|
2385
3320
|
type: Types.GET_TABLE_DATA,
|
|
2386
3321
|
text: "Get table data",
|
|
@@ -2390,7 +3325,7 @@ class StableBrowser {
|
|
|
2390
3325
|
status: "FAILED",
|
|
2391
3326
|
startTime,
|
|
2392
3327
|
endTime,
|
|
2393
|
-
message: error
|
|
3328
|
+
message: error?.message,
|
|
2394
3329
|
}
|
|
2395
3330
|
: {
|
|
2396
3331
|
status: "PASSED",
|
|
@@ -2402,7 +3337,7 @@ class StableBrowser {
|
|
|
2402
3337
|
}
|
|
2403
3338
|
}
|
|
2404
3339
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2405
|
-
|
|
3340
|
+
_validateSelectors(selectors);
|
|
2406
3341
|
if (!query) {
|
|
2407
3342
|
throw new Error("query is null");
|
|
2408
3343
|
}
|
|
@@ -2435,7 +3370,7 @@ class StableBrowser {
|
|
|
2435
3370
|
info.operation = "analyzeTable";
|
|
2436
3371
|
info.selectors = selectors;
|
|
2437
3372
|
info.query = query;
|
|
2438
|
-
query =
|
|
3373
|
+
query = _fixUsingParams(query, _params);
|
|
2439
3374
|
info.query_fixed = query;
|
|
2440
3375
|
info.operator = operator;
|
|
2441
3376
|
info.value = value;
|
|
@@ -2541,11 +3476,12 @@ class StableBrowser {
|
|
|
2541
3476
|
info.screenshotPath = screenshotPath;
|
|
2542
3477
|
Object.assign(e, { info: info });
|
|
2543
3478
|
error = e;
|
|
2544
|
-
throw e;
|
|
3479
|
+
// throw e;
|
|
3480
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2545
3481
|
}
|
|
2546
3482
|
finally {
|
|
2547
3483
|
const endTime = Date.now();
|
|
2548
|
-
|
|
3484
|
+
_reportToWorld(world, {
|
|
2549
3485
|
element_name: selectors.element_name,
|
|
2550
3486
|
type: Types.ANALYZE_TABLE,
|
|
2551
3487
|
text: "Analyze table",
|
|
@@ -2555,7 +3491,7 @@ class StableBrowser {
|
|
|
2555
3491
|
status: "FAILED",
|
|
2556
3492
|
startTime,
|
|
2557
3493
|
endTime,
|
|
2558
|
-
message: error
|
|
3494
|
+
message: error?.message,
|
|
2559
3495
|
}
|
|
2560
3496
|
: {
|
|
2561
3497
|
status: "PASSED",
|
|
@@ -2566,28 +3502,51 @@ class StableBrowser {
|
|
|
2566
3502
|
});
|
|
2567
3503
|
}
|
|
2568
3504
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
3505
|
+
/**
|
|
3506
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3507
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3508
|
+
* @param options - Optional configuration object
|
|
3509
|
+
* @param world - Optional world context
|
|
3510
|
+
* @returns Promise that resolves after the specified duration
|
|
3511
|
+
*/
|
|
3512
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3513
|
+
const state = {
|
|
3514
|
+
duration,
|
|
3515
|
+
options,
|
|
3516
|
+
world,
|
|
3517
|
+
locate: false,
|
|
3518
|
+
scroll: false,
|
|
3519
|
+
screenshot: false,
|
|
3520
|
+
highlight: false,
|
|
3521
|
+
type: Types.SLEEP,
|
|
3522
|
+
text: `Sleep for ${duration} ms`,
|
|
3523
|
+
_text: `Sleep for ${duration} ms`,
|
|
3524
|
+
operation: "sleep",
|
|
3525
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3526
|
+
};
|
|
3527
|
+
try {
|
|
3528
|
+
await _preCommand(state, this);
|
|
3529
|
+
if (duration < 0) {
|
|
3530
|
+
throw new Error("Sleep duration cannot be negative");
|
|
2585
3531
|
}
|
|
3532
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3533
|
+
return state.info;
|
|
3534
|
+
}
|
|
3535
|
+
catch (e) {
|
|
3536
|
+
await _commandError(state, e, this);
|
|
3537
|
+
}
|
|
3538
|
+
finally {
|
|
3539
|
+
await _commandFinally(state, this);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
3543
|
+
try {
|
|
3544
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2586
3545
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
3546
|
+
catch (error) {
|
|
3547
|
+
this.logger.debug(error);
|
|
3548
|
+
throw error;
|
|
2589
3549
|
}
|
|
2590
|
-
return value;
|
|
2591
3550
|
}
|
|
2592
3551
|
_getLoadTimeout(options) {
|
|
2593
3552
|
let timeout = 15000;
|
|
@@ -2599,6 +3558,37 @@ class StableBrowser {
|
|
|
2599
3558
|
}
|
|
2600
3559
|
return timeout;
|
|
2601
3560
|
}
|
|
3561
|
+
_getFindElementTimeout(options) {
|
|
3562
|
+
if (options && options.timeout) {
|
|
3563
|
+
return options.timeout;
|
|
3564
|
+
}
|
|
3565
|
+
if (this.configuration.find_element_timeout) {
|
|
3566
|
+
return this.configuration.find_element_timeout;
|
|
3567
|
+
}
|
|
3568
|
+
return 30000;
|
|
3569
|
+
}
|
|
3570
|
+
async saveStoreState(path = null, world = null) {
|
|
3571
|
+
const storageState = await this.page.context().storageState();
|
|
3572
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
3573
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
3574
|
+
if (path) {
|
|
3575
|
+
// save { storageState: storageState } into the path
|
|
3576
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
3577
|
+
}
|
|
3578
|
+
else {
|
|
3579
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
async restoreSaveState(path = null, world = null) {
|
|
3583
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
3584
|
+
await refreshBrowser(this, path, world);
|
|
3585
|
+
this.registerEventListeners(this.context);
|
|
3586
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
3587
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
3588
|
+
if (this.onRestoreSaveState) {
|
|
3589
|
+
this.onRestoreSaveState(path);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
2602
3592
|
async waitForPageLoad(options = {}, world = null) {
|
|
2603
3593
|
let timeout = this._getLoadTimeout(options);
|
|
2604
3594
|
const promiseArray = [];
|
|
@@ -2638,7 +3628,7 @@ class StableBrowser {
|
|
|
2638
3628
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2639
3629
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2640
3630
|
const endTime = Date.now();
|
|
2641
|
-
|
|
3631
|
+
_reportToWorld(world, {
|
|
2642
3632
|
type: Types.GET_PAGE_STATUS,
|
|
2643
3633
|
text: "Wait for page load",
|
|
2644
3634
|
screenshotId,
|
|
@@ -2647,7 +3637,7 @@ class StableBrowser {
|
|
|
2647
3637
|
status: "FAILED",
|
|
2648
3638
|
startTime,
|
|
2649
3639
|
endTime,
|
|
2650
|
-
message: error
|
|
3640
|
+
message: error?.message,
|
|
2651
3641
|
}
|
|
2652
3642
|
: {
|
|
2653
3643
|
status: "PASSED",
|
|
@@ -2658,41 +3648,123 @@ class StableBrowser {
|
|
|
2658
3648
|
}
|
|
2659
3649
|
}
|
|
2660
3650
|
async closePage(options = {}, world = null) {
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
3651
|
+
const state = {
|
|
3652
|
+
options,
|
|
3653
|
+
world,
|
|
3654
|
+
locate: false,
|
|
3655
|
+
scroll: false,
|
|
3656
|
+
highlight: false,
|
|
3657
|
+
type: Types.CLOSE_PAGE,
|
|
3658
|
+
text: `Close page`,
|
|
3659
|
+
_text: `Close the page`,
|
|
3660
|
+
operation: "closePage",
|
|
3661
|
+
log: "***** close page *****\n",
|
|
3662
|
+
throwError: false,
|
|
3663
|
+
};
|
|
2666
3664
|
try {
|
|
3665
|
+
await _preCommand(state, this);
|
|
2667
3666
|
await this.page.close();
|
|
2668
3667
|
}
|
|
2669
3668
|
catch (e) {
|
|
2670
3669
|
console.log(".");
|
|
3670
|
+
await _commandError(state, e, this);
|
|
2671
3671
|
}
|
|
2672
3672
|
finally {
|
|
2673
|
-
await
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
3673
|
+
await _commandFinally(state, this);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3677
|
+
let operation = null;
|
|
3678
|
+
if (!options || !options.operation) {
|
|
3679
|
+
throw new Error("operation is not defined");
|
|
3680
|
+
}
|
|
3681
|
+
operation = options.operation;
|
|
3682
|
+
// validate operation is one of the supported operations
|
|
3683
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3684
|
+
throw new Error("operation is not supported");
|
|
3685
|
+
}
|
|
3686
|
+
const state = {
|
|
3687
|
+
options,
|
|
3688
|
+
world,
|
|
3689
|
+
locate: false,
|
|
3690
|
+
scroll: false,
|
|
3691
|
+
highlight: false,
|
|
3692
|
+
type: Types.TABLE_OPERATION,
|
|
3693
|
+
text: `Table operation`,
|
|
3694
|
+
_text: `Table ${operation} operation`,
|
|
3695
|
+
operation: operation,
|
|
3696
|
+
log: "***** Table operation *****\n",
|
|
3697
|
+
};
|
|
3698
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3699
|
+
try {
|
|
3700
|
+
await _preCommand(state, this);
|
|
3701
|
+
const start = Date.now();
|
|
3702
|
+
let cellArea = null;
|
|
3703
|
+
while (true) {
|
|
3704
|
+
try {
|
|
3705
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3706
|
+
if (cellArea) {
|
|
3707
|
+
break;
|
|
2686
3708
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
3709
|
+
}
|
|
3710
|
+
catch (e) {
|
|
3711
|
+
// ignore
|
|
3712
|
+
}
|
|
3713
|
+
if (Date.now() - start > timeout) {
|
|
3714
|
+
throw new Error(`Cell not found in table`);
|
|
3715
|
+
}
|
|
3716
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3717
|
+
}
|
|
3718
|
+
switch (operation) {
|
|
3719
|
+
case "click":
|
|
3720
|
+
if (!options.css) {
|
|
3721
|
+
// will click in the center of the cell
|
|
3722
|
+
let xOffset = 0;
|
|
3723
|
+
let yOffset = 0;
|
|
3724
|
+
if (options.xOffset) {
|
|
3725
|
+
xOffset = options.xOffset;
|
|
3726
|
+
}
|
|
3727
|
+
if (options.yOffset) {
|
|
3728
|
+
yOffset = options.yOffset;
|
|
3729
|
+
}
|
|
3730
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3731
|
+
}
|
|
3732
|
+
else {
|
|
3733
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3734
|
+
if (results.length === 0) {
|
|
3735
|
+
throw new Error(`Element not found in cell area`);
|
|
3736
|
+
}
|
|
3737
|
+
state.element = results[0];
|
|
3738
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3739
|
+
}
|
|
3740
|
+
break;
|
|
3741
|
+
case "hover+click":
|
|
3742
|
+
if (!options.css) {
|
|
3743
|
+
throw new Error("css is not defined");
|
|
3744
|
+
}
|
|
3745
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3746
|
+
if (results.length === 0) {
|
|
3747
|
+
throw new Error(`Element not found in cell area`);
|
|
3748
|
+
}
|
|
3749
|
+
state.element = results[0];
|
|
3750
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3751
|
+
break;
|
|
3752
|
+
default:
|
|
3753
|
+
throw new Error("operation is not supported");
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
catch (e) {
|
|
3757
|
+
await _commandError(state, e, this);
|
|
3758
|
+
}
|
|
3759
|
+
finally {
|
|
3760
|
+
await _commandFinally(state, this);
|
|
2694
3761
|
}
|
|
2695
3762
|
}
|
|
3763
|
+
saveTestDataAsGlobal(options, world) {
|
|
3764
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
3765
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
3766
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
3767
|
+
}
|
|
2696
3768
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2697
3769
|
const startTime = Date.now();
|
|
2698
3770
|
let error = null;
|
|
@@ -2710,21 +3782,23 @@ class StableBrowser {
|
|
|
2710
3782
|
}
|
|
2711
3783
|
catch (e) {
|
|
2712
3784
|
console.log(".");
|
|
3785
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2713
3786
|
}
|
|
2714
3787
|
finally {
|
|
2715
3788
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2716
3789
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2717
3790
|
const endTime = Date.now();
|
|
2718
|
-
|
|
3791
|
+
_reportToWorld(world, {
|
|
2719
3792
|
type: Types.SET_VIEWPORT,
|
|
2720
3793
|
text: "set viewport size to " + width + "x" + hight,
|
|
3794
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2721
3795
|
screenshotId,
|
|
2722
3796
|
result: error
|
|
2723
3797
|
? {
|
|
2724
3798
|
status: "FAILED",
|
|
2725
3799
|
startTime,
|
|
2726
3800
|
endTime,
|
|
2727
|
-
message: error
|
|
3801
|
+
message: error?.message,
|
|
2728
3802
|
}
|
|
2729
3803
|
: {
|
|
2730
3804
|
status: "PASSED",
|
|
@@ -2746,12 +3820,13 @@ class StableBrowser {
|
|
|
2746
3820
|
}
|
|
2747
3821
|
catch (e) {
|
|
2748
3822
|
console.log(".");
|
|
3823
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2749
3824
|
}
|
|
2750
3825
|
finally {
|
|
2751
3826
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2752
3827
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2753
3828
|
const endTime = Date.now();
|
|
2754
|
-
|
|
3829
|
+
_reportToWorld(world, {
|
|
2755
3830
|
type: Types.GET_PAGE_STATUS,
|
|
2756
3831
|
text: "page relaod",
|
|
2757
3832
|
screenshotId,
|
|
@@ -2760,7 +3835,7 @@ class StableBrowser {
|
|
|
2760
3835
|
status: "FAILED",
|
|
2761
3836
|
startTime,
|
|
2762
3837
|
endTime,
|
|
2763
|
-
message: error
|
|
3838
|
+
message: error?.message,
|
|
2764
3839
|
}
|
|
2765
3840
|
: {
|
|
2766
3841
|
status: "PASSED",
|
|
@@ -2787,11 +3862,195 @@ class StableBrowser {
|
|
|
2787
3862
|
console.log("#-#");
|
|
2788
3863
|
}
|
|
2789
3864
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
3865
|
+
async beforeScenario(world, scenario) {
|
|
3866
|
+
this.beforeScenarioCalled = true;
|
|
3867
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3868
|
+
this.scenarioName = scenario.pickle.name;
|
|
3869
|
+
}
|
|
3870
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3871
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3872
|
+
}
|
|
3873
|
+
if (this.context) {
|
|
3874
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3875
|
+
}
|
|
3876
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3877
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3878
|
+
// check if @global_test_data tag is present
|
|
3879
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3880
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
// update test data based on feature/scenario
|
|
3884
|
+
let envName = null;
|
|
3885
|
+
if (this.context && this.context.environment) {
|
|
3886
|
+
envName = this.context.environment.name;
|
|
3887
|
+
}
|
|
3888
|
+
if (!process.env.TEMP_RUN) {
|
|
3889
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
3890
|
+
}
|
|
3891
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3892
|
+
}
|
|
3893
|
+
async afterScenario(world, scenario) { }
|
|
3894
|
+
async beforeStep(world, step) {
|
|
3895
|
+
if (!this.beforeScenarioCalled) {
|
|
3896
|
+
this.beforeScenario(world, step);
|
|
3897
|
+
}
|
|
3898
|
+
if (this.stepIndex === undefined) {
|
|
3899
|
+
this.stepIndex = 0;
|
|
3900
|
+
}
|
|
3901
|
+
else {
|
|
3902
|
+
this.stepIndex++;
|
|
3903
|
+
}
|
|
3904
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3905
|
+
this.stepName = step.pickleStep.text;
|
|
3906
|
+
this.logger.info("step: " + this.stepName);
|
|
3907
|
+
}
|
|
3908
|
+
else if (step && step.text) {
|
|
3909
|
+
this.stepName = step.text;
|
|
3910
|
+
}
|
|
3911
|
+
else {
|
|
3912
|
+
this.stepName = "step " + this.stepIndex;
|
|
3913
|
+
}
|
|
3914
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3915
|
+
if (this.context.browserObject.context) {
|
|
3916
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
if (this.initSnapshotTaken === false) {
|
|
3920
|
+
this.initSnapshotTaken = true;
|
|
3921
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3922
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3923
|
+
if (snapshot) {
|
|
3924
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
async getAriaSnapshot() {
|
|
3930
|
+
try {
|
|
3931
|
+
// find the page url
|
|
3932
|
+
const url = await this.page.url();
|
|
3933
|
+
// extract the path from the url
|
|
3934
|
+
const path = new URL(url).pathname;
|
|
3935
|
+
// get the page title
|
|
3936
|
+
const title = await this.page.title();
|
|
3937
|
+
// go over other frams
|
|
3938
|
+
const frames = this.page.frames();
|
|
3939
|
+
const snapshots = [];
|
|
3940
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3941
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3942
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3943
|
+
const frame = frames[i];
|
|
3944
|
+
try {
|
|
3945
|
+
// Ensure frame is attached and has body
|
|
3946
|
+
const body = frame.locator("body");
|
|
3947
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3948
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3949
|
+
content.push(`- frame: ${i}`);
|
|
3950
|
+
content.push(snapshot);
|
|
3951
|
+
}
|
|
3952
|
+
catch (innerErr) { }
|
|
3953
|
+
}
|
|
3954
|
+
return content.join("\n");
|
|
3955
|
+
}
|
|
3956
|
+
catch (e) {
|
|
3957
|
+
console.log("Error in getAriaSnapshot");
|
|
3958
|
+
//console.debug(e);
|
|
3959
|
+
}
|
|
3960
|
+
return null;
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Sends command with custom payload to report.
|
|
3964
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3965
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3966
|
+
* @param content - Content of the command to be shown in the report.
|
|
3967
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3968
|
+
* @param world - Optional world context.
|
|
3969
|
+
* @public
|
|
3970
|
+
*/
|
|
3971
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3972
|
+
const state = {
|
|
3973
|
+
options,
|
|
3974
|
+
world,
|
|
3975
|
+
locate: false,
|
|
3976
|
+
scroll: false,
|
|
3977
|
+
screenshot: options.screenshot ?? false,
|
|
3978
|
+
highlight: options.highlight ?? false,
|
|
3979
|
+
type: Types.REPORT_COMMAND,
|
|
3980
|
+
text: commandText,
|
|
3981
|
+
_text: commandText,
|
|
3982
|
+
operation: "report_command",
|
|
3983
|
+
log: "***** " + commandText + " *****\n",
|
|
3984
|
+
};
|
|
3985
|
+
try {
|
|
3986
|
+
await _preCommand(state, this);
|
|
3987
|
+
const payload = {
|
|
3988
|
+
type: options.type ?? "text",
|
|
3989
|
+
content: content,
|
|
3990
|
+
screenshotId: null,
|
|
3991
|
+
};
|
|
3992
|
+
state.payload = payload;
|
|
3993
|
+
if (commandStatus === "FAILED") {
|
|
3994
|
+
state.throwError = true;
|
|
3995
|
+
throw new Error("Command failed");
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
catch (e) {
|
|
3999
|
+
await _commandError(state, e, this);
|
|
4000
|
+
}
|
|
4001
|
+
finally {
|
|
4002
|
+
await _commandFinally(state, this);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
async afterStep(world, step) {
|
|
4006
|
+
this.stepName = null;
|
|
4007
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4008
|
+
if (this.context.browserObject.context) {
|
|
4009
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
4010
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4011
|
+
});
|
|
4012
|
+
if (world && world.attach) {
|
|
4013
|
+
await world.attach(JSON.stringify({
|
|
4014
|
+
type: "trace",
|
|
4015
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4016
|
+
}), "application/json+trace");
|
|
4017
|
+
}
|
|
4018
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
if (this.context) {
|
|
4022
|
+
this.context.examplesRow = null;
|
|
4023
|
+
}
|
|
4024
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
4025
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4026
|
+
if (snapshot) {
|
|
4027
|
+
const obj = {};
|
|
4028
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
if (!process.env.TEMP_RUN) {
|
|
4032
|
+
const state = {
|
|
4033
|
+
world,
|
|
4034
|
+
locate: false,
|
|
4035
|
+
scroll: false,
|
|
4036
|
+
screenshot: true,
|
|
4037
|
+
highlight: true,
|
|
4038
|
+
type: Types.STEP_COMPLETE,
|
|
4039
|
+
text: "end of scenario",
|
|
4040
|
+
_text: "end of scenario",
|
|
4041
|
+
operation: "step_complete",
|
|
4042
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4043
|
+
};
|
|
4044
|
+
try {
|
|
4045
|
+
await _preCommand(state, this);
|
|
4046
|
+
}
|
|
4047
|
+
catch (e) {
|
|
4048
|
+
await _commandError(state, e, this);
|
|
4049
|
+
}
|
|
4050
|
+
finally {
|
|
4051
|
+
await _commandFinally(state, this);
|
|
4052
|
+
}
|
|
2793
4053
|
}
|
|
2794
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2795
4054
|
}
|
|
2796
4055
|
}
|
|
2797
4056
|
function createTimedPromise(promise, label) {
|
|
@@ -2799,151 +4058,5 @@ function createTimedPromise(promise, label) {
|
|
|
2799
4058
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2800
4059
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2801
4060
|
}
|
|
2802
|
-
const KEYBOARD_EVENTS = [
|
|
2803
|
-
"ALT",
|
|
2804
|
-
"AltGraph",
|
|
2805
|
-
"CapsLock",
|
|
2806
|
-
"Control",
|
|
2807
|
-
"Fn",
|
|
2808
|
-
"FnLock",
|
|
2809
|
-
"Hyper",
|
|
2810
|
-
"Meta",
|
|
2811
|
-
"NumLock",
|
|
2812
|
-
"ScrollLock",
|
|
2813
|
-
"Shift",
|
|
2814
|
-
"Super",
|
|
2815
|
-
"Symbol",
|
|
2816
|
-
"SymbolLock",
|
|
2817
|
-
"Enter",
|
|
2818
|
-
"Tab",
|
|
2819
|
-
"ArrowDown",
|
|
2820
|
-
"ArrowLeft",
|
|
2821
|
-
"ArrowRight",
|
|
2822
|
-
"ArrowUp",
|
|
2823
|
-
"End",
|
|
2824
|
-
"Home",
|
|
2825
|
-
"PageDown",
|
|
2826
|
-
"PageUp",
|
|
2827
|
-
"Backspace",
|
|
2828
|
-
"Clear",
|
|
2829
|
-
"Copy",
|
|
2830
|
-
"CrSel",
|
|
2831
|
-
"Cut",
|
|
2832
|
-
"Delete",
|
|
2833
|
-
"EraseEof",
|
|
2834
|
-
"ExSel",
|
|
2835
|
-
"Insert",
|
|
2836
|
-
"Paste",
|
|
2837
|
-
"Redo",
|
|
2838
|
-
"Undo",
|
|
2839
|
-
"Accept",
|
|
2840
|
-
"Again",
|
|
2841
|
-
"Attn",
|
|
2842
|
-
"Cancel",
|
|
2843
|
-
"ContextMenu",
|
|
2844
|
-
"Escape",
|
|
2845
|
-
"Execute",
|
|
2846
|
-
"Find",
|
|
2847
|
-
"Finish",
|
|
2848
|
-
"Help",
|
|
2849
|
-
"Pause",
|
|
2850
|
-
"Play",
|
|
2851
|
-
"Props",
|
|
2852
|
-
"Select",
|
|
2853
|
-
"ZoomIn",
|
|
2854
|
-
"ZoomOut",
|
|
2855
|
-
"BrightnessDown",
|
|
2856
|
-
"BrightnessUp",
|
|
2857
|
-
"Eject",
|
|
2858
|
-
"LogOff",
|
|
2859
|
-
"Power",
|
|
2860
|
-
"PowerOff",
|
|
2861
|
-
"PrintScreen",
|
|
2862
|
-
"Hibernate",
|
|
2863
|
-
"Standby",
|
|
2864
|
-
"WakeUp",
|
|
2865
|
-
"AllCandidates",
|
|
2866
|
-
"Alphanumeric",
|
|
2867
|
-
"CodeInput",
|
|
2868
|
-
"Compose",
|
|
2869
|
-
"Convert",
|
|
2870
|
-
"Dead",
|
|
2871
|
-
"FinalMode",
|
|
2872
|
-
"GroupFirst",
|
|
2873
|
-
"GroupLast",
|
|
2874
|
-
"GroupNext",
|
|
2875
|
-
"GroupPrevious",
|
|
2876
|
-
"ModeChange",
|
|
2877
|
-
"NextCandidate",
|
|
2878
|
-
"NonConvert",
|
|
2879
|
-
"PreviousCandidate",
|
|
2880
|
-
"Process",
|
|
2881
|
-
"SingleCandidate",
|
|
2882
|
-
"HangulMode",
|
|
2883
|
-
"HanjaMode",
|
|
2884
|
-
"JunjaMode",
|
|
2885
|
-
"Eisu",
|
|
2886
|
-
"Hankaku",
|
|
2887
|
-
"Hiragana",
|
|
2888
|
-
"HiraganaKatakana",
|
|
2889
|
-
"KanaMode",
|
|
2890
|
-
"KanjiMode",
|
|
2891
|
-
"Katakana",
|
|
2892
|
-
"Romaji",
|
|
2893
|
-
"Zenkaku",
|
|
2894
|
-
"ZenkakuHanaku",
|
|
2895
|
-
"F1",
|
|
2896
|
-
"F2",
|
|
2897
|
-
"F3",
|
|
2898
|
-
"F4",
|
|
2899
|
-
"F5",
|
|
2900
|
-
"F6",
|
|
2901
|
-
"F7",
|
|
2902
|
-
"F8",
|
|
2903
|
-
"F9",
|
|
2904
|
-
"F10",
|
|
2905
|
-
"F11",
|
|
2906
|
-
"F12",
|
|
2907
|
-
"Soft1",
|
|
2908
|
-
"Soft2",
|
|
2909
|
-
"Soft3",
|
|
2910
|
-
"Soft4",
|
|
2911
|
-
"ChannelDown",
|
|
2912
|
-
"ChannelUp",
|
|
2913
|
-
"Close",
|
|
2914
|
-
"MailForward",
|
|
2915
|
-
"MailReply",
|
|
2916
|
-
"MailSend",
|
|
2917
|
-
"MediaFastForward",
|
|
2918
|
-
"MediaPause",
|
|
2919
|
-
"MediaPlay",
|
|
2920
|
-
"MediaPlayPause",
|
|
2921
|
-
"MediaRecord",
|
|
2922
|
-
"MediaRewind",
|
|
2923
|
-
"MediaStop",
|
|
2924
|
-
"MediaTrackNext",
|
|
2925
|
-
"MediaTrackPrevious",
|
|
2926
|
-
"AudioBalanceLeft",
|
|
2927
|
-
"AudioBalanceRight",
|
|
2928
|
-
"AudioBassBoostDown",
|
|
2929
|
-
"AudioBassBoostToggle",
|
|
2930
|
-
"AudioBassBoostUp",
|
|
2931
|
-
"AudioFaderFront",
|
|
2932
|
-
"AudioFaderRear",
|
|
2933
|
-
"AudioSurroundModeNext",
|
|
2934
|
-
"AudioTrebleDown",
|
|
2935
|
-
"AudioTrebleUp",
|
|
2936
|
-
"AudioVolumeDown",
|
|
2937
|
-
"AudioVolumeMute",
|
|
2938
|
-
"AudioVolumeUp",
|
|
2939
|
-
"MicrophoneToggle",
|
|
2940
|
-
"MicrophoneVolumeDown",
|
|
2941
|
-
"MicrophoneVolumeMute",
|
|
2942
|
-
"MicrophoneVolumeUp",
|
|
2943
|
-
"TV",
|
|
2944
|
-
"TV3DMode",
|
|
2945
|
-
"TVAntennaCable",
|
|
2946
|
-
"TVAudioDescription",
|
|
2947
|
-
];
|
|
2948
4061
|
export { StableBrowser };
|
|
2949
4062
|
//# sourceMappingURL=stable_browser.js.map
|