automation_model 1.0.511-dev → 1.0.511
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 +2 -2
- package/lib/api.js +162 -121
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +7 -2
- package/lib/auto_page.js +308 -20
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +4 -2
- package/lib/browser_manager.js +229 -58
- 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/check_performance.d.ts +1 -0
- package/lib/check_performance.js +57 -0
- package/lib/check_performance.js.map +1 -0
- package/lib/command_common.d.ts +5 -4
- package/lib/command_common.js +104 -20
- package/lib/command_common.js.map +1 -1
- package/lib/constants.d.ts +4 -0
- package/lib/constants.js +2 -0
- package/lib/constants.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 +1 -0
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.js +26 -2
- package/lib/error-messages.js.map +1 -1
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +172 -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 +4 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +4 -3
- package/lib/init_browser.js +173 -98
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js +16 -14
- package/lib/locate_element.js.map +1 -1
- 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 -1
- package/lib/network.js +418 -68
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +83 -0
- package/lib/route.js +695 -0
- package/lib/route.js.map +1 -0
- package/lib/scripts/axe.mini.js +23989 -0
- package/lib/snapshot_validation.d.ts +37 -0
- package/lib/snapshot_validation.js +360 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +141 -41
- package/lib/stable_browser.js +2444 -666
- 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 +130 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +6 -0
- package/lib/test_context.js +5 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +38 -3
- package/lib/utils.js +777 -35
- package/lib/utils.js.map +1 -1
- package/package.json +32 -13
- package/lib/axe/axe.mini.js +0 -12
package/lib/stable_browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
+
import { check_performance } from "./check_performance.js";
|
|
2
3
|
import { expect } from "@playwright/test";
|
|
3
4
|
import dayjs from "dayjs";
|
|
4
5
|
import fs from "fs";
|
|
@@ -10,60 +11,103 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
11
|
import drawRectangle from "./drawRect.js";
|
|
11
12
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
13
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
+
import errorStackParser from "error-stack-parser";
|
|
15
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
14
16
|
import csv from "csv-parser";
|
|
15
17
|
import { Readable } from "node:stream";
|
|
16
18
|
import readline from "readline";
|
|
17
|
-
import { getContext } from "./init_browser.js";
|
|
19
|
+
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
20
|
+
import { getTestData } from "./auto_page.js";
|
|
18
21
|
import { locate_element } from "./locate_element.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
+
import { randomUUID } from "crypto";
|
|
23
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
24
|
+
import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
25
|
+
import { LocatorLog } from "./locator_log.js";
|
|
26
|
+
import axios from "axios";
|
|
27
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
28
|
+
import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
29
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
30
|
+
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
32
|
+
import _ from "lodash";
|
|
33
|
+
export const Types = {
|
|
22
34
|
CLICK: "click_element",
|
|
35
|
+
WAIT_ELEMENT: "wait_element",
|
|
23
36
|
NAVIGATE: "navigate",
|
|
37
|
+
GO_BACK: "go_back",
|
|
38
|
+
GO_FORWARD: "go_forward",
|
|
24
39
|
FILL: "fill_element",
|
|
25
|
-
EXECUTE: "execute_page_method",
|
|
26
|
-
OPEN: "open_environment",
|
|
40
|
+
EXECUTE: "execute_page_method", //
|
|
41
|
+
OPEN: "open_environment", //
|
|
27
42
|
COMPLETE: "step_complete",
|
|
28
43
|
ASK: "information_needed",
|
|
29
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
30
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
44
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
45
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
31
46
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
47
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
48
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
32
49
|
ANALYZE_TABLE: "analyze_table",
|
|
33
|
-
SELECT: "select_combobox",
|
|
50
|
+
SELECT: "select_combobox", //
|
|
51
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
34
52
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
53
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
35
54
|
TYPE_PRESS: "type_press",
|
|
36
55
|
PRESS: "press_key",
|
|
37
56
|
HOVER: "hover_element",
|
|
38
57
|
CHECK: "check_element",
|
|
39
58
|
UNCHECK: "uncheck_element",
|
|
40
59
|
EXTRACT: "extract_attribute",
|
|
60
|
+
EXTRACT_PROPERTY: "extract_property",
|
|
41
61
|
CLOSE_PAGE: "close_page",
|
|
62
|
+
TABLE_OPERATION: "table_operation",
|
|
42
63
|
SET_DATE_TIME: "set_date_time",
|
|
43
64
|
SET_VIEWPORT: "set_viewport",
|
|
44
65
|
VERIFY_VISUAL: "verify_visual",
|
|
45
66
|
LOAD_DATA: "load_data",
|
|
46
67
|
SET_INPUT: "set_input",
|
|
47
68
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
69
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
70
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
71
|
+
BRUNO: "bruno",
|
|
72
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
73
|
+
SET_INPUT_FILES: "set_input_files",
|
|
74
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
75
|
+
REPORT_COMMAND: "report_command",
|
|
76
|
+
STEP_COMPLETE: "step_complete",
|
|
77
|
+
SLEEP: "sleep",
|
|
78
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
48
79
|
};
|
|
49
80
|
export const apps = {};
|
|
81
|
+
const formatElementName = (elementName) => {
|
|
82
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
83
|
+
};
|
|
50
84
|
class StableBrowser {
|
|
51
85
|
browser;
|
|
52
86
|
page;
|
|
53
87
|
logger;
|
|
54
88
|
context;
|
|
55
89
|
world;
|
|
90
|
+
fastMode;
|
|
91
|
+
stepTags;
|
|
56
92
|
project_path = null;
|
|
57
93
|
webLogFile = null;
|
|
58
94
|
networkLogger = null;
|
|
59
95
|
configuration = null;
|
|
60
96
|
appName = "main";
|
|
61
|
-
|
|
97
|
+
tags = null;
|
|
98
|
+
isRecording = false;
|
|
99
|
+
initSnapshotTaken = false;
|
|
100
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
101
|
+
// set to true if the step issue a report
|
|
102
|
+
inStepReport = false;
|
|
103
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
62
104
|
this.browser = browser;
|
|
63
105
|
this.page = page;
|
|
64
106
|
this.logger = logger;
|
|
65
107
|
this.context = context;
|
|
66
108
|
this.world = world;
|
|
109
|
+
this.fastMode = fastMode;
|
|
110
|
+
this.stepTags = stepTags;
|
|
67
111
|
if (!this.logger) {
|
|
68
112
|
this.logger = console;
|
|
69
113
|
}
|
|
@@ -92,16 +136,32 @@ class StableBrowser {
|
|
|
92
136
|
context.pages = [this.page];
|
|
93
137
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
94
138
|
this.world = world;
|
|
139
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
140
|
+
this.fastMode = true;
|
|
141
|
+
}
|
|
142
|
+
if (process.env.FAST_MODE === "true") {
|
|
143
|
+
// console.log("Fast mode enabled from environment variable");
|
|
144
|
+
this.fastMode = true;
|
|
145
|
+
}
|
|
146
|
+
if (process.env.FAST_MODE === "false") {
|
|
147
|
+
this.fastMode = false;
|
|
148
|
+
}
|
|
149
|
+
if (this.context) {
|
|
150
|
+
this.context.fastMode = this.fastMode;
|
|
151
|
+
}
|
|
95
152
|
this.registerEventListeners(this.context);
|
|
96
153
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
97
154
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
98
155
|
}
|
|
99
156
|
registerEventListeners(context) {
|
|
100
157
|
this.registerConsoleLogListener(this.page, context);
|
|
101
|
-
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
158
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
102
159
|
if (!context.pageLoading) {
|
|
103
160
|
context.pageLoading = { status: false };
|
|
104
161
|
}
|
|
162
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
163
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
164
|
+
}
|
|
105
165
|
context.playContext.on("page", async function (page) {
|
|
106
166
|
if (this.configuration && this.configuration.closePopups === true) {
|
|
107
167
|
console.log("close unexpected popups");
|
|
@@ -110,11 +170,20 @@ class StableBrowser {
|
|
|
110
170
|
}
|
|
111
171
|
context.pageLoading.status = true;
|
|
112
172
|
this.page = page;
|
|
173
|
+
try {
|
|
174
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
175
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error("Error on dialog accept registration", error);
|
|
180
|
+
}
|
|
113
181
|
context.page = page;
|
|
114
182
|
context.pages.push(page);
|
|
115
183
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
116
184
|
registerDownloadEvent(this.page, this.world, context);
|
|
117
185
|
page.on("close", async () => {
|
|
186
|
+
// return if browser context is already closed
|
|
118
187
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
119
188
|
this.context.pages.pop();
|
|
120
189
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -124,7 +193,12 @@ class StableBrowser {
|
|
|
124
193
|
console.log("Switched to page " + title);
|
|
125
194
|
}
|
|
126
195
|
catch (error) {
|
|
127
|
-
|
|
196
|
+
if (error?.message?.includes("Target page, context or browser has been closed")) {
|
|
197
|
+
// Ignore this error
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.error("Error on page close", error);
|
|
201
|
+
}
|
|
128
202
|
}
|
|
129
203
|
}
|
|
130
204
|
});
|
|
@@ -133,7 +207,12 @@ class StableBrowser {
|
|
|
133
207
|
console.log("Switch page: " + (await page.title()));
|
|
134
208
|
}
|
|
135
209
|
catch (e) {
|
|
136
|
-
|
|
210
|
+
if (e?.message?.includes("Target page, context or browser has been closed")) {
|
|
211
|
+
// Ignore this error
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
this.logger.error("error on page load " + e);
|
|
215
|
+
}
|
|
137
216
|
}
|
|
138
217
|
context.pageLoading.status = false;
|
|
139
218
|
}.bind(this));
|
|
@@ -145,7 +224,7 @@ class StableBrowser {
|
|
|
145
224
|
}
|
|
146
225
|
let newContextCreated = false;
|
|
147
226
|
if (!apps[appName]) {
|
|
148
|
-
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
227
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder, null, null, this.tags);
|
|
149
228
|
newContextCreated = true;
|
|
150
229
|
apps[appName] = {
|
|
151
230
|
context: newContext,
|
|
@@ -154,31 +233,41 @@ class StableBrowser {
|
|
|
154
233
|
};
|
|
155
234
|
}
|
|
156
235
|
const tempContext = {};
|
|
157
|
-
|
|
158
|
-
|
|
236
|
+
_copyContext(this, tempContext);
|
|
237
|
+
_copyContext(apps[appName], this);
|
|
159
238
|
apps[this.appName] = tempContext;
|
|
160
239
|
this.appName = appName;
|
|
161
240
|
if (newContextCreated) {
|
|
162
241
|
this.registerEventListeners(this.context);
|
|
163
242
|
await this.goto(this.context.environment.baseUrl);
|
|
164
|
-
|
|
243
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
244
|
+
await this.waitForPageLoad();
|
|
245
|
+
}
|
|
165
246
|
}
|
|
166
247
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
248
|
+
async switchTab(tabTitleOrIndex) {
|
|
249
|
+
// first check if the tabNameOrIndex is a number
|
|
250
|
+
let index = parseInt(tabTitleOrIndex);
|
|
251
|
+
if (!isNaN(index)) {
|
|
252
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
253
|
+
this.page = this.context.pages[index];
|
|
254
|
+
this.context.page = this.page;
|
|
255
|
+
await this.page.bringToFront();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
175
258
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
259
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
260
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
261
|
+
let page = this.context.pages[i];
|
|
262
|
+
let title = await page.title();
|
|
263
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
264
|
+
this.page = page;
|
|
265
|
+
this.context.page = this.page;
|
|
266
|
+
await this.page.bringToFront();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
179
269
|
}
|
|
180
|
-
|
|
181
|
-
return path.join(logFolder, fileName);
|
|
270
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
182
271
|
}
|
|
183
272
|
registerConsoleLogListener(page, context) {
|
|
184
273
|
if (!this.context.webLogger) {
|
|
@@ -229,7 +318,7 @@ class StableBrowser {
|
|
|
229
318
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
230
319
|
}
|
|
231
320
|
catch (error) {
|
|
232
|
-
console.error("Error in request listener", error);
|
|
321
|
+
// console.error("Error in request listener", error);
|
|
233
322
|
context.networkLogger.push({
|
|
234
323
|
error: "not able to listen",
|
|
235
324
|
message: error.message,
|
|
@@ -243,55 +332,117 @@ class StableBrowser {
|
|
|
243
332
|
// async closeUnexpectedPopups() {
|
|
244
333
|
// await closeUnexpectedPopups(this.page);
|
|
245
334
|
// }
|
|
246
|
-
async goto(url) {
|
|
335
|
+
async goto(url, world = null, options = {}) {
|
|
336
|
+
if (!url) {
|
|
337
|
+
throw new Error("url is null, verify that the environment file is correct");
|
|
338
|
+
}
|
|
339
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
247
340
|
if (!url.startsWith("http")) {
|
|
248
341
|
url = "https://" + url;
|
|
249
342
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
343
|
+
const state = {
|
|
344
|
+
value: url,
|
|
345
|
+
world: world,
|
|
346
|
+
type: Types.NAVIGATE,
|
|
347
|
+
text: `Navigate Page to: ${url}`,
|
|
348
|
+
operation: "goto",
|
|
349
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
350
|
+
info: {},
|
|
351
|
+
locate: false,
|
|
352
|
+
scroll: false,
|
|
353
|
+
screenshot: false,
|
|
354
|
+
highlight: false,
|
|
355
|
+
};
|
|
356
|
+
let timeout = 60000;
|
|
357
|
+
if (this.configuration && this.configuration.page_timeout) {
|
|
358
|
+
timeout = this.configuration.page_timeout;
|
|
257
359
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
360
|
+
if (options && options["timeout"]) {
|
|
361
|
+
timeout = options["timeout"];
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
await _preCommand(state, this);
|
|
365
|
+
await this.page.goto(url, {
|
|
366
|
+
timeout: timeout,
|
|
367
|
+
});
|
|
368
|
+
await _screenshot(state, this);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
console.error("Error on goto", error);
|
|
372
|
+
_commandError(state, error, this);
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
await _commandFinally(state, this);
|
|
265
376
|
}
|
|
266
|
-
return text;
|
|
267
377
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
378
|
+
async goBack(options, world = null) {
|
|
379
|
+
const state = {
|
|
380
|
+
value: "",
|
|
381
|
+
world: world,
|
|
382
|
+
type: Types.GO_BACK,
|
|
383
|
+
text: `Browser navigate back`,
|
|
384
|
+
operation: "goBack",
|
|
385
|
+
log: "***** navigate back *****\n",
|
|
386
|
+
info: {},
|
|
387
|
+
locate: false,
|
|
388
|
+
scroll: false,
|
|
389
|
+
screenshot: false,
|
|
390
|
+
highlight: false,
|
|
391
|
+
};
|
|
392
|
+
try {
|
|
393
|
+
await _preCommand(state, this);
|
|
394
|
+
await this.page.goBack({
|
|
395
|
+
waitUntil: "load",
|
|
396
|
+
});
|
|
397
|
+
await _screenshot(state, this);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
console.error("Error on goBack", error);
|
|
401
|
+
_commandError(state, error, this);
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
await _commandFinally(state, this);
|
|
272
405
|
}
|
|
273
|
-
// clone the locator
|
|
274
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
275
|
-
this.scanAndManipulate(locator, _params);
|
|
276
|
-
return locator;
|
|
277
406
|
}
|
|
278
|
-
|
|
279
|
-
|
|
407
|
+
async goForward(options, world = null) {
|
|
408
|
+
const state = {
|
|
409
|
+
value: "",
|
|
410
|
+
world: world,
|
|
411
|
+
type: Types.GO_FORWARD,
|
|
412
|
+
text: `Browser navigate forward`,
|
|
413
|
+
operation: "goForward",
|
|
414
|
+
log: "***** navigate forward *****\n",
|
|
415
|
+
info: {},
|
|
416
|
+
locate: false,
|
|
417
|
+
scroll: false,
|
|
418
|
+
screenshot: false,
|
|
419
|
+
highlight: false,
|
|
420
|
+
};
|
|
421
|
+
try {
|
|
422
|
+
await _preCommand(state, this);
|
|
423
|
+
await this.page.goForward({
|
|
424
|
+
waitUntil: "load",
|
|
425
|
+
});
|
|
426
|
+
await _screenshot(state, this);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
console.error("Error on goForward", error);
|
|
430
|
+
_commandError(state, error, this);
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
await _commandFinally(state, this);
|
|
434
|
+
}
|
|
280
435
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
436
|
+
async _getLocator(locator, scope, _params) {
|
|
437
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
438
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
439
|
+
for (let key in locator) {
|
|
440
|
+
if (typeof locator[key] !== "string")
|
|
441
|
+
continue;
|
|
442
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
443
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
290
444
|
}
|
|
291
445
|
}
|
|
292
|
-
}
|
|
293
|
-
_getLocator(locator, scope, _params) {
|
|
294
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
295
446
|
let locatorReturn;
|
|
296
447
|
if (locator.role) {
|
|
297
448
|
if (locator.role[1].nameReg) {
|
|
@@ -299,7 +450,7 @@ class StableBrowser {
|
|
|
299
450
|
delete locator.role[1].nameReg;
|
|
300
451
|
}
|
|
301
452
|
// if (locator.role[1].name) {
|
|
302
|
-
// locator.role[1].name =
|
|
453
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
303
454
|
// }
|
|
304
455
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
305
456
|
}
|
|
@@ -342,174 +493,100 @@ class StableBrowser {
|
|
|
342
493
|
if (css && css.locator) {
|
|
343
494
|
css = css.locator;
|
|
344
495
|
}
|
|
345
|
-
let result = await this._locateElementByText(scope,
|
|
496
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
346
497
|
if (result.elementCount === 0) {
|
|
347
498
|
return;
|
|
348
499
|
}
|
|
349
|
-
let textElementCss = "[data-blinq-id
|
|
500
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
350
501
|
// css climb to parent element
|
|
351
502
|
const climbArray = [];
|
|
352
503
|
for (let i = 0; i < climb; i++) {
|
|
353
504
|
climbArray.push("..");
|
|
354
505
|
}
|
|
355
506
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return new RegExp(pattern, flags);
|
|
379
|
-
}
|
|
380
|
-
document.getRegex = getRegex;
|
|
381
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
382
|
-
// Check and add the element if it has a shadow root
|
|
383
|
-
if (element.shadowRoot) {
|
|
384
|
-
result.push(element);
|
|
385
|
-
// Also search within the shadow root
|
|
386
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
387
|
-
}
|
|
388
|
-
// Iterate over child nodes
|
|
389
|
-
element.childNodes.forEach((child) => {
|
|
390
|
-
// Recursively call the function for each child node
|
|
391
|
-
document.collectAllShadowDomElements(child, result);
|
|
392
|
-
});
|
|
393
|
-
return result;
|
|
394
|
-
}
|
|
395
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
396
|
-
if (!tag) {
|
|
397
|
-
tag = "*:not(script, style, head)";
|
|
398
|
-
}
|
|
399
|
-
let regexpSearch = document.getRegex(text);
|
|
400
|
-
if (regexpSearch) {
|
|
401
|
-
regex = true;
|
|
402
|
-
}
|
|
403
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
404
|
-
let shadowHosts = [];
|
|
405
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
406
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
407
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
408
|
-
if (!shadowElement) {
|
|
409
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
413
|
-
elements = elements.concat(shadowElements);
|
|
414
|
-
}
|
|
415
|
-
let randomToken = null;
|
|
416
|
-
const foundElements = [];
|
|
417
|
-
if (regex) {
|
|
418
|
-
if (!regexpSearch) {
|
|
419
|
-
regexpSearch = new RegExp(text, "im");
|
|
420
|
-
}
|
|
421
|
-
for (let i = 0; i < elements.length; i++) {
|
|
422
|
-
const element = elements[i];
|
|
423
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
424
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
425
|
-
foundElements.push(element);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
text = text.trim();
|
|
431
|
-
for (let i = 0; i < elements.length; i++) {
|
|
432
|
-
const element = elements[i];
|
|
433
|
-
if (partial) {
|
|
434
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
435
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
436
|
-
foundElements.push(element);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
441
|
-
(element.value && element.value === text)) {
|
|
442
|
-
foundElements.push(element);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
let noChildElements = [];
|
|
448
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
449
|
-
let element = foundElements[i];
|
|
450
|
-
let hasChild = false;
|
|
451
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
452
|
-
if (i === j) {
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
if (isParent(element, foundElements[j])) {
|
|
456
|
-
hasChild = true;
|
|
457
|
-
break;
|
|
507
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
508
|
+
if (css) {
|
|
509
|
+
resultCss = resultCss + " >> " + css;
|
|
510
|
+
}
|
|
511
|
+
return resultCss;
|
|
512
|
+
}
|
|
513
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
514
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
515
|
+
const locator = scope.locator(query);
|
|
516
|
+
const count = await locator.count();
|
|
517
|
+
if (!tag1) {
|
|
518
|
+
tag1 = "*";
|
|
519
|
+
}
|
|
520
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
521
|
+
let tagCount = 0;
|
|
522
|
+
for (let i = 0; i < count; i++) {
|
|
523
|
+
const element = locator.nth(i);
|
|
524
|
+
// check if the tag matches
|
|
525
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
526
|
+
if (!tag.startsWith("*")) {
|
|
527
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
528
|
+
return false;
|
|
458
529
|
}
|
|
459
530
|
}
|
|
460
|
-
if (!
|
|
461
|
-
|
|
531
|
+
if (!el.setAttribute) {
|
|
532
|
+
el = el.parentElement;
|
|
462
533
|
}
|
|
534
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
535
|
+
return true;
|
|
536
|
+
}, [tag1, randomToken]))) {
|
|
537
|
+
continue;
|
|
463
538
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (randomToken === null) {
|
|
468
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
469
|
-
}
|
|
470
|
-
let element = noChildElements[i];
|
|
471
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
472
|
-
elementCount++;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return { elementCount: elementCount, randomToken: randomToken };
|
|
476
|
-
}, [text1, tag1, regex1, partial1]);
|
|
539
|
+
tagCount++;
|
|
540
|
+
}
|
|
541
|
+
return { elementCount: tagCount, randomToken };
|
|
477
542
|
}
|
|
478
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
543
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
544
|
+
if (!info) {
|
|
545
|
+
info = {};
|
|
546
|
+
}
|
|
547
|
+
if (!info.failCause) {
|
|
548
|
+
info.failCause = {};
|
|
549
|
+
}
|
|
550
|
+
if (!info.log) {
|
|
551
|
+
info.log = "";
|
|
552
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
553
|
+
}
|
|
479
554
|
let locatorSearch = selectorHierarchy[index];
|
|
480
555
|
try {
|
|
481
|
-
locatorSearch =
|
|
556
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
482
557
|
}
|
|
483
558
|
catch (e) {
|
|
484
559
|
console.error(e);
|
|
485
560
|
}
|
|
561
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
486
562
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
487
563
|
let locator = null;
|
|
488
564
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
489
|
-
|
|
565
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
566
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
490
567
|
if (!locatorString) {
|
|
491
568
|
info.failCause.textNotFound = true;
|
|
492
|
-
info.failCause.lastError =
|
|
569
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
493
570
|
return;
|
|
494
571
|
}
|
|
495
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
572
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
496
573
|
}
|
|
497
574
|
else if (locatorSearch.text) {
|
|
498
|
-
let text =
|
|
499
|
-
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, _params);
|
|
575
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
576
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
500
577
|
if (result.elementCount === 0) {
|
|
501
578
|
info.failCause.textNotFound = true;
|
|
502
|
-
info.failCause.lastError =
|
|
579
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
503
580
|
return;
|
|
504
581
|
}
|
|
505
|
-
locatorSearch.css = "[data-blinq-id
|
|
582
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
506
583
|
if (locatorSearch.childCss) {
|
|
507
584
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
508
585
|
}
|
|
509
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
586
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
510
587
|
}
|
|
511
588
|
else {
|
|
512
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
589
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
513
590
|
}
|
|
514
591
|
// let cssHref = false;
|
|
515
592
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
@@ -522,27 +599,44 @@ class StableBrowser {
|
|
|
522
599
|
//info.log += "total elements found " + count + "\n";
|
|
523
600
|
//let visibleCount = 0;
|
|
524
601
|
let visibleLocator = null;
|
|
525
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
602
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
526
603
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
604
|
+
if (info.locatorLog) {
|
|
605
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
606
|
+
}
|
|
527
607
|
return;
|
|
528
608
|
}
|
|
609
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
610
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
611
|
+
}
|
|
529
612
|
for (let j = 0; j < count; j++) {
|
|
530
613
|
let visible = await locator.nth(j).isVisible();
|
|
531
614
|
const enabled = await locator.nth(j).isEnabled();
|
|
532
615
|
if (!visibleOnly) {
|
|
533
616
|
visible = true;
|
|
534
617
|
}
|
|
535
|
-
if (visible && enabled) {
|
|
618
|
+
if (visible && (allowDisabled || enabled)) {
|
|
536
619
|
foundLocators.push(locator.nth(j));
|
|
620
|
+
if (info.locatorLog) {
|
|
621
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
622
|
+
}
|
|
537
623
|
}
|
|
538
|
-
else {
|
|
624
|
+
else if (logErrors) {
|
|
539
625
|
info.failCause.visible = visible;
|
|
540
626
|
info.failCause.enabled = enabled;
|
|
541
627
|
if (!info.printMessages) {
|
|
542
628
|
info.printMessages = {};
|
|
543
629
|
}
|
|
630
|
+
if (info.locatorLog && !visible) {
|
|
631
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
632
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
633
|
+
}
|
|
634
|
+
if (info.locatorLog && !enabled) {
|
|
635
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
636
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
637
|
+
}
|
|
544
638
|
if (!info.printMessages[j.toString()]) {
|
|
545
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
639
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
546
640
|
info.printMessages[j.toString()] = true;
|
|
547
641
|
}
|
|
548
642
|
}
|
|
@@ -558,12 +652,12 @@ class StableBrowser {
|
|
|
558
652
|
if (!info) {
|
|
559
653
|
info = {};
|
|
560
654
|
}
|
|
561
|
-
info.log += "scan for popup handlers" + "\n";
|
|
655
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
562
656
|
const handlerGroup = [];
|
|
563
657
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
564
658
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
565
659
|
}
|
|
566
|
-
const scopes =
|
|
660
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
567
661
|
let result = null;
|
|
568
662
|
let scope = null;
|
|
569
663
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -585,28 +679,207 @@ class StableBrowser {
|
|
|
585
679
|
}
|
|
586
680
|
if (result.foundElements.length > 0) {
|
|
587
681
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
682
|
+
try {
|
|
683
|
+
await scope?.evaluate(() => {
|
|
684
|
+
window.__isClosingPopups = true;
|
|
685
|
+
});
|
|
686
|
+
await dialogCloseLocator.click();
|
|
687
|
+
// wait for the dialog to close
|
|
688
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
689
|
+
}
|
|
690
|
+
catch (e) {
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
await scope?.evaluate(() => {
|
|
694
|
+
window.__isClosingPopups = false;
|
|
695
|
+
});
|
|
696
|
+
}
|
|
591
697
|
return { rerun: true };
|
|
592
698
|
}
|
|
593
699
|
}
|
|
594
700
|
}
|
|
595
701
|
return { rerun: false };
|
|
596
702
|
}
|
|
597
|
-
|
|
703
|
+
getFilePath() {
|
|
704
|
+
const stackFrames = errorStackParser.parse(new Error());
|
|
705
|
+
const mjsFrames = stackFrames.filter((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
|
|
706
|
+
const stackFrame = mjsFrames[mjsFrames.length - 2];
|
|
707
|
+
const filepath = stackFrame?.fileName;
|
|
708
|
+
if (filepath) {
|
|
709
|
+
let jsonFilePath = filepath.replace(".mjs", ".json");
|
|
710
|
+
if (existsSync(jsonFilePath)) {
|
|
711
|
+
return jsonFilePath;
|
|
712
|
+
}
|
|
713
|
+
const config = this.configuration ?? {};
|
|
714
|
+
if (!config?.locatorsMetadataDir) {
|
|
715
|
+
config.locatorsMetadataDir = "features/step_definitions/locators";
|
|
716
|
+
}
|
|
717
|
+
if (config && config.locatorsMetadataDir) {
|
|
718
|
+
jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
|
|
719
|
+
}
|
|
720
|
+
if (existsSync(jsonFilePath)) {
|
|
721
|
+
return jsonFilePath;
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
getFullElementLocators(selectors, filePath) {
|
|
728
|
+
if (!filePath || !existsSync(filePath)) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
732
|
+
try {
|
|
733
|
+
const allElements = JSON.parse(content);
|
|
734
|
+
const element_key = selectors?.element_key;
|
|
735
|
+
if (element_key && allElements[element_key]) {
|
|
736
|
+
return allElements[element_key];
|
|
737
|
+
}
|
|
738
|
+
for (const elementKey in allElements) {
|
|
739
|
+
const element = allElements[elementKey];
|
|
740
|
+
let foundStrategy = null;
|
|
741
|
+
for (const key in element) {
|
|
742
|
+
if (key === "strategy") {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const locators = element[key];
|
|
746
|
+
if (!locators || !locators.length) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
for (const locator of locators) {
|
|
750
|
+
delete locator.score;
|
|
751
|
+
}
|
|
752
|
+
if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
|
|
753
|
+
foundStrategy = key;
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (foundStrategy) {
|
|
758
|
+
return element;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
console.error("Error parsing locators from file: " + filePath, error);
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
598
768
|
if (!timeout) {
|
|
599
769
|
timeout = 30000;
|
|
600
770
|
}
|
|
771
|
+
let element = null;
|
|
772
|
+
let allStrategyLocators = null;
|
|
773
|
+
let selectedStrategy = null;
|
|
774
|
+
if (this.tryAllStrategies) {
|
|
775
|
+
allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
|
|
776
|
+
selectedStrategy = allStrategyLocators?.strategy;
|
|
777
|
+
}
|
|
601
778
|
for (let i = 0; i < 3; i++) {
|
|
602
779
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
603
780
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
604
781
|
let selector = selectors.locators[j];
|
|
605
782
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
606
783
|
}
|
|
607
|
-
|
|
784
|
+
if (this.tryAllStrategies && selectedStrategy) {
|
|
785
|
+
const strategyLocators = allStrategyLocators[selectedStrategy];
|
|
786
|
+
let err;
|
|
787
|
+
if (strategyLocators && strategyLocators.length) {
|
|
788
|
+
try {
|
|
789
|
+
selectors.locators = strategyLocators;
|
|
790
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
791
|
+
info.selectedStrategy = selectedStrategy;
|
|
792
|
+
info.log += "element found using strategy " + selectedStrategy + "\n";
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
err = error;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (!element) {
|
|
799
|
+
for (const key in allStrategyLocators) {
|
|
800
|
+
if (key === "strategy" || key === selectedStrategy) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const strategyLocators = allStrategyLocators[key];
|
|
804
|
+
if (strategyLocators && strategyLocators.length) {
|
|
805
|
+
try {
|
|
806
|
+
info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
|
|
807
|
+
selectors.locators = strategyLocators;
|
|
808
|
+
element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
|
|
809
|
+
err = null;
|
|
810
|
+
info.selectedStrategy = key;
|
|
811
|
+
info.log += "element found using strategy " + key + "\n";
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
err = error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (err) {
|
|
821
|
+
throw err;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
826
|
+
}
|
|
608
827
|
if (!element.rerun) {
|
|
609
|
-
|
|
828
|
+
let newElementSelector = "";
|
|
829
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
|
|
830
|
+
const cssSelector = await element.evaluate((el) => {
|
|
831
|
+
function getCssSelector(el) {
|
|
832
|
+
if (!el || el.nodeType !== 1 || el === document.body)
|
|
833
|
+
return el.tagName.toLowerCase();
|
|
834
|
+
const parent = el.parentElement;
|
|
835
|
+
const tag = el.tagName.toLowerCase();
|
|
836
|
+
// Find the index of the element among its siblings of the same tag
|
|
837
|
+
let index = 1;
|
|
838
|
+
for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
839
|
+
if (sibling.tagName === el.tagName) {
|
|
840
|
+
index++;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Use nth-child if necessary (i.e., if there's more than one of the same tag)
|
|
844
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
845
|
+
const needsNthChild = siblings.length > 1;
|
|
846
|
+
const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
|
|
847
|
+
return getCssSelector(parent) + " > " + selector;
|
|
848
|
+
}
|
|
849
|
+
const cssSelector = getCssSelector(el);
|
|
850
|
+
return cssSelector;
|
|
851
|
+
});
|
|
852
|
+
newElementSelector = cssSelector;
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
856
|
+
if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
|
|
857
|
+
const dataAttribute = "data-blinq-id";
|
|
858
|
+
await element.evaluate((el, [dataAttribute, randomToken]) => {
|
|
859
|
+
el.setAttribute(dataAttribute, randomToken);
|
|
860
|
+
}, [dataAttribute, randomToken]);
|
|
861
|
+
newElementSelector = `[${dataAttribute}="${randomToken}"]`;
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
// the default case just return the located element
|
|
865
|
+
// will not work for click and type if the locator is placeholder and the placeholder change due to the click event
|
|
866
|
+
return element;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const scope = element._frame ?? element.page();
|
|
870
|
+
let prefixSelector = "";
|
|
871
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
872
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
873
|
+
if (frameSelectorIndex !== -1) {
|
|
874
|
+
// remove everything after the >> internal:control=enter-frame
|
|
875
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
876
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
877
|
+
}
|
|
878
|
+
// if (element?._frame?._selector) {
|
|
879
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
880
|
+
// }
|
|
881
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
882
|
+
return scope.locator(newSelector).first();
|
|
610
883
|
}
|
|
611
884
|
}
|
|
612
885
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -617,6 +890,7 @@ class StableBrowser {
|
|
|
617
890
|
info.failCause = {};
|
|
618
891
|
info.log = "";
|
|
619
892
|
}
|
|
893
|
+
let startTime = Date.now();
|
|
620
894
|
let scope = this.page;
|
|
621
895
|
if (selectors.frame) {
|
|
622
896
|
return selectors.frame;
|
|
@@ -626,7 +900,7 @@ class StableBrowser {
|
|
|
626
900
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
627
901
|
let frameLocator = frame.selectors[i];
|
|
628
902
|
if (frameLocator.css) {
|
|
629
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
903
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
630
904
|
if (frameLocator.index) {
|
|
631
905
|
testframescope = framescope.nth(frameLocator.index);
|
|
632
906
|
}
|
|
@@ -638,7 +912,7 @@ class StableBrowser {
|
|
|
638
912
|
break;
|
|
639
913
|
}
|
|
640
914
|
catch (error) {
|
|
641
|
-
console.error("frame not found " + frameLocator.css);
|
|
915
|
+
// console.error("frame not found " + frameLocator.css);
|
|
642
916
|
}
|
|
643
917
|
}
|
|
644
918
|
}
|
|
@@ -647,9 +921,11 @@ class StableBrowser {
|
|
|
647
921
|
}
|
|
648
922
|
return framescope;
|
|
649
923
|
};
|
|
924
|
+
let fLocator = null;
|
|
650
925
|
while (true) {
|
|
651
926
|
let frameFound = false;
|
|
652
927
|
if (selectors.nestFrmLoc) {
|
|
928
|
+
fLocator = selectors.nestFrmLoc;
|
|
653
929
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
654
930
|
frameFound = true;
|
|
655
931
|
break;
|
|
@@ -658,6 +934,7 @@ class StableBrowser {
|
|
|
658
934
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
659
935
|
let frameLocator = selectors.frameLocators[i];
|
|
660
936
|
if (frameLocator.css) {
|
|
937
|
+
fLocator = frameLocator.css;
|
|
661
938
|
scope = scope.frameLocator(frameLocator.css);
|
|
662
939
|
frameFound = true;
|
|
663
940
|
break;
|
|
@@ -665,18 +942,25 @@ class StableBrowser {
|
|
|
665
942
|
}
|
|
666
943
|
}
|
|
667
944
|
if (!frameFound && selectors.iframe_src) {
|
|
945
|
+
fLocator = selectors.iframe_src;
|
|
668
946
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
669
947
|
}
|
|
670
948
|
if (!scope) {
|
|
671
|
-
info
|
|
672
|
-
|
|
949
|
+
if (info && info.locatorLog) {
|
|
950
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
951
|
+
}
|
|
952
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
953
|
+
if (Date.now() - startTime > timeout) {
|
|
673
954
|
info.failCause.iframeNotFound = true;
|
|
674
|
-
info.failCause.lastError =
|
|
955
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
675
956
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
676
957
|
}
|
|
677
958
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
678
959
|
}
|
|
679
960
|
else {
|
|
961
|
+
if (info && info.locatorLog) {
|
|
962
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
963
|
+
}
|
|
680
964
|
break;
|
|
681
965
|
}
|
|
682
966
|
}
|
|
@@ -693,18 +977,28 @@ class StableBrowser {
|
|
|
693
977
|
return bodyContent;
|
|
694
978
|
});
|
|
695
979
|
}
|
|
696
|
-
async _locate_internal(selectors, info, _params, timeout = 30000) {
|
|
980
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
981
|
+
if (selectors.locators && Array.isArray(selectors.locators)) {
|
|
982
|
+
selectors.locators.forEach((locator) => {
|
|
983
|
+
locator.index = locator.index ?? 0;
|
|
984
|
+
locator.visible = locator.visible ?? true;
|
|
985
|
+
if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
|
|
986
|
+
locator.css = locator.css + " >> visible=true";
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
}
|
|
697
990
|
if (!info) {
|
|
698
991
|
info = {};
|
|
699
992
|
info.failCause = {};
|
|
700
993
|
info.log = "";
|
|
994
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
701
995
|
}
|
|
702
996
|
let highPriorityTimeout = 5000;
|
|
703
997
|
let visibleOnlyTimeout = 6000;
|
|
704
|
-
let startTime =
|
|
998
|
+
let startTime = Date.now();
|
|
705
999
|
let locatorsCount = 0;
|
|
1000
|
+
let lazy_scroll = false;
|
|
706
1001
|
//let arrayMode = Array.isArray(selectors);
|
|
707
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
708
1002
|
let selectorsLocators = null;
|
|
709
1003
|
selectorsLocators = selectors.locators;
|
|
710
1004
|
// group selectors by priority
|
|
@@ -732,6 +1026,7 @@ class StableBrowser {
|
|
|
732
1026
|
let highPriorityOnly = true;
|
|
733
1027
|
let visibleOnly = true;
|
|
734
1028
|
while (true) {
|
|
1029
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
735
1030
|
locatorsCount = 0;
|
|
736
1031
|
let result = [];
|
|
737
1032
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -740,18 +1035,13 @@ class StableBrowser {
|
|
|
740
1035
|
}
|
|
741
1036
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
742
1037
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
743
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
1038
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
744
1039
|
if (result.foundElements.length === 0) {
|
|
745
1040
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
746
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
1041
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
747
1042
|
}
|
|
748
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
749
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
753
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
754
|
-
}
|
|
1043
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1044
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
755
1045
|
}
|
|
756
1046
|
let foundElements = result.foundElements;
|
|
757
1047
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -791,26 +1081,43 @@ class StableBrowser {
|
|
|
791
1081
|
return maxCountElement.locator;
|
|
792
1082
|
}
|
|
793
1083
|
}
|
|
794
|
-
if (
|
|
1084
|
+
if (Date.now() - startTime > timeout) {
|
|
795
1085
|
break;
|
|
796
1086
|
}
|
|
797
|
-
if (
|
|
798
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
1087
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
1088
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
799
1089
|
highPriorityOnly = false;
|
|
1090
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
1091
|
+
lazy_scroll = true;
|
|
1092
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
1093
|
+
}
|
|
800
1094
|
}
|
|
801
|
-
if (
|
|
802
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
1095
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
1096
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
803
1097
|
visibleOnly = false;
|
|
804
1098
|
}
|
|
805
1099
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1100
|
+
// sheck of more of half of the timeout has passed
|
|
1101
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
1102
|
+
highPriorityOnly = false;
|
|
1103
|
+
visibleOnly = false;
|
|
1104
|
+
}
|
|
806
1105
|
}
|
|
807
1106
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
808
|
-
info.
|
|
1107
|
+
// if (info.locatorLog) {
|
|
1108
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
1109
|
+
// for (let line of lines) {
|
|
1110
|
+
// this.logger.debug(line);
|
|
1111
|
+
// }
|
|
1112
|
+
// }
|
|
1113
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
809
1114
|
info.failCause.locatorNotFound = true;
|
|
810
|
-
info
|
|
1115
|
+
if (!info?.failCause?.lastError) {
|
|
1116
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
1117
|
+
}
|
|
811
1118
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
812
1119
|
}
|
|
813
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1120
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
814
1121
|
let foundElements = [];
|
|
815
1122
|
const result = {
|
|
816
1123
|
foundElements: foundElements,
|
|
@@ -818,34 +1125,88 @@ class StableBrowser {
|
|
|
818
1125
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
819
1126
|
let foundLocators = [];
|
|
820
1127
|
try {
|
|
821
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1128
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
822
1129
|
}
|
|
823
1130
|
catch (e) {
|
|
824
|
-
this
|
|
825
|
-
this.logger.debug(
|
|
1131
|
+
// this call can fail it the browser is navigating
|
|
1132
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
1133
|
+
// this.logger.debug(e);
|
|
826
1134
|
foundLocators = [];
|
|
827
1135
|
try {
|
|
828
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
1136
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
829
1137
|
}
|
|
830
1138
|
catch (e) {
|
|
831
|
-
|
|
1139
|
+
if (logErrors) {
|
|
1140
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1141
|
+
}
|
|
832
1142
|
}
|
|
833
1143
|
}
|
|
834
1144
|
if (foundLocators.length === 1) {
|
|
1145
|
+
let box = null;
|
|
1146
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1147
|
+
box = await foundLocators[0].boundingBox();
|
|
1148
|
+
}
|
|
835
1149
|
result.foundElements.push({
|
|
836
1150
|
locator: foundLocators[0],
|
|
837
|
-
box:
|
|
1151
|
+
box: box,
|
|
838
1152
|
unique: true,
|
|
839
1153
|
});
|
|
840
1154
|
result.locatorIndex = i;
|
|
841
1155
|
}
|
|
842
1156
|
if (foundLocators.length > 1) {
|
|
843
|
-
|
|
1157
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
1158
|
+
const boxes = [];
|
|
1159
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
1160
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
1161
|
+
}
|
|
1162
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
1163
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
1164
|
+
if (j === k) {
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
1168
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
1169
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
1170
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
1171
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
1172
|
+
// as the element is not unique, will remove it
|
|
1173
|
+
boxes.splice(k, 1);
|
|
1174
|
+
k--;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (boxes.length === 1) {
|
|
1179
|
+
result.foundElements.push({
|
|
1180
|
+
locator: boxes[0].locator.first(),
|
|
1181
|
+
box: boxes[0].box,
|
|
1182
|
+
unique: true,
|
|
1183
|
+
});
|
|
1184
|
+
result.locatorIndex = i;
|
|
1185
|
+
}
|
|
1186
|
+
else if (logErrors) {
|
|
1187
|
+
info.failCause.foundMultiple = true;
|
|
1188
|
+
if (info.locatorLog) {
|
|
1189
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
844
1192
|
}
|
|
845
1193
|
}
|
|
846
1194
|
return result;
|
|
847
1195
|
}
|
|
848
1196
|
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
1197
|
+
const state = {
|
|
1198
|
+
locate: false,
|
|
1199
|
+
scroll: false,
|
|
1200
|
+
highlight: false,
|
|
1201
|
+
_params,
|
|
1202
|
+
options,
|
|
1203
|
+
world,
|
|
1204
|
+
type: Types.CLICK,
|
|
1205
|
+
text: "Click element",
|
|
1206
|
+
operation: "simpleClick",
|
|
1207
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
1208
|
+
};
|
|
1209
|
+
_preCommand(state, this);
|
|
849
1210
|
const startTime = Date.now();
|
|
850
1211
|
let timeout = 30000;
|
|
851
1212
|
if (options && options.timeout) {
|
|
@@ -870,13 +1231,31 @@ class StableBrowser {
|
|
|
870
1231
|
catch (e) {
|
|
871
1232
|
if (performance.now() - startTime > timeout) {
|
|
872
1233
|
// throw e;
|
|
873
|
-
|
|
1234
|
+
try {
|
|
1235
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1236
|
+
}
|
|
1237
|
+
finally {
|
|
1238
|
+
await _commandFinally(state, this);
|
|
1239
|
+
}
|
|
874
1240
|
}
|
|
875
1241
|
}
|
|
876
1242
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
877
1243
|
}
|
|
878
1244
|
}
|
|
879
1245
|
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
1246
|
+
const state = {
|
|
1247
|
+
locate: false,
|
|
1248
|
+
scroll: false,
|
|
1249
|
+
highlight: false,
|
|
1250
|
+
_params,
|
|
1251
|
+
options,
|
|
1252
|
+
world,
|
|
1253
|
+
type: Types.FILL,
|
|
1254
|
+
text: "Fill element",
|
|
1255
|
+
operation: "simpleClickType",
|
|
1256
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
1257
|
+
};
|
|
1258
|
+
_preCommand(state, this);
|
|
880
1259
|
const startTime = Date.now();
|
|
881
1260
|
let timeout = 30000;
|
|
882
1261
|
if (options && options.timeout) {
|
|
@@ -901,7 +1280,12 @@ class StableBrowser {
|
|
|
901
1280
|
catch (e) {
|
|
902
1281
|
if (performance.now() - startTime > timeout) {
|
|
903
1282
|
// throw e;
|
|
904
|
-
|
|
1283
|
+
try {
|
|
1284
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1285
|
+
}
|
|
1286
|
+
finally {
|
|
1287
|
+
await _commandFinally(state, this);
|
|
1288
|
+
}
|
|
905
1289
|
}
|
|
906
1290
|
}
|
|
907
1291
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -914,34 +1298,74 @@ class StableBrowser {
|
|
|
914
1298
|
options,
|
|
915
1299
|
world,
|
|
916
1300
|
text: "Click element",
|
|
1301
|
+
_text: "Click on " + selectors.element_name,
|
|
917
1302
|
type: Types.CLICK,
|
|
918
1303
|
operation: "click",
|
|
919
1304
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
920
1305
|
};
|
|
1306
|
+
check_performance("click_all ***", this.context, true);
|
|
1307
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
1308
|
+
if (stepFastMode) {
|
|
1309
|
+
state.onlyFailuresScreenshot = true;
|
|
1310
|
+
state.scroll = false;
|
|
1311
|
+
state.highlight = false;
|
|
1312
|
+
}
|
|
921
1313
|
try {
|
|
1314
|
+
check_performance("click_preCommand", this.context, true);
|
|
922
1315
|
await _preCommand(state, this);
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
await
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
catch (e) {
|
|
931
|
-
// await this.closeUnexpectedPopups();
|
|
932
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
933
|
-
await state.element.dispatchEvent("click");
|
|
934
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1316
|
+
check_performance("click_preCommand", this.context, false);
|
|
1317
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1318
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
1319
|
+
check_performance("click_waitForPageLoad", this.context, true);
|
|
1320
|
+
await this.waitForPageLoad({ noSleep: true });
|
|
1321
|
+
check_performance("click_waitForPageLoad", this.context, false);
|
|
935
1322
|
}
|
|
936
|
-
await this.waitForPageLoad();
|
|
937
1323
|
return state.info;
|
|
938
1324
|
}
|
|
939
1325
|
catch (e) {
|
|
940
1326
|
await _commandError(state, e, this);
|
|
941
1327
|
}
|
|
942
1328
|
finally {
|
|
943
|
-
|
|
1329
|
+
check_performance("click_commandFinally", this.context, true);
|
|
1330
|
+
await _commandFinally(state, this);
|
|
1331
|
+
check_performance("click_commandFinally", this.context, false);
|
|
1332
|
+
check_performance("click_all ***", this.context, false);
|
|
1333
|
+
if (this.context.profile) {
|
|
1334
|
+
console.log(JSON.stringify(this.context.profile, null, 2));
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1339
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1340
|
+
const state = {
|
|
1341
|
+
selectors,
|
|
1342
|
+
_params,
|
|
1343
|
+
options,
|
|
1344
|
+
world,
|
|
1345
|
+
text: "Wait for element",
|
|
1346
|
+
_text: "Wait for " + selectors.element_name,
|
|
1347
|
+
type: Types.WAIT_ELEMENT,
|
|
1348
|
+
operation: "waitForElement",
|
|
1349
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1350
|
+
};
|
|
1351
|
+
let found = false;
|
|
1352
|
+
try {
|
|
1353
|
+
await _preCommand(state, this);
|
|
1354
|
+
// if (state.options && state.options.context) {
|
|
1355
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1356
|
+
// }
|
|
1357
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1358
|
+
found = true;
|
|
1359
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1360
|
+
}
|
|
1361
|
+
catch (e) {
|
|
1362
|
+
console.error("Error on waitForElement", e);
|
|
1363
|
+
// await _commandError(state, e, this);
|
|
1364
|
+
}
|
|
1365
|
+
finally {
|
|
1366
|
+
await _commandFinally(state, this);
|
|
944
1367
|
}
|
|
1368
|
+
return found;
|
|
945
1369
|
}
|
|
946
1370
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
947
1371
|
const state = {
|
|
@@ -951,6 +1375,7 @@ class StableBrowser {
|
|
|
951
1375
|
world,
|
|
952
1376
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
953
1377
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1378
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
954
1379
|
operation: "setCheck",
|
|
955
1380
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
956
1381
|
};
|
|
@@ -960,30 +1385,53 @@ class StableBrowser {
|
|
|
960
1385
|
// let element = await this._locate(selectors, info, _params);
|
|
961
1386
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
962
1387
|
try {
|
|
963
|
-
//
|
|
964
|
-
|
|
1388
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1389
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1390
|
+
await this._highlightElements(state.element);
|
|
1391
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
965
1392
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1393
|
+
// await this._unHighlightElements(element);
|
|
1394
|
+
// }
|
|
1395
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1396
|
+
// await this._unHighlightElements(element);
|
|
966
1397
|
}
|
|
967
1398
|
catch (e) {
|
|
968
1399
|
if (e.message && e.message.includes("did not change its state")) {
|
|
969
1400
|
this.logger.info("element did not change its state, ignoring...");
|
|
970
1401
|
}
|
|
971
1402
|
else {
|
|
1403
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
972
1404
|
//await this.closeUnexpectedPopups();
|
|
973
1405
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
974
|
-
state.
|
|
975
|
-
|
|
976
|
-
|
|
1406
|
+
state.element_found = false;
|
|
1407
|
+
try {
|
|
1408
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1409
|
+
state.element_found = true;
|
|
1410
|
+
// check the check state
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
// element dismissed
|
|
1414
|
+
}
|
|
1415
|
+
if (state.element_found) {
|
|
1416
|
+
const isChecked = await state.element.isChecked();
|
|
1417
|
+
if (isChecked !== checked) {
|
|
1418
|
+
// perform click
|
|
1419
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
977
1425
|
}
|
|
978
1426
|
}
|
|
979
|
-
await this.waitForPageLoad();
|
|
1427
|
+
//await this.waitForPageLoad();
|
|
980
1428
|
return state.info;
|
|
981
1429
|
}
|
|
982
1430
|
catch (e) {
|
|
983
1431
|
await _commandError(state, e, this);
|
|
984
1432
|
}
|
|
985
1433
|
finally {
|
|
986
|
-
_commandFinally(state, this);
|
|
1434
|
+
await _commandFinally(state, this);
|
|
987
1435
|
}
|
|
988
1436
|
}
|
|
989
1437
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -994,31 +1442,22 @@ class StableBrowser {
|
|
|
994
1442
|
world,
|
|
995
1443
|
type: Types.HOVER,
|
|
996
1444
|
text: `Hover element`,
|
|
1445
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
997
1446
|
operation: "hover",
|
|
998
1447
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
999
1448
|
};
|
|
1000
1449
|
try {
|
|
1001
1450
|
await _preCommand(state, this);
|
|
1002
|
-
|
|
1003
|
-
await state.element.hover();
|
|
1004
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1005
|
-
}
|
|
1006
|
-
catch (e) {
|
|
1007
|
-
//await this.closeUnexpectedPopups();
|
|
1008
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1009
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1010
|
-
await state.element.hover({ timeout: 10000 });
|
|
1011
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1012
|
-
}
|
|
1451
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1013
1452
|
await _screenshot(state, this);
|
|
1014
|
-
await this.waitForPageLoad();
|
|
1453
|
+
//await this.waitForPageLoad();
|
|
1015
1454
|
return state.info;
|
|
1016
1455
|
}
|
|
1017
1456
|
catch (e) {
|
|
1018
1457
|
await _commandError(state, e, this);
|
|
1019
1458
|
}
|
|
1020
1459
|
finally {
|
|
1021
|
-
_commandFinally(state, this);
|
|
1460
|
+
await _commandFinally(state, this);
|
|
1022
1461
|
}
|
|
1023
1462
|
}
|
|
1024
1463
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1033,6 +1472,7 @@ class StableBrowser {
|
|
|
1033
1472
|
value: values.toString(),
|
|
1034
1473
|
type: Types.SELECT,
|
|
1035
1474
|
text: `Select option: ${values}`,
|
|
1475
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1036
1476
|
operation: "selectOption",
|
|
1037
1477
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1038
1478
|
};
|
|
@@ -1046,14 +1486,14 @@ class StableBrowser {
|
|
|
1046
1486
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1047
1487
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1048
1488
|
}
|
|
1049
|
-
await this.waitForPageLoad();
|
|
1489
|
+
//await this.waitForPageLoad();
|
|
1050
1490
|
return state.info;
|
|
1051
1491
|
}
|
|
1052
1492
|
catch (e) {
|
|
1053
1493
|
await _commandError(state, e, this);
|
|
1054
1494
|
}
|
|
1055
1495
|
finally {
|
|
1056
|
-
_commandFinally(state, this);
|
|
1496
|
+
await _commandFinally(state, this);
|
|
1057
1497
|
}
|
|
1058
1498
|
}
|
|
1059
1499
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1067,6 +1507,7 @@ class StableBrowser {
|
|
|
1067
1507
|
highlight: false,
|
|
1068
1508
|
type: Types.TYPE_PRESS,
|
|
1069
1509
|
text: `Type value: ${_value}`,
|
|
1510
|
+
_text: `Type value: ${_value}`,
|
|
1070
1511
|
operation: "type",
|
|
1071
1512
|
log: "",
|
|
1072
1513
|
};
|
|
@@ -1098,7 +1539,7 @@ class StableBrowser {
|
|
|
1098
1539
|
await _commandError(state, e, this);
|
|
1099
1540
|
}
|
|
1100
1541
|
finally {
|
|
1101
|
-
_commandFinally(state, this);
|
|
1542
|
+
await _commandFinally(state, this);
|
|
1102
1543
|
}
|
|
1103
1544
|
}
|
|
1104
1545
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1134,7 +1575,7 @@ class StableBrowser {
|
|
|
1134
1575
|
await _commandError(state, e, this);
|
|
1135
1576
|
}
|
|
1136
1577
|
finally {
|
|
1137
|
-
_commandFinally(state, this);
|
|
1578
|
+
await _commandFinally(state, this);
|
|
1138
1579
|
}
|
|
1139
1580
|
}
|
|
1140
1581
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1146,6 +1587,7 @@ class StableBrowser {
|
|
|
1146
1587
|
world,
|
|
1147
1588
|
type: Types.SET_DATE_TIME,
|
|
1148
1589
|
text: `Set date time value: ${value}`,
|
|
1590
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1149
1591
|
operation: "setDateTime",
|
|
1150
1592
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1151
1593
|
throwError: false,
|
|
@@ -1153,7 +1595,7 @@ class StableBrowser {
|
|
|
1153
1595
|
try {
|
|
1154
1596
|
await _preCommand(state, this);
|
|
1155
1597
|
try {
|
|
1156
|
-
await state.element
|
|
1598
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1157
1599
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1158
1600
|
if (format) {
|
|
1159
1601
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1202,7 +1644,7 @@ class StableBrowser {
|
|
|
1202
1644
|
await _commandError(state, e, this);
|
|
1203
1645
|
}
|
|
1204
1646
|
finally {
|
|
1205
|
-
_commandFinally(state, this);
|
|
1647
|
+
await _commandFinally(state, this);
|
|
1206
1648
|
}
|
|
1207
1649
|
}
|
|
1208
1650
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1217,17 +1659,29 @@ class StableBrowser {
|
|
|
1217
1659
|
world,
|
|
1218
1660
|
type: Types.FILL,
|
|
1219
1661
|
text: `Click type input with value: ${_value}`,
|
|
1662
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1220
1663
|
operation: "clickType",
|
|
1221
1664
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1222
1665
|
};
|
|
1666
|
+
if (!options) {
|
|
1667
|
+
options = {};
|
|
1668
|
+
}
|
|
1223
1669
|
if (newValue !== _value) {
|
|
1224
1670
|
//this.logger.info(_value + "=" + newValue);
|
|
1225
1671
|
_value = newValue;
|
|
1226
1672
|
}
|
|
1227
1673
|
try {
|
|
1228
1674
|
await _preCommand(state, this);
|
|
1675
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1676
|
+
// tag the element
|
|
1677
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1678
|
+
// use attribute and not id
|
|
1679
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1680
|
+
el.setAttribute(attrName, "");
|
|
1681
|
+
return `[${attrName}]`;
|
|
1682
|
+
}, randomToken);
|
|
1229
1683
|
state.info.value = _value;
|
|
1230
|
-
if (
|
|
1684
|
+
if (!options.press) {
|
|
1231
1685
|
try {
|
|
1232
1686
|
let currentValue = await state.element.inputValue();
|
|
1233
1687
|
if (currentValue) {
|
|
@@ -1238,23 +1692,38 @@ class StableBrowser {
|
|
|
1238
1692
|
this.logger.info("unable to clear input value");
|
|
1239
1693
|
}
|
|
1240
1694
|
}
|
|
1241
|
-
if (options
|
|
1695
|
+
if (options.press) {
|
|
1696
|
+
options.timeout = 5000;
|
|
1697
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1698
|
+
}
|
|
1699
|
+
else {
|
|
1242
1700
|
try {
|
|
1243
|
-
await state.element.
|
|
1244
|
-
}
|
|
1245
|
-
catch (e) {
|
|
1246
|
-
await state.element.dispatchEvent("click");
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
else {
|
|
1250
|
-
try {
|
|
1251
|
-
await state.element.focus();
|
|
1701
|
+
await state.element.focus();
|
|
1252
1702
|
}
|
|
1253
1703
|
catch (e) {
|
|
1254
1704
|
await state.element.dispatchEvent("focus");
|
|
1255
1705
|
}
|
|
1256
1706
|
}
|
|
1257
1707
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1708
|
+
// check if the element exist after the click (no wait)
|
|
1709
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1710
|
+
if (count === 0) {
|
|
1711
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1712
|
+
const scope = state.element._frame ?? element.page();
|
|
1713
|
+
let prefixSelector = "";
|
|
1714
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1715
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1716
|
+
if (frameSelectorIndex !== -1) {
|
|
1717
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1718
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1719
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1720
|
+
}
|
|
1721
|
+
// if (element?._frame?._selector) {
|
|
1722
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1723
|
+
// }
|
|
1724
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1725
|
+
state.element = scope.locator(newSelector).first();
|
|
1726
|
+
}
|
|
1258
1727
|
const valueSegment = state.value.split("&&");
|
|
1259
1728
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1260
1729
|
if (i > 0) {
|
|
@@ -1275,14 +1744,21 @@ class StableBrowser {
|
|
|
1275
1744
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1276
1745
|
}
|
|
1277
1746
|
}
|
|
1747
|
+
//if (!this.fastMode) {
|
|
1278
1748
|
await _screenshot(state, this);
|
|
1749
|
+
//}
|
|
1279
1750
|
if (enter === true) {
|
|
1280
1751
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1281
1752
|
await this.page.keyboard.press("Enter");
|
|
1282
1753
|
await this.waitForPageLoad();
|
|
1283
1754
|
}
|
|
1284
1755
|
else if (enter === false) {
|
|
1285
|
-
|
|
1756
|
+
try {
|
|
1757
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1758
|
+
}
|
|
1759
|
+
catch (e) {
|
|
1760
|
+
// ignore
|
|
1761
|
+
}
|
|
1286
1762
|
//await this.page.keyboard.press("Tab");
|
|
1287
1763
|
}
|
|
1288
1764
|
else {
|
|
@@ -1297,7 +1773,7 @@ class StableBrowser {
|
|
|
1297
1773
|
await _commandError(state, e, this);
|
|
1298
1774
|
}
|
|
1299
1775
|
finally {
|
|
1300
|
-
_commandFinally(state, this);
|
|
1776
|
+
await _commandFinally(state, this);
|
|
1301
1777
|
}
|
|
1302
1778
|
}
|
|
1303
1779
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1319,30 +1795,67 @@ class StableBrowser {
|
|
|
1319
1795
|
if (enter) {
|
|
1320
1796
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1321
1797
|
await this.page.keyboard.press("Enter");
|
|
1798
|
+
await this.waitForPageLoad();
|
|
1799
|
+
}
|
|
1800
|
+
return state.info;
|
|
1801
|
+
}
|
|
1802
|
+
catch (e) {
|
|
1803
|
+
await _commandError(state, e, this);
|
|
1804
|
+
}
|
|
1805
|
+
finally {
|
|
1806
|
+
await _commandFinally(state, this);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1810
|
+
const state = {
|
|
1811
|
+
selectors,
|
|
1812
|
+
_params,
|
|
1813
|
+
files,
|
|
1814
|
+
value: '"' + files.join('", "') + '"',
|
|
1815
|
+
options,
|
|
1816
|
+
world,
|
|
1817
|
+
type: Types.SET_INPUT_FILES,
|
|
1818
|
+
text: `Set input files`,
|
|
1819
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1820
|
+
operation: "setInputFiles",
|
|
1821
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1822
|
+
};
|
|
1823
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1824
|
+
try {
|
|
1825
|
+
await _preCommand(state, this);
|
|
1826
|
+
for (let i = 0; i < files.length; i++) {
|
|
1827
|
+
const file = files[i];
|
|
1828
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1829
|
+
if (!fs.existsSync(filePath)) {
|
|
1830
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1831
|
+
}
|
|
1832
|
+
state.files[i] = filePath;
|
|
1322
1833
|
}
|
|
1323
|
-
await
|
|
1834
|
+
await state.element.setInputFiles(files);
|
|
1324
1835
|
return state.info;
|
|
1325
1836
|
}
|
|
1326
1837
|
catch (e) {
|
|
1327
1838
|
await _commandError(state, e, this);
|
|
1328
1839
|
}
|
|
1329
1840
|
finally {
|
|
1330
|
-
_commandFinally(state, this);
|
|
1841
|
+
await _commandFinally(state, this);
|
|
1331
1842
|
}
|
|
1332
1843
|
}
|
|
1333
1844
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1334
1845
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1335
1846
|
}
|
|
1336
1847
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1848
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1337
1849
|
_validateSelectors(selectors);
|
|
1338
1850
|
let screenshotId = null;
|
|
1339
1851
|
let screenshotPath = null;
|
|
1340
1852
|
if (!info.log) {
|
|
1341
1853
|
info.log = "";
|
|
1854
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1342
1855
|
}
|
|
1343
1856
|
info.operation = "getText";
|
|
1344
1857
|
info.selectors = selectors;
|
|
1345
|
-
let element = await this._locate(selectors, info, _params);
|
|
1858
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1346
1859
|
if (climb > 0) {
|
|
1347
1860
|
const climbArray = [];
|
|
1348
1861
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1361,6 +1874,18 @@ class StableBrowser {
|
|
|
1361
1874
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1362
1875
|
try {
|
|
1363
1876
|
await this._highlightElements(element);
|
|
1877
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1878
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1879
|
+
// this._highlightElements(element)
|
|
1880
|
+
// .then(async () => {
|
|
1881
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1882
|
+
// this._unhighlightElements(element).then(
|
|
1883
|
+
// () => {}
|
|
1884
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1885
|
+
// );
|
|
1886
|
+
// })
|
|
1887
|
+
// .catch(e);
|
|
1888
|
+
// }
|
|
1364
1889
|
const elementText = await element.innerText();
|
|
1365
1890
|
return {
|
|
1366
1891
|
text: elementText,
|
|
@@ -1372,7 +1897,7 @@ class StableBrowser {
|
|
|
1372
1897
|
}
|
|
1373
1898
|
catch (e) {
|
|
1374
1899
|
//await this.closeUnexpectedPopups();
|
|
1375
|
-
this.logger.info("no innerText will use textContent");
|
|
1900
|
+
this.logger.info("no innerText, will use textContent");
|
|
1376
1901
|
const elementText = await element.textContent();
|
|
1377
1902
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1378
1903
|
}
|
|
@@ -1397,6 +1922,7 @@ class StableBrowser {
|
|
|
1397
1922
|
highlight: false,
|
|
1398
1923
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1399
1924
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1925
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1400
1926
|
operation: "containsPattern",
|
|
1401
1927
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1402
1928
|
};
|
|
@@ -1428,10 +1954,12 @@ class StableBrowser {
|
|
|
1428
1954
|
await _commandError(state, e, this);
|
|
1429
1955
|
}
|
|
1430
1956
|
finally {
|
|
1431
|
-
_commandFinally(state, this);
|
|
1957
|
+
await _commandFinally(state, this);
|
|
1432
1958
|
}
|
|
1433
1959
|
}
|
|
1434
1960
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1961
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1962
|
+
const startTime = Date.now();
|
|
1435
1963
|
const state = {
|
|
1436
1964
|
selectors,
|
|
1437
1965
|
_params,
|
|
@@ -1458,61 +1986,137 @@ class StableBrowser {
|
|
|
1458
1986
|
}
|
|
1459
1987
|
let foundObj = null;
|
|
1460
1988
|
try {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1468
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1469
|
-
if (dateAlternatives.date) {
|
|
1470
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1471
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1472
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1473
|
-
return state.info;
|
|
1989
|
+
while (Date.now() - startTime < timeout) {
|
|
1990
|
+
try {
|
|
1991
|
+
await _preCommand(state, this);
|
|
1992
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1993
|
+
if (foundObj && foundObj.element) {
|
|
1994
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1474
1995
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1996
|
+
await _screenshot(state, this);
|
|
1997
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1998
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1999
|
+
if (dateAlternatives.date) {
|
|
2000
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
2001
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
2002
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
2003
|
+
return state.info;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
else if (numberAlternatives.number) {
|
|
2008
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
2009
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
2010
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
2011
|
+
return state.info;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1482
2016
|
return state.info;
|
|
1483
2017
|
}
|
|
1484
2018
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
throw new Error("element doesn't contain text " + text);
|
|
2019
|
+
catch (e) {
|
|
2020
|
+
// Log error but continue retrying until timeout is reached
|
|
2021
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
2022
|
+
}
|
|
2023
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1491
2024
|
}
|
|
1492
|
-
|
|
2025
|
+
state.info.foundText = foundObj?.text;
|
|
2026
|
+
state.info.value = foundObj?.value;
|
|
2027
|
+
throw new Error("element doesn't contain text " + text);
|
|
1493
2028
|
}
|
|
1494
2029
|
catch (e) {
|
|
1495
2030
|
await _commandError(state, e, this);
|
|
2031
|
+
throw e;
|
|
1496
2032
|
}
|
|
1497
2033
|
finally {
|
|
1498
|
-
_commandFinally(state, this);
|
|
2034
|
+
await _commandFinally(state, this);
|
|
1499
2035
|
}
|
|
1500
2036
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2037
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
2038
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2039
|
+
const startTime = Date.now();
|
|
2040
|
+
const state = {
|
|
2041
|
+
_params,
|
|
2042
|
+
value: referanceSnapshot,
|
|
2043
|
+
options,
|
|
2044
|
+
world,
|
|
2045
|
+
locate: false,
|
|
2046
|
+
scroll: false,
|
|
2047
|
+
screenshot: true,
|
|
2048
|
+
highlight: false,
|
|
2049
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
2050
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
2051
|
+
operation: "snapshotValidation",
|
|
2052
|
+
log: "***** verify snapshot *****\n",
|
|
2053
|
+
};
|
|
2054
|
+
if (!referanceSnapshot) {
|
|
2055
|
+
throw new Error("referanceSnapshot is null");
|
|
2056
|
+
}
|
|
2057
|
+
let text = null;
|
|
2058
|
+
const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
|
|
2059
|
+
if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
|
|
2060
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
|
|
1505
2061
|
}
|
|
1506
|
-
else if (
|
|
1507
|
-
|
|
2062
|
+
else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
|
|
2063
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
|
|
1508
2064
|
}
|
|
1509
|
-
else if (
|
|
1510
|
-
|
|
2065
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2066
|
+
text = referanceSnapshot.substring(5);
|
|
1511
2067
|
}
|
|
1512
2068
|
else {
|
|
1513
|
-
|
|
2069
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
2070
|
+
}
|
|
2071
|
+
state.text = text;
|
|
2072
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2073
|
+
await _preCommand(state, this);
|
|
2074
|
+
let foundObj = null;
|
|
2075
|
+
try {
|
|
2076
|
+
let matchResult = null;
|
|
2077
|
+
while (Date.now() - startTime < timeout) {
|
|
2078
|
+
try {
|
|
2079
|
+
let scope = null;
|
|
2080
|
+
if (!frameSelectors) {
|
|
2081
|
+
scope = this.page;
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
2085
|
+
}
|
|
2086
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
2087
|
+
if (snapshot && snapshot.length <= 10) {
|
|
2088
|
+
console.log("Page snapshot length is suspiciously small:", snapshot);
|
|
2089
|
+
}
|
|
2090
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
2091
|
+
if (matchResult === undefined) {
|
|
2092
|
+
console.log("snapshotValidation returned undefined");
|
|
2093
|
+
}
|
|
2094
|
+
if (matchResult.errorLine !== -1) {
|
|
2095
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
2096
|
+
}
|
|
2097
|
+
// highlight and screenshot
|
|
2098
|
+
try {
|
|
2099
|
+
await await highlightSnapshot(newValue, scope);
|
|
2100
|
+
await _screenshot(state, this);
|
|
2101
|
+
}
|
|
2102
|
+
catch (e) { }
|
|
2103
|
+
return state.info;
|
|
2104
|
+
}
|
|
2105
|
+
catch (e) {
|
|
2106
|
+
// Log error but continue retrying until timeout is reached
|
|
2107
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
2108
|
+
}
|
|
2109
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
2110
|
+
}
|
|
2111
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
2112
|
+
}
|
|
2113
|
+
catch (e) {
|
|
2114
|
+
await _commandError(state, e, this);
|
|
2115
|
+
throw e;
|
|
2116
|
+
}
|
|
2117
|
+
finally {
|
|
2118
|
+
await _commandFinally(state, this);
|
|
1514
2119
|
}
|
|
1515
|
-
return dataFile;
|
|
1516
2120
|
}
|
|
1517
2121
|
async waitForUserInput(message, world = null) {
|
|
1518
2122
|
if (!message) {
|
|
@@ -1542,13 +2146,22 @@ class StableBrowser {
|
|
|
1542
2146
|
return;
|
|
1543
2147
|
}
|
|
1544
2148
|
// if data file exists, load it
|
|
1545
|
-
const dataFile =
|
|
2149
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1546
2150
|
let data = this.getTestData(world);
|
|
1547
2151
|
// merge the testData with the existing data
|
|
1548
2152
|
Object.assign(data, testData);
|
|
1549
2153
|
// save the data to the file
|
|
1550
2154
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1551
2155
|
}
|
|
2156
|
+
overwriteTestData(testData, world = null) {
|
|
2157
|
+
if (!testData) {
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
// if data file exists, load it
|
|
2161
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2162
|
+
// save the data to the file
|
|
2163
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
2164
|
+
}
|
|
1552
2165
|
_getDataFilePath(fileName) {
|
|
1553
2166
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1554
2167
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1645,14 +2258,12 @@ class StableBrowser {
|
|
|
1645
2258
|
}
|
|
1646
2259
|
}
|
|
1647
2260
|
getTestData(world = null) {
|
|
1648
|
-
|
|
1649
|
-
let data = {};
|
|
1650
|
-
if (fs.existsSync(dataFile)) {
|
|
1651
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1652
|
-
}
|
|
1653
|
-
return data;
|
|
2261
|
+
return _getTestData(world, this.context, this);
|
|
1654
2262
|
}
|
|
1655
2263
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2264
|
+
if (!options) {
|
|
2265
|
+
options = {};
|
|
2266
|
+
}
|
|
1656
2267
|
// collect url/path/title
|
|
1657
2268
|
if (info) {
|
|
1658
2269
|
if (!info.title) {
|
|
@@ -1677,13 +2288,11 @@ class StableBrowser {
|
|
|
1677
2288
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1678
2289
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1679
2290
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
1684
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
2291
|
+
// to make sure the path doesn't start with -
|
|
2292
|
+
const uuidStr = "id_" + randomUUID();
|
|
2293
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1685
2294
|
try {
|
|
1686
|
-
await this.takeScreenshot(screenshotPath);
|
|
2295
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
1687
2296
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1688
2297
|
// // save the buffer to the screenshot path asynchrously
|
|
1689
2298
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -1691,20 +2300,20 @@ class StableBrowser {
|
|
|
1691
2300
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1692
2301
|
// }
|
|
1693
2302
|
// });
|
|
2303
|
+
result.screenshotId = uuidStr;
|
|
2304
|
+
result.screenshotPath = screenshotPath;
|
|
2305
|
+
if (info && info.box) {
|
|
2306
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
2307
|
+
}
|
|
1694
2308
|
}
|
|
1695
2309
|
catch (e) {
|
|
1696
2310
|
this.logger.info("unable to take screenshot, ignored");
|
|
1697
2311
|
}
|
|
1698
|
-
result.screenshotId = nextIndex;
|
|
1699
|
-
result.screenshotPath = screenshotPath;
|
|
1700
|
-
if (info && info.box) {
|
|
1701
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1702
|
-
}
|
|
1703
2312
|
}
|
|
1704
2313
|
else if (options && options.screenshot) {
|
|
1705
2314
|
result.screenshotPath = options.screenshotPath;
|
|
1706
2315
|
try {
|
|
1707
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2316
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
1708
2317
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1709
2318
|
// // save the buffer to the screenshot path asynchrously
|
|
1710
2319
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -1722,7 +2331,7 @@ class StableBrowser {
|
|
|
1722
2331
|
}
|
|
1723
2332
|
return result;
|
|
1724
2333
|
}
|
|
1725
|
-
async takeScreenshot(screenshotPath) {
|
|
2334
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
1726
2335
|
const playContext = this.context.playContext;
|
|
1727
2336
|
// Using CDP to capture the screenshot
|
|
1728
2337
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -1734,17 +2343,20 @@ class StableBrowser {
|
|
|
1734
2343
|
document.documentElement.clientWidth,
|
|
1735
2344
|
])));
|
|
1736
2345
|
let screenshotBuffer = null;
|
|
2346
|
+
// if (focusedElement) {
|
|
2347
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2348
|
+
// await this._unhighlightElements(focusedElement);
|
|
2349
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2350
|
+
// console.log(`Unhighlighted previous element`);
|
|
2351
|
+
// }
|
|
2352
|
+
// if (focusedElement) {
|
|
2353
|
+
// await this._highlightElements(focusedElement);
|
|
2354
|
+
// }
|
|
1737
2355
|
if (this.context.browserName === "chromium") {
|
|
1738
2356
|
const client = await playContext.newCDPSession(this.page);
|
|
1739
2357
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1740
2358
|
format: "png",
|
|
1741
|
-
|
|
1742
|
-
// x: 0,
|
|
1743
|
-
// y: 0,
|
|
1744
|
-
// width: viewportWidth,
|
|
1745
|
-
// height: viewportHeight,
|
|
1746
|
-
// scale: 1,
|
|
1747
|
-
// },
|
|
2359
|
+
captureBeyondViewport: fullPage,
|
|
1748
2360
|
});
|
|
1749
2361
|
await client.detach();
|
|
1750
2362
|
if (!screenshotPath) {
|
|
@@ -1753,8 +2365,12 @@ class StableBrowser {
|
|
|
1753
2365
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
1754
2366
|
}
|
|
1755
2367
|
else {
|
|
1756
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2368
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
1757
2369
|
}
|
|
2370
|
+
// if (focusedElement) {
|
|
2371
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2372
|
+
// await this._unhighlightElements(focusedElement);
|
|
2373
|
+
// }
|
|
1758
2374
|
let image = await Jimp.read(screenshotBuffer);
|
|
1759
2375
|
// Get the image dimensions
|
|
1760
2376
|
const { width, height } = image.bitmap;
|
|
@@ -1767,6 +2383,7 @@ class StableBrowser {
|
|
|
1767
2383
|
else {
|
|
1768
2384
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1769
2385
|
}
|
|
2386
|
+
return screenshotBuffer;
|
|
1770
2387
|
}
|
|
1771
2388
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1772
2389
|
const state = {
|
|
@@ -1789,7 +2406,7 @@ class StableBrowser {
|
|
|
1789
2406
|
await _commandError(state, e, this);
|
|
1790
2407
|
}
|
|
1791
2408
|
finally {
|
|
1792
|
-
_commandFinally(state, this);
|
|
2409
|
+
await _commandFinally(state, this);
|
|
1793
2410
|
}
|
|
1794
2411
|
}
|
|
1795
2412
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1802,8 +2419,10 @@ class StableBrowser {
|
|
|
1802
2419
|
world,
|
|
1803
2420
|
type: Types.EXTRACT,
|
|
1804
2421
|
text: `Extract attribute from element`,
|
|
2422
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1805
2423
|
operation: "extractAttribute",
|
|
1806
2424
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2425
|
+
allowDisabled: true,
|
|
1807
2426
|
};
|
|
1808
2427
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1809
2428
|
try {
|
|
@@ -1818,22 +2437,480 @@ class StableBrowser {
|
|
|
1818
2437
|
case "value":
|
|
1819
2438
|
state.value = await state.element.inputValue();
|
|
1820
2439
|
break;
|
|
2440
|
+
case "text":
|
|
2441
|
+
state.value = await state.element.textContent();
|
|
2442
|
+
break;
|
|
1821
2443
|
default:
|
|
1822
2444
|
state.value = await state.element.getAttribute(attribute);
|
|
1823
2445
|
break;
|
|
1824
2446
|
}
|
|
2447
|
+
if (options !== null) {
|
|
2448
|
+
if (options.regex && options.regex !== "") {
|
|
2449
|
+
// Construct a regex pattern from the provided string
|
|
2450
|
+
const regex = options.regex.slice(1, -1);
|
|
2451
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2452
|
+
const matches = state.value.match(regexPattern);
|
|
2453
|
+
if (matches) {
|
|
2454
|
+
let newValue = "";
|
|
2455
|
+
for (const match of matches) {
|
|
2456
|
+
newValue += match;
|
|
2457
|
+
}
|
|
2458
|
+
state.value = newValue;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2462
|
+
state.value = state.value.trim();
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
state.info.value = state.value;
|
|
2466
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2467
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2468
|
+
if (process.env.MODE === "executions") {
|
|
2469
|
+
const globalDataFile = "global_test_data.json";
|
|
2470
|
+
if (existsSync(globalDataFile)) {
|
|
2471
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2475
|
+
return state.info;
|
|
2476
|
+
}
|
|
2477
|
+
catch (e) {
|
|
2478
|
+
await _commandError(state, e, this);
|
|
2479
|
+
}
|
|
2480
|
+
finally {
|
|
2481
|
+
await _commandFinally(state, this);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2485
|
+
const state = {
|
|
2486
|
+
selectors,
|
|
2487
|
+
_params,
|
|
2488
|
+
property,
|
|
2489
|
+
variable,
|
|
2490
|
+
options,
|
|
2491
|
+
world,
|
|
2492
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2493
|
+
text: `Extract property from element`,
|
|
2494
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2495
|
+
operation: "extractProperty",
|
|
2496
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2497
|
+
allowDisabled: true,
|
|
2498
|
+
};
|
|
2499
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2500
|
+
try {
|
|
2501
|
+
await _preCommand(state, this);
|
|
2502
|
+
switch (property) {
|
|
2503
|
+
case "inner_text":
|
|
2504
|
+
state.value = await state.element.innerText();
|
|
2505
|
+
break;
|
|
2506
|
+
case "href":
|
|
2507
|
+
state.value = await state.element.getAttribute("href");
|
|
2508
|
+
break;
|
|
2509
|
+
case "value":
|
|
2510
|
+
state.value = await state.element.inputValue();
|
|
2511
|
+
break;
|
|
2512
|
+
case "text":
|
|
2513
|
+
state.value = await state.element.textContent();
|
|
2514
|
+
break;
|
|
2515
|
+
default:
|
|
2516
|
+
if (property.startsWith("dataset.")) {
|
|
2517
|
+
const dataAttribute = property.substring(8);
|
|
2518
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2519
|
+
}
|
|
2520
|
+
else {
|
|
2521
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
if (options !== null) {
|
|
2525
|
+
if (options.regex && options.regex !== "") {
|
|
2526
|
+
// Construct a regex pattern from the provided string
|
|
2527
|
+
const regex = options.regex.slice(1, -1);
|
|
2528
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2529
|
+
const matches = state.value.match(regexPattern);
|
|
2530
|
+
if (matches) {
|
|
2531
|
+
let newValue = "";
|
|
2532
|
+
for (const match of matches) {
|
|
2533
|
+
newValue += match;
|
|
2534
|
+
}
|
|
2535
|
+
state.value = newValue;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2539
|
+
state.value = state.value.trim();
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
1825
2542
|
state.info.value = state.value;
|
|
1826
2543
|
this.setTestData({ [variable]: state.value }, world);
|
|
1827
2544
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2545
|
+
if (process.env.MODE === "executions") {
|
|
2546
|
+
const globalDataFile = "global_test_data.json";
|
|
2547
|
+
if (existsSync(globalDataFile)) {
|
|
2548
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1828
2552
|
return state.info;
|
|
1829
2553
|
}
|
|
1830
2554
|
catch (e) {
|
|
1831
2555
|
await _commandError(state, e, this);
|
|
1832
2556
|
}
|
|
1833
2557
|
finally {
|
|
1834
|
-
_commandFinally(state, this);
|
|
2558
|
+
await _commandFinally(state, this);
|
|
1835
2559
|
}
|
|
1836
2560
|
}
|
|
2561
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2562
|
+
const state = {
|
|
2563
|
+
selectors,
|
|
2564
|
+
_params,
|
|
2565
|
+
attribute,
|
|
2566
|
+
value,
|
|
2567
|
+
options,
|
|
2568
|
+
world,
|
|
2569
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2570
|
+
highlight: true,
|
|
2571
|
+
screenshot: true,
|
|
2572
|
+
text: `Verify element attribute`,
|
|
2573
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2574
|
+
operation: "verifyAttribute",
|
|
2575
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2576
|
+
allowDisabled: true,
|
|
2577
|
+
};
|
|
2578
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2579
|
+
let val;
|
|
2580
|
+
let expectedValue;
|
|
2581
|
+
try {
|
|
2582
|
+
await _preCommand(state, this);
|
|
2583
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2584
|
+
state.info.expectedValue = expectedValue;
|
|
2585
|
+
switch (attribute) {
|
|
2586
|
+
case "innerText":
|
|
2587
|
+
val = String(await state.element.innerText());
|
|
2588
|
+
break;
|
|
2589
|
+
case "text":
|
|
2590
|
+
val = String(await state.element.textContent());
|
|
2591
|
+
break;
|
|
2592
|
+
case "value":
|
|
2593
|
+
val = String(await state.element.inputValue());
|
|
2594
|
+
break;
|
|
2595
|
+
case "checked":
|
|
2596
|
+
val = String(await state.element.isChecked());
|
|
2597
|
+
break;
|
|
2598
|
+
case "disabled":
|
|
2599
|
+
val = String(await state.element.isDisabled());
|
|
2600
|
+
break;
|
|
2601
|
+
case "readOnly":
|
|
2602
|
+
const isEditable = await state.element.isEditable();
|
|
2603
|
+
val = String(!isEditable);
|
|
2604
|
+
break;
|
|
2605
|
+
default:
|
|
2606
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2607
|
+
break;
|
|
2608
|
+
}
|
|
2609
|
+
state.info.value = val;
|
|
2610
|
+
let regex;
|
|
2611
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2612
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2613
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2614
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2615
|
+
state.info.regex = true;
|
|
2616
|
+
}
|
|
2617
|
+
else {
|
|
2618
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2619
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2620
|
+
}
|
|
2621
|
+
if (attribute === "innerText") {
|
|
2622
|
+
if (state.info.regex) {
|
|
2623
|
+
if (!regex.test(val)) {
|
|
2624
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2625
|
+
state.info.failCause.assertionFailed = true;
|
|
2626
|
+
state.info.failCause.lastError = errorMessage;
|
|
2627
|
+
throw new Error(errorMessage);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
else {
|
|
2631
|
+
const valLines = val.split("\n");
|
|
2632
|
+
const expectedLines = expectedValue.split("\n");
|
|
2633
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2634
|
+
if (!isPart) {
|
|
2635
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2636
|
+
state.info.failCause.assertionFailed = true;
|
|
2637
|
+
state.info.failCause.lastError = errorMessage;
|
|
2638
|
+
throw new Error(errorMessage);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
else {
|
|
2643
|
+
if (!val.match(regex)) {
|
|
2644
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2645
|
+
state.info.failCause.assertionFailed = true;
|
|
2646
|
+
state.info.failCause.lastError = errorMessage;
|
|
2647
|
+
throw new Error(errorMessage);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return state.info;
|
|
2651
|
+
}
|
|
2652
|
+
catch (e) {
|
|
2653
|
+
await _commandError(state, e, this);
|
|
2654
|
+
}
|
|
2655
|
+
finally {
|
|
2656
|
+
await _commandFinally(state, this);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2660
|
+
const state = {
|
|
2661
|
+
selectors,
|
|
2662
|
+
_params,
|
|
2663
|
+
property,
|
|
2664
|
+
value,
|
|
2665
|
+
options,
|
|
2666
|
+
world,
|
|
2667
|
+
type: Types.VERIFY_PROPERTY,
|
|
2668
|
+
highlight: true,
|
|
2669
|
+
screenshot: true,
|
|
2670
|
+
text: `Verify element property`,
|
|
2671
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2672
|
+
operation: "verifyProperty",
|
|
2673
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2674
|
+
allowDisabled: true,
|
|
2675
|
+
};
|
|
2676
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2677
|
+
let val;
|
|
2678
|
+
let expectedValue;
|
|
2679
|
+
try {
|
|
2680
|
+
await _preCommand(state, this);
|
|
2681
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2682
|
+
state.info.expectedValue = expectedValue;
|
|
2683
|
+
switch (property) {
|
|
2684
|
+
case "innerText":
|
|
2685
|
+
val = String(await state.element.innerText());
|
|
2686
|
+
break;
|
|
2687
|
+
case "text":
|
|
2688
|
+
val = String(await state.element.textContent());
|
|
2689
|
+
break;
|
|
2690
|
+
case "value":
|
|
2691
|
+
val = String(await state.element.inputValue());
|
|
2692
|
+
break;
|
|
2693
|
+
case "checked":
|
|
2694
|
+
val = String(await state.element.isChecked());
|
|
2695
|
+
break;
|
|
2696
|
+
case "disabled":
|
|
2697
|
+
val = String(await state.element.isDisabled());
|
|
2698
|
+
break;
|
|
2699
|
+
case "readOnly":
|
|
2700
|
+
const isEditable = await state.element.isEditable();
|
|
2701
|
+
val = String(!isEditable);
|
|
2702
|
+
break;
|
|
2703
|
+
case "innerHTML":
|
|
2704
|
+
val = String(await state.element.innerHTML());
|
|
2705
|
+
break;
|
|
2706
|
+
case "outerHTML":
|
|
2707
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2708
|
+
break;
|
|
2709
|
+
default:
|
|
2710
|
+
if (property.startsWith("dataset.")) {
|
|
2711
|
+
const dataAttribute = property.substring(8);
|
|
2712
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2713
|
+
}
|
|
2714
|
+
else {
|
|
2715
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
// Helper function to remove all style="" attributes
|
|
2719
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2720
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2721
|
+
};
|
|
2722
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2723
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2724
|
+
val = removeStyleAttributes(val);
|
|
2725
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2726
|
+
}
|
|
2727
|
+
state.info.value = val;
|
|
2728
|
+
let regex;
|
|
2729
|
+
state.info.value = val;
|
|
2730
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2731
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2732
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2733
|
+
let matchPassed = false;
|
|
2734
|
+
if (isRegex) {
|
|
2735
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2736
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2737
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2738
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2739
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2740
|
+
const regex = new RegExp(patternBody, flags);
|
|
2741
|
+
state.info.regex = true;
|
|
2742
|
+
matchPassed = regex.test(val);
|
|
2743
|
+
}
|
|
2744
|
+
else {
|
|
2745
|
+
// Fallback: treat as literal
|
|
2746
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2747
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2748
|
+
matchPassed = regex.test(val);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
else if (isContains) {
|
|
2752
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2753
|
+
matchPassed = val.includes(containsValue);
|
|
2754
|
+
}
|
|
2755
|
+
else if (isExact) {
|
|
2756
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2757
|
+
matchPassed = val === exactValue;
|
|
2758
|
+
}
|
|
2759
|
+
else if (property === "innerText") {
|
|
2760
|
+
// Default innerText logic
|
|
2761
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2762
|
+
const valLines = val.split("\n");
|
|
2763
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2764
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2765
|
+
}
|
|
2766
|
+
else {
|
|
2767
|
+
// Fallback exact or loose match
|
|
2768
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2769
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2770
|
+
matchPassed = regex.test(val);
|
|
2771
|
+
}
|
|
2772
|
+
if (!matchPassed) {
|
|
2773
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2774
|
+
state.info.failCause.assertionFailed = true;
|
|
2775
|
+
state.info.failCause.lastError = errorMessage;
|
|
2776
|
+
throw new Error(errorMessage);
|
|
2777
|
+
}
|
|
2778
|
+
return state.info;
|
|
2779
|
+
}
|
|
2780
|
+
catch (e) {
|
|
2781
|
+
await _commandError(state, e, this);
|
|
2782
|
+
}
|
|
2783
|
+
finally {
|
|
2784
|
+
await _commandFinally(state, this);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2788
|
+
// Convert timeout from seconds to milliseconds
|
|
2789
|
+
const timeoutMs = timeout * 1000;
|
|
2790
|
+
const state = {
|
|
2791
|
+
selectors,
|
|
2792
|
+
_params,
|
|
2793
|
+
condition,
|
|
2794
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2795
|
+
options,
|
|
2796
|
+
world,
|
|
2797
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2798
|
+
highlight: true,
|
|
2799
|
+
screenshot: true,
|
|
2800
|
+
text: `Conditional wait for element`,
|
|
2801
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2802
|
+
operation: "conditionalWait",
|
|
2803
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2804
|
+
allowDisabled: true,
|
|
2805
|
+
info: {},
|
|
2806
|
+
};
|
|
2807
|
+
state.options ??= { timeout: timeoutMs };
|
|
2808
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2809
|
+
const startTime = Date.now();
|
|
2810
|
+
let conditionMet = false;
|
|
2811
|
+
let currentValue = null;
|
|
2812
|
+
let lastError = null;
|
|
2813
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2814
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2815
|
+
const elapsedTime = Date.now() - startTime;
|
|
2816
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2817
|
+
try {
|
|
2818
|
+
// Try to execute _preCommand (element location)
|
|
2819
|
+
await _preCommand(state, this);
|
|
2820
|
+
// If _preCommand succeeds, start condition checking
|
|
2821
|
+
const checkCondition = async () => {
|
|
2822
|
+
try {
|
|
2823
|
+
switch (condition.toLowerCase()) {
|
|
2824
|
+
case "checked":
|
|
2825
|
+
currentValue = await state.element.isChecked();
|
|
2826
|
+
return currentValue === true;
|
|
2827
|
+
case "unchecked":
|
|
2828
|
+
currentValue = await state.element.isChecked();
|
|
2829
|
+
return currentValue === false;
|
|
2830
|
+
case "visible":
|
|
2831
|
+
currentValue = await state.element.isVisible();
|
|
2832
|
+
return currentValue === true;
|
|
2833
|
+
case "hidden":
|
|
2834
|
+
currentValue = await state.element.isVisible();
|
|
2835
|
+
return currentValue === false;
|
|
2836
|
+
case "enabled":
|
|
2837
|
+
currentValue = await state.element.isDisabled();
|
|
2838
|
+
return currentValue === false;
|
|
2839
|
+
case "disabled":
|
|
2840
|
+
currentValue = await state.element.isDisabled();
|
|
2841
|
+
return currentValue === true;
|
|
2842
|
+
case "editable":
|
|
2843
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2844
|
+
currentValue = await state.element.isContentEditable();
|
|
2845
|
+
return currentValue === true;
|
|
2846
|
+
default:
|
|
2847
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2848
|
+
state.info.success = false;
|
|
2849
|
+
return false;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
catch (error) {
|
|
2853
|
+
// Don't throw here, just return false to continue retrying
|
|
2854
|
+
return false;
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
// Inner loop for condition checking (once element is located)
|
|
2858
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2859
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2860
|
+
conditionMet = await checkCondition();
|
|
2861
|
+
if (conditionMet) {
|
|
2862
|
+
break;
|
|
2863
|
+
}
|
|
2864
|
+
// Check if we still have time for another attempt
|
|
2865
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2866
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2867
|
+
}
|
|
2868
|
+
else {
|
|
2869
|
+
break;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
// If we got here and condition is met, break out of main loop
|
|
2873
|
+
if (conditionMet) {
|
|
2874
|
+
break;
|
|
2875
|
+
}
|
|
2876
|
+
// If condition not met but no exception, we've timed out
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
catch (e) {
|
|
2880
|
+
lastError = e;
|
|
2881
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2882
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2883
|
+
// Check if we have enough time left to retry
|
|
2884
|
+
if (timeLeft > 100) {
|
|
2885
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
break;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2893
|
+
state.info = {
|
|
2894
|
+
success: conditionMet,
|
|
2895
|
+
conditionMet,
|
|
2896
|
+
actualWaitTime,
|
|
2897
|
+
currentValue,
|
|
2898
|
+
lastError: lastError?.message || null,
|
|
2899
|
+
message: conditionMet
|
|
2900
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2901
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2902
|
+
};
|
|
2903
|
+
if (lastError) {
|
|
2904
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2905
|
+
}
|
|
2906
|
+
try {
|
|
2907
|
+
await _commandFinally(state, this);
|
|
2908
|
+
}
|
|
2909
|
+
catch (finallyError) {
|
|
2910
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2911
|
+
}
|
|
2912
|
+
return state.info;
|
|
2913
|
+
}
|
|
1837
2914
|
async extractEmailData(emailAddress, options, world) {
|
|
1838
2915
|
if (!emailAddress) {
|
|
1839
2916
|
throw new Error("email address is null");
|
|
@@ -1852,7 +2929,7 @@ class StableBrowser {
|
|
|
1852
2929
|
if (options && options.timeout) {
|
|
1853
2930
|
timeout = options.timeout;
|
|
1854
2931
|
}
|
|
1855
|
-
const serviceUrl =
|
|
2932
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1856
2933
|
const request = {
|
|
1857
2934
|
method: "POST",
|
|
1858
2935
|
url: serviceUrl,
|
|
@@ -1897,6 +2974,12 @@ class StableBrowser {
|
|
|
1897
2974
|
emailUrl = url;
|
|
1898
2975
|
codeOrUrlFound = true;
|
|
1899
2976
|
}
|
|
2977
|
+
if (process.env.MODE === "executions") {
|
|
2978
|
+
const globalDataFile = "global_test_data.json";
|
|
2979
|
+
if (existsSync(globalDataFile)) {
|
|
2980
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
1900
2983
|
if (codeOrUrlFound) {
|
|
1901
2984
|
return { emailUrl, emailCode };
|
|
1902
2985
|
}
|
|
@@ -1923,27 +3006,32 @@ class StableBrowser {
|
|
|
1923
3006
|
async _highlightElements(scope, css) {
|
|
1924
3007
|
try {
|
|
1925
3008
|
if (!scope) {
|
|
3009
|
+
// console.log(`Scope is not defined`);
|
|
1926
3010
|
return;
|
|
1927
3011
|
}
|
|
1928
3012
|
if (!css) {
|
|
1929
3013
|
scope
|
|
1930
3014
|
.evaluate((node) => {
|
|
1931
3015
|
if (node && node.style) {
|
|
1932
|
-
let
|
|
1933
|
-
|
|
3016
|
+
let originalOutline = node.style.outline;
|
|
3017
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
3018
|
+
// node.__previousOutline = originalOutline;
|
|
3019
|
+
node.style.outline = "2px solid red";
|
|
3020
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
1934
3021
|
if (window) {
|
|
1935
3022
|
window.addEventListener("beforeunload", function (e) {
|
|
1936
|
-
node.style.
|
|
3023
|
+
node.style.outline = originalOutline;
|
|
1937
3024
|
});
|
|
1938
3025
|
}
|
|
1939
3026
|
setTimeout(function () {
|
|
1940
|
-
node.style.
|
|
3027
|
+
node.style.outline = originalOutline;
|
|
1941
3028
|
}, 2000);
|
|
1942
3029
|
}
|
|
1943
3030
|
})
|
|
1944
3031
|
.then(() => { })
|
|
1945
3032
|
.catch((e) => {
|
|
1946
3033
|
// ignore
|
|
3034
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1947
3035
|
});
|
|
1948
3036
|
}
|
|
1949
3037
|
else {
|
|
@@ -1959,17 +3047,18 @@ class StableBrowser {
|
|
|
1959
3047
|
if (!element.style) {
|
|
1960
3048
|
return;
|
|
1961
3049
|
}
|
|
1962
|
-
|
|
3050
|
+
let originalOutline = element.style.outline;
|
|
3051
|
+
element.__previousOutline = originalOutline;
|
|
1963
3052
|
// Set the new border to be red and 2px solid
|
|
1964
|
-
element.style.
|
|
3053
|
+
element.style.outline = "2px solid red";
|
|
1965
3054
|
if (window) {
|
|
1966
3055
|
window.addEventListener("beforeunload", function (e) {
|
|
1967
|
-
element.style.
|
|
3056
|
+
element.style.outline = originalOutline;
|
|
1968
3057
|
});
|
|
1969
3058
|
}
|
|
1970
3059
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1971
3060
|
setTimeout(function () {
|
|
1972
|
-
element.style.
|
|
3061
|
+
element.style.outline = originalOutline;
|
|
1973
3062
|
}, 2000);
|
|
1974
3063
|
}
|
|
1975
3064
|
return;
|
|
@@ -1977,6 +3066,7 @@ class StableBrowser {
|
|
|
1977
3066
|
.then(() => { })
|
|
1978
3067
|
.catch((e) => {
|
|
1979
3068
|
// ignore
|
|
3069
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1980
3070
|
});
|
|
1981
3071
|
}
|
|
1982
3072
|
}
|
|
@@ -1984,8 +3074,49 @@ class StableBrowser {
|
|
|
1984
3074
|
console.debug(error);
|
|
1985
3075
|
}
|
|
1986
3076
|
}
|
|
3077
|
+
_matcher(text) {
|
|
3078
|
+
if (!text) {
|
|
3079
|
+
return { matcher: "contains", queryText: "" };
|
|
3080
|
+
}
|
|
3081
|
+
if (text.length < 2) {
|
|
3082
|
+
return { matcher: "contains", queryText: text };
|
|
3083
|
+
}
|
|
3084
|
+
const split = text.split(":");
|
|
3085
|
+
const matcher = split[0].toLowerCase();
|
|
3086
|
+
const queryText = split.slice(1).join(":").trim();
|
|
3087
|
+
return { matcher, queryText };
|
|
3088
|
+
}
|
|
3089
|
+
_getDomain(url) {
|
|
3090
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
3091
|
+
return "";
|
|
3092
|
+
}
|
|
3093
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
3094
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
3095
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
3096
|
+
}
|
|
3097
|
+
let n = hostnameFragments.length;
|
|
3098
|
+
let fragments = [...hostnameFragments];
|
|
3099
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
3100
|
+
hostnameFragments.pop();
|
|
3101
|
+
n = hostnameFragments.length;
|
|
3102
|
+
}
|
|
3103
|
+
if (n == 0) {
|
|
3104
|
+
if (fragments[0] === "www")
|
|
3105
|
+
fragments = fragments.slice(1);
|
|
3106
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
3107
|
+
}
|
|
3108
|
+
if (hostnameFragments[0] === "www")
|
|
3109
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
3110
|
+
return hostnameFragments.join(".");
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Verify the page path matches the given path.
|
|
3114
|
+
* @param {string} pathPart - The path to verify.
|
|
3115
|
+
* @param {object} options - Options for verification.
|
|
3116
|
+
* @param {object} world - The world context.
|
|
3117
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3118
|
+
*/
|
|
1987
3119
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
1988
|
-
const startTime = Date.now();
|
|
1989
3120
|
let error = null;
|
|
1990
3121
|
let screenshotId = null;
|
|
1991
3122
|
let screenshotPath = null;
|
|
@@ -1999,51 +3130,242 @@ class StableBrowser {
|
|
|
1999
3130
|
pathPart = newValue;
|
|
2000
3131
|
}
|
|
2001
3132
|
info.pathPart = pathPart;
|
|
3133
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
3134
|
+
const state = {
|
|
3135
|
+
text_search: queryText,
|
|
3136
|
+
options,
|
|
3137
|
+
world,
|
|
3138
|
+
locate: false,
|
|
3139
|
+
scroll: false,
|
|
3140
|
+
highlight: false,
|
|
3141
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
3142
|
+
text: `Verify the page url is ${queryText}`,
|
|
3143
|
+
_text: `Verify the page url is ${queryText}`,
|
|
3144
|
+
operation: "verifyPagePath",
|
|
3145
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
3146
|
+
};
|
|
2002
3147
|
try {
|
|
3148
|
+
await _preCommand(state, this);
|
|
3149
|
+
state.info.text = queryText;
|
|
2003
3150
|
for (let i = 0; i < 30; i++) {
|
|
2004
3151
|
const url = await this.page.url();
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
3152
|
+
switch (matcher) {
|
|
3153
|
+
case "exact":
|
|
3154
|
+
if (url !== queryText) {
|
|
3155
|
+
if (i === 29) {
|
|
3156
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
3157
|
+
}
|
|
3158
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
3161
|
+
break;
|
|
3162
|
+
case "contains":
|
|
3163
|
+
if (!url.includes(queryText)) {
|
|
3164
|
+
if (i === 29) {
|
|
3165
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
3166
|
+
}
|
|
3167
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
break;
|
|
3171
|
+
case "starts-with":
|
|
3172
|
+
{
|
|
3173
|
+
const domain = this._getDomain(url);
|
|
3174
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
3175
|
+
if (i === 29) {
|
|
3176
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
3177
|
+
}
|
|
3178
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3179
|
+
continue;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
break;
|
|
3183
|
+
case "ends-with":
|
|
3184
|
+
{
|
|
3185
|
+
const urlObj = new URL(url);
|
|
3186
|
+
let route = "/";
|
|
3187
|
+
if (urlObj.pathname !== "/") {
|
|
3188
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
3189
|
+
}
|
|
3190
|
+
else {
|
|
3191
|
+
route = "/";
|
|
3192
|
+
}
|
|
3193
|
+
if (route !== queryText) {
|
|
3194
|
+
if (i === 29) {
|
|
3195
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
3196
|
+
}
|
|
3197
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
break;
|
|
3202
|
+
case "regex":
|
|
3203
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3204
|
+
if (!regex.test(url)) {
|
|
3205
|
+
if (i === 29) {
|
|
3206
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
3207
|
+
}
|
|
3208
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3211
|
+
break;
|
|
3212
|
+
default:
|
|
3213
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3214
|
+
if (!url.includes(pathPart)) {
|
|
3215
|
+
if (i === 29) {
|
|
3216
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
3217
|
+
}
|
|
3218
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
2011
3221
|
}
|
|
2012
|
-
|
|
2013
|
-
return info;
|
|
3222
|
+
await _screenshot(state, this);
|
|
3223
|
+
return state.info;
|
|
2014
3224
|
}
|
|
2015
3225
|
}
|
|
2016
3226
|
catch (e) {
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
info.screenshotPath = screenshotPath;
|
|
2021
|
-
Object.assign(e, { info: info });
|
|
2022
|
-
error = e;
|
|
2023
|
-
// throw e;
|
|
2024
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
3227
|
+
state.info.failCause.lastError = e.message;
|
|
3228
|
+
state.info.failCause.assertionFailed = true;
|
|
3229
|
+
await _commandError(state, e, this);
|
|
2025
3230
|
}
|
|
2026
3231
|
finally {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
3232
|
+
await _commandFinally(state, this);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* Verify the page title matches the given title.
|
|
3237
|
+
* @param {string} title - The title to verify.
|
|
3238
|
+
* @param {object} options - Options for verification.
|
|
3239
|
+
* @param {object} world - The world context.
|
|
3240
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3241
|
+
*/
|
|
3242
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
3243
|
+
let error = null;
|
|
3244
|
+
let screenshotId = null;
|
|
3245
|
+
let screenshotPath = null;
|
|
3246
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3247
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
3248
|
+
if (newValue !== title) {
|
|
3249
|
+
this.logger.info(title + "=" + newValue);
|
|
3250
|
+
title = newValue;
|
|
3251
|
+
}
|
|
3252
|
+
const { matcher, queryText } = this._matcher(title);
|
|
3253
|
+
const state = {
|
|
3254
|
+
text_search: queryText,
|
|
3255
|
+
options,
|
|
3256
|
+
world,
|
|
3257
|
+
locate: false,
|
|
3258
|
+
scroll: false,
|
|
3259
|
+
highlight: false,
|
|
3260
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
3261
|
+
text: `Verify the page title is ${queryText}`,
|
|
3262
|
+
_text: `Verify the page title is ${queryText}`,
|
|
3263
|
+
operation: "verifyPageTitle",
|
|
3264
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
3265
|
+
};
|
|
3266
|
+
try {
|
|
3267
|
+
await _preCommand(state, this);
|
|
3268
|
+
state.info.text = queryText;
|
|
3269
|
+
for (let i = 0; i < 30; i++) {
|
|
3270
|
+
const foundTitle = await this.page.title();
|
|
3271
|
+
switch (matcher) {
|
|
3272
|
+
case "exact":
|
|
3273
|
+
if (foundTitle !== queryText) {
|
|
3274
|
+
if (i === 29) {
|
|
3275
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
3276
|
+
}
|
|
3277
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
break;
|
|
3281
|
+
case "contains":
|
|
3282
|
+
if (!foundTitle.includes(queryText)) {
|
|
3283
|
+
if (i === 29) {
|
|
3284
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
3285
|
+
}
|
|
3286
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3287
|
+
continue;
|
|
3288
|
+
}
|
|
3289
|
+
break;
|
|
3290
|
+
case "starts-with":
|
|
3291
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
3292
|
+
if (i === 29) {
|
|
3293
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
3294
|
+
}
|
|
3295
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
break;
|
|
3299
|
+
case "ends-with":
|
|
3300
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
3301
|
+
if (i === 29) {
|
|
3302
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
3303
|
+
}
|
|
3304
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
break;
|
|
3308
|
+
case "regex":
|
|
3309
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3310
|
+
if (!regex.test(foundTitle)) {
|
|
3311
|
+
if (i === 29) {
|
|
3312
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
3313
|
+
}
|
|
3314
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
break;
|
|
3318
|
+
default:
|
|
3319
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3320
|
+
if (!foundTitle.includes(title)) {
|
|
3321
|
+
if (i === 29) {
|
|
3322
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
3323
|
+
}
|
|
3324
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
await _screenshot(state, this);
|
|
3329
|
+
return state.info;
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
catch (e) {
|
|
3333
|
+
state.info.failCause.lastError = e.message;
|
|
3334
|
+
state.info.failCause.assertionFailed = true;
|
|
3335
|
+
await _commandError(state, e, this);
|
|
3336
|
+
}
|
|
3337
|
+
finally {
|
|
3338
|
+
await _commandFinally(state, this);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
3342
|
+
const frames = this.page.frames();
|
|
3343
|
+
let results = [];
|
|
3344
|
+
// let ignoreCase = false;
|
|
3345
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3346
|
+
if (dateAlternatives.date) {
|
|
3347
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
3348
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3349
|
+
result.frame = frames[i];
|
|
3350
|
+
results.push(result);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
else if (numberAlternatives.number) {
|
|
3354
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
3355
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3356
|
+
result.frame = frames[i];
|
|
3357
|
+
results.push(result);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
else {
|
|
3361
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3362
|
+
result.frame = frames[i];
|
|
3363
|
+
results.push(result);
|
|
3364
|
+
}
|
|
2046
3365
|
}
|
|
3366
|
+
state.info.results = results;
|
|
3367
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3368
|
+
return resultWithElementsFound;
|
|
2047
3369
|
}
|
|
2048
3370
|
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2049
3371
|
text = unEscapeString(text);
|
|
@@ -2054,13 +3376,26 @@ class StableBrowser {
|
|
|
2054
3376
|
locate: false,
|
|
2055
3377
|
scroll: false,
|
|
2056
3378
|
highlight: false,
|
|
2057
|
-
type: Types.
|
|
2058
|
-
text: `Verify text exists in page`,
|
|
3379
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
3380
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
3381
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2059
3382
|
operation: "verifyTextExistInPage",
|
|
2060
3383
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2061
3384
|
};
|
|
2062
|
-
|
|
2063
|
-
|
|
3385
|
+
if (testForRegex(text)) {
|
|
3386
|
+
text = text.replace(/\\"/g, '"');
|
|
3387
|
+
}
|
|
3388
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3389
|
+
//if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
3390
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
3391
|
+
if (!stepFastMode) {
|
|
3392
|
+
if (!this.fastMode) {
|
|
3393
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3394
|
+
}
|
|
3395
|
+
else {
|
|
3396
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
2064
3399
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2065
3400
|
if (newValue !== text) {
|
|
2066
3401
|
this.logger.info(text + "=" + newValue);
|
|
@@ -2068,35 +3403,24 @@ class StableBrowser {
|
|
|
2068
3403
|
}
|
|
2069
3404
|
let dateAlternatives = findDateAlternatives(text);
|
|
2070
3405
|
let numberAlternatives = findNumberAlternatives(text);
|
|
3406
|
+
if (stepFastMode) {
|
|
3407
|
+
state.onlyFailuresScreenshot = true;
|
|
3408
|
+
state.scroll = false;
|
|
3409
|
+
state.highlight = false;
|
|
3410
|
+
}
|
|
2071
3411
|
try {
|
|
2072
3412
|
await _preCommand(state, this);
|
|
2073
3413
|
state.info.text = text;
|
|
2074
3414
|
while (true) {
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
else if (numberAlternatives.number) {
|
|
2086
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2087
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
|
|
2088
|
-
result.frame = frames[i];
|
|
2089
|
-
results.push(result);
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
else {
|
|
2093
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
|
|
2094
|
-
result.frame = frames[i];
|
|
2095
|
-
results.push(result);
|
|
2096
|
-
}
|
|
3415
|
+
let resultWithElementsFound = {
|
|
3416
|
+
length: 0,
|
|
3417
|
+
};
|
|
3418
|
+
try {
|
|
3419
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3420
|
+
}
|
|
3421
|
+
catch (error) {
|
|
3422
|
+
// ignore
|
|
2097
3423
|
}
|
|
2098
|
-
state.info.results = results;
|
|
2099
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2100
3424
|
if (resultWithElementsFound.length === 0) {
|
|
2101
3425
|
if (Date.now() - state.startTime > timeout) {
|
|
2102
3426
|
throw new Error(`Text ${text} not found in page`);
|
|
@@ -2104,26 +3428,30 @@ class StableBrowser {
|
|
|
2104
3428
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2105
3429
|
continue;
|
|
2106
3430
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
3431
|
+
try {
|
|
3432
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
3433
|
+
const frame = resultWithElementsFound[0].frame;
|
|
3434
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
3435
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3436
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3437
|
+
if (element) {
|
|
3438
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3439
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3440
|
+
}
|
|
2115
3441
|
}
|
|
3442
|
+
await _screenshot(state, this);
|
|
3443
|
+
return state.info;
|
|
3444
|
+
}
|
|
3445
|
+
catch (error) {
|
|
3446
|
+
console.error(error);
|
|
2116
3447
|
}
|
|
2117
|
-
await _screenshot(state, this);
|
|
2118
|
-
return state.info;
|
|
2119
3448
|
}
|
|
2120
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2121
3449
|
}
|
|
2122
3450
|
catch (e) {
|
|
2123
3451
|
await _commandError(state, e, this);
|
|
2124
3452
|
}
|
|
2125
3453
|
finally {
|
|
2126
|
-
_commandFinally(state, this);
|
|
3454
|
+
await _commandFinally(state, this);
|
|
2127
3455
|
}
|
|
2128
3456
|
}
|
|
2129
3457
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2136,11 +3464,15 @@ class StableBrowser {
|
|
|
2136
3464
|
scroll: false,
|
|
2137
3465
|
highlight: false,
|
|
2138
3466
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2139
|
-
text: `Verify text does not exist in page`,
|
|
3467
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
3468
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2140
3469
|
operation: "verifyTextNotExistInPage",
|
|
2141
3470
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2142
3471
|
};
|
|
2143
|
-
|
|
3472
|
+
if (testForRegex(text)) {
|
|
3473
|
+
text = text.replace(/\\"/g, '"');
|
|
3474
|
+
}
|
|
3475
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2144
3476
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2145
3477
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2146
3478
|
if (newValue !== text) {
|
|
@@ -2152,32 +3484,16 @@ class StableBrowser {
|
|
|
2152
3484
|
try {
|
|
2153
3485
|
await _preCommand(state, this);
|
|
2154
3486
|
state.info.text = text;
|
|
3487
|
+
let resultWithElementsFound = {
|
|
3488
|
+
length: null, // initial cannot be 0
|
|
3489
|
+
};
|
|
2155
3490
|
while (true) {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
|
|
2162
|
-
result.frame = frames[i];
|
|
2163
|
-
results.push(result);
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
else if (numberAlternatives.number) {
|
|
2167
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2168
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
|
|
2169
|
-
result.frame = frames[i];
|
|
2170
|
-
results.push(result);
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
else {
|
|
2174
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
|
|
2175
|
-
result.frame = frames[i];
|
|
2176
|
-
results.push(result);
|
|
2177
|
-
}
|
|
3491
|
+
try {
|
|
3492
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3493
|
+
}
|
|
3494
|
+
catch (error) {
|
|
3495
|
+
// ignore
|
|
2178
3496
|
}
|
|
2179
|
-
state.info.results = results;
|
|
2180
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2181
3497
|
if (resultWithElementsFound.length === 0) {
|
|
2182
3498
|
await _screenshot(state, this);
|
|
2183
3499
|
return state.info;
|
|
@@ -2192,18 +3508,158 @@ class StableBrowser {
|
|
|
2192
3508
|
await _commandError(state, e, this);
|
|
2193
3509
|
}
|
|
2194
3510
|
finally {
|
|
2195
|
-
_commandFinally(state, this);
|
|
3511
|
+
await _commandFinally(state, this);
|
|
2196
3512
|
}
|
|
2197
3513
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
3514
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
3515
|
+
textAnchor = unEscapeString(textAnchor);
|
|
3516
|
+
textToVerify = unEscapeString(textToVerify);
|
|
3517
|
+
const state = {
|
|
3518
|
+
text_search: textToVerify,
|
|
3519
|
+
options,
|
|
3520
|
+
world,
|
|
3521
|
+
locate: false,
|
|
3522
|
+
scroll: false,
|
|
3523
|
+
highlight: false,
|
|
3524
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
3525
|
+
text: `Verify text with relation to another text`,
|
|
3526
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
3527
|
+
operation: "verify_text_with_relation",
|
|
3528
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3529
|
+
};
|
|
3530
|
+
const cmdStartTime = Date.now();
|
|
3531
|
+
let cmdEndTime = null;
|
|
3532
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3533
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3534
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
3535
|
+
if (newValue !== textAnchor) {
|
|
3536
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
3537
|
+
textAnchor = newValue;
|
|
3538
|
+
}
|
|
3539
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
3540
|
+
if (newValue !== textToVerify) {
|
|
3541
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
3542
|
+
textToVerify = newValue;
|
|
3543
|
+
}
|
|
3544
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
3545
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
3546
|
+
let foundAncore = false;
|
|
3547
|
+
try {
|
|
3548
|
+
await _preCommand(state, this);
|
|
3549
|
+
state.info.text = textToVerify;
|
|
3550
|
+
let resultWithElementsFound = {
|
|
3551
|
+
length: 0,
|
|
3552
|
+
};
|
|
3553
|
+
while (true) {
|
|
3554
|
+
try {
|
|
3555
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3556
|
+
}
|
|
3557
|
+
catch (error) {
|
|
3558
|
+
// ignore
|
|
3559
|
+
}
|
|
3560
|
+
if (resultWithElementsFound.length === 0) {
|
|
3561
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3562
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
3563
|
+
}
|
|
3564
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
else {
|
|
3568
|
+
cmdEndTime = Date.now();
|
|
3569
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3570
|
+
if (foundAncore) {
|
|
3571
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3572
|
+
}
|
|
3573
|
+
else {
|
|
3574
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
try {
|
|
3579
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3580
|
+
foundAncore = true;
|
|
3581
|
+
const result = resultWithElementsFound[i];
|
|
3582
|
+
const token = result.randomToken;
|
|
3583
|
+
const frame = result.frame;
|
|
3584
|
+
let css = `[data-blinq-id-${token}]`;
|
|
3585
|
+
const climbArray1 = [];
|
|
3586
|
+
for (let i = 0; i < climb; i++) {
|
|
3587
|
+
climbArray1.push("..");
|
|
3588
|
+
}
|
|
3589
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
3590
|
+
if (Number(climb) > 0) {
|
|
3591
|
+
css = css + " >> " + climbXpath;
|
|
3592
|
+
}
|
|
3593
|
+
const count = await frame.locator(css).count();
|
|
3594
|
+
for (let j = 0; j < count; j++) {
|
|
3595
|
+
const continer = await frame.locator(css).nth(j);
|
|
3596
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
3597
|
+
if (result.elementCount > 0) {
|
|
3598
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
3599
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3600
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
3601
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
3602
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
3603
|
+
// this._highlightElements(frame, dataAttribute)
|
|
3604
|
+
// .then(async () => {
|
|
3605
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3606
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
3607
|
+
// () => {}
|
|
3608
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
3609
|
+
// );
|
|
3610
|
+
// })
|
|
3611
|
+
// .catch(e);
|
|
3612
|
+
// }
|
|
3613
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
3614
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3615
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3616
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
3617
|
+
if (element) {
|
|
3618
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3619
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3620
|
+
}
|
|
3621
|
+
await _screenshot(state, this);
|
|
3622
|
+
return state.info;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
catch (error) {
|
|
3628
|
+
console.error(error);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
3632
|
+
}
|
|
3633
|
+
catch (e) {
|
|
3634
|
+
await _commandError(state, e, this);
|
|
3635
|
+
}
|
|
3636
|
+
finally {
|
|
3637
|
+
await _commandFinally(state, this);
|
|
2202
3638
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
3639
|
+
}
|
|
3640
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
3641
|
+
const frames = this.page.frames();
|
|
3642
|
+
let results = [];
|
|
3643
|
+
let ignoreCase = false;
|
|
3644
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3645
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
3646
|
+
result.frame = frames[i];
|
|
3647
|
+
const climbArray = [];
|
|
3648
|
+
for (let i = 0; i < climb; i++) {
|
|
3649
|
+
climbArray.push("..");
|
|
3650
|
+
}
|
|
3651
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
3652
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
3653
|
+
const count = await frames[i].locator(newLocator).count();
|
|
3654
|
+
if (count > 0) {
|
|
3655
|
+
result.elementCount = count;
|
|
3656
|
+
result.locator = newLocator;
|
|
3657
|
+
results.push(result);
|
|
3658
|
+
}
|
|
2205
3659
|
}
|
|
2206
|
-
|
|
3660
|
+
// state.info.results = results;
|
|
3661
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3662
|
+
return resultWithElementsFound;
|
|
2207
3663
|
}
|
|
2208
3664
|
async visualVerification(text, options = {}, world = null) {
|
|
2209
3665
|
const startTime = Date.now();
|
|
@@ -2219,14 +3675,17 @@ class StableBrowser {
|
|
|
2219
3675
|
throw new Error("TOKEN is not set");
|
|
2220
3676
|
}
|
|
2221
3677
|
try {
|
|
2222
|
-
let serviceUrl =
|
|
3678
|
+
let serviceUrl = _getServerUrl();
|
|
2223
3679
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2224
3680
|
info.screenshotPath = screenshotPath;
|
|
2225
3681
|
const screenshot = await this.takeScreenshot();
|
|
2226
|
-
|
|
2227
|
-
method: "
|
|
3682
|
+
let request = {
|
|
3683
|
+
method: "post",
|
|
3684
|
+
maxBodyLength: Infinity,
|
|
2228
3685
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2229
3686
|
headers: {
|
|
3687
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3688
|
+
"x-source": "aaa",
|
|
2230
3689
|
"Content-Type": "application/json",
|
|
2231
3690
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2232
3691
|
},
|
|
@@ -2235,7 +3694,7 @@ class StableBrowser {
|
|
|
2235
3694
|
screenshot: screenshot,
|
|
2236
3695
|
}),
|
|
2237
3696
|
};
|
|
2238
|
-
|
|
3697
|
+
const result = await axios.request(request);
|
|
2239
3698
|
if (result.data.status !== true) {
|
|
2240
3699
|
throw new Error("Visual validation failed");
|
|
2241
3700
|
}
|
|
@@ -2256,13 +3715,14 @@ class StableBrowser {
|
|
|
2256
3715
|
Object.assign(e, { info: info });
|
|
2257
3716
|
error = e;
|
|
2258
3717
|
// throw e;
|
|
2259
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3718
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2260
3719
|
}
|
|
2261
3720
|
finally {
|
|
2262
3721
|
const endTime = Date.now();
|
|
2263
|
-
|
|
3722
|
+
_reportToWorld(world, {
|
|
2264
3723
|
type: Types.VERIFY_VISUAL,
|
|
2265
3724
|
text: "Visual verification",
|
|
3725
|
+
_text: "Visual verification of " + text,
|
|
2266
3726
|
screenshotId,
|
|
2267
3727
|
result: error
|
|
2268
3728
|
? {
|
|
@@ -2308,6 +3768,7 @@ class StableBrowser {
|
|
|
2308
3768
|
let screenshotPath = null;
|
|
2309
3769
|
const info = {};
|
|
2310
3770
|
info.log = "";
|
|
3771
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2311
3772
|
info.operation = "getTableData";
|
|
2312
3773
|
info.selectors = selectors;
|
|
2313
3774
|
try {
|
|
@@ -2328,7 +3789,7 @@ class StableBrowser {
|
|
|
2328
3789
|
}
|
|
2329
3790
|
finally {
|
|
2330
3791
|
const endTime = Date.now();
|
|
2331
|
-
|
|
3792
|
+
_reportToWorld(world, {
|
|
2332
3793
|
element_name: selectors.element_name,
|
|
2333
3794
|
type: Types.GET_TABLE_DATA,
|
|
2334
3795
|
text: "Get table data",
|
|
@@ -2383,7 +3844,7 @@ class StableBrowser {
|
|
|
2383
3844
|
info.operation = "analyzeTable";
|
|
2384
3845
|
info.selectors = selectors;
|
|
2385
3846
|
info.query = query;
|
|
2386
|
-
query =
|
|
3847
|
+
query = _fixUsingParams(query, _params);
|
|
2387
3848
|
info.query_fixed = query;
|
|
2388
3849
|
info.operator = operator;
|
|
2389
3850
|
info.value = value;
|
|
@@ -2494,7 +3955,7 @@ class StableBrowser {
|
|
|
2494
3955
|
}
|
|
2495
3956
|
finally {
|
|
2496
3957
|
const endTime = Date.now();
|
|
2497
|
-
|
|
3958
|
+
_reportToWorld(world, {
|
|
2498
3959
|
element_name: selectors.element_name,
|
|
2499
3960
|
type: Types.ANALYZE_TABLE,
|
|
2500
3961
|
text: "Analyze table",
|
|
@@ -2515,8 +3976,51 @@ class StableBrowser {
|
|
|
2515
3976
|
});
|
|
2516
3977
|
}
|
|
2517
3978
|
}
|
|
3979
|
+
/**
|
|
3980
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3981
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3982
|
+
* @param options - Optional configuration object
|
|
3983
|
+
* @param world - Optional world context
|
|
3984
|
+
* @returns Promise that resolves after the specified duration
|
|
3985
|
+
*/
|
|
3986
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3987
|
+
const state = {
|
|
3988
|
+
duration,
|
|
3989
|
+
options,
|
|
3990
|
+
world,
|
|
3991
|
+
locate: false,
|
|
3992
|
+
scroll: false,
|
|
3993
|
+
screenshot: false,
|
|
3994
|
+
highlight: false,
|
|
3995
|
+
type: Types.SLEEP,
|
|
3996
|
+
text: `Sleep for ${duration} ms`,
|
|
3997
|
+
_text: `Sleep for ${duration} ms`,
|
|
3998
|
+
operation: "sleep",
|
|
3999
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
4000
|
+
};
|
|
4001
|
+
try {
|
|
4002
|
+
await _preCommand(state, this);
|
|
4003
|
+
if (duration < 0) {
|
|
4004
|
+
throw new Error("Sleep duration cannot be negative");
|
|
4005
|
+
}
|
|
4006
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
4007
|
+
return state.info;
|
|
4008
|
+
}
|
|
4009
|
+
catch (e) {
|
|
4010
|
+
await _commandError(state, e, this);
|
|
4011
|
+
}
|
|
4012
|
+
finally {
|
|
4013
|
+
await _commandFinally(state, this);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
2518
4016
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2519
|
-
|
|
4017
|
+
try {
|
|
4018
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
4019
|
+
}
|
|
4020
|
+
catch (error) {
|
|
4021
|
+
this.logger.debug(error);
|
|
4022
|
+
throw error;
|
|
4023
|
+
}
|
|
2520
4024
|
}
|
|
2521
4025
|
_getLoadTimeout(options) {
|
|
2522
4026
|
let timeout = 15000;
|
|
@@ -2528,7 +4032,54 @@ class StableBrowser {
|
|
|
2528
4032
|
}
|
|
2529
4033
|
return timeout;
|
|
2530
4034
|
}
|
|
4035
|
+
_getFindElementTimeout(options) {
|
|
4036
|
+
if (options && options.timeout) {
|
|
4037
|
+
return options.timeout;
|
|
4038
|
+
}
|
|
4039
|
+
if (this.configuration.find_element_timeout) {
|
|
4040
|
+
return this.configuration.find_element_timeout;
|
|
4041
|
+
}
|
|
4042
|
+
return 30000;
|
|
4043
|
+
}
|
|
4044
|
+
async saveStoreState(path = null, world = null) {
|
|
4045
|
+
const storageState = await this.page.context().storageState();
|
|
4046
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4047
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
4048
|
+
if (path) {
|
|
4049
|
+
// save { storageState: storageState } into the path
|
|
4050
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
4051
|
+
}
|
|
4052
|
+
else {
|
|
4053
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
async restoreSaveState(path = null, world = null) {
|
|
4057
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4058
|
+
await refreshBrowser(this, path, world);
|
|
4059
|
+
this.registerEventListeners(this.context);
|
|
4060
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
4061
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
4062
|
+
if (this.onRestoreSaveState) {
|
|
4063
|
+
await this.onRestoreSaveState(path);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
2531
4066
|
async waitForPageLoad(options = {}, world = null) {
|
|
4067
|
+
// try {
|
|
4068
|
+
// let currentPagePath = null;
|
|
4069
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4070
|
+
// if (this.latestPagePath) {
|
|
4071
|
+
// // get the currect page path and compare with the latest page path
|
|
4072
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4073
|
+
// // if the page path is the same, do not wait for page load
|
|
4074
|
+
// console.log("No page change: " + currentPagePath);
|
|
4075
|
+
// return;
|
|
4076
|
+
// }
|
|
4077
|
+
// }
|
|
4078
|
+
// this.latestPagePath = currentPagePath;
|
|
4079
|
+
// } catch (e) {
|
|
4080
|
+
// console.debug("Error getting current page path: ", e);
|
|
4081
|
+
// }
|
|
4082
|
+
//console.log("Waiting for page load");
|
|
2532
4083
|
let timeout = this._getLoadTimeout(options);
|
|
2533
4084
|
const promiseArray = [];
|
|
2534
4085
|
// let waitForNetworkIdle = true;
|
|
@@ -2561,13 +4112,15 @@ class StableBrowser {
|
|
|
2561
4112
|
else if (e.label === "domcontentloaded") {
|
|
2562
4113
|
console.log("waited for the domcontent loaded timeout");
|
|
2563
4114
|
}
|
|
2564
|
-
console.log(".");
|
|
2565
4115
|
}
|
|
2566
4116
|
finally {
|
|
2567
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4117
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4118
|
+
if (options && !options.noSleep) {
|
|
4119
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4120
|
+
}
|
|
2568
4121
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2569
4122
|
const endTime = Date.now();
|
|
2570
|
-
|
|
4123
|
+
_reportToWorld(world, {
|
|
2571
4124
|
type: Types.GET_PAGE_STATUS,
|
|
2572
4125
|
text: "Wait for page load",
|
|
2573
4126
|
screenshotId,
|
|
@@ -2595,6 +4148,7 @@ class StableBrowser {
|
|
|
2595
4148
|
highlight: false,
|
|
2596
4149
|
type: Types.CLOSE_PAGE,
|
|
2597
4150
|
text: `Close page`,
|
|
4151
|
+
_text: `Close the page`,
|
|
2598
4152
|
operation: "closePage",
|
|
2599
4153
|
log: "***** close page *****\n",
|
|
2600
4154
|
throwError: false,
|
|
@@ -2604,12 +4158,123 @@ class StableBrowser {
|
|
|
2604
4158
|
await this.page.close();
|
|
2605
4159
|
}
|
|
2606
4160
|
catch (e) {
|
|
2607
|
-
console.log(".");
|
|
2608
4161
|
await _commandError(state, e, this);
|
|
2609
4162
|
}
|
|
2610
4163
|
finally {
|
|
2611
|
-
_commandFinally(state, this);
|
|
4164
|
+
await _commandFinally(state, this);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
4168
|
+
let operation = null;
|
|
4169
|
+
if (!options || !options.operation) {
|
|
4170
|
+
throw new Error("operation is not defined");
|
|
4171
|
+
}
|
|
4172
|
+
operation = options.operation;
|
|
4173
|
+
// validate operation is one of the supported operations
|
|
4174
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
4175
|
+
throw new Error("operation is not supported");
|
|
4176
|
+
}
|
|
4177
|
+
const state = {
|
|
4178
|
+
options,
|
|
4179
|
+
world,
|
|
4180
|
+
locate: false,
|
|
4181
|
+
scroll: false,
|
|
4182
|
+
highlight: false,
|
|
4183
|
+
type: Types.TABLE_OPERATION,
|
|
4184
|
+
text: `Table operation`,
|
|
4185
|
+
_text: `Table ${operation} operation`,
|
|
4186
|
+
operation: operation,
|
|
4187
|
+
log: "***** Table operation *****\n",
|
|
4188
|
+
};
|
|
4189
|
+
const timeout = this._getFindElementTimeout(options);
|
|
4190
|
+
try {
|
|
4191
|
+
await _preCommand(state, this);
|
|
4192
|
+
const start = Date.now();
|
|
4193
|
+
let cellArea = null;
|
|
4194
|
+
while (true) {
|
|
4195
|
+
try {
|
|
4196
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
4197
|
+
if (cellArea) {
|
|
4198
|
+
break;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
catch (e) {
|
|
4202
|
+
// ignore
|
|
4203
|
+
}
|
|
4204
|
+
if (Date.now() - start > timeout) {
|
|
4205
|
+
throw new Error(`Cell not found in table`);
|
|
4206
|
+
}
|
|
4207
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
4208
|
+
}
|
|
4209
|
+
switch (operation) {
|
|
4210
|
+
case "click":
|
|
4211
|
+
if (!options.css) {
|
|
4212
|
+
// will click in the center of the cell
|
|
4213
|
+
let xOffset = 0;
|
|
4214
|
+
let yOffset = 0;
|
|
4215
|
+
if (options.xOffset) {
|
|
4216
|
+
xOffset = options.xOffset;
|
|
4217
|
+
}
|
|
4218
|
+
if (options.yOffset) {
|
|
4219
|
+
yOffset = options.yOffset;
|
|
4220
|
+
}
|
|
4221
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
4222
|
+
}
|
|
4223
|
+
else {
|
|
4224
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4225
|
+
if (results.length === 0) {
|
|
4226
|
+
throw new Error(`Element not found in cell area`);
|
|
4227
|
+
}
|
|
4228
|
+
state.element = results[0];
|
|
4229
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
4230
|
+
}
|
|
4231
|
+
break;
|
|
4232
|
+
case "hover+click":
|
|
4233
|
+
if (!options.css) {
|
|
4234
|
+
throw new Error("css is not defined");
|
|
4235
|
+
}
|
|
4236
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4237
|
+
if (results.length === 0) {
|
|
4238
|
+
throw new Error(`Element not found in cell area`);
|
|
4239
|
+
}
|
|
4240
|
+
state.element = results[0];
|
|
4241
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
4242
|
+
break;
|
|
4243
|
+
case "hover":
|
|
4244
|
+
if (!options.css) {
|
|
4245
|
+
throw new Error("css is not defined");
|
|
4246
|
+
}
|
|
4247
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4248
|
+
if (result1.length === 0) {
|
|
4249
|
+
throw new Error(`Element not found in cell area`);
|
|
4250
|
+
}
|
|
4251
|
+
state.element = result1[0];
|
|
4252
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4253
|
+
break;
|
|
4254
|
+
default:
|
|
4255
|
+
throw new Error("operation is not supported");
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
catch (e) {
|
|
4259
|
+
await _commandError(state, e, this);
|
|
4260
|
+
}
|
|
4261
|
+
finally {
|
|
4262
|
+
await _commandFinally(state, this);
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
saveTestDataAsGlobal(options, world) {
|
|
4266
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
4267
|
+
if (process.env.MODE === "executions") {
|
|
4268
|
+
const globalDataFile = path.join(this.project_path, "global_test_data.json");
|
|
4269
|
+
const dataFileContents = fs.existsSync(dataFile) ? JSON.parse(fs.readFileSync(dataFile)) : {};
|
|
4270
|
+
const globalDataFileContents = fs.existsSync(globalDataFile) ? JSON.parse(fs.readFileSync(globalDataFile)) : {};
|
|
4271
|
+
const mergedData = JSON.stringify(_.merge({}, dataFileContents, globalDataFileContents), null, 2);
|
|
4272
|
+
fs.writeFileSync(dataFile, mergedData);
|
|
4273
|
+
this.logger.info("Save the scenario test data to " + dataFile + " as global for the following scenarios.");
|
|
4274
|
+
return;
|
|
2612
4275
|
}
|
|
4276
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4277
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2613
4278
|
}
|
|
2614
4279
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2615
4280
|
const startTime = Date.now();
|
|
@@ -2627,16 +4292,16 @@ class StableBrowser {
|
|
|
2627
4292
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2628
4293
|
}
|
|
2629
4294
|
catch (e) {
|
|
2630
|
-
console.log(".");
|
|
2631
4295
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2632
4296
|
}
|
|
2633
4297
|
finally {
|
|
2634
4298
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2635
4299
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2636
4300
|
const endTime = Date.now();
|
|
2637
|
-
|
|
4301
|
+
_reportToWorld(world, {
|
|
2638
4302
|
type: Types.SET_VIEWPORT,
|
|
2639
4303
|
text: "set viewport size to " + width + "x" + hight,
|
|
4304
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2640
4305
|
screenshotId,
|
|
2641
4306
|
result: error
|
|
2642
4307
|
? {
|
|
@@ -2664,14 +4329,13 @@ class StableBrowser {
|
|
|
2664
4329
|
await this.page.reload();
|
|
2665
4330
|
}
|
|
2666
4331
|
catch (e) {
|
|
2667
|
-
console.log(".");
|
|
2668
4332
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2669
4333
|
}
|
|
2670
4334
|
finally {
|
|
2671
4335
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2672
4336
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2673
4337
|
const endTime = Date.now();
|
|
2674
|
-
|
|
4338
|
+
_reportToWorld(world, {
|
|
2675
4339
|
type: Types.GET_PAGE_STATUS,
|
|
2676
4340
|
text: "page relaod",
|
|
2677
4341
|
screenshotId,
|
|
@@ -2707,11 +4371,276 @@ class StableBrowser {
|
|
|
2707
4371
|
console.log("#-#");
|
|
2708
4372
|
}
|
|
2709
4373
|
}
|
|
2710
|
-
|
|
2711
|
-
if (
|
|
2712
|
-
|
|
4374
|
+
async beforeScenario(world, scenario) {
|
|
4375
|
+
if (world && world.attach) {
|
|
4376
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4377
|
+
}
|
|
4378
|
+
this.context.loadedRoutes = null;
|
|
4379
|
+
this.beforeScenarioCalled = true;
|
|
4380
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4381
|
+
this.scenarioName = scenario.pickle.name;
|
|
4382
|
+
}
|
|
4383
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
4384
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
4385
|
+
}
|
|
4386
|
+
if (this.context) {
|
|
4387
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
4388
|
+
}
|
|
4389
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4390
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4391
|
+
// check if @global_test_data tag is present
|
|
4392
|
+
if (this.tags.includes("@global_test_data" /* TAG_CONSTANTS.GLOBAL_TEST_DATA */)) {
|
|
4393
|
+
this.saveTestDataAsGlobal({}, world);
|
|
4394
|
+
}
|
|
4395
|
+
if (this.tags.includes("@fast_mode" /* TAG_CONSTANTS.FAST_MODE */)) {
|
|
4396
|
+
this.fastMode = true;
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
// update test data based on feature/scenario
|
|
4400
|
+
let envName = null;
|
|
4401
|
+
if (this.context && this.context.environment) {
|
|
4402
|
+
envName = this.context.environment.name;
|
|
4403
|
+
}
|
|
4404
|
+
if (!process.env.TEMP_RUN) {
|
|
4405
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4406
|
+
}
|
|
4407
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4408
|
+
if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
|
|
4409
|
+
this.trace = true;
|
|
4410
|
+
const traceFolder = path.join(this.context.reportFolder, "trace");
|
|
4411
|
+
if (!fs.existsSync(traceFolder)) {
|
|
4412
|
+
fs.mkdirSync(traceFolder, { recursive: true });
|
|
4413
|
+
}
|
|
4414
|
+
this.traceFolder = traceFolder;
|
|
4415
|
+
await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
async afterScenario(world, scenario) {
|
|
4419
|
+
const id = scenario.testCaseStartedId;
|
|
4420
|
+
if (this.trace) {
|
|
4421
|
+
await this.context.playContext.tracing.stop({
|
|
4422
|
+
path: path.join(this.traceFolder, `trace-${id}.zip`),
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
getGherkinKeyword(step) {
|
|
4427
|
+
if (!step?.type) {
|
|
4428
|
+
return "";
|
|
4429
|
+
}
|
|
4430
|
+
switch (step.type) {
|
|
4431
|
+
case "Context":
|
|
4432
|
+
return "Given";
|
|
4433
|
+
case "Action":
|
|
4434
|
+
return "When";
|
|
4435
|
+
case "Outcome":
|
|
4436
|
+
return "Then";
|
|
4437
|
+
case "Conjunction":
|
|
4438
|
+
return "And";
|
|
4439
|
+
default:
|
|
4440
|
+
return "";
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
async beforeStep(world, step) {
|
|
4444
|
+
if (step?.pickleStep && this.trace) {
|
|
4445
|
+
const keyword = this.getGherkinKeyword(step.pickleStep);
|
|
4446
|
+
this.traceGroupName = `${keyword} ${step.pickleStep.text}`;
|
|
4447
|
+
await this.context.playContext.tracing.group(this.traceGroupName);
|
|
4448
|
+
}
|
|
4449
|
+
this.stepTags = [];
|
|
4450
|
+
if (!this.beforeScenarioCalled) {
|
|
4451
|
+
this.beforeScenario(world, step);
|
|
4452
|
+
this.context.loadedRoutes = null;
|
|
4453
|
+
}
|
|
4454
|
+
if (this.stepIndex === undefined) {
|
|
4455
|
+
this.stepIndex = 0;
|
|
4456
|
+
}
|
|
4457
|
+
else {
|
|
4458
|
+
this.stepIndex++;
|
|
4459
|
+
}
|
|
4460
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4461
|
+
this.stepName = step.pickleStep.text;
|
|
4462
|
+
let printableStepName = this.stepName;
|
|
4463
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4464
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4465
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4466
|
+
});
|
|
4467
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4468
|
+
}
|
|
4469
|
+
else if (step && step.text) {
|
|
4470
|
+
this.stepName = step.text;
|
|
4471
|
+
}
|
|
4472
|
+
else {
|
|
4473
|
+
this.stepName = "step " + this.stepIndex;
|
|
4474
|
+
}
|
|
4475
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4476
|
+
if (this.context.browserObject.context) {
|
|
4477
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
if (this.initSnapshotTaken === false) {
|
|
4481
|
+
this.initSnapshotTaken = true;
|
|
4482
|
+
if (world &&
|
|
4483
|
+
world.attach &&
|
|
4484
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4485
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4486
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4487
|
+
if (snapshot) {
|
|
4488
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
this.context.routeResults = null;
|
|
4493
|
+
this.context.loadedRoutes = null;
|
|
4494
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4495
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4496
|
+
this.inStepReport = false;
|
|
4497
|
+
}
|
|
4498
|
+
setStepTags(tags) {
|
|
4499
|
+
this.stepTags = tags;
|
|
4500
|
+
}
|
|
4501
|
+
async getAriaSnapshot() {
|
|
4502
|
+
try {
|
|
4503
|
+
// find the page url
|
|
4504
|
+
const url = await this.page.url();
|
|
4505
|
+
// extract the path from the url
|
|
4506
|
+
const path = new URL(url).pathname;
|
|
4507
|
+
// get the page title
|
|
4508
|
+
const title = await this.page.title();
|
|
4509
|
+
// go over other frams
|
|
4510
|
+
const frames = this.page.frames();
|
|
4511
|
+
const snapshots = [];
|
|
4512
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4513
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4514
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4515
|
+
const frame = frames[i];
|
|
4516
|
+
try {
|
|
4517
|
+
// Ensure frame is attached and has body
|
|
4518
|
+
const body = frame.locator("body");
|
|
4519
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4520
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4521
|
+
if (!snapshot) {
|
|
4522
|
+
continue;
|
|
4523
|
+
}
|
|
4524
|
+
content.push(`- frame: ${i}`);
|
|
4525
|
+
content.push(snapshot);
|
|
4526
|
+
}
|
|
4527
|
+
catch (innerErr) {
|
|
4528
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4529
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
return content.join("\n");
|
|
4533
|
+
}
|
|
4534
|
+
catch (e) {
|
|
4535
|
+
console.log("Error in getAriaSnapshot");
|
|
4536
|
+
//console.debug(e);
|
|
4537
|
+
}
|
|
4538
|
+
return null;
|
|
4539
|
+
}
|
|
4540
|
+
/**
|
|
4541
|
+
* Sends command with custom payload to report.
|
|
4542
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4543
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4544
|
+
* @param content - Content of the command to be shown in the report.
|
|
4545
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4546
|
+
* @param world - Optional world context.
|
|
4547
|
+
* @public
|
|
4548
|
+
*/
|
|
4549
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4550
|
+
const state = {
|
|
4551
|
+
options,
|
|
4552
|
+
world,
|
|
4553
|
+
locate: false,
|
|
4554
|
+
scroll: false,
|
|
4555
|
+
screenshot: options.screenshot ?? false,
|
|
4556
|
+
highlight: options.highlight ?? false,
|
|
4557
|
+
type: Types.REPORT_COMMAND,
|
|
4558
|
+
text: commandText,
|
|
4559
|
+
_text: commandText,
|
|
4560
|
+
operation: "report_command",
|
|
4561
|
+
log: "***** " + commandText + " *****\n",
|
|
4562
|
+
};
|
|
4563
|
+
try {
|
|
4564
|
+
await _preCommand(state, this);
|
|
4565
|
+
const payload = {
|
|
4566
|
+
type: options.type ?? "text",
|
|
4567
|
+
content: content,
|
|
4568
|
+
screenshotId: null,
|
|
4569
|
+
};
|
|
4570
|
+
state.payload = payload;
|
|
4571
|
+
if (commandStatus === "FAILED") {
|
|
4572
|
+
state.throwError = true;
|
|
4573
|
+
throw new Error(commandText);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
catch (e) {
|
|
4577
|
+
await _commandError(state, e, this);
|
|
4578
|
+
}
|
|
4579
|
+
finally {
|
|
4580
|
+
await _commandFinally(state, this);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
async afterStep(world, step, result) {
|
|
4584
|
+
this.stepName = null;
|
|
4585
|
+
if (this.context) {
|
|
4586
|
+
this.context.examplesRow = null;
|
|
4587
|
+
}
|
|
4588
|
+
if (!this.inStepReport) {
|
|
4589
|
+
// check the step result
|
|
4590
|
+
if (result && result.status === "FAILED" && world && world.attach) {
|
|
4591
|
+
await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
if (world &&
|
|
4595
|
+
world.attach &&
|
|
4596
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4597
|
+
!this.fastMode &&
|
|
4598
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4599
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4600
|
+
if (snapshot) {
|
|
4601
|
+
const obj = {};
|
|
4602
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4606
|
+
if (this.context.routeResults) {
|
|
4607
|
+
if (world && world.attach) {
|
|
4608
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
if (!process.env.TEMP_RUN) {
|
|
4612
|
+
const state = {
|
|
4613
|
+
world,
|
|
4614
|
+
locate: false,
|
|
4615
|
+
scroll: false,
|
|
4616
|
+
screenshot: true,
|
|
4617
|
+
highlight: true,
|
|
4618
|
+
type: Types.STEP_COMPLETE,
|
|
4619
|
+
text: "end of scenario",
|
|
4620
|
+
_text: "end of scenario",
|
|
4621
|
+
operation: "step_complete",
|
|
4622
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4623
|
+
};
|
|
4624
|
+
try {
|
|
4625
|
+
await _preCommand(state, this);
|
|
4626
|
+
}
|
|
4627
|
+
catch (e) {
|
|
4628
|
+
await _commandError(state, e, this);
|
|
4629
|
+
}
|
|
4630
|
+
finally {
|
|
4631
|
+
await _commandFinally(state, this);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
networkAfterStep(this.stepName, this.context);
|
|
4635
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4636
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4637
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4638
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
if (this.trace) {
|
|
4642
|
+
await this.context.playContext.tracing.groupEnd();
|
|
2713
4643
|
}
|
|
2714
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2715
4644
|
}
|
|
2716
4645
|
}
|
|
2717
4646
|
function createTimedPromise(promise, label) {
|
|
@@ -2719,156 +4648,5 @@ function createTimedPromise(promise, label) {
|
|
|
2719
4648
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2720
4649
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2721
4650
|
}
|
|
2722
|
-
const KEYBOARD_EVENTS = [
|
|
2723
|
-
"ALT",
|
|
2724
|
-
"AltGraph",
|
|
2725
|
-
"CapsLock",
|
|
2726
|
-
"Control",
|
|
2727
|
-
"Fn",
|
|
2728
|
-
"FnLock",
|
|
2729
|
-
"Hyper",
|
|
2730
|
-
"Meta",
|
|
2731
|
-
"NumLock",
|
|
2732
|
-
"ScrollLock",
|
|
2733
|
-
"Shift",
|
|
2734
|
-
"Super",
|
|
2735
|
-
"Symbol",
|
|
2736
|
-
"SymbolLock",
|
|
2737
|
-
"Enter",
|
|
2738
|
-
"Tab",
|
|
2739
|
-
"ArrowDown",
|
|
2740
|
-
"ArrowLeft",
|
|
2741
|
-
"ArrowRight",
|
|
2742
|
-
"ArrowUp",
|
|
2743
|
-
"End",
|
|
2744
|
-
"Home",
|
|
2745
|
-
"PageDown",
|
|
2746
|
-
"PageUp",
|
|
2747
|
-
"Backspace",
|
|
2748
|
-
"Clear",
|
|
2749
|
-
"Copy",
|
|
2750
|
-
"CrSel",
|
|
2751
|
-
"Cut",
|
|
2752
|
-
"Delete",
|
|
2753
|
-
"EraseEof",
|
|
2754
|
-
"ExSel",
|
|
2755
|
-
"Insert",
|
|
2756
|
-
"Paste",
|
|
2757
|
-
"Redo",
|
|
2758
|
-
"Undo",
|
|
2759
|
-
"Accept",
|
|
2760
|
-
"Again",
|
|
2761
|
-
"Attn",
|
|
2762
|
-
"Cancel",
|
|
2763
|
-
"ContextMenu",
|
|
2764
|
-
"Escape",
|
|
2765
|
-
"Execute",
|
|
2766
|
-
"Find",
|
|
2767
|
-
"Finish",
|
|
2768
|
-
"Help",
|
|
2769
|
-
"Pause",
|
|
2770
|
-
"Play",
|
|
2771
|
-
"Props",
|
|
2772
|
-
"Select",
|
|
2773
|
-
"ZoomIn",
|
|
2774
|
-
"ZoomOut",
|
|
2775
|
-
"BrightnessDown",
|
|
2776
|
-
"BrightnessUp",
|
|
2777
|
-
"Eject",
|
|
2778
|
-
"LogOff",
|
|
2779
|
-
"Power",
|
|
2780
|
-
"PowerOff",
|
|
2781
|
-
"PrintScreen",
|
|
2782
|
-
"Hibernate",
|
|
2783
|
-
"Standby",
|
|
2784
|
-
"WakeUp",
|
|
2785
|
-
"AllCandidates",
|
|
2786
|
-
"Alphanumeric",
|
|
2787
|
-
"CodeInput",
|
|
2788
|
-
"Compose",
|
|
2789
|
-
"Convert",
|
|
2790
|
-
"Dead",
|
|
2791
|
-
"FinalMode",
|
|
2792
|
-
"GroupFirst",
|
|
2793
|
-
"GroupLast",
|
|
2794
|
-
"GroupNext",
|
|
2795
|
-
"GroupPrevious",
|
|
2796
|
-
"ModeChange",
|
|
2797
|
-
"NextCandidate",
|
|
2798
|
-
"NonConvert",
|
|
2799
|
-
"PreviousCandidate",
|
|
2800
|
-
"Process",
|
|
2801
|
-
"SingleCandidate",
|
|
2802
|
-
"HangulMode",
|
|
2803
|
-
"HanjaMode",
|
|
2804
|
-
"JunjaMode",
|
|
2805
|
-
"Eisu",
|
|
2806
|
-
"Hankaku",
|
|
2807
|
-
"Hiragana",
|
|
2808
|
-
"HiraganaKatakana",
|
|
2809
|
-
"KanaMode",
|
|
2810
|
-
"KanjiMode",
|
|
2811
|
-
"Katakana",
|
|
2812
|
-
"Romaji",
|
|
2813
|
-
"Zenkaku",
|
|
2814
|
-
"ZenkakuHanaku",
|
|
2815
|
-
"F1",
|
|
2816
|
-
"F2",
|
|
2817
|
-
"F3",
|
|
2818
|
-
"F4",
|
|
2819
|
-
"F5",
|
|
2820
|
-
"F6",
|
|
2821
|
-
"F7",
|
|
2822
|
-
"F8",
|
|
2823
|
-
"F9",
|
|
2824
|
-
"F10",
|
|
2825
|
-
"F11",
|
|
2826
|
-
"F12",
|
|
2827
|
-
"Soft1",
|
|
2828
|
-
"Soft2",
|
|
2829
|
-
"Soft3",
|
|
2830
|
-
"Soft4",
|
|
2831
|
-
"ChannelDown",
|
|
2832
|
-
"ChannelUp",
|
|
2833
|
-
"Close",
|
|
2834
|
-
"MailForward",
|
|
2835
|
-
"MailReply",
|
|
2836
|
-
"MailSend",
|
|
2837
|
-
"MediaFastForward",
|
|
2838
|
-
"MediaPause",
|
|
2839
|
-
"MediaPlay",
|
|
2840
|
-
"MediaPlayPause",
|
|
2841
|
-
"MediaRecord",
|
|
2842
|
-
"MediaRewind",
|
|
2843
|
-
"MediaStop",
|
|
2844
|
-
"MediaTrackNext",
|
|
2845
|
-
"MediaTrackPrevious",
|
|
2846
|
-
"AudioBalanceLeft",
|
|
2847
|
-
"AudioBalanceRight",
|
|
2848
|
-
"AudioBassBoostDown",
|
|
2849
|
-
"AudioBassBoostToggle",
|
|
2850
|
-
"AudioBassBoostUp",
|
|
2851
|
-
"AudioFaderFront",
|
|
2852
|
-
"AudioFaderRear",
|
|
2853
|
-
"AudioSurroundModeNext",
|
|
2854
|
-
"AudioTrebleDown",
|
|
2855
|
-
"AudioTrebleUp",
|
|
2856
|
-
"AudioVolumeDown",
|
|
2857
|
-
"AudioVolumeMute",
|
|
2858
|
-
"AudioVolumeUp",
|
|
2859
|
-
"MicrophoneToggle",
|
|
2860
|
-
"MicrophoneVolumeDown",
|
|
2861
|
-
"MicrophoneVolumeMute",
|
|
2862
|
-
"MicrophoneVolumeUp",
|
|
2863
|
-
"TV",
|
|
2864
|
-
"TV3DMode",
|
|
2865
|
-
"TVAntennaCable",
|
|
2866
|
-
"TVAudioDescription",
|
|
2867
|
-
];
|
|
2868
|
-
function unEscapeString(str) {
|
|
2869
|
-
const placeholder = "__NEWLINE__";
|
|
2870
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2871
|
-
return str;
|
|
2872
|
-
}
|
|
2873
4651
|
export { StableBrowser };
|
|
2874
4652
|
//# sourceMappingURL=stable_browser.js.map
|