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