automation_model 1.0.443-dev → 1.0.443
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 +231 -49
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +7 -3
- package/lib/browser_manager.js +172 -48
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.d.ts +2 -0
- package/lib/bruno.js +381 -0
- package/lib/bruno.js.map +1 -0
- package/lib/command_common.d.ts +6 -0
- package/lib/command_common.js +202 -0
- package/lib/command_common.js.map +1 -0
- package/lib/date_time.js.map +1 -1
- package/lib/drawRect.js.map +1 -1
- package/lib/environment.d.ts +4 -0
- package/lib/environment.js +6 -2
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.d.ts +6 -0
- package/lib/error-messages.js +206 -0
- package/lib/error-messages.js.map +1 -0
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +61 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/find_function.js.map +1 -1
- package/lib/generation_scripts.d.ts +4 -0
- package/lib/generation_scripts.js +2 -0
- package/lib/generation_scripts.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +5 -2
- package/lib/init_browser.js +128 -11
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.d.ts +7 -0
- package/lib/locate_element.js +215 -0
- package/lib/locate_element.js.map +1 -0
- package/lib/locator.d.ts +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 -1305
- 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,55 +577,108 @@ class StableBrowser {
|
|
|
475
577
|
}
|
|
476
578
|
if (result.foundElements.length > 0) {
|
|
477
579
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
}
|
|
481
595
|
return { rerun: true };
|
|
482
596
|
}
|
|
483
597
|
}
|
|
484
598
|
}
|
|
485
599
|
return { rerun: false };
|
|
486
600
|
}
|
|
487
|
-
async _locate(selectors, info, _params, timeout =
|
|
601
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
602
|
+
if (!timeout) {
|
|
603
|
+
timeout = 30000;
|
|
604
|
+
}
|
|
488
605
|
for (let i = 0; i < 3; i++) {
|
|
489
606
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
490
607
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
491
608
|
let selector = selectors.locators[j];
|
|
492
609
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
493
610
|
}
|
|
494
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
611
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
495
612
|
if (!element.rerun) {
|
|
496
|
-
|
|
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);
|
|
497
635
|
}
|
|
498
636
|
}
|
|
499
637
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
500
638
|
}
|
|
501
|
-
async
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
639
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
640
|
+
if (!info) {
|
|
641
|
+
info = {};
|
|
642
|
+
info.failCause = {};
|
|
643
|
+
info.log = "";
|
|
644
|
+
}
|
|
645
|
+
let startTime = Date.now();
|
|
507
646
|
let scope = this.page;
|
|
647
|
+
if (selectors.frame) {
|
|
648
|
+
return selectors.frame;
|
|
649
|
+
}
|
|
508
650
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
509
|
-
const findFrame = (frame, framescope) => {
|
|
651
|
+
const findFrame = async (frame, framescope) => {
|
|
510
652
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
511
653
|
let frameLocator = frame.selectors[i];
|
|
512
654
|
if (frameLocator.css) {
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
669
|
}
|
|
516
670
|
}
|
|
517
671
|
if (frame.children) {
|
|
518
|
-
return findFrame(frame.children, framescope);
|
|
672
|
+
return await findFrame(frame.children, framescope);
|
|
519
673
|
}
|
|
520
674
|
return framescope;
|
|
521
675
|
};
|
|
522
|
-
|
|
676
|
+
let fLocator = null;
|
|
523
677
|
while (true) {
|
|
524
678
|
let frameFound = false;
|
|
525
679
|
if (selectors.nestFrmLoc) {
|
|
526
|
-
|
|
680
|
+
fLocator = selectors.nestFrmLoc;
|
|
681
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
527
682
|
frameFound = true;
|
|
528
683
|
break;
|
|
529
684
|
}
|
|
@@ -531,6 +686,7 @@ class StableBrowser {
|
|
|
531
686
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
532
687
|
let frameLocator = selectors.frameLocators[i];
|
|
533
688
|
if (frameLocator.css) {
|
|
689
|
+
fLocator = frameLocator.css;
|
|
534
690
|
scope = scope.frameLocator(frameLocator.css);
|
|
535
691
|
frameFound = true;
|
|
536
692
|
break;
|
|
@@ -538,20 +694,55 @@ class StableBrowser {
|
|
|
538
694
|
}
|
|
539
695
|
}
|
|
540
696
|
if (!frameFound && selectors.iframe_src) {
|
|
697
|
+
fLocator = selectors.iframe_src;
|
|
541
698
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
542
699
|
}
|
|
543
700
|
if (!scope) {
|
|
544
|
-
info
|
|
545
|
-
|
|
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}"`;
|
|
546
708
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
547
709
|
}
|
|
548
710
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
549
711
|
}
|
|
550
712
|
else {
|
|
713
|
+
if (info && info.locatorLog) {
|
|
714
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
715
|
+
}
|
|
551
716
|
break;
|
|
552
717
|
}
|
|
553
718
|
}
|
|
554
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);
|
|
555
746
|
let selectorsLocators = null;
|
|
556
747
|
selectorsLocators = selectors.locators;
|
|
557
748
|
// group selectors by priority
|
|
@@ -587,18 +778,13 @@ class StableBrowser {
|
|
|
587
778
|
}
|
|
588
779
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
589
780
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
590
|
-
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);
|
|
591
782
|
if (result.foundElements.length === 0) {
|
|
592
783
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
593
|
-
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);
|
|
594
785
|
}
|
|
595
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
596
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
600
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
601
|
-
}
|
|
786
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
787
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
602
788
|
}
|
|
603
789
|
let foundElements = result.foundElements;
|
|
604
790
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -638,24 +824,43 @@ class StableBrowser {
|
|
|
638
824
|
return maxCountElement.locator;
|
|
639
825
|
}
|
|
640
826
|
}
|
|
641
|
-
if (
|
|
827
|
+
if (Date.now() - startTime > timeout) {
|
|
642
828
|
break;
|
|
643
829
|
}
|
|
644
|
-
if (
|
|
645
|
-
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";
|
|
646
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
|
+
}
|
|
647
837
|
}
|
|
648
|
-
if (
|
|
649
|
-
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";
|
|
650
840
|
visibleOnly = false;
|
|
651
841
|
}
|
|
652
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
|
+
}
|
|
653
848
|
}
|
|
654
849
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
655
|
-
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
|
+
}
|
|
656
861
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
657
862
|
}
|
|
658
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
863
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
659
864
|
let foundElements = [];
|
|
660
865
|
const result = {
|
|
661
866
|
foundElements: foundElements,
|
|
@@ -663,17 +868,20 @@ class StableBrowser {
|
|
|
663
868
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
664
869
|
let foundLocators = [];
|
|
665
870
|
try {
|
|
666
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
871
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
667
872
|
}
|
|
668
873
|
catch (e) {
|
|
669
|
-
this
|
|
670
|
-
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);
|
|
671
877
|
foundLocators = [];
|
|
672
878
|
try {
|
|
673
|
-
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);
|
|
674
880
|
}
|
|
675
881
|
catch (e) {
|
|
676
|
-
|
|
882
|
+
if (logErrors) {
|
|
883
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
884
|
+
}
|
|
677
885
|
}
|
|
678
886
|
}
|
|
679
887
|
if (foundLocators.length === 1) {
|
|
@@ -684,270 +892,348 @@ class StableBrowser {
|
|
|
684
892
|
});
|
|
685
893
|
result.locatorIndex = i;
|
|
686
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
|
+
}
|
|
687
932
|
}
|
|
688
933
|
return result;
|
|
689
934
|
}
|
|
690
|
-
async
|
|
691
|
-
|
|
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);
|
|
692
949
|
const startTime = Date.now();
|
|
693
|
-
|
|
694
|
-
|
|
950
|
+
let timeout = 30000;
|
|
951
|
+
if (options && options.timeout) {
|
|
952
|
+
timeout = options.timeout;
|
|
695
953
|
}
|
|
696
|
-
|
|
697
|
-
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
698
|
-
info.operation = "click";
|
|
699
|
-
info.selectors = selectors;
|
|
700
|
-
let error = null;
|
|
701
|
-
let screenshotId = null;
|
|
702
|
-
let screenshotPath = null;
|
|
703
|
-
try {
|
|
704
|
-
let element = await this._locate(selectors, info, _params);
|
|
705
|
-
await this.scrollIfNeeded(element, info);
|
|
706
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
954
|
+
while (true) {
|
|
707
955
|
try {
|
|
708
|
-
await this.
|
|
709
|
-
|
|
710
|
-
|
|
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
|
+
}
|
|
711
969
|
}
|
|
712
970
|
catch (e) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
}
|
|
718
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);
|
|
719
1048
|
await this.waitForPageLoad();
|
|
720
|
-
return info;
|
|
1049
|
+
return state.info;
|
|
721
1050
|
}
|
|
722
1051
|
catch (e) {
|
|
723
|
-
|
|
724
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
725
|
-
info.screenshotPath = screenshotPath;
|
|
726
|
-
Object.assign(e, { info: info });
|
|
727
|
-
error = e;
|
|
728
|
-
throw e;
|
|
1052
|
+
await _commandError(state, e, this);
|
|
729
1053
|
}
|
|
730
1054
|
finally {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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);
|
|
751
1087
|
}
|
|
1088
|
+
return found;
|
|
752
1089
|
}
|
|
753
1090
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
+
};
|
|
764
1102
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
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));
|
|
767
1107
|
try {
|
|
768
|
-
|
|
769
|
-
|
|
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 });
|
|
770
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);
|
|
771
1117
|
}
|
|
772
1118
|
catch (e) {
|
|
773
1119
|
if (e.message && e.message.includes("did not change its state")) {
|
|
774
1120
|
this.logger.info("element did not change its state, ignoring...");
|
|
775
1121
|
}
|
|
776
1122
|
else {
|
|
777
|
-
//await this.closeUnexpectedPopups();
|
|
778
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
779
|
-
element = await this._locate(selectors, info, _params);
|
|
780
|
-
await element.setChecked(checked, { timeout: 5000, force: true });
|
|
781
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
|
+
}
|
|
782
1145
|
}
|
|
783
1146
|
}
|
|
784
1147
|
await this.waitForPageLoad();
|
|
785
|
-
return info;
|
|
1148
|
+
return state.info;
|
|
786
1149
|
}
|
|
787
1150
|
catch (e) {
|
|
788
|
-
|
|
789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
790
|
-
info.screenshotPath = screenshotPath;
|
|
791
|
-
Object.assign(e, { info: info });
|
|
792
|
-
error = e;
|
|
793
|
-
throw e;
|
|
1151
|
+
await _commandError(state, e, this);
|
|
794
1152
|
}
|
|
795
1153
|
finally {
|
|
796
|
-
|
|
797
|
-
this._reportToWorld(world, {
|
|
798
|
-
element_name: selectors.element_name,
|
|
799
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
800
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
801
|
-
screenshotId,
|
|
802
|
-
result: error
|
|
803
|
-
? {
|
|
804
|
-
status: "FAILED",
|
|
805
|
-
startTime,
|
|
806
|
-
endTime,
|
|
807
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
808
|
-
}
|
|
809
|
-
: {
|
|
810
|
-
status: "PASSED",
|
|
811
|
-
startTime,
|
|
812
|
-
endTime,
|
|
813
|
-
},
|
|
814
|
-
info: info,
|
|
815
|
-
});
|
|
1154
|
+
await _commandFinally(state, this);
|
|
816
1155
|
}
|
|
817
1156
|
}
|
|
818
1157
|
async hover(selectors, _params, options = {}, world = null) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
+
};
|
|
828
1169
|
try {
|
|
829
|
-
|
|
830
|
-
(
|
|
831
|
-
|
|
832
|
-
await this._highlightElements(element);
|
|
833
|
-
await element.hover();
|
|
834
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
835
|
-
}
|
|
836
|
-
catch (e) {
|
|
837
|
-
//await this.closeUnexpectedPopups();
|
|
838
|
-
info.log += "hover failed, will try again" + "\n";
|
|
839
|
-
element = await this._locate(selectors, info, _params);
|
|
840
|
-
await element.hover({ timeout: 10000 });
|
|
841
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
842
|
-
}
|
|
1170
|
+
await _preCommand(state, this);
|
|
1171
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1172
|
+
await _screenshot(state, this);
|
|
843
1173
|
await this.waitForPageLoad();
|
|
844
|
-
return info;
|
|
1174
|
+
return state.info;
|
|
845
1175
|
}
|
|
846
1176
|
catch (e) {
|
|
847
|
-
|
|
848
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
849
|
-
info.screenshotPath = screenshotPath;
|
|
850
|
-
Object.assign(e, { info: info });
|
|
851
|
-
error = e;
|
|
852
|
-
throw e;
|
|
1177
|
+
await _commandError(state, e, this);
|
|
853
1178
|
}
|
|
854
1179
|
finally {
|
|
855
|
-
|
|
856
|
-
this._reportToWorld(world, {
|
|
857
|
-
element_name: selectors.element_name,
|
|
858
|
-
type: Types.HOVER,
|
|
859
|
-
text: `Hover element`,
|
|
860
|
-
screenshotId,
|
|
861
|
-
result: error
|
|
862
|
-
? {
|
|
863
|
-
status: "FAILED",
|
|
864
|
-
startTime,
|
|
865
|
-
endTime,
|
|
866
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
867
|
-
}
|
|
868
|
-
: {
|
|
869
|
-
status: "PASSED",
|
|
870
|
-
startTime,
|
|
871
|
-
endTime,
|
|
872
|
-
},
|
|
873
|
-
info: info,
|
|
874
|
-
});
|
|
1180
|
+
await _commandFinally(state, this);
|
|
875
1181
|
}
|
|
876
1182
|
}
|
|
877
1183
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
878
|
-
this._validateSelectors(selectors);
|
|
879
1184
|
if (!values) {
|
|
880
1185
|
throw new Error("values is null");
|
|
881
1186
|
}
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
};
|
|
890
1199
|
try {
|
|
891
|
-
|
|
892
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1200
|
+
await _preCommand(state, this);
|
|
893
1201
|
try {
|
|
894
|
-
await
|
|
895
|
-
await element.selectOption(values);
|
|
1202
|
+
await state.element.selectOption(values);
|
|
896
1203
|
}
|
|
897
1204
|
catch (e) {
|
|
898
1205
|
//await this.closeUnexpectedPopups();
|
|
899
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
900
|
-
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 });
|
|
901
1208
|
}
|
|
902
1209
|
await this.waitForPageLoad();
|
|
903
|
-
return info;
|
|
1210
|
+
return state.info;
|
|
904
1211
|
}
|
|
905
1212
|
catch (e) {
|
|
906
|
-
|
|
907
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
908
|
-
info.screenshotPath = screenshotPath;
|
|
909
|
-
Object.assign(e, { info: info });
|
|
910
|
-
this.logger.info("click failed, will try next selector");
|
|
911
|
-
error = e;
|
|
912
|
-
throw e;
|
|
1213
|
+
await _commandError(state, e, this);
|
|
913
1214
|
}
|
|
914
1215
|
finally {
|
|
915
|
-
|
|
916
|
-
this._reportToWorld(world, {
|
|
917
|
-
element_name: selectors.element_name,
|
|
918
|
-
type: Types.SELECT,
|
|
919
|
-
text: `Select option: ${values}`,
|
|
920
|
-
value: values.toString(),
|
|
921
|
-
screenshotId,
|
|
922
|
-
result: error
|
|
923
|
-
? {
|
|
924
|
-
status: "FAILED",
|
|
925
|
-
startTime,
|
|
926
|
-
endTime,
|
|
927
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
928
|
-
}
|
|
929
|
-
: {
|
|
930
|
-
status: "PASSED",
|
|
931
|
-
startTime,
|
|
932
|
-
endTime,
|
|
933
|
-
},
|
|
934
|
-
info: info,
|
|
935
|
-
});
|
|
1216
|
+
await _commandFinally(state, this);
|
|
936
1217
|
}
|
|
937
1218
|
}
|
|
938
1219
|
async type(_value, _params = null, options = {}, world = null) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
};
|
|
948
1234
|
try {
|
|
949
|
-
|
|
950
|
-
const valueSegment =
|
|
1235
|
+
await _preCommand(state, this);
|
|
1236
|
+
const valueSegment = state.value.split("&&");
|
|
951
1237
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
952
1238
|
if (i > 0) {
|
|
953
1239
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -967,134 +1253,77 @@ class StableBrowser {
|
|
|
967
1253
|
await this.page.keyboard.type(value);
|
|
968
1254
|
}
|
|
969
1255
|
}
|
|
970
|
-
return info;
|
|
1256
|
+
return state.info;
|
|
971
1257
|
}
|
|
972
1258
|
catch (e) {
|
|
973
|
-
|
|
974
|
-
this.logger.error("type failed " + info.log);
|
|
975
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
976
|
-
info.screenshotPath = screenshotPath;
|
|
977
|
-
Object.assign(e, { info: info });
|
|
978
|
-
error = e;
|
|
979
|
-
throw e;
|
|
1259
|
+
await _commandError(state, e, this);
|
|
980
1260
|
}
|
|
981
1261
|
finally {
|
|
982
|
-
|
|
983
|
-
this._reportToWorld(world, {
|
|
984
|
-
type: Types.TYPE_PRESS,
|
|
985
|
-
screenshotId,
|
|
986
|
-
value: _value,
|
|
987
|
-
text: `type value: ${_value}`,
|
|
988
|
-
result: error
|
|
989
|
-
? {
|
|
990
|
-
status: "FAILED",
|
|
991
|
-
startTime,
|
|
992
|
-
endTime,
|
|
993
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
994
|
-
}
|
|
995
|
-
: {
|
|
996
|
-
status: "PASSED",
|
|
997
|
-
startTime,
|
|
998
|
-
endTime,
|
|
999
|
-
},
|
|
1000
|
-
info: info,
|
|
1001
|
-
});
|
|
1262
|
+
await _commandFinally(state, this);
|
|
1002
1263
|
}
|
|
1003
1264
|
}
|
|
1004
1265
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
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
|
+
};
|
|
1017
1277
|
try {
|
|
1018
|
-
|
|
1019
|
-
let
|
|
1020
|
-
await this.scrollIfNeeded(element, info);
|
|
1021
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1022
|
-
await this._highlightElements(element);
|
|
1278
|
+
await _preCommand(state, this);
|
|
1279
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
1023
1280
|
try {
|
|
1024
|
-
await element.evaluateHandle((el, value) => {
|
|
1281
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1025
1282
|
el.value = value;
|
|
1026
1283
|
}, value);
|
|
1027
1284
|
}
|
|
1028
1285
|
catch (error) {
|
|
1029
1286
|
this.logger.error("setInputValue failed, will try again");
|
|
1030
|
-
|
|
1031
|
-
info.
|
|
1032
|
-
|
|
1033
|
-
await element.evaluateHandle((el, value) => {
|
|
1287
|
+
await _screenshot(state, this);
|
|
1288
|
+
Object.assign(error, { info: state.info });
|
|
1289
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1034
1290
|
el.value = value;
|
|
1035
1291
|
});
|
|
1036
1292
|
}
|
|
1037
1293
|
}
|
|
1038
1294
|
catch (e) {
|
|
1039
|
-
|
|
1040
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1041
|
-
info.screenshotPath = screenshotPath;
|
|
1042
|
-
Object.assign(e, { info: info });
|
|
1043
|
-
error = e;
|
|
1044
|
-
throw e;
|
|
1295
|
+
await _commandError(state, e, this);
|
|
1045
1296
|
}
|
|
1046
1297
|
finally {
|
|
1047
|
-
|
|
1048
|
-
this._reportToWorld(world, {
|
|
1049
|
-
element_name: selectors.element_name,
|
|
1050
|
-
type: Types.SET_INPUT,
|
|
1051
|
-
text: `Set input value`,
|
|
1052
|
-
value: value,
|
|
1053
|
-
screenshotId,
|
|
1054
|
-
result: error
|
|
1055
|
-
? {
|
|
1056
|
-
status: "FAILED",
|
|
1057
|
-
startTime,
|
|
1058
|
-
endTime,
|
|
1059
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1060
|
-
}
|
|
1061
|
-
: {
|
|
1062
|
-
status: "PASSED",
|
|
1063
|
-
startTime,
|
|
1064
|
-
endTime,
|
|
1065
|
-
},
|
|
1066
|
-
info: info,
|
|
1067
|
-
});
|
|
1298
|
+
await _commandFinally(state, this);
|
|
1068
1299
|
}
|
|
1069
1300
|
}
|
|
1070
1301
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
+
};
|
|
1081
1315
|
try {
|
|
1082
|
-
|
|
1083
|
-
let element = await this._locate(selectors, info, _params);
|
|
1084
|
-
//insert red border around the element
|
|
1085
|
-
await this.scrollIfNeeded(element, info);
|
|
1086
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1087
|
-
await this._highlightElements(element);
|
|
1316
|
+
await _preCommand(state, this);
|
|
1088
1317
|
try {
|
|
1089
|
-
await element
|
|
1318
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1090
1319
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1091
1320
|
if (format) {
|
|
1092
|
-
value = dayjs(value).format(format);
|
|
1093
|
-
await element.fill(value);
|
|
1321
|
+
state.value = dayjs(state.value).format(format);
|
|
1322
|
+
await state.element.fill(state.value);
|
|
1094
1323
|
}
|
|
1095
1324
|
else {
|
|
1096
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1097
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1325
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1326
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1098
1327
|
el.value = ""; // clear input
|
|
1099
1328
|
el.value = dateTimeValue;
|
|
1100
1329
|
}, dateTimeValue);
|
|
@@ -1107,20 +1336,19 @@ class StableBrowser {
|
|
|
1107
1336
|
}
|
|
1108
1337
|
catch (err) {
|
|
1109
1338
|
//await this.closeUnexpectedPopups();
|
|
1110
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1339
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1111
1340
|
this.logger.info("Trying again");
|
|
1112
|
-
|
|
1113
|
-
info.
|
|
1114
|
-
Object.assign(err, { info: info });
|
|
1341
|
+
await _screenshot(state, this);
|
|
1342
|
+
Object.assign(err, { info: state.info });
|
|
1115
1343
|
await element.click();
|
|
1116
1344
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1117
1345
|
if (format) {
|
|
1118
|
-
value = dayjs(value).format(format);
|
|
1119
|
-
await element.fill(value);
|
|
1346
|
+
state.value = dayjs(state.value).format(format);
|
|
1347
|
+
await state.element.fill(state.value);
|
|
1120
1348
|
}
|
|
1121
1349
|
else {
|
|
1122
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1123
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1350
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1351
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1124
1352
|
el.value = ""; // clear input
|
|
1125
1353
|
el.value = dateTimeValue;
|
|
1126
1354
|
}, dateTimeValue);
|
|
@@ -1133,84 +1361,63 @@ class StableBrowser {
|
|
|
1133
1361
|
}
|
|
1134
1362
|
}
|
|
1135
1363
|
catch (e) {
|
|
1136
|
-
|
|
1137
|
-
throw e;
|
|
1364
|
+
await _commandError(state, e, this);
|
|
1138
1365
|
}
|
|
1139
1366
|
finally {
|
|
1140
|
-
|
|
1141
|
-
this._reportToWorld(world, {
|
|
1142
|
-
element_name: selectors.element_name,
|
|
1143
|
-
type: Types.SET_DATE_TIME,
|
|
1144
|
-
screenshotId,
|
|
1145
|
-
value: value,
|
|
1146
|
-
text: `setDateTime input with value: ${value}`,
|
|
1147
|
-
result: error
|
|
1148
|
-
? {
|
|
1149
|
-
status: "FAILED",
|
|
1150
|
-
startTime,
|
|
1151
|
-
endTime,
|
|
1152
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1153
|
-
}
|
|
1154
|
-
: {
|
|
1155
|
-
status: "PASSED",
|
|
1156
|
-
startTime,
|
|
1157
|
-
endTime,
|
|
1158
|
-
},
|
|
1159
|
-
info: info,
|
|
1160
|
-
});
|
|
1367
|
+
await _commandFinally(state, this);
|
|
1161
1368
|
}
|
|
1162
1369
|
}
|
|
1163
1370
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1164
|
-
|
|
1165
|
-
const startTime = Date.now();
|
|
1166
|
-
let error = null;
|
|
1167
|
-
let screenshotId = null;
|
|
1168
|
-
let screenshotPath = null;
|
|
1169
|
-
const info = {};
|
|
1170
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1171
|
-
info.operation = "clickType";
|
|
1172
|
-
info.selectors = selectors;
|
|
1371
|
+
_value = unEscapeString(_value);
|
|
1173
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
|
+
}
|
|
1174
1389
|
if (newValue !== _value) {
|
|
1175
1390
|
//this.logger.info(_value + "=" + newValue);
|
|
1176
1391
|
_value = newValue;
|
|
1177
1392
|
}
|
|
1178
|
-
info.value = _value;
|
|
1179
1393
|
try {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1184
|
-
await this._highlightElements(element);
|
|
1185
|
-
if (options === null || options === undefined || !options.press) {
|
|
1394
|
+
await _preCommand(state, this);
|
|
1395
|
+
state.info.value = _value;
|
|
1396
|
+
if (!options.press) {
|
|
1186
1397
|
try {
|
|
1187
|
-
let currentValue = await element.inputValue();
|
|
1398
|
+
let currentValue = await state.element.inputValue();
|
|
1188
1399
|
if (currentValue) {
|
|
1189
|
-
await element.fill("");
|
|
1400
|
+
await state.element.fill("");
|
|
1190
1401
|
}
|
|
1191
1402
|
}
|
|
1192
1403
|
catch (e) {
|
|
1193
1404
|
this.logger.info("unable to clear input value");
|
|
1194
1405
|
}
|
|
1195
1406
|
}
|
|
1196
|
-
if (options
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1200
|
-
catch (e) {
|
|
1201
|
-
await element.dispatchEvent("click");
|
|
1202
|
-
}
|
|
1407
|
+
if (options.press) {
|
|
1408
|
+
options.timeout = 5000;
|
|
1409
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1203
1410
|
}
|
|
1204
1411
|
else {
|
|
1205
1412
|
try {
|
|
1206
|
-
await element.focus();
|
|
1413
|
+
await state.element.focus();
|
|
1207
1414
|
}
|
|
1208
1415
|
catch (e) {
|
|
1209
|
-
await element.dispatchEvent("focus");
|
|
1416
|
+
await state.element.dispatchEvent("focus");
|
|
1210
1417
|
}
|
|
1211
1418
|
}
|
|
1212
1419
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1213
|
-
const valueSegment =
|
|
1420
|
+
const valueSegment = state.value.split("&&");
|
|
1214
1421
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1215
1422
|
if (i > 0) {
|
|
1216
1423
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1230,13 +1437,19 @@ class StableBrowser {
|
|
|
1230
1437
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1231
1438
|
}
|
|
1232
1439
|
}
|
|
1440
|
+
await _screenshot(state, this);
|
|
1233
1441
|
if (enter === true) {
|
|
1234
1442
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1235
1443
|
await this.page.keyboard.press("Enter");
|
|
1236
1444
|
await this.waitForPageLoad();
|
|
1237
1445
|
}
|
|
1238
1446
|
else if (enter === false) {
|
|
1239
|
-
|
|
1447
|
+
try {
|
|
1448
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1449
|
+
}
|
|
1450
|
+
catch (e) {
|
|
1451
|
+
// ignore
|
|
1452
|
+
}
|
|
1240
1453
|
//await this.page.keyboard.press("Tab");
|
|
1241
1454
|
}
|
|
1242
1455
|
else {
|
|
@@ -1245,111 +1458,95 @@ class StableBrowser {
|
|
|
1245
1458
|
await this.waitForPageLoad();
|
|
1246
1459
|
}
|
|
1247
1460
|
}
|
|
1248
|
-
return info;
|
|
1461
|
+
return state.info;
|
|
1249
1462
|
}
|
|
1250
1463
|
catch (e) {
|
|
1251
|
-
|
|
1252
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1253
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1254
|
-
info.screenshotPath = screenshotPath;
|
|
1255
|
-
Object.assign(e, { info: info });
|
|
1256
|
-
error = e;
|
|
1257
|
-
throw e;
|
|
1464
|
+
await _commandError(state, e, this);
|
|
1258
1465
|
}
|
|
1259
1466
|
finally {
|
|
1260
|
-
|
|
1261
|
-
this._reportToWorld(world, {
|
|
1262
|
-
element_name: selectors.element_name,
|
|
1263
|
-
type: Types.FILL,
|
|
1264
|
-
screenshotId,
|
|
1265
|
-
value: _value,
|
|
1266
|
-
text: `clickType input with value: ${_value}`,
|
|
1267
|
-
result: error
|
|
1268
|
-
? {
|
|
1269
|
-
status: "FAILED",
|
|
1270
|
-
startTime,
|
|
1271
|
-
endTime,
|
|
1272
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1273
|
-
}
|
|
1274
|
-
: {
|
|
1275
|
-
status: "PASSED",
|
|
1276
|
-
startTime,
|
|
1277
|
-
endTime,
|
|
1278
|
-
},
|
|
1279
|
-
info: info,
|
|
1280
|
-
});
|
|
1467
|
+
await _commandFinally(state, this);
|
|
1281
1468
|
}
|
|
1282
1469
|
}
|
|
1283
1470
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
+
};
|
|
1294
1482
|
try {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
await
|
|
1298
|
-
await element.fill(value);
|
|
1299
|
-
await element.dispatchEvent("change");
|
|
1483
|
+
await _preCommand(state, this);
|
|
1484
|
+
await state.element.fill(value);
|
|
1485
|
+
await state.element.dispatchEvent("change");
|
|
1300
1486
|
if (enter) {
|
|
1301
1487
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1302
1488
|
await this.page.keyboard.press("Enter");
|
|
1303
1489
|
}
|
|
1304
1490
|
await this.waitForPageLoad();
|
|
1305
|
-
return info;
|
|
1491
|
+
return state.info;
|
|
1306
1492
|
}
|
|
1307
1493
|
catch (e) {
|
|
1308
|
-
|
|
1309
|
-
this.logger.error("fill failed " + info.log);
|
|
1310
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1311
|
-
info.screenshotPath = screenshotPath;
|
|
1312
|
-
Object.assign(e, { info: info });
|
|
1313
|
-
error = e;
|
|
1314
|
-
throw e;
|
|
1494
|
+
await _commandError(state, e, this);
|
|
1315
1495
|
}
|
|
1316
1496
|
finally {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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);
|
|
1338
1533
|
}
|
|
1339
1534
|
}
|
|
1340
1535
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1341
1536
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1342
1537
|
}
|
|
1343
1538
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1344
|
-
this.
|
|
1539
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1540
|
+
_validateSelectors(selectors);
|
|
1345
1541
|
let screenshotId = null;
|
|
1346
1542
|
let screenshotPath = null;
|
|
1347
1543
|
if (!info.log) {
|
|
1348
1544
|
info.log = "";
|
|
1545
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1349
1546
|
}
|
|
1350
1547
|
info.operation = "getText";
|
|
1351
1548
|
info.selectors = selectors;
|
|
1352
|
-
let element = await this._locate(selectors, info, _params);
|
|
1549
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1353
1550
|
if (climb > 0) {
|
|
1354
1551
|
const climbArray = [];
|
|
1355
1552
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1368,6 +1565,18 @@ class StableBrowser {
|
|
|
1368
1565
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1369
1566
|
try {
|
|
1370
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
|
+
// }
|
|
1371
1580
|
const elementText = await element.innerText();
|
|
1372
1581
|
return {
|
|
1373
1582
|
text: elementText,
|
|
@@ -1379,188 +1588,214 @@ class StableBrowser {
|
|
|
1379
1588
|
}
|
|
1380
1589
|
catch (e) {
|
|
1381
1590
|
//await this.closeUnexpectedPopups();
|
|
1382
|
-
this.logger.info("no innerText will use textContent");
|
|
1591
|
+
this.logger.info("no innerText, will use textContent");
|
|
1383
1592
|
const elementText = await element.textContent();
|
|
1384
1593
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1385
1594
|
}
|
|
1386
1595
|
}
|
|
1387
1596
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1388
|
-
var _a;
|
|
1389
|
-
this._validateSelectors(selectors);
|
|
1390
1597
|
if (!pattern) {
|
|
1391
1598
|
throw new Error("pattern is null");
|
|
1392
1599
|
}
|
|
1393
1600
|
if (!text) {
|
|
1394
1601
|
throw new Error("text is null");
|
|
1395
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
|
+
};
|
|
1396
1620
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1397
1621
|
if (newValue !== text) {
|
|
1398
1622
|
this.logger.info(text + "=" + newValue);
|
|
1399
1623
|
text = newValue;
|
|
1400
1624
|
}
|
|
1401
|
-
const startTime = Date.now();
|
|
1402
|
-
let error = null;
|
|
1403
|
-
let screenshotId = null;
|
|
1404
|
-
let screenshotPath = null;
|
|
1405
|
-
const info = {};
|
|
1406
|
-
info.log =
|
|
1407
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1408
|
-
info.operation = "containsPattern";
|
|
1409
|
-
info.selectors = selectors;
|
|
1410
|
-
info.value = text;
|
|
1411
|
-
info.pattern = pattern;
|
|
1412
1625
|
let foundObj = null;
|
|
1413
1626
|
try {
|
|
1414
|
-
|
|
1627
|
+
await _preCommand(state, this);
|
|
1628
|
+
state.info.pattern = pattern;
|
|
1629
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1415
1630
|
if (foundObj && foundObj.element) {
|
|
1416
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1631
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1417
1632
|
}
|
|
1418
|
-
|
|
1633
|
+
await _screenshot(state, this);
|
|
1419
1634
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1420
1635
|
pattern = pattern.replace("{text}", escapedText);
|
|
1421
1636
|
let regex = new RegExp(pattern, "im");
|
|
1422
|
-
if (!regex.test(foundObj
|
|
1423
|
-
info.foundText = foundObj
|
|
1637
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1638
|
+
state.info.foundText = foundObj?.text;
|
|
1424
1639
|
throw new Error("element doesn't contain text " + text);
|
|
1425
1640
|
}
|
|
1426
|
-
return info;
|
|
1641
|
+
return state.info;
|
|
1427
1642
|
}
|
|
1428
1643
|
catch (e) {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1432
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1433
|
-
info.screenshotPath = screenshotPath;
|
|
1434
|
-
Object.assign(e, { info: info });
|
|
1435
|
-
error = e;
|
|
1436
|
-
throw e;
|
|
1644
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1645
|
+
await _commandError(state, e, this);
|
|
1437
1646
|
}
|
|
1438
1647
|
finally {
|
|
1439
|
-
|
|
1440
|
-
this._reportToWorld(world, {
|
|
1441
|
-
element_name: selectors.element_name,
|
|
1442
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1443
|
-
value: pattern,
|
|
1444
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1445
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1446
|
-
result: error
|
|
1447
|
-
? {
|
|
1448
|
-
status: "FAILED",
|
|
1449
|
-
startTime,
|
|
1450
|
-
endTime,
|
|
1451
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1452
|
-
}
|
|
1453
|
-
: {
|
|
1454
|
-
status: "PASSED",
|
|
1455
|
-
startTime,
|
|
1456
|
-
endTime,
|
|
1457
|
-
},
|
|
1458
|
-
info: info,
|
|
1459
|
-
});
|
|
1648
|
+
await _commandFinally(state, this);
|
|
1460
1649
|
}
|
|
1461
1650
|
}
|
|
1462
1651
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
+
};
|
|
1465
1669
|
if (!text) {
|
|
1466
1670
|
throw new Error("text is null");
|
|
1467
1671
|
}
|
|
1468
|
-
|
|
1469
|
-
let error = null;
|
|
1470
|
-
let screenshotId = null;
|
|
1471
|
-
let screenshotPath = null;
|
|
1472
|
-
const info = {};
|
|
1473
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1474
|
-
info.operation = "containsText";
|
|
1475
|
-
info.selectors = selectors;
|
|
1672
|
+
text = unEscapeString(text);
|
|
1476
1673
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1477
1674
|
if (newValue !== text) {
|
|
1478
1675
|
this.logger.info(text + "=" + newValue);
|
|
1479
1676
|
text = newValue;
|
|
1480
1677
|
}
|
|
1481
|
-
info.value = text;
|
|
1482
1678
|
let foundObj = null;
|
|
1483
1679
|
try {
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
+
}
|
|
1496
1697
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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;
|
|
1505
1708
|
}
|
|
1506
1709
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
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
|
|
1513
1715
|
}
|
|
1514
|
-
|
|
1716
|
+
state.info.foundText = foundObj?.text;
|
|
1717
|
+
state.info.value = foundObj?.value;
|
|
1718
|
+
throw new Error("element doesn't contain text " + text);
|
|
1515
1719
|
}
|
|
1516
1720
|
catch (e) {
|
|
1517
|
-
|
|
1518
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1519
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1520
|
-
info.screenshotPath = screenshotPath;
|
|
1521
|
-
Object.assign(e, { info: info });
|
|
1522
|
-
error = e;
|
|
1721
|
+
await _commandError(state, e, this);
|
|
1523
1722
|
throw e;
|
|
1524
1723
|
}
|
|
1525
1724
|
finally {
|
|
1526
|
-
|
|
1527
|
-
this._reportToWorld(world, {
|
|
1528
|
-
element_name: selectors.element_name,
|
|
1529
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1530
|
-
text: `Verify element contains text: ${text}`,
|
|
1531
|
-
value: text,
|
|
1532
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1533
|
-
result: error
|
|
1534
|
-
? {
|
|
1535
|
-
status: "FAILED",
|
|
1536
|
-
startTime,
|
|
1537
|
-
endTime,
|
|
1538
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1539
|
-
}
|
|
1540
|
-
: {
|
|
1541
|
-
status: "PASSED",
|
|
1542
|
-
startTime,
|
|
1543
|
-
endTime,
|
|
1544
|
-
},
|
|
1545
|
-
info: info,
|
|
1546
|
-
});
|
|
1725
|
+
await _commandFinally(state, this);
|
|
1547
1726
|
}
|
|
1548
1727
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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");
|
|
1553
1751
|
}
|
|
1554
|
-
else if (this.
|
|
1555
|
-
|
|
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");
|
|
1556
1754
|
}
|
|
1557
|
-
else if (
|
|
1558
|
-
|
|
1755
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1756
|
+
text = referanceSnapshot.substring(5);
|
|
1559
1757
|
}
|
|
1560
1758
|
else {
|
|
1561
|
-
|
|
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);
|
|
1562
1798
|
}
|
|
1563
|
-
return dataFile;
|
|
1564
1799
|
}
|
|
1565
1800
|
async waitForUserInput(message, world = null) {
|
|
1566
1801
|
if (!message) {
|
|
@@ -1590,13 +1825,22 @@ class StableBrowser {
|
|
|
1590
1825
|
return;
|
|
1591
1826
|
}
|
|
1592
1827
|
// if data file exists, load it
|
|
1593
|
-
const dataFile =
|
|
1828
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1594
1829
|
let data = this.getTestData(world);
|
|
1595
1830
|
// merge the testData with the existing data
|
|
1596
1831
|
Object.assign(data, testData);
|
|
1597
1832
|
// save the data to the file
|
|
1598
1833
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1599
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
|
+
}
|
|
1600
1844
|
_getDataFilePath(fileName) {
|
|
1601
1845
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1602
1846
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1693,7 +1937,7 @@ class StableBrowser {
|
|
|
1693
1937
|
}
|
|
1694
1938
|
}
|
|
1695
1939
|
getTestData(world = null) {
|
|
1696
|
-
const dataFile =
|
|
1940
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1697
1941
|
let data = {};
|
|
1698
1942
|
if (fs.existsSync(dataFile)) {
|
|
1699
1943
|
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
@@ -1725,11 +1969,9 @@ class StableBrowser {
|
|
|
1725
1969
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1726
1970
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1727
1971
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
}
|
|
1732
|
-
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");
|
|
1733
1975
|
try {
|
|
1734
1976
|
await this.takeScreenshot(screenshotPath);
|
|
1735
1977
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1739,15 +1981,15 @@ class StableBrowser {
|
|
|
1739
1981
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1740
1982
|
// }
|
|
1741
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
|
+
}
|
|
1742
1989
|
}
|
|
1743
1990
|
catch (e) {
|
|
1744
1991
|
this.logger.info("unable to take screenshot, ignored");
|
|
1745
1992
|
}
|
|
1746
|
-
result.screenshotId = nextIndex;
|
|
1747
|
-
result.screenshotPath = screenshotPath;
|
|
1748
|
-
if (info && info.box) {
|
|
1749
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1750
|
-
}
|
|
1751
1993
|
}
|
|
1752
1994
|
else if (options && options.screenshot) {
|
|
1753
1995
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1772,7 +2014,6 @@ class StableBrowser {
|
|
|
1772
2014
|
}
|
|
1773
2015
|
async takeScreenshot(screenshotPath) {
|
|
1774
2016
|
const playContext = this.context.playContext;
|
|
1775
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1776
2017
|
// Using CDP to capture the screenshot
|
|
1777
2018
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1778
2019
|
document.body.scrollWidth,
|
|
@@ -1782,164 +2023,241 @@ class StableBrowser {
|
|
|
1782
2023
|
document.body.clientWidth,
|
|
1783
2024
|
document.documentElement.clientWidth,
|
|
1784
2025
|
])));
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
screenshotBuffer =
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
|
|
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;
|
|
1820
2074
|
}
|
|
1821
2075
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
+
};
|
|
1827
2086
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1828
|
-
const info = {};
|
|
1829
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1830
|
-
info.operation = "verify";
|
|
1831
|
-
info.selectors = selectors;
|
|
1832
2087
|
try {
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1837
|
-
await this._highlightElements(element);
|
|
1838
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1839
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1840
|
-
return info;
|
|
2088
|
+
await _preCommand(state, this);
|
|
2089
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
2090
|
+
return state.info;
|
|
1841
2091
|
}
|
|
1842
2092
|
catch (e) {
|
|
1843
|
-
|
|
1844
|
-
this.logger.error("verify failed " + info.log);
|
|
1845
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1846
|
-
info.screenshotPath = screenshotPath;
|
|
1847
|
-
Object.assign(e, { info: info });
|
|
1848
|
-
error = e;
|
|
1849
|
-
throw e;
|
|
2093
|
+
await _commandError(state, e, this);
|
|
1850
2094
|
}
|
|
1851
2095
|
finally {
|
|
1852
|
-
|
|
1853
|
-
this._reportToWorld(world, {
|
|
1854
|
-
element_name: selectors.element_name,
|
|
1855
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1856
|
-
text: "Verify element exists in page",
|
|
1857
|
-
screenshotId,
|
|
1858
|
-
result: error
|
|
1859
|
-
? {
|
|
1860
|
-
status: "FAILED",
|
|
1861
|
-
startTime,
|
|
1862
|
-
endTime,
|
|
1863
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1864
|
-
}
|
|
1865
|
-
: {
|
|
1866
|
-
status: "PASSED",
|
|
1867
|
-
startTime,
|
|
1868
|
-
endTime,
|
|
1869
|
-
},
|
|
1870
|
-
info: info,
|
|
1871
|
-
});
|
|
2096
|
+
await _commandFinally(state, this);
|
|
1872
2097
|
}
|
|
1873
2098
|
}
|
|
1874
2099
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
+
};
|
|
1880
2114
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1881
|
-
const info = {};
|
|
1882
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1883
|
-
info.operation = "extract";
|
|
1884
|
-
info.selectors = selectors;
|
|
1885
2115
|
try {
|
|
1886
|
-
|
|
1887
|
-
await this._highlightElements(element);
|
|
1888
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2116
|
+
await _preCommand(state, this);
|
|
1889
2117
|
switch (attribute) {
|
|
1890
2118
|
case "inner_text":
|
|
1891
|
-
|
|
2119
|
+
state.value = await state.element.innerText();
|
|
1892
2120
|
break;
|
|
1893
2121
|
case "href":
|
|
1894
|
-
|
|
2122
|
+
state.value = await state.element.getAttribute("href");
|
|
1895
2123
|
break;
|
|
1896
2124
|
case "value":
|
|
1897
|
-
|
|
2125
|
+
state.value = await state.element.inputValue();
|
|
2126
|
+
break;
|
|
2127
|
+
case "text":
|
|
2128
|
+
state.value = await state.element.textContent();
|
|
1898
2129
|
break;
|
|
1899
2130
|
default:
|
|
1900
|
-
|
|
2131
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1901
2132
|
break;
|
|
1902
2133
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
+
}
|
|
1906
2151
|
}
|
|
1907
|
-
|
|
1908
|
-
this.
|
|
1909
|
-
|
|
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;
|
|
1910
2157
|
}
|
|
1911
2158
|
catch (e) {
|
|
1912
|
-
|
|
1913
|
-
this.logger.error("extract failed " + info.log);
|
|
1914
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1915
|
-
info.screenshotPath = screenshotPath;
|
|
1916
|
-
Object.assign(e, { info: info });
|
|
1917
|
-
error = e;
|
|
1918
|
-
throw e;
|
|
2159
|
+
await _commandError(state, e, this);
|
|
1919
2160
|
}
|
|
1920
2161
|
finally {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
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);
|
|
1935
2232
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
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);
|
|
1943
2261
|
}
|
|
1944
2262
|
}
|
|
1945
2263
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -1960,7 +2278,7 @@ class StableBrowser {
|
|
|
1960
2278
|
if (options && options.timeout) {
|
|
1961
2279
|
timeout = options.timeout;
|
|
1962
2280
|
}
|
|
1963
|
-
const serviceUrl =
|
|
2281
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1964
2282
|
const request = {
|
|
1965
2283
|
method: "POST",
|
|
1966
2284
|
url: serviceUrl,
|
|
@@ -2016,7 +2334,8 @@ class StableBrowser {
|
|
|
2016
2334
|
catch (e) {
|
|
2017
2335
|
errorCount++;
|
|
2018
2336
|
if (errorCount > 3) {
|
|
2019
|
-
throw e;
|
|
2337
|
+
// throw e;
|
|
2338
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
2020
2339
|
}
|
|
2021
2340
|
// ignore
|
|
2022
2341
|
}
|
|
@@ -2030,27 +2349,32 @@ class StableBrowser {
|
|
|
2030
2349
|
async _highlightElements(scope, css) {
|
|
2031
2350
|
try {
|
|
2032
2351
|
if (!scope) {
|
|
2352
|
+
// console.log(`Scope is not defined`);
|
|
2033
2353
|
return;
|
|
2034
2354
|
}
|
|
2035
2355
|
if (!css) {
|
|
2036
2356
|
scope
|
|
2037
2357
|
.evaluate((node) => {
|
|
2038
2358
|
if (node && node.style) {
|
|
2039
|
-
let
|
|
2040
|
-
|
|
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}`);
|
|
2041
2364
|
if (window) {
|
|
2042
2365
|
window.addEventListener("beforeunload", function (e) {
|
|
2043
|
-
node.style.
|
|
2366
|
+
node.style.outline = originalOutline;
|
|
2044
2367
|
});
|
|
2045
2368
|
}
|
|
2046
2369
|
setTimeout(function () {
|
|
2047
|
-
node.style.
|
|
2370
|
+
node.style.outline = originalOutline;
|
|
2048
2371
|
}, 2000);
|
|
2049
2372
|
}
|
|
2050
2373
|
})
|
|
2051
2374
|
.then(() => { })
|
|
2052
2375
|
.catch((e) => {
|
|
2053
2376
|
// ignore
|
|
2377
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2054
2378
|
});
|
|
2055
2379
|
}
|
|
2056
2380
|
else {
|
|
@@ -2066,17 +2390,18 @@ class StableBrowser {
|
|
|
2066
2390
|
if (!element.style) {
|
|
2067
2391
|
return;
|
|
2068
2392
|
}
|
|
2069
|
-
|
|
2393
|
+
let originalOutline = element.style.outline;
|
|
2394
|
+
element.__previousOutline = originalOutline;
|
|
2070
2395
|
// Set the new border to be red and 2px solid
|
|
2071
|
-
element.style.
|
|
2396
|
+
element.style.outline = "2px solid red";
|
|
2072
2397
|
if (window) {
|
|
2073
2398
|
window.addEventListener("beforeunload", function (e) {
|
|
2074
|
-
element.style.
|
|
2399
|
+
element.style.outline = originalOutline;
|
|
2075
2400
|
});
|
|
2076
2401
|
}
|
|
2077
2402
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2078
2403
|
setTimeout(function () {
|
|
2079
|
-
element.style.
|
|
2404
|
+
element.style.outline = originalOutline;
|
|
2080
2405
|
}, 2000);
|
|
2081
2406
|
}
|
|
2082
2407
|
return;
|
|
@@ -2084,6 +2409,7 @@ class StableBrowser {
|
|
|
2084
2409
|
.then(() => { })
|
|
2085
2410
|
.catch((e) => {
|
|
2086
2411
|
// ignore
|
|
2412
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2087
2413
|
});
|
|
2088
2414
|
}
|
|
2089
2415
|
}
|
|
@@ -2091,173 +2417,563 @@ class StableBrowser {
|
|
|
2091
2417
|
console.debug(error);
|
|
2092
2418
|
}
|
|
2093
2419
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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);
|
|
2099
2732
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
if (newValue !== pathPart) {
|
|
2105
|
-
this.logger.info(pathPart + "=" + newValue);
|
|
2106
|
-
pathPart = newValue;
|
|
2733
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2734
|
+
if (newValue !== text) {
|
|
2735
|
+
this.logger.info(text + "=" + newValue);
|
|
2736
|
+
text = newValue;
|
|
2107
2737
|
}
|
|
2108
|
-
|
|
2738
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2739
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2109
2740
|
try {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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`);
|
|
2115
2756
|
}
|
|
2116
2757
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2117
2758
|
continue;
|
|
2118
2759
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
+
}
|
|
2121
2777
|
}
|
|
2122
2778
|
}
|
|
2123
2779
|
catch (e) {
|
|
2124
|
-
|
|
2125
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2126
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2127
|
-
info.screenshotPath = screenshotPath;
|
|
2128
|
-
Object.assign(e, { info: info });
|
|
2129
|
-
error = e;
|
|
2130
|
-
throw e;
|
|
2780
|
+
await _commandError(state, e, this);
|
|
2131
2781
|
}
|
|
2132
2782
|
finally {
|
|
2133
|
-
|
|
2134
|
-
this._reportToWorld(world, {
|
|
2135
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2136
|
-
text: "Verify page path",
|
|
2137
|
-
screenshotId,
|
|
2138
|
-
result: error
|
|
2139
|
-
? {
|
|
2140
|
-
status: "FAILED",
|
|
2141
|
-
startTime,
|
|
2142
|
-
endTime,
|
|
2143
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2144
|
-
}
|
|
2145
|
-
: {
|
|
2146
|
-
status: "PASSED",
|
|
2147
|
-
startTime,
|
|
2148
|
-
endTime,
|
|
2149
|
-
},
|
|
2150
|
-
info: info,
|
|
2151
|
-
});
|
|
2783
|
+
await _commandFinally(state, this);
|
|
2152
2784
|
}
|
|
2153
2785
|
}
|
|
2154
|
-
async
|
|
2155
|
-
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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);
|
|
2160
2805
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2161
|
-
const info = {};
|
|
2162
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2163
|
-
info.operation = "verifyTextExistInPage";
|
|
2164
2806
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2165
2807
|
if (newValue !== text) {
|
|
2166
2808
|
this.logger.info(text + "=" + newValue);
|
|
2167
2809
|
text = newValue;
|
|
2168
2810
|
}
|
|
2169
|
-
info.text = text;
|
|
2170
2811
|
let dateAlternatives = findDateAlternatives(text);
|
|
2171
2812
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2172
2813
|
try {
|
|
2814
|
+
await _preCommand(state, this);
|
|
2815
|
+
state.info.text = text;
|
|
2816
|
+
let resultWithElementsFound = {
|
|
2817
|
+
length: null, // initial cannot be 0
|
|
2818
|
+
};
|
|
2173
2819
|
while (true) {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
|
|
2180
|
-
result.frame = frames[i];
|
|
2181
|
-
results.push(result);
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
else if (numberAlternatives.number) {
|
|
2185
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2186
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
|
|
2187
|
-
result.frame = frames[i];
|
|
2188
|
-
results.push(result);
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
else {
|
|
2192
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, {});
|
|
2193
|
-
result.frame = frames[i];
|
|
2194
|
-
results.push(result);
|
|
2195
|
-
}
|
|
2820
|
+
try {
|
|
2821
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2822
|
+
}
|
|
2823
|
+
catch (error) {
|
|
2824
|
+
// ignore
|
|
2196
2825
|
}
|
|
2197
|
-
info.results = results;
|
|
2198
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2199
2826
|
if (resultWithElementsFound.length === 0) {
|
|
2200
|
-
|
|
2201
|
-
|
|
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`);
|
|
2202
2890
|
}
|
|
2203
2891
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2204
2892
|
continue;
|
|
2205
2893
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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
|
+
}
|
|
2214
2939
|
}
|
|
2215
2940
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2941
|
+
catch (error) {
|
|
2942
|
+
console.error(error);
|
|
2943
|
+
}
|
|
2218
2944
|
}
|
|
2219
2945
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2220
2946
|
}
|
|
2221
2947
|
catch (e) {
|
|
2222
|
-
|
|
2223
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2224
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2225
|
-
info.screenshotPath = screenshotPath;
|
|
2226
|
-
Object.assign(e, { info: info });
|
|
2227
|
-
error = e;
|
|
2228
|
-
throw e;
|
|
2948
|
+
await _commandError(state, e, this);
|
|
2229
2949
|
}
|
|
2230
2950
|
finally {
|
|
2231
|
-
|
|
2232
|
-
this._reportToWorld(world, {
|
|
2233
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2234
|
-
text: "Verify text exists in page",
|
|
2235
|
-
screenshotId,
|
|
2236
|
-
result: error
|
|
2237
|
-
? {
|
|
2238
|
-
status: "FAILED",
|
|
2239
|
-
startTime,
|
|
2240
|
-
endTime,
|
|
2241
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2242
|
-
}
|
|
2243
|
-
: {
|
|
2244
|
-
status: "PASSED",
|
|
2245
|
-
startTime,
|
|
2246
|
-
endTime,
|
|
2247
|
-
},
|
|
2248
|
-
info: info,
|
|
2249
|
-
});
|
|
2951
|
+
await _commandFinally(state, this);
|
|
2250
2952
|
}
|
|
2251
2953
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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
|
+
}
|
|
2259
2973
|
}
|
|
2260
|
-
|
|
2974
|
+
// state.info.results = results;
|
|
2975
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2976
|
+
return resultWithElementsFound;
|
|
2261
2977
|
}
|
|
2262
2978
|
async visualVerification(text, options = {}, world = null) {
|
|
2263
2979
|
const startTime = Date.now();
|
|
@@ -2273,14 +2989,17 @@ class StableBrowser {
|
|
|
2273
2989
|
throw new Error("TOKEN is not set");
|
|
2274
2990
|
}
|
|
2275
2991
|
try {
|
|
2276
|
-
let serviceUrl =
|
|
2992
|
+
let serviceUrl = _getServerUrl();
|
|
2277
2993
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2278
2994
|
info.screenshotPath = screenshotPath;
|
|
2279
2995
|
const screenshot = await this.takeScreenshot();
|
|
2280
|
-
|
|
2281
|
-
method: "
|
|
2996
|
+
let request = {
|
|
2997
|
+
method: "post",
|
|
2998
|
+
maxBodyLength: Infinity,
|
|
2282
2999
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2283
3000
|
headers: {
|
|
3001
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3002
|
+
"x-source": "aaa",
|
|
2284
3003
|
"Content-Type": "application/json",
|
|
2285
3004
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2286
3005
|
},
|
|
@@ -2289,7 +3008,7 @@ class StableBrowser {
|
|
|
2289
3008
|
screenshot: screenshot,
|
|
2290
3009
|
}),
|
|
2291
3010
|
};
|
|
2292
|
-
|
|
3011
|
+
const result = await axios.request(request);
|
|
2293
3012
|
if (result.data.status !== true) {
|
|
2294
3013
|
throw new Error("Visual validation failed");
|
|
2295
3014
|
}
|
|
@@ -2309,20 +3028,22 @@ class StableBrowser {
|
|
|
2309
3028
|
info.screenshotPath = screenshotPath;
|
|
2310
3029
|
Object.assign(e, { info: info });
|
|
2311
3030
|
error = e;
|
|
2312
|
-
throw e;
|
|
3031
|
+
// throw e;
|
|
3032
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2313
3033
|
}
|
|
2314
3034
|
finally {
|
|
2315
3035
|
const endTime = Date.now();
|
|
2316
|
-
|
|
3036
|
+
_reportToWorld(world, {
|
|
2317
3037
|
type: Types.VERIFY_VISUAL,
|
|
2318
3038
|
text: "Visual verification",
|
|
3039
|
+
_text: "Visual verification of " + text,
|
|
2319
3040
|
screenshotId,
|
|
2320
3041
|
result: error
|
|
2321
3042
|
? {
|
|
2322
3043
|
status: "FAILED",
|
|
2323
3044
|
startTime,
|
|
2324
3045
|
endTime,
|
|
2325
|
-
message: error
|
|
3046
|
+
message: error?.message,
|
|
2326
3047
|
}
|
|
2327
3048
|
: {
|
|
2328
3049
|
status: "PASSED",
|
|
@@ -2354,13 +3075,14 @@ class StableBrowser {
|
|
|
2354
3075
|
this.logger.info("Table data verified");
|
|
2355
3076
|
}
|
|
2356
3077
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2357
|
-
|
|
3078
|
+
_validateSelectors(selectors);
|
|
2358
3079
|
const startTime = Date.now();
|
|
2359
3080
|
let error = null;
|
|
2360
3081
|
let screenshotId = null;
|
|
2361
3082
|
let screenshotPath = null;
|
|
2362
3083
|
const info = {};
|
|
2363
3084
|
info.log = "";
|
|
3085
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2364
3086
|
info.operation = "getTableData";
|
|
2365
3087
|
info.selectors = selectors;
|
|
2366
3088
|
try {
|
|
@@ -2376,11 +3098,12 @@ class StableBrowser {
|
|
|
2376
3098
|
info.screenshotPath = screenshotPath;
|
|
2377
3099
|
Object.assign(e, { info: info });
|
|
2378
3100
|
error = e;
|
|
2379
|
-
throw e;
|
|
3101
|
+
// throw e;
|
|
3102
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2380
3103
|
}
|
|
2381
3104
|
finally {
|
|
2382
3105
|
const endTime = Date.now();
|
|
2383
|
-
|
|
3106
|
+
_reportToWorld(world, {
|
|
2384
3107
|
element_name: selectors.element_name,
|
|
2385
3108
|
type: Types.GET_TABLE_DATA,
|
|
2386
3109
|
text: "Get table data",
|
|
@@ -2390,7 +3113,7 @@ class StableBrowser {
|
|
|
2390
3113
|
status: "FAILED",
|
|
2391
3114
|
startTime,
|
|
2392
3115
|
endTime,
|
|
2393
|
-
message: error
|
|
3116
|
+
message: error?.message,
|
|
2394
3117
|
}
|
|
2395
3118
|
: {
|
|
2396
3119
|
status: "PASSED",
|
|
@@ -2402,7 +3125,7 @@ class StableBrowser {
|
|
|
2402
3125
|
}
|
|
2403
3126
|
}
|
|
2404
3127
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2405
|
-
|
|
3128
|
+
_validateSelectors(selectors);
|
|
2406
3129
|
if (!query) {
|
|
2407
3130
|
throw new Error("query is null");
|
|
2408
3131
|
}
|
|
@@ -2435,7 +3158,7 @@ class StableBrowser {
|
|
|
2435
3158
|
info.operation = "analyzeTable";
|
|
2436
3159
|
info.selectors = selectors;
|
|
2437
3160
|
info.query = query;
|
|
2438
|
-
query =
|
|
3161
|
+
query = _fixUsingParams(query, _params);
|
|
2439
3162
|
info.query_fixed = query;
|
|
2440
3163
|
info.operator = operator;
|
|
2441
3164
|
info.value = value;
|
|
@@ -2541,11 +3264,12 @@ class StableBrowser {
|
|
|
2541
3264
|
info.screenshotPath = screenshotPath;
|
|
2542
3265
|
Object.assign(e, { info: info });
|
|
2543
3266
|
error = e;
|
|
2544
|
-
throw e;
|
|
3267
|
+
// throw e;
|
|
3268
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2545
3269
|
}
|
|
2546
3270
|
finally {
|
|
2547
3271
|
const endTime = Date.now();
|
|
2548
|
-
|
|
3272
|
+
_reportToWorld(world, {
|
|
2549
3273
|
element_name: selectors.element_name,
|
|
2550
3274
|
type: Types.ANALYZE_TABLE,
|
|
2551
3275
|
text: "Analyze table",
|
|
@@ -2555,7 +3279,7 @@ class StableBrowser {
|
|
|
2555
3279
|
status: "FAILED",
|
|
2556
3280
|
startTime,
|
|
2557
3281
|
endTime,
|
|
2558
|
-
message: error
|
|
3282
|
+
message: error?.message,
|
|
2559
3283
|
}
|
|
2560
3284
|
: {
|
|
2561
3285
|
status: "PASSED",
|
|
@@ -2567,27 +3291,13 @@ class StableBrowser {
|
|
|
2567
3291
|
}
|
|
2568
3292
|
}
|
|
2569
3293
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2570
|
-
|
|
2571
|
-
return value;
|
|
2572
|
-
}
|
|
2573
|
-
// find all the accurance of {{(.*?)}} and replace with the value
|
|
2574
|
-
let regex = /{{(.*?)}}/g;
|
|
2575
|
-
let matches = value.match(regex);
|
|
2576
|
-
if (matches) {
|
|
2577
|
-
const testData = this.getTestData(world);
|
|
2578
|
-
for (let i = 0; i < matches.length; i++) {
|
|
2579
|
-
let match = matches[i];
|
|
2580
|
-
let key = match.substring(2, match.length - 2);
|
|
2581
|
-
let newValue = objectPath.get(testData, key, null);
|
|
2582
|
-
if (newValue !== null) {
|
|
2583
|
-
value = value.replace(match, newValue);
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
3294
|
+
try {
|
|
3295
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2586
3296
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
3297
|
+
catch (error) {
|
|
3298
|
+
this.logger.debug(error);
|
|
3299
|
+
throw error;
|
|
2589
3300
|
}
|
|
2590
|
-
return value;
|
|
2591
3301
|
}
|
|
2592
3302
|
_getLoadTimeout(options) {
|
|
2593
3303
|
let timeout = 15000;
|
|
@@ -2599,6 +3309,37 @@ class StableBrowser {
|
|
|
2599
3309
|
}
|
|
2600
3310
|
return timeout;
|
|
2601
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
|
+
}
|
|
2602
3343
|
async waitForPageLoad(options = {}, world = null) {
|
|
2603
3344
|
let timeout = this._getLoadTimeout(options);
|
|
2604
3345
|
const promiseArray = [];
|
|
@@ -2638,7 +3379,7 @@ class StableBrowser {
|
|
|
2638
3379
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2639
3380
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2640
3381
|
const endTime = Date.now();
|
|
2641
|
-
|
|
3382
|
+
_reportToWorld(world, {
|
|
2642
3383
|
type: Types.GET_PAGE_STATUS,
|
|
2643
3384
|
text: "Wait for page load",
|
|
2644
3385
|
screenshotId,
|
|
@@ -2647,7 +3388,7 @@ class StableBrowser {
|
|
|
2647
3388
|
status: "FAILED",
|
|
2648
3389
|
startTime,
|
|
2649
3390
|
endTime,
|
|
2650
|
-
message: error
|
|
3391
|
+
message: error?.message,
|
|
2651
3392
|
}
|
|
2652
3393
|
: {
|
|
2653
3394
|
status: "PASSED",
|
|
@@ -2658,41 +3399,123 @@ class StableBrowser {
|
|
|
2658
3399
|
}
|
|
2659
3400
|
}
|
|
2660
3401
|
async closePage(options = {}, world = null) {
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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
|
+
};
|
|
2666
3415
|
try {
|
|
3416
|
+
await _preCommand(state, this);
|
|
2667
3417
|
await this.page.close();
|
|
2668
3418
|
}
|
|
2669
3419
|
catch (e) {
|
|
2670
3420
|
console.log(".");
|
|
3421
|
+
await _commandError(state, e, this);
|
|
2671
3422
|
}
|
|
2672
3423
|
finally {
|
|
2673
|
-
await
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
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;
|
|
2686
3459
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
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);
|
|
2694
3512
|
}
|
|
2695
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
|
+
}
|
|
2696
3519
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2697
3520
|
const startTime = Date.now();
|
|
2698
3521
|
let error = null;
|
|
@@ -2710,21 +3533,23 @@ class StableBrowser {
|
|
|
2710
3533
|
}
|
|
2711
3534
|
catch (e) {
|
|
2712
3535
|
console.log(".");
|
|
3536
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2713
3537
|
}
|
|
2714
3538
|
finally {
|
|
2715
3539
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2716
3540
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2717
3541
|
const endTime = Date.now();
|
|
2718
|
-
|
|
3542
|
+
_reportToWorld(world, {
|
|
2719
3543
|
type: Types.SET_VIEWPORT,
|
|
2720
3544
|
text: "set viewport size to " + width + "x" + hight,
|
|
3545
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2721
3546
|
screenshotId,
|
|
2722
3547
|
result: error
|
|
2723
3548
|
? {
|
|
2724
3549
|
status: "FAILED",
|
|
2725
3550
|
startTime,
|
|
2726
3551
|
endTime,
|
|
2727
|
-
message: error
|
|
3552
|
+
message: error?.message,
|
|
2728
3553
|
}
|
|
2729
3554
|
: {
|
|
2730
3555
|
status: "PASSED",
|
|
@@ -2746,12 +3571,13 @@ class StableBrowser {
|
|
|
2746
3571
|
}
|
|
2747
3572
|
catch (e) {
|
|
2748
3573
|
console.log(".");
|
|
3574
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2749
3575
|
}
|
|
2750
3576
|
finally {
|
|
2751
3577
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2752
3578
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2753
3579
|
const endTime = Date.now();
|
|
2754
|
-
|
|
3580
|
+
_reportToWorld(world, {
|
|
2755
3581
|
type: Types.GET_PAGE_STATUS,
|
|
2756
3582
|
text: "page relaod",
|
|
2757
3583
|
screenshotId,
|
|
@@ -2760,7 +3586,7 @@ class StableBrowser {
|
|
|
2760
3586
|
status: "FAILED",
|
|
2761
3587
|
startTime,
|
|
2762
3588
|
endTime,
|
|
2763
|
-
message: error
|
|
3589
|
+
message: error?.message,
|
|
2764
3590
|
}
|
|
2765
3591
|
: {
|
|
2766
3592
|
status: "PASSED",
|
|
@@ -2787,11 +3613,195 @@ class StableBrowser {
|
|
|
2787
3613
|
console.log("#-#");
|
|
2788
3614
|
}
|
|
2789
3615
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
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, this.context);
|
|
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
|
+
}
|
|
2793
3804
|
}
|
|
2794
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2795
3805
|
}
|
|
2796
3806
|
}
|
|
2797
3807
|
function createTimedPromise(promise, label) {
|
|
@@ -2799,151 +3809,5 @@ function createTimedPromise(promise, label) {
|
|
|
2799
3809
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2800
3810
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2801
3811
|
}
|
|
2802
|
-
const KEYBOARD_EVENTS = [
|
|
2803
|
-
"ALT",
|
|
2804
|
-
"AltGraph",
|
|
2805
|
-
"CapsLock",
|
|
2806
|
-
"Control",
|
|
2807
|
-
"Fn",
|
|
2808
|
-
"FnLock",
|
|
2809
|
-
"Hyper",
|
|
2810
|
-
"Meta",
|
|
2811
|
-
"NumLock",
|
|
2812
|
-
"ScrollLock",
|
|
2813
|
-
"Shift",
|
|
2814
|
-
"Super",
|
|
2815
|
-
"Symbol",
|
|
2816
|
-
"SymbolLock",
|
|
2817
|
-
"Enter",
|
|
2818
|
-
"Tab",
|
|
2819
|
-
"ArrowDown",
|
|
2820
|
-
"ArrowLeft",
|
|
2821
|
-
"ArrowRight",
|
|
2822
|
-
"ArrowUp",
|
|
2823
|
-
"End",
|
|
2824
|
-
"Home",
|
|
2825
|
-
"PageDown",
|
|
2826
|
-
"PageUp",
|
|
2827
|
-
"Backspace",
|
|
2828
|
-
"Clear",
|
|
2829
|
-
"Copy",
|
|
2830
|
-
"CrSel",
|
|
2831
|
-
"Cut",
|
|
2832
|
-
"Delete",
|
|
2833
|
-
"EraseEof",
|
|
2834
|
-
"ExSel",
|
|
2835
|
-
"Insert",
|
|
2836
|
-
"Paste",
|
|
2837
|
-
"Redo",
|
|
2838
|
-
"Undo",
|
|
2839
|
-
"Accept",
|
|
2840
|
-
"Again",
|
|
2841
|
-
"Attn",
|
|
2842
|
-
"Cancel",
|
|
2843
|
-
"ContextMenu",
|
|
2844
|
-
"Escape",
|
|
2845
|
-
"Execute",
|
|
2846
|
-
"Find",
|
|
2847
|
-
"Finish",
|
|
2848
|
-
"Help",
|
|
2849
|
-
"Pause",
|
|
2850
|
-
"Play",
|
|
2851
|
-
"Props",
|
|
2852
|
-
"Select",
|
|
2853
|
-
"ZoomIn",
|
|
2854
|
-
"ZoomOut",
|
|
2855
|
-
"BrightnessDown",
|
|
2856
|
-
"BrightnessUp",
|
|
2857
|
-
"Eject",
|
|
2858
|
-
"LogOff",
|
|
2859
|
-
"Power",
|
|
2860
|
-
"PowerOff",
|
|
2861
|
-
"PrintScreen",
|
|
2862
|
-
"Hibernate",
|
|
2863
|
-
"Standby",
|
|
2864
|
-
"WakeUp",
|
|
2865
|
-
"AllCandidates",
|
|
2866
|
-
"Alphanumeric",
|
|
2867
|
-
"CodeInput",
|
|
2868
|
-
"Compose",
|
|
2869
|
-
"Convert",
|
|
2870
|
-
"Dead",
|
|
2871
|
-
"FinalMode",
|
|
2872
|
-
"GroupFirst",
|
|
2873
|
-
"GroupLast",
|
|
2874
|
-
"GroupNext",
|
|
2875
|
-
"GroupPrevious",
|
|
2876
|
-
"ModeChange",
|
|
2877
|
-
"NextCandidate",
|
|
2878
|
-
"NonConvert",
|
|
2879
|
-
"PreviousCandidate",
|
|
2880
|
-
"Process",
|
|
2881
|
-
"SingleCandidate",
|
|
2882
|
-
"HangulMode",
|
|
2883
|
-
"HanjaMode",
|
|
2884
|
-
"JunjaMode",
|
|
2885
|
-
"Eisu",
|
|
2886
|
-
"Hankaku",
|
|
2887
|
-
"Hiragana",
|
|
2888
|
-
"HiraganaKatakana",
|
|
2889
|
-
"KanaMode",
|
|
2890
|
-
"KanjiMode",
|
|
2891
|
-
"Katakana",
|
|
2892
|
-
"Romaji",
|
|
2893
|
-
"Zenkaku",
|
|
2894
|
-
"ZenkakuHanaku",
|
|
2895
|
-
"F1",
|
|
2896
|
-
"F2",
|
|
2897
|
-
"F3",
|
|
2898
|
-
"F4",
|
|
2899
|
-
"F5",
|
|
2900
|
-
"F6",
|
|
2901
|
-
"F7",
|
|
2902
|
-
"F8",
|
|
2903
|
-
"F9",
|
|
2904
|
-
"F10",
|
|
2905
|
-
"F11",
|
|
2906
|
-
"F12",
|
|
2907
|
-
"Soft1",
|
|
2908
|
-
"Soft2",
|
|
2909
|
-
"Soft3",
|
|
2910
|
-
"Soft4",
|
|
2911
|
-
"ChannelDown",
|
|
2912
|
-
"ChannelUp",
|
|
2913
|
-
"Close",
|
|
2914
|
-
"MailForward",
|
|
2915
|
-
"MailReply",
|
|
2916
|
-
"MailSend",
|
|
2917
|
-
"MediaFastForward",
|
|
2918
|
-
"MediaPause",
|
|
2919
|
-
"MediaPlay",
|
|
2920
|
-
"MediaPlayPause",
|
|
2921
|
-
"MediaRecord",
|
|
2922
|
-
"MediaRewind",
|
|
2923
|
-
"MediaStop",
|
|
2924
|
-
"MediaTrackNext",
|
|
2925
|
-
"MediaTrackPrevious",
|
|
2926
|
-
"AudioBalanceLeft",
|
|
2927
|
-
"AudioBalanceRight",
|
|
2928
|
-
"AudioBassBoostDown",
|
|
2929
|
-
"AudioBassBoostToggle",
|
|
2930
|
-
"AudioBassBoostUp",
|
|
2931
|
-
"AudioFaderFront",
|
|
2932
|
-
"AudioFaderRear",
|
|
2933
|
-
"AudioSurroundModeNext",
|
|
2934
|
-
"AudioTrebleDown",
|
|
2935
|
-
"AudioTrebleUp",
|
|
2936
|
-
"AudioVolumeDown",
|
|
2937
|
-
"AudioVolumeMute",
|
|
2938
|
-
"AudioVolumeUp",
|
|
2939
|
-
"MicrophoneToggle",
|
|
2940
|
-
"MicrophoneVolumeDown",
|
|
2941
|
-
"MicrophoneVolumeMute",
|
|
2942
|
-
"MicrophoneVolumeUp",
|
|
2943
|
-
"TV",
|
|
2944
|
-
"TV3DMode",
|
|
2945
|
-
"TVAntennaCable",
|
|
2946
|
-
"TVAudioDescription",
|
|
2947
|
-
];
|
|
2948
3812
|
export { StableBrowser };
|
|
2949
3813
|
//# sourceMappingURL=stable_browser.js.map
|