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