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