automation_model 1.0.447-dev → 1.0.447
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 +2389 -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 +20 -11
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,85 +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
1390
|
_value = unEscapeString(_value);
|
|
1202
|
-
this._validateSelectors(selectors);
|
|
1203
|
-
const startTime = Date.now();
|
|
1204
|
-
let error = null;
|
|
1205
|
-
let screenshotId = null;
|
|
1206
|
-
let screenshotPath = null;
|
|
1207
|
-
const info = {};
|
|
1208
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1209
|
-
info.operation = "clickType";
|
|
1210
|
-
info.selectors = selectors;
|
|
1211
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
|
+
}
|
|
1212
1408
|
if (newValue !== _value) {
|
|
1213
1409
|
//this.logger.info(_value + "=" + newValue);
|
|
1214
1410
|
_value = newValue;
|
|
1215
1411
|
}
|
|
1216
|
-
info.value = _value;
|
|
1217
1412
|
try {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1222
|
-
await this._highlightElements(element);
|
|
1223
|
-
if (options === null || options === undefined || !options.press) {
|
|
1413
|
+
await _preCommand(state, this);
|
|
1414
|
+
state.info.value = _value;
|
|
1415
|
+
if (!options.press) {
|
|
1224
1416
|
try {
|
|
1225
|
-
let currentValue = await element.inputValue();
|
|
1417
|
+
let currentValue = await state.element.inputValue();
|
|
1226
1418
|
if (currentValue) {
|
|
1227
|
-
await element.fill("");
|
|
1419
|
+
await state.element.fill("");
|
|
1228
1420
|
}
|
|
1229
1421
|
}
|
|
1230
1422
|
catch (e) {
|
|
1231
1423
|
this.logger.info("unable to clear input value");
|
|
1232
1424
|
}
|
|
1233
1425
|
}
|
|
1234
|
-
if (options
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
catch (e) {
|
|
1239
|
-
await element.dispatchEvent("click");
|
|
1240
|
-
}
|
|
1426
|
+
if (options.press) {
|
|
1427
|
+
options.timeout = 5000;
|
|
1428
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1241
1429
|
}
|
|
1242
1430
|
else {
|
|
1243
1431
|
try {
|
|
1244
|
-
await element.focus();
|
|
1432
|
+
await state.element.focus();
|
|
1245
1433
|
}
|
|
1246
1434
|
catch (e) {
|
|
1247
|
-
await element.dispatchEvent("focus");
|
|
1435
|
+
await state.element.dispatchEvent("focus");
|
|
1248
1436
|
}
|
|
1249
1437
|
}
|
|
1250
1438
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1251
|
-
const valueSegment =
|
|
1439
|
+
const valueSegment = state.value.split("&&");
|
|
1252
1440
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1253
1441
|
if (i > 0) {
|
|
1254
1442
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1268,13 +1456,21 @@ class StableBrowser {
|
|
|
1268
1456
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1269
1457
|
}
|
|
1270
1458
|
}
|
|
1459
|
+
//if (!this.fastMode) {
|
|
1460
|
+
await _screenshot(state, this);
|
|
1461
|
+
//}
|
|
1271
1462
|
if (enter === true) {
|
|
1272
1463
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1273
1464
|
await this.page.keyboard.press("Enter");
|
|
1274
1465
|
await this.waitForPageLoad();
|
|
1275
1466
|
}
|
|
1276
1467
|
else if (enter === false) {
|
|
1277
|
-
|
|
1468
|
+
try {
|
|
1469
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1470
|
+
}
|
|
1471
|
+
catch (e) {
|
|
1472
|
+
// ignore
|
|
1473
|
+
}
|
|
1278
1474
|
//await this.page.keyboard.press("Tab");
|
|
1279
1475
|
}
|
|
1280
1476
|
else {
|
|
@@ -1283,112 +1479,95 @@ class StableBrowser {
|
|
|
1283
1479
|
await this.waitForPageLoad();
|
|
1284
1480
|
}
|
|
1285
1481
|
}
|
|
1286
|
-
return info;
|
|
1482
|
+
return state.info;
|
|
1287
1483
|
}
|
|
1288
1484
|
catch (e) {
|
|
1289
|
-
|
|
1290
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1291
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1292
|
-
info.screenshotPath = screenshotPath;
|
|
1293
|
-
Object.assign(e, { info: info });
|
|
1294
|
-
error = e;
|
|
1295
|
-
throw e;
|
|
1485
|
+
await _commandError(state, e, this);
|
|
1296
1486
|
}
|
|
1297
1487
|
finally {
|
|
1298
|
-
|
|
1299
|
-
this._reportToWorld(world, {
|
|
1300
|
-
element_name: selectors.element_name,
|
|
1301
|
-
type: Types.FILL,
|
|
1302
|
-
screenshotId,
|
|
1303
|
-
value: _value,
|
|
1304
|
-
text: `clickType input with value: ${_value}`,
|
|
1305
|
-
result: error
|
|
1306
|
-
? {
|
|
1307
|
-
status: "FAILED",
|
|
1308
|
-
startTime,
|
|
1309
|
-
endTime,
|
|
1310
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1311
|
-
}
|
|
1312
|
-
: {
|
|
1313
|
-
status: "PASSED",
|
|
1314
|
-
startTime,
|
|
1315
|
-
endTime,
|
|
1316
|
-
},
|
|
1317
|
-
info: info,
|
|
1318
|
-
});
|
|
1488
|
+
await _commandFinally(state, this);
|
|
1319
1489
|
}
|
|
1320
1490
|
}
|
|
1321
1491
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
+
};
|
|
1333
1503
|
try {
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
await
|
|
1337
|
-
await element.fill(value);
|
|
1338
|
-
await element.dispatchEvent("change");
|
|
1504
|
+
await _preCommand(state, this);
|
|
1505
|
+
await state.element.fill(value);
|
|
1506
|
+
await state.element.dispatchEvent("change");
|
|
1339
1507
|
if (enter) {
|
|
1340
1508
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1341
1509
|
await this.page.keyboard.press("Enter");
|
|
1342
1510
|
}
|
|
1343
1511
|
await this.waitForPageLoad();
|
|
1344
|
-
return info;
|
|
1512
|
+
return state.info;
|
|
1345
1513
|
}
|
|
1346
1514
|
catch (e) {
|
|
1347
|
-
|
|
1348
|
-
this.logger.error("fill failed " + info.log);
|
|
1349
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1350
|
-
info.screenshotPath = screenshotPath;
|
|
1351
|
-
Object.assign(e, { info: info });
|
|
1352
|
-
error = e;
|
|
1353
|
-
throw e;
|
|
1515
|
+
await _commandError(state, e, this);
|
|
1354
1516
|
}
|
|
1355
1517
|
finally {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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);
|
|
1377
1554
|
}
|
|
1378
1555
|
}
|
|
1379
1556
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1380
1557
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1381
1558
|
}
|
|
1382
1559
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1383
|
-
this.
|
|
1560
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1561
|
+
_validateSelectors(selectors);
|
|
1384
1562
|
let screenshotId = null;
|
|
1385
1563
|
let screenshotPath = null;
|
|
1386
1564
|
if (!info.log) {
|
|
1387
1565
|
info.log = "";
|
|
1566
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1388
1567
|
}
|
|
1389
1568
|
info.operation = "getText";
|
|
1390
1569
|
info.selectors = selectors;
|
|
1391
|
-
let element = await this._locate(selectors, info, _params);
|
|
1570
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1392
1571
|
if (climb > 0) {
|
|
1393
1572
|
const climbArray = [];
|
|
1394
1573
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1407,6 +1586,18 @@ class StableBrowser {
|
|
|
1407
1586
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1408
1587
|
try {
|
|
1409
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
|
+
// }
|
|
1410
1601
|
const elementText = await element.innerText();
|
|
1411
1602
|
return {
|
|
1412
1603
|
text: elementText,
|
|
@@ -1418,189 +1609,219 @@ class StableBrowser {
|
|
|
1418
1609
|
}
|
|
1419
1610
|
catch (e) {
|
|
1420
1611
|
//await this.closeUnexpectedPopups();
|
|
1421
|
-
this.logger.info("no innerText will use textContent");
|
|
1612
|
+
this.logger.info("no innerText, will use textContent");
|
|
1422
1613
|
const elementText = await element.textContent();
|
|
1423
1614
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1424
1615
|
}
|
|
1425
1616
|
}
|
|
1426
1617
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1427
|
-
var _a;
|
|
1428
|
-
this._validateSelectors(selectors);
|
|
1429
1618
|
if (!pattern) {
|
|
1430
1619
|
throw new Error("pattern is null");
|
|
1431
1620
|
}
|
|
1432
1621
|
if (!text) {
|
|
1433
1622
|
throw new Error("text is null");
|
|
1434
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
|
+
};
|
|
1435
1641
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1436
1642
|
if (newValue !== text) {
|
|
1437
1643
|
this.logger.info(text + "=" + newValue);
|
|
1438
1644
|
text = newValue;
|
|
1439
1645
|
}
|
|
1440
|
-
const startTime = Date.now();
|
|
1441
|
-
let error = null;
|
|
1442
|
-
let screenshotId = null;
|
|
1443
|
-
let screenshotPath = null;
|
|
1444
|
-
const info = {};
|
|
1445
|
-
info.log =
|
|
1446
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1447
|
-
info.operation = "containsPattern";
|
|
1448
|
-
info.selectors = selectors;
|
|
1449
|
-
info.value = text;
|
|
1450
|
-
info.pattern = pattern;
|
|
1451
1646
|
let foundObj = null;
|
|
1452
1647
|
try {
|
|
1453
|
-
|
|
1648
|
+
await _preCommand(state, this);
|
|
1649
|
+
state.info.pattern = pattern;
|
|
1650
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1454
1651
|
if (foundObj && foundObj.element) {
|
|
1455
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1652
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1456
1653
|
}
|
|
1457
|
-
|
|
1654
|
+
await _screenshot(state, this);
|
|
1458
1655
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1459
1656
|
pattern = pattern.replace("{text}", escapedText);
|
|
1460
1657
|
let regex = new RegExp(pattern, "im");
|
|
1461
|
-
if (!regex.test(foundObj
|
|
1462
|
-
info.foundText = foundObj
|
|
1658
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1659
|
+
state.info.foundText = foundObj?.text;
|
|
1463
1660
|
throw new Error("element doesn't contain text " + text);
|
|
1464
1661
|
}
|
|
1465
|
-
return info;
|
|
1662
|
+
return state.info;
|
|
1466
1663
|
}
|
|
1467
1664
|
catch (e) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1471
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1472
|
-
info.screenshotPath = screenshotPath;
|
|
1473
|
-
Object.assign(e, { info: info });
|
|
1474
|
-
error = e;
|
|
1475
|
-
throw e;
|
|
1665
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1666
|
+
await _commandError(state, e, this);
|
|
1476
1667
|
}
|
|
1477
1668
|
finally {
|
|
1478
|
-
|
|
1479
|
-
this._reportToWorld(world, {
|
|
1480
|
-
element_name: selectors.element_name,
|
|
1481
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1482
|
-
value: pattern,
|
|
1483
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1484
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1485
|
-
result: error
|
|
1486
|
-
? {
|
|
1487
|
-
status: "FAILED",
|
|
1488
|
-
startTime,
|
|
1489
|
-
endTime,
|
|
1490
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1491
|
-
}
|
|
1492
|
-
: {
|
|
1493
|
-
status: "PASSED",
|
|
1494
|
-
startTime,
|
|
1495
|
-
endTime,
|
|
1496
|
-
},
|
|
1497
|
-
info: info,
|
|
1498
|
-
});
|
|
1669
|
+
await _commandFinally(state, this);
|
|
1499
1670
|
}
|
|
1500
1671
|
}
|
|
1501
1672
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
+
};
|
|
1505
1690
|
if (!text) {
|
|
1506
1691
|
throw new Error("text is null");
|
|
1507
1692
|
}
|
|
1508
|
-
|
|
1509
|
-
let error = null;
|
|
1510
|
-
let screenshotId = null;
|
|
1511
|
-
let screenshotPath = null;
|
|
1512
|
-
const info = {};
|
|
1513
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1514
|
-
info.operation = "containsText";
|
|
1515
|
-
info.selectors = selectors;
|
|
1693
|
+
text = unEscapeString(text);
|
|
1516
1694
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1517
1695
|
if (newValue !== text) {
|
|
1518
1696
|
this.logger.info(text + "=" + newValue);
|
|
1519
1697
|
text = newValue;
|
|
1520
1698
|
}
|
|
1521
|
-
info.value = text;
|
|
1522
1699
|
let foundObj = null;
|
|
1523
1700
|
try {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
+
}
|
|
1536
1718
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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;
|
|
1545
1729
|
}
|
|
1546
1730
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
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
|
|
1553
1736
|
}
|
|
1554
|
-
|
|
1737
|
+
state.info.foundText = foundObj?.text;
|
|
1738
|
+
state.info.value = foundObj?.value;
|
|
1739
|
+
throw new Error("element doesn't contain text " + text);
|
|
1555
1740
|
}
|
|
1556
1741
|
catch (e) {
|
|
1557
|
-
|
|
1558
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1559
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1560
|
-
info.screenshotPath = screenshotPath;
|
|
1561
|
-
Object.assign(e, { info: info });
|
|
1562
|
-
error = e;
|
|
1742
|
+
await _commandError(state, e, this);
|
|
1563
1743
|
throw e;
|
|
1564
1744
|
}
|
|
1565
1745
|
finally {
|
|
1566
|
-
|
|
1567
|
-
this._reportToWorld(world, {
|
|
1568
|
-
element_name: selectors.element_name,
|
|
1569
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1570
|
-
text: `Verify element contains text: ${text}`,
|
|
1571
|
-
value: text,
|
|
1572
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1573
|
-
result: error
|
|
1574
|
-
? {
|
|
1575
|
-
status: "FAILED",
|
|
1576
|
-
startTime,
|
|
1577
|
-
endTime,
|
|
1578
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1579
|
-
}
|
|
1580
|
-
: {
|
|
1581
|
-
status: "PASSED",
|
|
1582
|
-
startTime,
|
|
1583
|
-
endTime,
|
|
1584
|
-
},
|
|
1585
|
-
info: info,
|
|
1586
|
-
});
|
|
1746
|
+
await _commandFinally(state, this);
|
|
1587
1747
|
}
|
|
1588
1748
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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");
|
|
1593
1772
|
}
|
|
1594
|
-
else if (this.
|
|
1595
|
-
|
|
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");
|
|
1775
|
+
}
|
|
1776
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1777
|
+
text = referanceSnapshot.substring(5);
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
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);
|
|
1596
1817
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1818
|
+
catch (e) {
|
|
1819
|
+
await _commandError(state, e, this);
|
|
1820
|
+
throw e;
|
|
1599
1821
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1822
|
+
finally {
|
|
1823
|
+
await _commandFinally(state, this);
|
|
1602
1824
|
}
|
|
1603
|
-
return dataFile;
|
|
1604
1825
|
}
|
|
1605
1826
|
async waitForUserInput(message, world = null) {
|
|
1606
1827
|
if (!message) {
|
|
@@ -1630,13 +1851,22 @@ class StableBrowser {
|
|
|
1630
1851
|
return;
|
|
1631
1852
|
}
|
|
1632
1853
|
// if data file exists, load it
|
|
1633
|
-
const dataFile =
|
|
1854
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1634
1855
|
let data = this.getTestData(world);
|
|
1635
1856
|
// merge the testData with the existing data
|
|
1636
1857
|
Object.assign(data, testData);
|
|
1637
1858
|
// save the data to the file
|
|
1638
1859
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1639
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));
|
|
1869
|
+
}
|
|
1640
1870
|
_getDataFilePath(fileName) {
|
|
1641
1871
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1642
1872
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1733,7 +1963,7 @@ class StableBrowser {
|
|
|
1733
1963
|
}
|
|
1734
1964
|
}
|
|
1735
1965
|
getTestData(world = null) {
|
|
1736
|
-
const dataFile =
|
|
1966
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1737
1967
|
let data = {};
|
|
1738
1968
|
if (fs.existsSync(dataFile)) {
|
|
1739
1969
|
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
@@ -1765,11 +1995,9 @@ class StableBrowser {
|
|
|
1765
1995
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1766
1996
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1767
1997
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
}
|
|
1772
|
-
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");
|
|
1773
2001
|
try {
|
|
1774
2002
|
await this.takeScreenshot(screenshotPath);
|
|
1775
2003
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1779,15 +2007,15 @@ class StableBrowser {
|
|
|
1779
2007
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1780
2008
|
// }
|
|
1781
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
|
+
}
|
|
1782
2015
|
}
|
|
1783
2016
|
catch (e) {
|
|
1784
2017
|
this.logger.info("unable to take screenshot, ignored");
|
|
1785
2018
|
}
|
|
1786
|
-
result.screenshotId = nextIndex;
|
|
1787
|
-
result.screenshotPath = screenshotPath;
|
|
1788
|
-
if (info && info.box) {
|
|
1789
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1790
|
-
}
|
|
1791
2019
|
}
|
|
1792
2020
|
else if (options && options.screenshot) {
|
|
1793
2021
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1812,7 +2040,6 @@ class StableBrowser {
|
|
|
1812
2040
|
}
|
|
1813
2041
|
async takeScreenshot(screenshotPath) {
|
|
1814
2042
|
const playContext = this.context.playContext;
|
|
1815
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1816
2043
|
// Using CDP to capture the screenshot
|
|
1817
2044
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1818
2045
|
document.body.scrollWidth,
|
|
@@ -1822,164 +2049,433 @@ class StableBrowser {
|
|
|
1822
2049
|
document.body.clientWidth,
|
|
1823
2050
|
document.documentElement.clientWidth,
|
|
1824
2051
|
])));
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
screenshotBuffer =
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
|
|
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;
|
|
1860
2100
|
}
|
|
1861
2101
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
+
};
|
|
1867
2112
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1868
|
-
const info = {};
|
|
1869
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1870
|
-
info.operation = "verify";
|
|
1871
|
-
info.selectors = selectors;
|
|
1872
2113
|
try {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
}
|
|
1877
|
-
await this._highlightElements(element);
|
|
1878
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1879
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1880
|
-
return info;
|
|
2114
|
+
await _preCommand(state, this);
|
|
2115
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
2116
|
+
return state.info;
|
|
1881
2117
|
}
|
|
1882
2118
|
catch (e) {
|
|
1883
|
-
|
|
1884
|
-
this.logger.error("verify failed " + info.log);
|
|
1885
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1886
|
-
info.screenshotPath = screenshotPath;
|
|
1887
|
-
Object.assign(e, { info: info });
|
|
1888
|
-
error = e;
|
|
1889
|
-
throw e;
|
|
2119
|
+
await _commandError(state, e, this);
|
|
1890
2120
|
}
|
|
1891
2121
|
finally {
|
|
1892
|
-
|
|
1893
|
-
this._reportToWorld(world, {
|
|
1894
|
-
element_name: selectors.element_name,
|
|
1895
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1896
|
-
text: "Verify element exists in page",
|
|
1897
|
-
screenshotId,
|
|
1898
|
-
result: error
|
|
1899
|
-
? {
|
|
1900
|
-
status: "FAILED",
|
|
1901
|
-
startTime,
|
|
1902
|
-
endTime,
|
|
1903
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1904
|
-
}
|
|
1905
|
-
: {
|
|
1906
|
-
status: "PASSED",
|
|
1907
|
-
startTime,
|
|
1908
|
-
endTime,
|
|
1909
|
-
},
|
|
1910
|
-
info: info,
|
|
1911
|
-
});
|
|
2122
|
+
await _commandFinally(state, this);
|
|
1912
2123
|
}
|
|
1913
2124
|
}
|
|
1914
2125
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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
|
+
};
|
|
1920
2140
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1921
|
-
const info = {};
|
|
1922
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1923
|
-
info.operation = "extract";
|
|
1924
|
-
info.selectors = selectors;
|
|
1925
2141
|
try {
|
|
1926
|
-
|
|
1927
|
-
await this._highlightElements(element);
|
|
1928
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2142
|
+
await _preCommand(state, this);
|
|
1929
2143
|
switch (attribute) {
|
|
1930
2144
|
case "inner_text":
|
|
1931
|
-
|
|
2145
|
+
state.value = await state.element.innerText();
|
|
1932
2146
|
break;
|
|
1933
2147
|
case "href":
|
|
1934
|
-
|
|
2148
|
+
state.value = await state.element.getAttribute("href");
|
|
1935
2149
|
break;
|
|
1936
2150
|
case "value":
|
|
1937
|
-
|
|
2151
|
+
state.value = await state.element.inputValue();
|
|
2152
|
+
break;
|
|
2153
|
+
case "text":
|
|
2154
|
+
state.value = await state.element.textContent();
|
|
1938
2155
|
break;
|
|
1939
2156
|
default:
|
|
1940
|
-
|
|
2157
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1941
2158
|
break;
|
|
1942
2159
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
+
}
|
|
1946
2177
|
}
|
|
1947
|
-
|
|
1948
|
-
this.
|
|
1949
|
-
|
|
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;
|
|
1950
2183
|
}
|
|
1951
2184
|
catch (e) {
|
|
1952
|
-
|
|
1953
|
-
this.logger.error("extract failed " + info.log);
|
|
1954
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1955
|
-
info.screenshotPath = screenshotPath;
|
|
1956
|
-
Object.assign(e, { info: info });
|
|
1957
|
-
error = e;
|
|
1958
|
-
throw e;
|
|
2185
|
+
await _commandError(state, e, this);
|
|
1959
2186
|
}
|
|
1960
2187
|
finally {
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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}`)) || "";
|
|
1975
2226
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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);
|
|
1983
2479
|
}
|
|
1984
2480
|
}
|
|
1985
2481
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2000,7 +2496,7 @@ class StableBrowser {
|
|
|
2000
2496
|
if (options && options.timeout) {
|
|
2001
2497
|
timeout = options.timeout;
|
|
2002
2498
|
}
|
|
2003
|
-
const serviceUrl =
|
|
2499
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
2004
2500
|
const request = {
|
|
2005
2501
|
method: "POST",
|
|
2006
2502
|
url: serviceUrl,
|
|
@@ -2056,7 +2552,8 @@ class StableBrowser {
|
|
|
2056
2552
|
catch (e) {
|
|
2057
2553
|
errorCount++;
|
|
2058
2554
|
if (errorCount > 3) {
|
|
2059
|
-
throw e;
|
|
2555
|
+
// throw e;
|
|
2556
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2060
2557
|
}
|
|
2061
2558
|
// ignore
|
|
2062
2559
|
}
|
|
@@ -2070,27 +2567,32 @@ class StableBrowser {
|
|
|
2070
2567
|
async _highlightElements(scope, css) {
|
|
2071
2568
|
try {
|
|
2072
2569
|
if (!scope) {
|
|
2570
|
+
// console.log(`Scope is not defined`);
|
|
2073
2571
|
return;
|
|
2074
2572
|
}
|
|
2075
2573
|
if (!css) {
|
|
2076
2574
|
scope
|
|
2077
2575
|
.evaluate((node) => {
|
|
2078
2576
|
if (node && node.style) {
|
|
2079
|
-
let
|
|
2080
|
-
|
|
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}`);
|
|
2081
2582
|
if (window) {
|
|
2082
2583
|
window.addEventListener("beforeunload", function (e) {
|
|
2083
|
-
node.style.
|
|
2584
|
+
node.style.outline = originalOutline;
|
|
2084
2585
|
});
|
|
2085
2586
|
}
|
|
2086
2587
|
setTimeout(function () {
|
|
2087
|
-
node.style.
|
|
2588
|
+
node.style.outline = originalOutline;
|
|
2088
2589
|
}, 2000);
|
|
2089
2590
|
}
|
|
2090
2591
|
})
|
|
2091
2592
|
.then(() => { })
|
|
2092
2593
|
.catch((e) => {
|
|
2093
2594
|
// ignore
|
|
2595
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2094
2596
|
});
|
|
2095
2597
|
}
|
|
2096
2598
|
else {
|
|
@@ -2106,17 +2608,18 @@ class StableBrowser {
|
|
|
2106
2608
|
if (!element.style) {
|
|
2107
2609
|
return;
|
|
2108
2610
|
}
|
|
2109
|
-
|
|
2611
|
+
let originalOutline = element.style.outline;
|
|
2612
|
+
element.__previousOutline = originalOutline;
|
|
2110
2613
|
// Set the new border to be red and 2px solid
|
|
2111
|
-
element.style.
|
|
2614
|
+
element.style.outline = "2px solid red";
|
|
2112
2615
|
if (window) {
|
|
2113
2616
|
window.addEventListener("beforeunload", function (e) {
|
|
2114
|
-
element.style.
|
|
2617
|
+
element.style.outline = originalOutline;
|
|
2115
2618
|
});
|
|
2116
2619
|
}
|
|
2117
2620
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2118
2621
|
setTimeout(function () {
|
|
2119
|
-
element.style.
|
|
2622
|
+
element.style.outline = originalOutline;
|
|
2120
2623
|
}, 2000);
|
|
2121
2624
|
}
|
|
2122
2625
|
return;
|
|
@@ -2124,6 +2627,7 @@ class StableBrowser {
|
|
|
2124
2627
|
.then(() => { })
|
|
2125
2628
|
.catch((e) => {
|
|
2126
2629
|
// ignore
|
|
2630
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2127
2631
|
});
|
|
2128
2632
|
}
|
|
2129
2633
|
}
|
|
@@ -2131,8 +2635,49 @@ class StableBrowser {
|
|
|
2131
2635
|
console.debug(error);
|
|
2132
2636
|
}
|
|
2133
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
|
+
*/
|
|
2134
2680
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2135
|
-
const startTime = Date.now();
|
|
2136
2681
|
let error = null;
|
|
2137
2682
|
let screenshotId = null;
|
|
2138
2683
|
let screenshotPath = null;
|
|
@@ -2145,160 +2690,508 @@ class StableBrowser {
|
|
|
2145
2690
|
this.logger.info(pathPart + "=" + newValue);
|
|
2146
2691
|
pathPart = newValue;
|
|
2147
2692
|
}
|
|
2148
|
-
info.pathPart = pathPart;
|
|
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;
|
|
2955
|
+
}
|
|
2956
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2957
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2149
2958
|
try {
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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`);
|
|
2155
2974
|
}
|
|
2156
2975
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2157
2976
|
continue;
|
|
2158
2977
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
+
}
|
|
2161
2995
|
}
|
|
2162
2996
|
}
|
|
2163
2997
|
catch (e) {
|
|
2164
|
-
|
|
2165
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2166
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2167
|
-
info.screenshotPath = screenshotPath;
|
|
2168
|
-
Object.assign(e, { info: info });
|
|
2169
|
-
error = e;
|
|
2170
|
-
throw e;
|
|
2998
|
+
await _commandError(state, e, this);
|
|
2171
2999
|
}
|
|
2172
3000
|
finally {
|
|
2173
|
-
|
|
2174
|
-
this._reportToWorld(world, {
|
|
2175
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2176
|
-
text: "Verify page path",
|
|
2177
|
-
screenshotId,
|
|
2178
|
-
result: error
|
|
2179
|
-
? {
|
|
2180
|
-
status: "FAILED",
|
|
2181
|
-
startTime,
|
|
2182
|
-
endTime,
|
|
2183
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2184
|
-
}
|
|
2185
|
-
: {
|
|
2186
|
-
status: "PASSED",
|
|
2187
|
-
startTime,
|
|
2188
|
-
endTime,
|
|
2189
|
-
},
|
|
2190
|
-
info: info,
|
|
2191
|
-
});
|
|
3001
|
+
await _commandFinally(state, this);
|
|
2192
3002
|
}
|
|
2193
3003
|
}
|
|
2194
|
-
async
|
|
3004
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2195
3005
|
text = unEscapeString(text);
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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);
|
|
2201
3023
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2202
|
-
const info = {};
|
|
2203
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2204
|
-
info.operation = "verifyTextExistInPage";
|
|
2205
3024
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2206
3025
|
if (newValue !== text) {
|
|
2207
3026
|
this.logger.info(text + "=" + newValue);
|
|
2208
3027
|
text = newValue;
|
|
2209
3028
|
}
|
|
2210
|
-
info.text = text;
|
|
2211
3029
|
let dateAlternatives = findDateAlternatives(text);
|
|
2212
3030
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2213
3031
|
try {
|
|
3032
|
+
await _preCommand(state, this);
|
|
3033
|
+
state.info.text = text;
|
|
3034
|
+
let resultWithElementsFound = {
|
|
3035
|
+
length: null, // initial cannot be 0
|
|
3036
|
+
};
|
|
2214
3037
|
while (true) {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
|
|
2221
|
-
result.frame = frames[i];
|
|
2222
|
-
results.push(result);
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
else if (numberAlternatives.number) {
|
|
2226
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2227
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
|
|
2228
|
-
result.frame = frames[i];
|
|
2229
|
-
results.push(result);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
else {
|
|
2233
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, {});
|
|
2234
|
-
result.frame = frames[i];
|
|
2235
|
-
results.push(result);
|
|
2236
|
-
}
|
|
3038
|
+
try {
|
|
3039
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3040
|
+
}
|
|
3041
|
+
catch (error) {
|
|
3042
|
+
// ignore
|
|
2237
3043
|
}
|
|
2238
|
-
info.results = results;
|
|
2239
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2240
3044
|
if (resultWithElementsFound.length === 0) {
|
|
2241
|
-
|
|
2242
|
-
|
|
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`);
|
|
2243
3108
|
}
|
|
2244
3109
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2245
3110
|
continue;
|
|
2246
3111
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
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
|
+
}
|
|
2255
3157
|
}
|
|
2256
3158
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
3159
|
+
catch (error) {
|
|
3160
|
+
console.error(error);
|
|
3161
|
+
}
|
|
2259
3162
|
}
|
|
2260
3163
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2261
3164
|
}
|
|
2262
3165
|
catch (e) {
|
|
2263
|
-
|
|
2264
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2265
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2266
|
-
info.screenshotPath = screenshotPath;
|
|
2267
|
-
Object.assign(e, { info: info });
|
|
2268
|
-
error = e;
|
|
2269
|
-
throw e;
|
|
3166
|
+
await _commandError(state, e, this);
|
|
2270
3167
|
}
|
|
2271
3168
|
finally {
|
|
2272
|
-
|
|
2273
|
-
this._reportToWorld(world, {
|
|
2274
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2275
|
-
text: "Verify text exists in page",
|
|
2276
|
-
screenshotId,
|
|
2277
|
-
result: error
|
|
2278
|
-
? {
|
|
2279
|
-
status: "FAILED",
|
|
2280
|
-
startTime,
|
|
2281
|
-
endTime,
|
|
2282
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2283
|
-
}
|
|
2284
|
-
: {
|
|
2285
|
-
status: "PASSED",
|
|
2286
|
-
startTime,
|
|
2287
|
-
endTime,
|
|
2288
|
-
},
|
|
2289
|
-
info: info,
|
|
2290
|
-
});
|
|
3169
|
+
await _commandFinally(state, this);
|
|
2291
3170
|
}
|
|
2292
3171
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
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
|
+
}
|
|
2300
3191
|
}
|
|
2301
|
-
|
|
3192
|
+
// state.info.results = results;
|
|
3193
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3194
|
+
return resultWithElementsFound;
|
|
2302
3195
|
}
|
|
2303
3196
|
async visualVerification(text, options = {}, world = null) {
|
|
2304
3197
|
const startTime = Date.now();
|
|
@@ -2314,14 +3207,17 @@ class StableBrowser {
|
|
|
2314
3207
|
throw new Error("TOKEN is not set");
|
|
2315
3208
|
}
|
|
2316
3209
|
try {
|
|
2317
|
-
let serviceUrl =
|
|
3210
|
+
let serviceUrl = _getServerUrl();
|
|
2318
3211
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2319
3212
|
info.screenshotPath = screenshotPath;
|
|
2320
3213
|
const screenshot = await this.takeScreenshot();
|
|
2321
|
-
|
|
2322
|
-
method: "
|
|
3214
|
+
let request = {
|
|
3215
|
+
method: "post",
|
|
3216
|
+
maxBodyLength: Infinity,
|
|
2323
3217
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2324
3218
|
headers: {
|
|
3219
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3220
|
+
"x-source": "aaa",
|
|
2325
3221
|
"Content-Type": "application/json",
|
|
2326
3222
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2327
3223
|
},
|
|
@@ -2330,7 +3226,7 @@ class StableBrowser {
|
|
|
2330
3226
|
screenshot: screenshot,
|
|
2331
3227
|
}),
|
|
2332
3228
|
};
|
|
2333
|
-
|
|
3229
|
+
const result = await axios.request(request);
|
|
2334
3230
|
if (result.data.status !== true) {
|
|
2335
3231
|
throw new Error("Visual validation failed");
|
|
2336
3232
|
}
|
|
@@ -2350,20 +3246,22 @@ class StableBrowser {
|
|
|
2350
3246
|
info.screenshotPath = screenshotPath;
|
|
2351
3247
|
Object.assign(e, { info: info });
|
|
2352
3248
|
error = e;
|
|
2353
|
-
throw e;
|
|
3249
|
+
// throw e;
|
|
3250
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2354
3251
|
}
|
|
2355
3252
|
finally {
|
|
2356
3253
|
const endTime = Date.now();
|
|
2357
|
-
|
|
3254
|
+
_reportToWorld(world, {
|
|
2358
3255
|
type: Types.VERIFY_VISUAL,
|
|
2359
3256
|
text: "Visual verification",
|
|
3257
|
+
_text: "Visual verification of " + text,
|
|
2360
3258
|
screenshotId,
|
|
2361
3259
|
result: error
|
|
2362
3260
|
? {
|
|
2363
3261
|
status: "FAILED",
|
|
2364
3262
|
startTime,
|
|
2365
3263
|
endTime,
|
|
2366
|
-
message: error
|
|
3264
|
+
message: error?.message,
|
|
2367
3265
|
}
|
|
2368
3266
|
: {
|
|
2369
3267
|
status: "PASSED",
|
|
@@ -2395,13 +3293,14 @@ class StableBrowser {
|
|
|
2395
3293
|
this.logger.info("Table data verified");
|
|
2396
3294
|
}
|
|
2397
3295
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2398
|
-
|
|
3296
|
+
_validateSelectors(selectors);
|
|
2399
3297
|
const startTime = Date.now();
|
|
2400
3298
|
let error = null;
|
|
2401
3299
|
let screenshotId = null;
|
|
2402
3300
|
let screenshotPath = null;
|
|
2403
3301
|
const info = {};
|
|
2404
3302
|
info.log = "";
|
|
3303
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2405
3304
|
info.operation = "getTableData";
|
|
2406
3305
|
info.selectors = selectors;
|
|
2407
3306
|
try {
|
|
@@ -2417,11 +3316,12 @@ class StableBrowser {
|
|
|
2417
3316
|
info.screenshotPath = screenshotPath;
|
|
2418
3317
|
Object.assign(e, { info: info });
|
|
2419
3318
|
error = e;
|
|
2420
|
-
throw e;
|
|
3319
|
+
// throw e;
|
|
3320
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2421
3321
|
}
|
|
2422
3322
|
finally {
|
|
2423
3323
|
const endTime = Date.now();
|
|
2424
|
-
|
|
3324
|
+
_reportToWorld(world, {
|
|
2425
3325
|
element_name: selectors.element_name,
|
|
2426
3326
|
type: Types.GET_TABLE_DATA,
|
|
2427
3327
|
text: "Get table data",
|
|
@@ -2431,7 +3331,7 @@ class StableBrowser {
|
|
|
2431
3331
|
status: "FAILED",
|
|
2432
3332
|
startTime,
|
|
2433
3333
|
endTime,
|
|
2434
|
-
message: error
|
|
3334
|
+
message: error?.message,
|
|
2435
3335
|
}
|
|
2436
3336
|
: {
|
|
2437
3337
|
status: "PASSED",
|
|
@@ -2443,7 +3343,7 @@ class StableBrowser {
|
|
|
2443
3343
|
}
|
|
2444
3344
|
}
|
|
2445
3345
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2446
|
-
|
|
3346
|
+
_validateSelectors(selectors);
|
|
2447
3347
|
if (!query) {
|
|
2448
3348
|
throw new Error("query is null");
|
|
2449
3349
|
}
|
|
@@ -2476,7 +3376,7 @@ class StableBrowser {
|
|
|
2476
3376
|
info.operation = "analyzeTable";
|
|
2477
3377
|
info.selectors = selectors;
|
|
2478
3378
|
info.query = query;
|
|
2479
|
-
query =
|
|
3379
|
+
query = _fixUsingParams(query, _params);
|
|
2480
3380
|
info.query_fixed = query;
|
|
2481
3381
|
info.operator = operator;
|
|
2482
3382
|
info.value = value;
|
|
@@ -2582,11 +3482,12 @@ class StableBrowser {
|
|
|
2582
3482
|
info.screenshotPath = screenshotPath;
|
|
2583
3483
|
Object.assign(e, { info: info });
|
|
2584
3484
|
error = e;
|
|
2585
|
-
throw e;
|
|
3485
|
+
// throw e;
|
|
3486
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2586
3487
|
}
|
|
2587
3488
|
finally {
|
|
2588
3489
|
const endTime = Date.now();
|
|
2589
|
-
|
|
3490
|
+
_reportToWorld(world, {
|
|
2590
3491
|
element_name: selectors.element_name,
|
|
2591
3492
|
type: Types.ANALYZE_TABLE,
|
|
2592
3493
|
text: "Analyze table",
|
|
@@ -2596,7 +3497,7 @@ class StableBrowser {
|
|
|
2596
3497
|
status: "FAILED",
|
|
2597
3498
|
startTime,
|
|
2598
3499
|
endTime,
|
|
2599
|
-
message: error
|
|
3500
|
+
message: error?.message,
|
|
2600
3501
|
}
|
|
2601
3502
|
: {
|
|
2602
3503
|
status: "PASSED",
|
|
@@ -2607,28 +3508,51 @@ class StableBrowser {
|
|
|
2607
3508
|
});
|
|
2608
3509
|
}
|
|
2609
3510
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
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");
|
|
2626
3537
|
}
|
|
3538
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3539
|
+
return state.info;
|
|
3540
|
+
}
|
|
3541
|
+
catch (e) {
|
|
3542
|
+
await _commandError(state, e, this);
|
|
2627
3543
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
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);
|
|
3551
|
+
}
|
|
3552
|
+
catch (error) {
|
|
3553
|
+
this.logger.debug(error);
|
|
3554
|
+
throw error;
|
|
2630
3555
|
}
|
|
2631
|
-
return value;
|
|
2632
3556
|
}
|
|
2633
3557
|
_getLoadTimeout(options) {
|
|
2634
3558
|
let timeout = 15000;
|
|
@@ -2640,6 +3564,37 @@ class StableBrowser {
|
|
|
2640
3564
|
}
|
|
2641
3565
|
return timeout;
|
|
2642
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
|
+
}
|
|
2643
3598
|
async waitForPageLoad(options = {}, world = null) {
|
|
2644
3599
|
let timeout = this._getLoadTimeout(options);
|
|
2645
3600
|
const promiseArray = [];
|
|
@@ -2679,7 +3634,7 @@ class StableBrowser {
|
|
|
2679
3634
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2680
3635
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2681
3636
|
const endTime = Date.now();
|
|
2682
|
-
|
|
3637
|
+
_reportToWorld(world, {
|
|
2683
3638
|
type: Types.GET_PAGE_STATUS,
|
|
2684
3639
|
text: "Wait for page load",
|
|
2685
3640
|
screenshotId,
|
|
@@ -2688,7 +3643,7 @@ class StableBrowser {
|
|
|
2688
3643
|
status: "FAILED",
|
|
2689
3644
|
startTime,
|
|
2690
3645
|
endTime,
|
|
2691
|
-
message: error
|
|
3646
|
+
message: error?.message,
|
|
2692
3647
|
}
|
|
2693
3648
|
: {
|
|
2694
3649
|
status: "PASSED",
|
|
@@ -2699,41 +3654,123 @@ class StableBrowser {
|
|
|
2699
3654
|
}
|
|
2700
3655
|
}
|
|
2701
3656
|
async closePage(options = {}, world = null) {
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
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
|
+
};
|
|
2707
3670
|
try {
|
|
3671
|
+
await _preCommand(state, this);
|
|
2708
3672
|
await this.page.close();
|
|
2709
3673
|
}
|
|
2710
3674
|
catch (e) {
|
|
2711
3675
|
console.log(".");
|
|
3676
|
+
await _commandError(state, e, this);
|
|
2712
3677
|
}
|
|
2713
3678
|
finally {
|
|
2714
|
-
await
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
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;
|
|
2727
3714
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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);
|
|
2735
3767
|
}
|
|
2736
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
|
+
}
|
|
2737
3774
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2738
3775
|
const startTime = Date.now();
|
|
2739
3776
|
let error = null;
|
|
@@ -2751,21 +3788,23 @@ class StableBrowser {
|
|
|
2751
3788
|
}
|
|
2752
3789
|
catch (e) {
|
|
2753
3790
|
console.log(".");
|
|
3791
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2754
3792
|
}
|
|
2755
3793
|
finally {
|
|
2756
3794
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2757
3795
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2758
3796
|
const endTime = Date.now();
|
|
2759
|
-
|
|
3797
|
+
_reportToWorld(world, {
|
|
2760
3798
|
type: Types.SET_VIEWPORT,
|
|
2761
3799
|
text: "set viewport size to " + width + "x" + hight,
|
|
3800
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2762
3801
|
screenshotId,
|
|
2763
3802
|
result: error
|
|
2764
3803
|
? {
|
|
2765
3804
|
status: "FAILED",
|
|
2766
3805
|
startTime,
|
|
2767
3806
|
endTime,
|
|
2768
|
-
message: error
|
|
3807
|
+
message: error?.message,
|
|
2769
3808
|
}
|
|
2770
3809
|
: {
|
|
2771
3810
|
status: "PASSED",
|
|
@@ -2787,12 +3826,13 @@ class StableBrowser {
|
|
|
2787
3826
|
}
|
|
2788
3827
|
catch (e) {
|
|
2789
3828
|
console.log(".");
|
|
3829
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2790
3830
|
}
|
|
2791
3831
|
finally {
|
|
2792
3832
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2793
3833
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2794
3834
|
const endTime = Date.now();
|
|
2795
|
-
|
|
3835
|
+
_reportToWorld(world, {
|
|
2796
3836
|
type: Types.GET_PAGE_STATUS,
|
|
2797
3837
|
text: "page relaod",
|
|
2798
3838
|
screenshotId,
|
|
@@ -2801,7 +3841,7 @@ class StableBrowser {
|
|
|
2801
3841
|
status: "FAILED",
|
|
2802
3842
|
startTime,
|
|
2803
3843
|
endTime,
|
|
2804
|
-
message: error
|
|
3844
|
+
message: error?.message,
|
|
2805
3845
|
}
|
|
2806
3846
|
: {
|
|
2807
3847
|
status: "PASSED",
|
|
@@ -2828,11 +3868,198 @@ class StableBrowser {
|
|
|
2828
3868
|
console.log("#-#");
|
|
2829
3869
|
}
|
|
2830
3870
|
}
|
|
2831
|
-
|
|
2832
|
-
if (
|
|
2833
|
-
|
|
3871
|
+
async beforeScenario(world, scenario) {
|
|
3872
|
+
if (world && world.attach) {
|
|
3873
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
3874
|
+
}
|
|
3875
|
+
this.beforeScenarioCalled = true;
|
|
3876
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3877
|
+
this.scenarioName = scenario.pickle.name;
|
|
3878
|
+
}
|
|
3879
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3880
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3881
|
+
}
|
|
3882
|
+
if (this.context) {
|
|
3883
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3884
|
+
}
|
|
3885
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3886
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3887
|
+
// check if @global_test_data tag is present
|
|
3888
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3889
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
// update test data based on feature/scenario
|
|
3893
|
+
let envName = null;
|
|
3894
|
+
if (this.context && this.context.environment) {
|
|
3895
|
+
envName = this.context.environment.name;
|
|
3896
|
+
}
|
|
3897
|
+
if (!process.env.TEMP_RUN) {
|
|
3898
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
3899
|
+
}
|
|
3900
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3901
|
+
}
|
|
3902
|
+
async afterScenario(world, scenario) { }
|
|
3903
|
+
async beforeStep(world, step) {
|
|
3904
|
+
if (!this.beforeScenarioCalled) {
|
|
3905
|
+
this.beforeScenario(world, step);
|
|
3906
|
+
}
|
|
3907
|
+
if (this.stepIndex === undefined) {
|
|
3908
|
+
this.stepIndex = 0;
|
|
3909
|
+
}
|
|
3910
|
+
else {
|
|
3911
|
+
this.stepIndex++;
|
|
3912
|
+
}
|
|
3913
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3914
|
+
this.stepName = step.pickleStep.text;
|
|
3915
|
+
this.logger.info("step: " + this.stepName);
|
|
3916
|
+
}
|
|
3917
|
+
else if (step && step.text) {
|
|
3918
|
+
this.stepName = step.text;
|
|
3919
|
+
}
|
|
3920
|
+
else {
|
|
3921
|
+
this.stepName = "step " + this.stepIndex;
|
|
3922
|
+
}
|
|
3923
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3924
|
+
if (this.context.browserObject.context) {
|
|
3925
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
if (this.initSnapshotTaken === false) {
|
|
3929
|
+
this.initSnapshotTaken = true;
|
|
3930
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3931
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3932
|
+
if (snapshot) {
|
|
3933
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
async getAriaSnapshot() {
|
|
3939
|
+
try {
|
|
3940
|
+
// find the page url
|
|
3941
|
+
const url = await this.page.url();
|
|
3942
|
+
// extract the path from the url
|
|
3943
|
+
const path = new URL(url).pathname;
|
|
3944
|
+
// get the page title
|
|
3945
|
+
const title = await this.page.title();
|
|
3946
|
+
// go over other frams
|
|
3947
|
+
const frames = this.page.frames();
|
|
3948
|
+
const snapshots = [];
|
|
3949
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3950
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3951
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3952
|
+
const frame = frames[i];
|
|
3953
|
+
try {
|
|
3954
|
+
// Ensure frame is attached and has body
|
|
3955
|
+
const body = frame.locator("body");
|
|
3956
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3957
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3958
|
+
content.push(`- frame: ${i}`);
|
|
3959
|
+
content.push(snapshot);
|
|
3960
|
+
}
|
|
3961
|
+
catch (innerErr) { }
|
|
3962
|
+
}
|
|
3963
|
+
return content.join("\n");
|
|
3964
|
+
}
|
|
3965
|
+
catch (e) {
|
|
3966
|
+
console.log("Error in getAriaSnapshot");
|
|
3967
|
+
//console.debug(e);
|
|
3968
|
+
}
|
|
3969
|
+
return null;
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* Sends command with custom payload to report.
|
|
3973
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3974
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3975
|
+
* @param content - Content of the command to be shown in the report.
|
|
3976
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3977
|
+
* @param world - Optional world context.
|
|
3978
|
+
* @public
|
|
3979
|
+
*/
|
|
3980
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3981
|
+
const state = {
|
|
3982
|
+
options,
|
|
3983
|
+
world,
|
|
3984
|
+
locate: false,
|
|
3985
|
+
scroll: false,
|
|
3986
|
+
screenshot: options.screenshot ?? false,
|
|
3987
|
+
highlight: options.highlight ?? false,
|
|
3988
|
+
type: Types.REPORT_COMMAND,
|
|
3989
|
+
text: commandText,
|
|
3990
|
+
_text: commandText,
|
|
3991
|
+
operation: "report_command",
|
|
3992
|
+
log: "***** " + commandText + " *****\n",
|
|
3993
|
+
};
|
|
3994
|
+
try {
|
|
3995
|
+
await _preCommand(state, this);
|
|
3996
|
+
const payload = {
|
|
3997
|
+
type: options.type ?? "text",
|
|
3998
|
+
content: content,
|
|
3999
|
+
screenshotId: null,
|
|
4000
|
+
};
|
|
4001
|
+
state.payload = payload;
|
|
4002
|
+
if (commandStatus === "FAILED") {
|
|
4003
|
+
state.throwError = true;
|
|
4004
|
+
throw new Error("Command failed");
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
catch (e) {
|
|
4008
|
+
await _commandError(state, e, this);
|
|
4009
|
+
}
|
|
4010
|
+
finally {
|
|
4011
|
+
await _commandFinally(state, this);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
async afterStep(world, step) {
|
|
4015
|
+
this.stepName = null;
|
|
4016
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4017
|
+
if (this.context.browserObject.context) {
|
|
4018
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
4019
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4020
|
+
});
|
|
4021
|
+
if (world && world.attach) {
|
|
4022
|
+
await world.attach(JSON.stringify({
|
|
4023
|
+
type: "trace",
|
|
4024
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4025
|
+
}), "application/json+trace");
|
|
4026
|
+
}
|
|
4027
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
if (this.context) {
|
|
4031
|
+
this.context.examplesRow = null;
|
|
4032
|
+
}
|
|
4033
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
4034
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4035
|
+
if (snapshot) {
|
|
4036
|
+
const obj = {};
|
|
4037
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
if (!process.env.TEMP_RUN) {
|
|
4041
|
+
const state = {
|
|
4042
|
+
world,
|
|
4043
|
+
locate: false,
|
|
4044
|
+
scroll: false,
|
|
4045
|
+
screenshot: true,
|
|
4046
|
+
highlight: true,
|
|
4047
|
+
type: Types.STEP_COMPLETE,
|
|
4048
|
+
text: "end of scenario",
|
|
4049
|
+
_text: "end of scenario",
|
|
4050
|
+
operation: "step_complete",
|
|
4051
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4052
|
+
};
|
|
4053
|
+
try {
|
|
4054
|
+
await _preCommand(state, this);
|
|
4055
|
+
}
|
|
4056
|
+
catch (e) {
|
|
4057
|
+
await _commandError(state, e, this);
|
|
4058
|
+
}
|
|
4059
|
+
finally {
|
|
4060
|
+
await _commandFinally(state, this);
|
|
4061
|
+
}
|
|
2834
4062
|
}
|
|
2835
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2836
4063
|
}
|
|
2837
4064
|
}
|
|
2838
4065
|
function createTimedPromise(promise, label) {
|
|
@@ -2840,156 +4067,5 @@ function createTimedPromise(promise, label) {
|
|
|
2840
4067
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2841
4068
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2842
4069
|
}
|
|
2843
|
-
const KEYBOARD_EVENTS = [
|
|
2844
|
-
"ALT",
|
|
2845
|
-
"AltGraph",
|
|
2846
|
-
"CapsLock",
|
|
2847
|
-
"Control",
|
|
2848
|
-
"Fn",
|
|
2849
|
-
"FnLock",
|
|
2850
|
-
"Hyper",
|
|
2851
|
-
"Meta",
|
|
2852
|
-
"NumLock",
|
|
2853
|
-
"ScrollLock",
|
|
2854
|
-
"Shift",
|
|
2855
|
-
"Super",
|
|
2856
|
-
"Symbol",
|
|
2857
|
-
"SymbolLock",
|
|
2858
|
-
"Enter",
|
|
2859
|
-
"Tab",
|
|
2860
|
-
"ArrowDown",
|
|
2861
|
-
"ArrowLeft",
|
|
2862
|
-
"ArrowRight",
|
|
2863
|
-
"ArrowUp",
|
|
2864
|
-
"End",
|
|
2865
|
-
"Home",
|
|
2866
|
-
"PageDown",
|
|
2867
|
-
"PageUp",
|
|
2868
|
-
"Backspace",
|
|
2869
|
-
"Clear",
|
|
2870
|
-
"Copy",
|
|
2871
|
-
"CrSel",
|
|
2872
|
-
"Cut",
|
|
2873
|
-
"Delete",
|
|
2874
|
-
"EraseEof",
|
|
2875
|
-
"ExSel",
|
|
2876
|
-
"Insert",
|
|
2877
|
-
"Paste",
|
|
2878
|
-
"Redo",
|
|
2879
|
-
"Undo",
|
|
2880
|
-
"Accept",
|
|
2881
|
-
"Again",
|
|
2882
|
-
"Attn",
|
|
2883
|
-
"Cancel",
|
|
2884
|
-
"ContextMenu",
|
|
2885
|
-
"Escape",
|
|
2886
|
-
"Execute",
|
|
2887
|
-
"Find",
|
|
2888
|
-
"Finish",
|
|
2889
|
-
"Help",
|
|
2890
|
-
"Pause",
|
|
2891
|
-
"Play",
|
|
2892
|
-
"Props",
|
|
2893
|
-
"Select",
|
|
2894
|
-
"ZoomIn",
|
|
2895
|
-
"ZoomOut",
|
|
2896
|
-
"BrightnessDown",
|
|
2897
|
-
"BrightnessUp",
|
|
2898
|
-
"Eject",
|
|
2899
|
-
"LogOff",
|
|
2900
|
-
"Power",
|
|
2901
|
-
"PowerOff",
|
|
2902
|
-
"PrintScreen",
|
|
2903
|
-
"Hibernate",
|
|
2904
|
-
"Standby",
|
|
2905
|
-
"WakeUp",
|
|
2906
|
-
"AllCandidates",
|
|
2907
|
-
"Alphanumeric",
|
|
2908
|
-
"CodeInput",
|
|
2909
|
-
"Compose",
|
|
2910
|
-
"Convert",
|
|
2911
|
-
"Dead",
|
|
2912
|
-
"FinalMode",
|
|
2913
|
-
"GroupFirst",
|
|
2914
|
-
"GroupLast",
|
|
2915
|
-
"GroupNext",
|
|
2916
|
-
"GroupPrevious",
|
|
2917
|
-
"ModeChange",
|
|
2918
|
-
"NextCandidate",
|
|
2919
|
-
"NonConvert",
|
|
2920
|
-
"PreviousCandidate",
|
|
2921
|
-
"Process",
|
|
2922
|
-
"SingleCandidate",
|
|
2923
|
-
"HangulMode",
|
|
2924
|
-
"HanjaMode",
|
|
2925
|
-
"JunjaMode",
|
|
2926
|
-
"Eisu",
|
|
2927
|
-
"Hankaku",
|
|
2928
|
-
"Hiragana",
|
|
2929
|
-
"HiraganaKatakana",
|
|
2930
|
-
"KanaMode",
|
|
2931
|
-
"KanjiMode",
|
|
2932
|
-
"Katakana",
|
|
2933
|
-
"Romaji",
|
|
2934
|
-
"Zenkaku",
|
|
2935
|
-
"ZenkakuHanaku",
|
|
2936
|
-
"F1",
|
|
2937
|
-
"F2",
|
|
2938
|
-
"F3",
|
|
2939
|
-
"F4",
|
|
2940
|
-
"F5",
|
|
2941
|
-
"F6",
|
|
2942
|
-
"F7",
|
|
2943
|
-
"F8",
|
|
2944
|
-
"F9",
|
|
2945
|
-
"F10",
|
|
2946
|
-
"F11",
|
|
2947
|
-
"F12",
|
|
2948
|
-
"Soft1",
|
|
2949
|
-
"Soft2",
|
|
2950
|
-
"Soft3",
|
|
2951
|
-
"Soft4",
|
|
2952
|
-
"ChannelDown",
|
|
2953
|
-
"ChannelUp",
|
|
2954
|
-
"Close",
|
|
2955
|
-
"MailForward",
|
|
2956
|
-
"MailReply",
|
|
2957
|
-
"MailSend",
|
|
2958
|
-
"MediaFastForward",
|
|
2959
|
-
"MediaPause",
|
|
2960
|
-
"MediaPlay",
|
|
2961
|
-
"MediaPlayPause",
|
|
2962
|
-
"MediaRecord",
|
|
2963
|
-
"MediaRewind",
|
|
2964
|
-
"MediaStop",
|
|
2965
|
-
"MediaTrackNext",
|
|
2966
|
-
"MediaTrackPrevious",
|
|
2967
|
-
"AudioBalanceLeft",
|
|
2968
|
-
"AudioBalanceRight",
|
|
2969
|
-
"AudioBassBoostDown",
|
|
2970
|
-
"AudioBassBoostToggle",
|
|
2971
|
-
"AudioBassBoostUp",
|
|
2972
|
-
"AudioFaderFront",
|
|
2973
|
-
"AudioFaderRear",
|
|
2974
|
-
"AudioSurroundModeNext",
|
|
2975
|
-
"AudioTrebleDown",
|
|
2976
|
-
"AudioTrebleUp",
|
|
2977
|
-
"AudioVolumeDown",
|
|
2978
|
-
"AudioVolumeMute",
|
|
2979
|
-
"AudioVolumeUp",
|
|
2980
|
-
"MicrophoneToggle",
|
|
2981
|
-
"MicrophoneVolumeDown",
|
|
2982
|
-
"MicrophoneVolumeMute",
|
|
2983
|
-
"MicrophoneVolumeUp",
|
|
2984
|
-
"TV",
|
|
2985
|
-
"TV3DMode",
|
|
2986
|
-
"TVAntennaCable",
|
|
2987
|
-
"TVAudioDescription",
|
|
2988
|
-
];
|
|
2989
|
-
function unEscapeString(str) {
|
|
2990
|
-
const placeholder = "__NEWLINE__";
|
|
2991
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2992
|
-
return str;
|
|
2993
|
-
}
|
|
2994
4070
|
export { StableBrowser };
|
|
2995
4071
|
//# sourceMappingURL=stable_browser.js.map
|