automation_model 1.0.515-dev → 1.0.515
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 +3 -2
- package/lib/browser_manager.js +239 -59
- 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 +122 -21
- 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 -99
- 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 +140 -42
- package/lib/stable_browser.js +2431 -676
- 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 +5 -0
- package/lib/test_context.js +4 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +38 -3
- package/lib/utils.js +778 -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
|
+
await _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
|
+
await _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
|
+
await _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
|
+
await _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
|
+
await _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,37 +1442,29 @@ 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) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1464
|
+
// commented out this condition as some select elements have NULL values by default.
|
|
1465
|
+
// if (!values) {
|
|
1466
|
+
// throw new Error("values is null");
|
|
1467
|
+
// }
|
|
1028
1468
|
const state = {
|
|
1029
1469
|
selectors,
|
|
1030
1470
|
_params,
|
|
@@ -1033,6 +1473,7 @@ class StableBrowser {
|
|
|
1033
1473
|
value: values.toString(),
|
|
1034
1474
|
type: Types.SELECT,
|
|
1035
1475
|
text: `Select option: ${values}`,
|
|
1476
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1036
1477
|
operation: "selectOption",
|
|
1037
1478
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1038
1479
|
};
|
|
@@ -1046,14 +1487,14 @@ class StableBrowser {
|
|
|
1046
1487
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1047
1488
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1048
1489
|
}
|
|
1049
|
-
await this.waitForPageLoad();
|
|
1490
|
+
//await this.waitForPageLoad();
|
|
1050
1491
|
return state.info;
|
|
1051
1492
|
}
|
|
1052
1493
|
catch (e) {
|
|
1053
1494
|
await _commandError(state, e, this);
|
|
1054
1495
|
}
|
|
1055
1496
|
finally {
|
|
1056
|
-
_commandFinally(state, this);
|
|
1497
|
+
await _commandFinally(state, this);
|
|
1057
1498
|
}
|
|
1058
1499
|
}
|
|
1059
1500
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1067,6 +1508,7 @@ class StableBrowser {
|
|
|
1067
1508
|
highlight: false,
|
|
1068
1509
|
type: Types.TYPE_PRESS,
|
|
1069
1510
|
text: `Type value: ${_value}`,
|
|
1511
|
+
_text: `Type value: ${_value}`,
|
|
1070
1512
|
operation: "type",
|
|
1071
1513
|
log: "",
|
|
1072
1514
|
};
|
|
@@ -1098,7 +1540,7 @@ class StableBrowser {
|
|
|
1098
1540
|
await _commandError(state, e, this);
|
|
1099
1541
|
}
|
|
1100
1542
|
finally {
|
|
1101
|
-
_commandFinally(state, this);
|
|
1543
|
+
await _commandFinally(state, this);
|
|
1102
1544
|
}
|
|
1103
1545
|
}
|
|
1104
1546
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1134,7 +1576,7 @@ class StableBrowser {
|
|
|
1134
1576
|
await _commandError(state, e, this);
|
|
1135
1577
|
}
|
|
1136
1578
|
finally {
|
|
1137
|
-
_commandFinally(state, this);
|
|
1579
|
+
await _commandFinally(state, this);
|
|
1138
1580
|
}
|
|
1139
1581
|
}
|
|
1140
1582
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1146,14 +1588,15 @@ class StableBrowser {
|
|
|
1146
1588
|
world,
|
|
1147
1589
|
type: Types.SET_DATE_TIME,
|
|
1148
1590
|
text: `Set date time value: ${value}`,
|
|
1591
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1149
1592
|
operation: "setDateTime",
|
|
1150
1593
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1151
|
-
throwError: false,
|
|
1594
|
+
// throwError: false,
|
|
1152
1595
|
};
|
|
1153
1596
|
try {
|
|
1154
1597
|
await _preCommand(state, this);
|
|
1155
1598
|
try {
|
|
1156
|
-
await state.element
|
|
1599
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1157
1600
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1158
1601
|
if (format) {
|
|
1159
1602
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1202,7 +1645,7 @@ class StableBrowser {
|
|
|
1202
1645
|
await _commandError(state, e, this);
|
|
1203
1646
|
}
|
|
1204
1647
|
finally {
|
|
1205
|
-
_commandFinally(state, this);
|
|
1648
|
+
await _commandFinally(state, this);
|
|
1206
1649
|
}
|
|
1207
1650
|
}
|
|
1208
1651
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1217,17 +1660,29 @@ class StableBrowser {
|
|
|
1217
1660
|
world,
|
|
1218
1661
|
type: Types.FILL,
|
|
1219
1662
|
text: `Click type input with value: ${_value}`,
|
|
1663
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1220
1664
|
operation: "clickType",
|
|
1221
1665
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1222
1666
|
};
|
|
1667
|
+
if (!options) {
|
|
1668
|
+
options = {};
|
|
1669
|
+
}
|
|
1223
1670
|
if (newValue !== _value) {
|
|
1224
1671
|
//this.logger.info(_value + "=" + newValue);
|
|
1225
1672
|
_value = newValue;
|
|
1226
1673
|
}
|
|
1227
1674
|
try {
|
|
1228
1675
|
await _preCommand(state, this);
|
|
1676
|
+
const randomToken = "blinq_" + Math.random().toString(36).substring(7);
|
|
1677
|
+
// tag the element
|
|
1678
|
+
let newElementSelector = await state.element.evaluate((el, token) => {
|
|
1679
|
+
// use attribute and not id
|
|
1680
|
+
const attrName = `data-blinq-id-${token}`;
|
|
1681
|
+
el.setAttribute(attrName, "");
|
|
1682
|
+
return `[${attrName}]`;
|
|
1683
|
+
}, randomToken);
|
|
1229
1684
|
state.info.value = _value;
|
|
1230
|
-
if (
|
|
1685
|
+
if (!options.press) {
|
|
1231
1686
|
try {
|
|
1232
1687
|
let currentValue = await state.element.inputValue();
|
|
1233
1688
|
if (currentValue) {
|
|
@@ -1238,13 +1693,9 @@ class StableBrowser {
|
|
|
1238
1693
|
this.logger.info("unable to clear input value");
|
|
1239
1694
|
}
|
|
1240
1695
|
}
|
|
1241
|
-
if (options
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
}
|
|
1245
|
-
catch (e) {
|
|
1246
|
-
await state.element.dispatchEvent("click");
|
|
1247
|
-
}
|
|
1696
|
+
if (options.press) {
|
|
1697
|
+
options.timeout = 5000;
|
|
1698
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1248
1699
|
}
|
|
1249
1700
|
else {
|
|
1250
1701
|
try {
|
|
@@ -1255,6 +1706,25 @@ class StableBrowser {
|
|
|
1255
1706
|
}
|
|
1256
1707
|
}
|
|
1257
1708
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1709
|
+
// check if the element exist after the click (no wait)
|
|
1710
|
+
const count = await state.element.count({ timeout: 0 });
|
|
1711
|
+
if (count === 0) {
|
|
1712
|
+
// the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
|
|
1713
|
+
const scope = state.element._frame ?? element.page();
|
|
1714
|
+
let prefixSelector = "";
|
|
1715
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
1716
|
+
const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
|
|
1717
|
+
if (frameSelectorIndex !== -1) {
|
|
1718
|
+
// remove everything after the >> internal:control=enter-frame
|
|
1719
|
+
const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
|
|
1720
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
|
|
1721
|
+
}
|
|
1722
|
+
// if (element?._frame?._selector) {
|
|
1723
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
1724
|
+
// }
|
|
1725
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
1726
|
+
state.element = scope.locator(newSelector).first();
|
|
1727
|
+
}
|
|
1258
1728
|
const valueSegment = state.value.split("&&");
|
|
1259
1729
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1260
1730
|
if (i > 0) {
|
|
@@ -1275,14 +1745,21 @@ class StableBrowser {
|
|
|
1275
1745
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1276
1746
|
}
|
|
1277
1747
|
}
|
|
1748
|
+
//if (!this.fastMode) {
|
|
1278
1749
|
await _screenshot(state, this);
|
|
1750
|
+
//}
|
|
1279
1751
|
if (enter === true) {
|
|
1280
1752
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1281
1753
|
await this.page.keyboard.press("Enter");
|
|
1282
1754
|
await this.waitForPageLoad();
|
|
1283
1755
|
}
|
|
1284
1756
|
else if (enter === false) {
|
|
1285
|
-
|
|
1757
|
+
try {
|
|
1758
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1759
|
+
}
|
|
1760
|
+
catch (e) {
|
|
1761
|
+
// ignore
|
|
1762
|
+
}
|
|
1286
1763
|
//await this.page.keyboard.press("Tab");
|
|
1287
1764
|
}
|
|
1288
1765
|
else {
|
|
@@ -1297,7 +1774,7 @@ class StableBrowser {
|
|
|
1297
1774
|
await _commandError(state, e, this);
|
|
1298
1775
|
}
|
|
1299
1776
|
finally {
|
|
1300
|
-
_commandFinally(state, this);
|
|
1777
|
+
await _commandFinally(state, this);
|
|
1301
1778
|
}
|
|
1302
1779
|
}
|
|
1303
1780
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1319,30 +1796,67 @@ class StableBrowser {
|
|
|
1319
1796
|
if (enter) {
|
|
1320
1797
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1321
1798
|
await this.page.keyboard.press("Enter");
|
|
1799
|
+
await this.waitForPageLoad();
|
|
1322
1800
|
}
|
|
1323
|
-
await this.waitForPageLoad();
|
|
1324
1801
|
return state.info;
|
|
1325
1802
|
}
|
|
1326
1803
|
catch (e) {
|
|
1327
1804
|
await _commandError(state, e, this);
|
|
1328
1805
|
}
|
|
1329
1806
|
finally {
|
|
1330
|
-
_commandFinally(state, this);
|
|
1807
|
+
await _commandFinally(state, this);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1811
|
+
const state = {
|
|
1812
|
+
selectors,
|
|
1813
|
+
_params,
|
|
1814
|
+
files,
|
|
1815
|
+
value: '"' + files.join('", "') + '"',
|
|
1816
|
+
options,
|
|
1817
|
+
world,
|
|
1818
|
+
type: Types.SET_INPUT_FILES,
|
|
1819
|
+
text: `Set input files`,
|
|
1820
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1821
|
+
operation: "setInputFiles",
|
|
1822
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1823
|
+
};
|
|
1824
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1825
|
+
try {
|
|
1826
|
+
await _preCommand(state, this);
|
|
1827
|
+
for (let i = 0; i < files.length; i++) {
|
|
1828
|
+
const file = files[i];
|
|
1829
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1830
|
+
if (!fs.existsSync(filePath)) {
|
|
1831
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1832
|
+
}
|
|
1833
|
+
state.files[i] = filePath;
|
|
1834
|
+
}
|
|
1835
|
+
await state.element.setInputFiles(files);
|
|
1836
|
+
return state.info;
|
|
1837
|
+
}
|
|
1838
|
+
catch (e) {
|
|
1839
|
+
await _commandError(state, e, this);
|
|
1840
|
+
}
|
|
1841
|
+
finally {
|
|
1842
|
+
await _commandFinally(state, this);
|
|
1331
1843
|
}
|
|
1332
1844
|
}
|
|
1333
1845
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1334
1846
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1335
1847
|
}
|
|
1336
1848
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1849
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1337
1850
|
_validateSelectors(selectors);
|
|
1338
1851
|
let screenshotId = null;
|
|
1339
1852
|
let screenshotPath = null;
|
|
1340
1853
|
if (!info.log) {
|
|
1341
1854
|
info.log = "";
|
|
1855
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1342
1856
|
}
|
|
1343
1857
|
info.operation = "getText";
|
|
1344
1858
|
info.selectors = selectors;
|
|
1345
|
-
let element = await this._locate(selectors, info, _params);
|
|
1859
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1346
1860
|
if (climb > 0) {
|
|
1347
1861
|
const climbArray = [];
|
|
1348
1862
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1361,6 +1875,18 @@ class StableBrowser {
|
|
|
1361
1875
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1362
1876
|
try {
|
|
1363
1877
|
await this._highlightElements(element);
|
|
1878
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1879
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1880
|
+
// this._highlightElements(element)
|
|
1881
|
+
// .then(async () => {
|
|
1882
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1883
|
+
// this._unhighlightElements(element).then(
|
|
1884
|
+
// () => {}
|
|
1885
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1886
|
+
// );
|
|
1887
|
+
// })
|
|
1888
|
+
// .catch(e);
|
|
1889
|
+
// }
|
|
1364
1890
|
const elementText = await element.innerText();
|
|
1365
1891
|
return {
|
|
1366
1892
|
text: elementText,
|
|
@@ -1372,7 +1898,7 @@ class StableBrowser {
|
|
|
1372
1898
|
}
|
|
1373
1899
|
catch (e) {
|
|
1374
1900
|
//await this.closeUnexpectedPopups();
|
|
1375
|
-
this.logger.info("no innerText will use textContent");
|
|
1901
|
+
this.logger.info("no innerText, will use textContent");
|
|
1376
1902
|
const elementText = await element.textContent();
|
|
1377
1903
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1378
1904
|
}
|
|
@@ -1397,6 +1923,7 @@ class StableBrowser {
|
|
|
1397
1923
|
highlight: false,
|
|
1398
1924
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1399
1925
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1926
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1400
1927
|
operation: "containsPattern",
|
|
1401
1928
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1402
1929
|
};
|
|
@@ -1428,10 +1955,12 @@ class StableBrowser {
|
|
|
1428
1955
|
await _commandError(state, e, this);
|
|
1429
1956
|
}
|
|
1430
1957
|
finally {
|
|
1431
|
-
_commandFinally(state, this);
|
|
1958
|
+
await _commandFinally(state, this);
|
|
1432
1959
|
}
|
|
1433
1960
|
}
|
|
1434
1961
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1962
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1963
|
+
const startTime = Date.now();
|
|
1435
1964
|
const state = {
|
|
1436
1965
|
selectors,
|
|
1437
1966
|
_params,
|
|
@@ -1458,61 +1987,137 @@ class StableBrowser {
|
|
|
1458
1987
|
}
|
|
1459
1988
|
let foundObj = null;
|
|
1460
1989
|
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;
|
|
1990
|
+
while (Date.now() - startTime < timeout) {
|
|
1991
|
+
try {
|
|
1992
|
+
await _preCommand(state, this);
|
|
1993
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1994
|
+
if (foundObj && foundObj.element) {
|
|
1995
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1474
1996
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1997
|
+
await _screenshot(state, this);
|
|
1998
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1999
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
2000
|
+
if (dateAlternatives.date) {
|
|
2001
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
2002
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
2003
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
2004
|
+
return state.info;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
else if (numberAlternatives.number) {
|
|
2009
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
2010
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
2011
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
2012
|
+
return state.info;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1482
2017
|
return state.info;
|
|
1483
2018
|
}
|
|
1484
2019
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
throw new Error("element doesn't contain text " + text);
|
|
2020
|
+
catch (e) {
|
|
2021
|
+
// Log error but continue retrying until timeout is reached
|
|
2022
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
2023
|
+
}
|
|
2024
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1491
2025
|
}
|
|
1492
|
-
|
|
2026
|
+
state.info.foundText = foundObj?.text;
|
|
2027
|
+
state.info.value = foundObj?.value;
|
|
2028
|
+
throw new Error("element doesn't contain text " + text);
|
|
1493
2029
|
}
|
|
1494
2030
|
catch (e) {
|
|
1495
2031
|
await _commandError(state, e, this);
|
|
2032
|
+
throw e;
|
|
1496
2033
|
}
|
|
1497
2034
|
finally {
|
|
1498
|
-
_commandFinally(state, this);
|
|
2035
|
+
await _commandFinally(state, this);
|
|
1499
2036
|
}
|
|
1500
2037
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2038
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
2039
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2040
|
+
const startTime = Date.now();
|
|
2041
|
+
const state = {
|
|
2042
|
+
_params,
|
|
2043
|
+
value: referanceSnapshot,
|
|
2044
|
+
options,
|
|
2045
|
+
world,
|
|
2046
|
+
locate: false,
|
|
2047
|
+
scroll: false,
|
|
2048
|
+
screenshot: true,
|
|
2049
|
+
highlight: false,
|
|
2050
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
2051
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
2052
|
+
operation: "snapshotValidation",
|
|
2053
|
+
log: "***** verify snapshot *****\n",
|
|
2054
|
+
};
|
|
2055
|
+
if (!referanceSnapshot) {
|
|
2056
|
+
throw new Error("referanceSnapshot is null");
|
|
2057
|
+
}
|
|
2058
|
+
let text = null;
|
|
2059
|
+
const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
|
|
2060
|
+
if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
|
|
2061
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
|
|
1505
2062
|
}
|
|
1506
|
-
else if (
|
|
1507
|
-
|
|
2063
|
+
else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
|
|
2064
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
|
|
1508
2065
|
}
|
|
1509
|
-
else if (
|
|
1510
|
-
|
|
2066
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2067
|
+
text = referanceSnapshot.substring(5);
|
|
1511
2068
|
}
|
|
1512
2069
|
else {
|
|
1513
|
-
|
|
2070
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
2071
|
+
}
|
|
2072
|
+
state.text = text;
|
|
2073
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2074
|
+
await _preCommand(state, this);
|
|
2075
|
+
let foundObj = null;
|
|
2076
|
+
try {
|
|
2077
|
+
let matchResult = null;
|
|
2078
|
+
while (Date.now() - startTime < timeout) {
|
|
2079
|
+
try {
|
|
2080
|
+
let scope = null;
|
|
2081
|
+
if (!frameSelectors) {
|
|
2082
|
+
scope = this.page;
|
|
2083
|
+
}
|
|
2084
|
+
else {
|
|
2085
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
2086
|
+
}
|
|
2087
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
2088
|
+
if (snapshot && snapshot.length <= 10) {
|
|
2089
|
+
console.log("Page snapshot length is suspiciously small:", snapshot);
|
|
2090
|
+
}
|
|
2091
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
2092
|
+
if (matchResult === undefined) {
|
|
2093
|
+
console.log("snapshotValidation returned undefined");
|
|
2094
|
+
}
|
|
2095
|
+
if (matchResult.errorLine !== -1) {
|
|
2096
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
2097
|
+
}
|
|
2098
|
+
// highlight and screenshot
|
|
2099
|
+
try {
|
|
2100
|
+
await await highlightSnapshot(newValue, scope);
|
|
2101
|
+
await _screenshot(state, this);
|
|
2102
|
+
}
|
|
2103
|
+
catch (e) { }
|
|
2104
|
+
return state.info;
|
|
2105
|
+
}
|
|
2106
|
+
catch (e) {
|
|
2107
|
+
// Log error but continue retrying until timeout is reached
|
|
2108
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
2109
|
+
}
|
|
2110
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
2111
|
+
}
|
|
2112
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
2113
|
+
}
|
|
2114
|
+
catch (e) {
|
|
2115
|
+
await _commandError(state, e, this);
|
|
2116
|
+
throw e;
|
|
2117
|
+
}
|
|
2118
|
+
finally {
|
|
2119
|
+
await _commandFinally(state, this);
|
|
1514
2120
|
}
|
|
1515
|
-
return dataFile;
|
|
1516
2121
|
}
|
|
1517
2122
|
async waitForUserInput(message, world = null) {
|
|
1518
2123
|
if (!message) {
|
|
@@ -1542,13 +2147,22 @@ class StableBrowser {
|
|
|
1542
2147
|
return;
|
|
1543
2148
|
}
|
|
1544
2149
|
// if data file exists, load it
|
|
1545
|
-
const dataFile =
|
|
2150
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1546
2151
|
let data = this.getTestData(world);
|
|
1547
2152
|
// merge the testData with the existing data
|
|
1548
2153
|
Object.assign(data, testData);
|
|
1549
2154
|
// save the data to the file
|
|
1550
2155
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1551
2156
|
}
|
|
2157
|
+
overwriteTestData(testData, world = null) {
|
|
2158
|
+
if (!testData) {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
// if data file exists, load it
|
|
2162
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2163
|
+
// save the data to the file
|
|
2164
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
2165
|
+
}
|
|
1552
2166
|
_getDataFilePath(fileName) {
|
|
1553
2167
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1554
2168
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1645,14 +2259,12 @@ class StableBrowser {
|
|
|
1645
2259
|
}
|
|
1646
2260
|
}
|
|
1647
2261
|
getTestData(world = null) {
|
|
1648
|
-
|
|
1649
|
-
let data = {};
|
|
1650
|
-
if (fs.existsSync(dataFile)) {
|
|
1651
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1652
|
-
}
|
|
1653
|
-
return data;
|
|
2262
|
+
return _getTestData(world, this.context, this);
|
|
1654
2263
|
}
|
|
1655
2264
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2265
|
+
if (!options) {
|
|
2266
|
+
options = {};
|
|
2267
|
+
}
|
|
1656
2268
|
// collect url/path/title
|
|
1657
2269
|
if (info) {
|
|
1658
2270
|
if (!info.title) {
|
|
@@ -1677,13 +2289,11 @@ class StableBrowser {
|
|
|
1677
2289
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1678
2290
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1679
2291
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
1684
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
2292
|
+
// to make sure the path doesn't start with -
|
|
2293
|
+
const uuidStr = "id_" + randomUUID();
|
|
2294
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1685
2295
|
try {
|
|
1686
|
-
await this.takeScreenshot(screenshotPath);
|
|
2296
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
1687
2297
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1688
2298
|
// // save the buffer to the screenshot path asynchrously
|
|
1689
2299
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -1691,20 +2301,20 @@ class StableBrowser {
|
|
|
1691
2301
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1692
2302
|
// }
|
|
1693
2303
|
// });
|
|
2304
|
+
result.screenshotId = uuidStr;
|
|
2305
|
+
result.screenshotPath = screenshotPath;
|
|
2306
|
+
if (info && info.box) {
|
|
2307
|
+
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
2308
|
+
}
|
|
1694
2309
|
}
|
|
1695
2310
|
catch (e) {
|
|
1696
2311
|
this.logger.info("unable to take screenshot, ignored");
|
|
1697
2312
|
}
|
|
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
2313
|
}
|
|
1704
2314
|
else if (options && options.screenshot) {
|
|
1705
2315
|
result.screenshotPath = options.screenshotPath;
|
|
1706
2316
|
try {
|
|
1707
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2317
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
1708
2318
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1709
2319
|
// // save the buffer to the screenshot path asynchrously
|
|
1710
2320
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -1722,7 +2332,7 @@ class StableBrowser {
|
|
|
1722
2332
|
}
|
|
1723
2333
|
return result;
|
|
1724
2334
|
}
|
|
1725
|
-
async takeScreenshot(screenshotPath) {
|
|
2335
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
1726
2336
|
const playContext = this.context.playContext;
|
|
1727
2337
|
// Using CDP to capture the screenshot
|
|
1728
2338
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -1734,17 +2344,20 @@ class StableBrowser {
|
|
|
1734
2344
|
document.documentElement.clientWidth,
|
|
1735
2345
|
])));
|
|
1736
2346
|
let screenshotBuffer = null;
|
|
2347
|
+
// if (focusedElement) {
|
|
2348
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2349
|
+
// await this._unhighlightElements(focusedElement);
|
|
2350
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2351
|
+
// console.log(`Unhighlighted previous element`);
|
|
2352
|
+
// }
|
|
2353
|
+
// if (focusedElement) {
|
|
2354
|
+
// await this._highlightElements(focusedElement);
|
|
2355
|
+
// }
|
|
1737
2356
|
if (this.context.browserName === "chromium") {
|
|
1738
2357
|
const client = await playContext.newCDPSession(this.page);
|
|
1739
2358
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1740
2359
|
format: "png",
|
|
1741
|
-
|
|
1742
|
-
// x: 0,
|
|
1743
|
-
// y: 0,
|
|
1744
|
-
// width: viewportWidth,
|
|
1745
|
-
// height: viewportHeight,
|
|
1746
|
-
// scale: 1,
|
|
1747
|
-
// },
|
|
2360
|
+
captureBeyondViewport: fullPage,
|
|
1748
2361
|
});
|
|
1749
2362
|
await client.detach();
|
|
1750
2363
|
if (!screenshotPath) {
|
|
@@ -1753,8 +2366,12 @@ class StableBrowser {
|
|
|
1753
2366
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
1754
2367
|
}
|
|
1755
2368
|
else {
|
|
1756
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2369
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
1757
2370
|
}
|
|
2371
|
+
// if (focusedElement) {
|
|
2372
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2373
|
+
// await this._unhighlightElements(focusedElement);
|
|
2374
|
+
// }
|
|
1758
2375
|
let image = await Jimp.read(screenshotBuffer);
|
|
1759
2376
|
// Get the image dimensions
|
|
1760
2377
|
const { width, height } = image.bitmap;
|
|
@@ -1767,6 +2384,7 @@ class StableBrowser {
|
|
|
1767
2384
|
else {
|
|
1768
2385
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1769
2386
|
}
|
|
2387
|
+
return screenshotBuffer;
|
|
1770
2388
|
}
|
|
1771
2389
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1772
2390
|
const state = {
|
|
@@ -1789,7 +2407,7 @@ class StableBrowser {
|
|
|
1789
2407
|
await _commandError(state, e, this);
|
|
1790
2408
|
}
|
|
1791
2409
|
finally {
|
|
1792
|
-
_commandFinally(state, this);
|
|
2410
|
+
await _commandFinally(state, this);
|
|
1793
2411
|
}
|
|
1794
2412
|
}
|
|
1795
2413
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1802,8 +2420,10 @@ class StableBrowser {
|
|
|
1802
2420
|
world,
|
|
1803
2421
|
type: Types.EXTRACT,
|
|
1804
2422
|
text: `Extract attribute from element`,
|
|
2423
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1805
2424
|
operation: "extractAttribute",
|
|
1806
2425
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2426
|
+
allowDisabled: true,
|
|
1807
2427
|
};
|
|
1808
2428
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1809
2429
|
try {
|
|
@@ -1818,22 +2438,480 @@ class StableBrowser {
|
|
|
1818
2438
|
case "value":
|
|
1819
2439
|
state.value = await state.element.inputValue();
|
|
1820
2440
|
break;
|
|
2441
|
+
case "text":
|
|
2442
|
+
state.value = await state.element.textContent();
|
|
2443
|
+
break;
|
|
1821
2444
|
default:
|
|
1822
2445
|
state.value = await state.element.getAttribute(attribute);
|
|
1823
2446
|
break;
|
|
1824
2447
|
}
|
|
2448
|
+
if (options !== null) {
|
|
2449
|
+
if (options.regex && options.regex !== "") {
|
|
2450
|
+
// Construct a regex pattern from the provided string
|
|
2451
|
+
const regex = options.regex.slice(1, -1);
|
|
2452
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2453
|
+
const matches = state.value.match(regexPattern);
|
|
2454
|
+
if (matches) {
|
|
2455
|
+
let newValue = "";
|
|
2456
|
+
for (const match of matches) {
|
|
2457
|
+
newValue += match;
|
|
2458
|
+
}
|
|
2459
|
+
state.value = newValue;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2463
|
+
state.value = state.value.trim();
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
1825
2466
|
state.info.value = state.value;
|
|
1826
2467
|
this.setTestData({ [variable]: state.value }, world);
|
|
1827
2468
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2469
|
+
if (process.env.MODE === "executions") {
|
|
2470
|
+
const globalDataFile = "global_test_data.json";
|
|
2471
|
+
if (existsSync(globalDataFile)) {
|
|
2472
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1828
2476
|
return state.info;
|
|
1829
2477
|
}
|
|
1830
2478
|
catch (e) {
|
|
1831
2479
|
await _commandError(state, e, this);
|
|
1832
2480
|
}
|
|
1833
2481
|
finally {
|
|
1834
|
-
_commandFinally(state, this);
|
|
2482
|
+
await _commandFinally(state, this);
|
|
1835
2483
|
}
|
|
1836
2484
|
}
|
|
2485
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2486
|
+
const state = {
|
|
2487
|
+
selectors,
|
|
2488
|
+
_params,
|
|
2489
|
+
property,
|
|
2490
|
+
variable,
|
|
2491
|
+
options,
|
|
2492
|
+
world,
|
|
2493
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2494
|
+
text: `Extract property from element`,
|
|
2495
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2496
|
+
operation: "extractProperty",
|
|
2497
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2498
|
+
allowDisabled: true,
|
|
2499
|
+
};
|
|
2500
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2501
|
+
try {
|
|
2502
|
+
await _preCommand(state, this);
|
|
2503
|
+
switch (property) {
|
|
2504
|
+
case "inner_text":
|
|
2505
|
+
state.value = await state.element.innerText();
|
|
2506
|
+
break;
|
|
2507
|
+
case "href":
|
|
2508
|
+
state.value = await state.element.getAttribute("href");
|
|
2509
|
+
break;
|
|
2510
|
+
case "value":
|
|
2511
|
+
state.value = await state.element.inputValue();
|
|
2512
|
+
break;
|
|
2513
|
+
case "text":
|
|
2514
|
+
state.value = await state.element.textContent();
|
|
2515
|
+
break;
|
|
2516
|
+
default:
|
|
2517
|
+
if (property.startsWith("dataset.")) {
|
|
2518
|
+
const dataAttribute = property.substring(8);
|
|
2519
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2520
|
+
}
|
|
2521
|
+
else {
|
|
2522
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (options !== null) {
|
|
2526
|
+
if (options.regex && options.regex !== "") {
|
|
2527
|
+
// Construct a regex pattern from the provided string
|
|
2528
|
+
const regex = options.regex.slice(1, -1);
|
|
2529
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2530
|
+
const matches = state.value.match(regexPattern);
|
|
2531
|
+
if (matches) {
|
|
2532
|
+
let newValue = "";
|
|
2533
|
+
for (const match of matches) {
|
|
2534
|
+
newValue += match;
|
|
2535
|
+
}
|
|
2536
|
+
state.value = newValue;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2540
|
+
state.value = state.value.trim();
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
state.info.value = state.value;
|
|
2544
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2545
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2546
|
+
if (process.env.MODE === "executions") {
|
|
2547
|
+
const globalDataFile = "global_test_data.json";
|
|
2548
|
+
if (existsSync(globalDataFile)) {
|
|
2549
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2553
|
+
return state.info;
|
|
2554
|
+
}
|
|
2555
|
+
catch (e) {
|
|
2556
|
+
await _commandError(state, e, this);
|
|
2557
|
+
}
|
|
2558
|
+
finally {
|
|
2559
|
+
await _commandFinally(state, this);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2563
|
+
const state = {
|
|
2564
|
+
selectors,
|
|
2565
|
+
_params,
|
|
2566
|
+
attribute,
|
|
2567
|
+
value,
|
|
2568
|
+
options,
|
|
2569
|
+
world,
|
|
2570
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
2571
|
+
highlight: true,
|
|
2572
|
+
screenshot: true,
|
|
2573
|
+
text: `Verify element attribute`,
|
|
2574
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
2575
|
+
operation: "verifyAttribute",
|
|
2576
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2577
|
+
allowDisabled: true,
|
|
2578
|
+
};
|
|
2579
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2580
|
+
let val;
|
|
2581
|
+
let expectedValue;
|
|
2582
|
+
try {
|
|
2583
|
+
await _preCommand(state, this);
|
|
2584
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2585
|
+
state.info.expectedValue = expectedValue;
|
|
2586
|
+
switch (attribute) {
|
|
2587
|
+
case "innerText":
|
|
2588
|
+
val = String(await state.element.innerText());
|
|
2589
|
+
break;
|
|
2590
|
+
case "text":
|
|
2591
|
+
val = String(await state.element.textContent());
|
|
2592
|
+
break;
|
|
2593
|
+
case "value":
|
|
2594
|
+
val = String(await state.element.inputValue());
|
|
2595
|
+
break;
|
|
2596
|
+
case "checked":
|
|
2597
|
+
val = String(await state.element.isChecked());
|
|
2598
|
+
break;
|
|
2599
|
+
case "disabled":
|
|
2600
|
+
val = String(await state.element.isDisabled());
|
|
2601
|
+
break;
|
|
2602
|
+
case "readOnly":
|
|
2603
|
+
const isEditable = await state.element.isEditable();
|
|
2604
|
+
val = String(!isEditable);
|
|
2605
|
+
break;
|
|
2606
|
+
default:
|
|
2607
|
+
val = String(await state.element.getAttribute(attribute));
|
|
2608
|
+
break;
|
|
2609
|
+
}
|
|
2610
|
+
state.info.value = val;
|
|
2611
|
+
let regex;
|
|
2612
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2613
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2614
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2615
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2616
|
+
state.info.regex = true;
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2620
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2621
|
+
}
|
|
2622
|
+
if (attribute === "innerText") {
|
|
2623
|
+
if (state.info.regex) {
|
|
2624
|
+
if (!regex.test(val)) {
|
|
2625
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2626
|
+
state.info.failCause.assertionFailed = true;
|
|
2627
|
+
state.info.failCause.lastError = errorMessage;
|
|
2628
|
+
throw new Error(errorMessage);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
const valLines = val.split("\n");
|
|
2633
|
+
const expectedLines = expectedValue.split("\n");
|
|
2634
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2635
|
+
if (!isPart) {
|
|
2636
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2637
|
+
state.info.failCause.assertionFailed = true;
|
|
2638
|
+
state.info.failCause.lastError = errorMessage;
|
|
2639
|
+
throw new Error(errorMessage);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
else {
|
|
2644
|
+
if (!val.match(regex)) {
|
|
2645
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2646
|
+
state.info.failCause.assertionFailed = true;
|
|
2647
|
+
state.info.failCause.lastError = errorMessage;
|
|
2648
|
+
throw new Error(errorMessage);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return state.info;
|
|
2652
|
+
}
|
|
2653
|
+
catch (e) {
|
|
2654
|
+
await _commandError(state, e, this);
|
|
2655
|
+
}
|
|
2656
|
+
finally {
|
|
2657
|
+
await _commandFinally(state, this);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2661
|
+
const state = {
|
|
2662
|
+
selectors,
|
|
2663
|
+
_params,
|
|
2664
|
+
property,
|
|
2665
|
+
value,
|
|
2666
|
+
options,
|
|
2667
|
+
world,
|
|
2668
|
+
type: Types.VERIFY_PROPERTY,
|
|
2669
|
+
highlight: true,
|
|
2670
|
+
screenshot: true,
|
|
2671
|
+
text: `Verify element property`,
|
|
2672
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2673
|
+
operation: "verifyProperty",
|
|
2674
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2675
|
+
allowDisabled: true,
|
|
2676
|
+
};
|
|
2677
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2678
|
+
let val;
|
|
2679
|
+
let expectedValue;
|
|
2680
|
+
try {
|
|
2681
|
+
await _preCommand(state, this);
|
|
2682
|
+
expectedValue = await this._replaceWithLocalData(value, world);
|
|
2683
|
+
state.info.expectedValue = expectedValue;
|
|
2684
|
+
switch (property) {
|
|
2685
|
+
case "innerText":
|
|
2686
|
+
val = String(await state.element.innerText());
|
|
2687
|
+
break;
|
|
2688
|
+
case "text":
|
|
2689
|
+
val = String(await state.element.textContent());
|
|
2690
|
+
break;
|
|
2691
|
+
case "value":
|
|
2692
|
+
val = String(await state.element.inputValue());
|
|
2693
|
+
break;
|
|
2694
|
+
case "checked":
|
|
2695
|
+
val = String(await state.element.isChecked());
|
|
2696
|
+
break;
|
|
2697
|
+
case "disabled":
|
|
2698
|
+
val = String(await state.element.isDisabled());
|
|
2699
|
+
break;
|
|
2700
|
+
case "readOnly":
|
|
2701
|
+
const isEditable = await state.element.isEditable();
|
|
2702
|
+
val = String(!isEditable);
|
|
2703
|
+
break;
|
|
2704
|
+
case "innerHTML":
|
|
2705
|
+
val = String(await state.element.innerHTML());
|
|
2706
|
+
break;
|
|
2707
|
+
case "outerHTML":
|
|
2708
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2709
|
+
break;
|
|
2710
|
+
default:
|
|
2711
|
+
if (property.startsWith("dataset.")) {
|
|
2712
|
+
const dataAttribute = property.substring(8);
|
|
2713
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2714
|
+
}
|
|
2715
|
+
else {
|
|
2716
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
// Helper function to remove all style="" attributes
|
|
2720
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2721
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2722
|
+
};
|
|
2723
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2724
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2725
|
+
val = removeStyleAttributes(val);
|
|
2726
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2727
|
+
}
|
|
2728
|
+
state.info.value = val;
|
|
2729
|
+
let regex;
|
|
2730
|
+
state.info.value = val;
|
|
2731
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2732
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2733
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2734
|
+
let matchPassed = false;
|
|
2735
|
+
if (isRegex) {
|
|
2736
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2737
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2738
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2739
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2740
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2741
|
+
const regex = new RegExp(patternBody, flags);
|
|
2742
|
+
state.info.regex = true;
|
|
2743
|
+
matchPassed = regex.test(val);
|
|
2744
|
+
}
|
|
2745
|
+
else {
|
|
2746
|
+
// Fallback: treat as literal
|
|
2747
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2748
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2749
|
+
matchPassed = regex.test(val);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
else if (isContains) {
|
|
2753
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2754
|
+
matchPassed = val.includes(containsValue);
|
|
2755
|
+
}
|
|
2756
|
+
else if (isExact) {
|
|
2757
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2758
|
+
matchPassed = val === exactValue;
|
|
2759
|
+
}
|
|
2760
|
+
else if (property === "innerText") {
|
|
2761
|
+
// Default innerText logic
|
|
2762
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2763
|
+
const valLines = val.split("\n");
|
|
2764
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2765
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2766
|
+
}
|
|
2767
|
+
else {
|
|
2768
|
+
// Fallback exact or loose match
|
|
2769
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2770
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2771
|
+
matchPassed = regex.test(val);
|
|
2772
|
+
}
|
|
2773
|
+
if (!matchPassed) {
|
|
2774
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2775
|
+
state.info.failCause.assertionFailed = true;
|
|
2776
|
+
state.info.failCause.lastError = errorMessage;
|
|
2777
|
+
throw new Error(errorMessage);
|
|
2778
|
+
}
|
|
2779
|
+
return state.info;
|
|
2780
|
+
}
|
|
2781
|
+
catch (e) {
|
|
2782
|
+
await _commandError(state, e, this);
|
|
2783
|
+
}
|
|
2784
|
+
finally {
|
|
2785
|
+
await _commandFinally(state, this);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2789
|
+
// Convert timeout from seconds to milliseconds
|
|
2790
|
+
const timeoutMs = timeout * 1000;
|
|
2791
|
+
const state = {
|
|
2792
|
+
selectors,
|
|
2793
|
+
_params,
|
|
2794
|
+
condition,
|
|
2795
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2796
|
+
options,
|
|
2797
|
+
world,
|
|
2798
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2799
|
+
highlight: true,
|
|
2800
|
+
screenshot: true,
|
|
2801
|
+
text: `Conditional wait for element`,
|
|
2802
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2803
|
+
operation: "conditionalWait",
|
|
2804
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2805
|
+
allowDisabled: true,
|
|
2806
|
+
info: {},
|
|
2807
|
+
};
|
|
2808
|
+
state.options ??= { timeout: timeoutMs };
|
|
2809
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2810
|
+
const startTime = Date.now();
|
|
2811
|
+
let conditionMet = false;
|
|
2812
|
+
let currentValue = null;
|
|
2813
|
+
let lastError = null;
|
|
2814
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2815
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2816
|
+
const elapsedTime = Date.now() - startTime;
|
|
2817
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2818
|
+
try {
|
|
2819
|
+
// Try to execute _preCommand (element location)
|
|
2820
|
+
await _preCommand(state, this);
|
|
2821
|
+
// If _preCommand succeeds, start condition checking
|
|
2822
|
+
const checkCondition = async () => {
|
|
2823
|
+
try {
|
|
2824
|
+
switch (condition.toLowerCase()) {
|
|
2825
|
+
case "checked":
|
|
2826
|
+
currentValue = await state.element.isChecked();
|
|
2827
|
+
return currentValue === true;
|
|
2828
|
+
case "unchecked":
|
|
2829
|
+
currentValue = await state.element.isChecked();
|
|
2830
|
+
return currentValue === false;
|
|
2831
|
+
case "visible":
|
|
2832
|
+
currentValue = await state.element.isVisible();
|
|
2833
|
+
return currentValue === true;
|
|
2834
|
+
case "hidden":
|
|
2835
|
+
currentValue = await state.element.isVisible();
|
|
2836
|
+
return currentValue === false;
|
|
2837
|
+
case "enabled":
|
|
2838
|
+
currentValue = await state.element.isDisabled();
|
|
2839
|
+
return currentValue === false;
|
|
2840
|
+
case "disabled":
|
|
2841
|
+
currentValue = await state.element.isDisabled();
|
|
2842
|
+
return currentValue === true;
|
|
2843
|
+
case "editable":
|
|
2844
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2845
|
+
currentValue = await state.element.isContentEditable();
|
|
2846
|
+
return currentValue === true;
|
|
2847
|
+
default:
|
|
2848
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2849
|
+
state.info.success = false;
|
|
2850
|
+
return false;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
catch (error) {
|
|
2854
|
+
// Don't throw here, just return false to continue retrying
|
|
2855
|
+
return false;
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2858
|
+
// Inner loop for condition checking (once element is located)
|
|
2859
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2860
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2861
|
+
conditionMet = await checkCondition();
|
|
2862
|
+
if (conditionMet) {
|
|
2863
|
+
break;
|
|
2864
|
+
}
|
|
2865
|
+
// Check if we still have time for another attempt
|
|
2866
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2867
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2868
|
+
}
|
|
2869
|
+
else {
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
// If we got here and condition is met, break out of main loop
|
|
2874
|
+
if (conditionMet) {
|
|
2875
|
+
break;
|
|
2876
|
+
}
|
|
2877
|
+
// If condition not met but no exception, we've timed out
|
|
2878
|
+
break;
|
|
2879
|
+
}
|
|
2880
|
+
catch (e) {
|
|
2881
|
+
lastError = e;
|
|
2882
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2883
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2884
|
+
// Check if we have enough time left to retry
|
|
2885
|
+
if (timeLeft > 100) {
|
|
2886
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2887
|
+
}
|
|
2888
|
+
else {
|
|
2889
|
+
break;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2894
|
+
state.info = {
|
|
2895
|
+
success: conditionMet,
|
|
2896
|
+
conditionMet,
|
|
2897
|
+
actualWaitTime,
|
|
2898
|
+
currentValue,
|
|
2899
|
+
lastError: lastError?.message || null,
|
|
2900
|
+
message: conditionMet
|
|
2901
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2902
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2903
|
+
};
|
|
2904
|
+
if (lastError) {
|
|
2905
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2906
|
+
}
|
|
2907
|
+
try {
|
|
2908
|
+
await _commandFinally(state, this);
|
|
2909
|
+
}
|
|
2910
|
+
catch (finallyError) {
|
|
2911
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2912
|
+
}
|
|
2913
|
+
return state.info;
|
|
2914
|
+
}
|
|
1837
2915
|
async extractEmailData(emailAddress, options, world) {
|
|
1838
2916
|
if (!emailAddress) {
|
|
1839
2917
|
throw new Error("email address is null");
|
|
@@ -1852,7 +2930,7 @@ class StableBrowser {
|
|
|
1852
2930
|
if (options && options.timeout) {
|
|
1853
2931
|
timeout = options.timeout;
|
|
1854
2932
|
}
|
|
1855
|
-
const serviceUrl =
|
|
2933
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1856
2934
|
const request = {
|
|
1857
2935
|
method: "POST",
|
|
1858
2936
|
url: serviceUrl,
|
|
@@ -1897,6 +2975,12 @@ class StableBrowser {
|
|
|
1897
2975
|
emailUrl = url;
|
|
1898
2976
|
codeOrUrlFound = true;
|
|
1899
2977
|
}
|
|
2978
|
+
if (process.env.MODE === "executions") {
|
|
2979
|
+
const globalDataFile = "global_test_data.json";
|
|
2980
|
+
if (existsSync(globalDataFile)) {
|
|
2981
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
1900
2984
|
if (codeOrUrlFound) {
|
|
1901
2985
|
return { emailUrl, emailCode };
|
|
1902
2986
|
}
|
|
@@ -1923,27 +3007,32 @@ class StableBrowser {
|
|
|
1923
3007
|
async _highlightElements(scope, css) {
|
|
1924
3008
|
try {
|
|
1925
3009
|
if (!scope) {
|
|
3010
|
+
// console.log(`Scope is not defined`);
|
|
1926
3011
|
return;
|
|
1927
3012
|
}
|
|
1928
3013
|
if (!css) {
|
|
1929
3014
|
scope
|
|
1930
3015
|
.evaluate((node) => {
|
|
1931
3016
|
if (node && node.style) {
|
|
1932
|
-
let
|
|
1933
|
-
|
|
3017
|
+
let originalOutline = node.style.outline;
|
|
3018
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
3019
|
+
// node.__previousOutline = originalOutline;
|
|
3020
|
+
node.style.outline = "2px solid red";
|
|
3021
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
1934
3022
|
if (window) {
|
|
1935
3023
|
window.addEventListener("beforeunload", function (e) {
|
|
1936
|
-
node.style.
|
|
3024
|
+
node.style.outline = originalOutline;
|
|
1937
3025
|
});
|
|
1938
3026
|
}
|
|
1939
3027
|
setTimeout(function () {
|
|
1940
|
-
node.style.
|
|
3028
|
+
node.style.outline = originalOutline;
|
|
1941
3029
|
}, 2000);
|
|
1942
3030
|
}
|
|
1943
3031
|
})
|
|
1944
3032
|
.then(() => { })
|
|
1945
3033
|
.catch((e) => {
|
|
1946
3034
|
// ignore
|
|
3035
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1947
3036
|
});
|
|
1948
3037
|
}
|
|
1949
3038
|
else {
|
|
@@ -1959,17 +3048,18 @@ class StableBrowser {
|
|
|
1959
3048
|
if (!element.style) {
|
|
1960
3049
|
return;
|
|
1961
3050
|
}
|
|
1962
|
-
|
|
3051
|
+
let originalOutline = element.style.outline;
|
|
3052
|
+
element.__previousOutline = originalOutline;
|
|
1963
3053
|
// Set the new border to be red and 2px solid
|
|
1964
|
-
element.style.
|
|
3054
|
+
element.style.outline = "2px solid red";
|
|
1965
3055
|
if (window) {
|
|
1966
3056
|
window.addEventListener("beforeunload", function (e) {
|
|
1967
|
-
element.style.
|
|
3057
|
+
element.style.outline = originalOutline;
|
|
1968
3058
|
});
|
|
1969
3059
|
}
|
|
1970
3060
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1971
3061
|
setTimeout(function () {
|
|
1972
|
-
element.style.
|
|
3062
|
+
element.style.outline = originalOutline;
|
|
1973
3063
|
}, 2000);
|
|
1974
3064
|
}
|
|
1975
3065
|
return;
|
|
@@ -1977,6 +3067,7 @@ class StableBrowser {
|
|
|
1977
3067
|
.then(() => { })
|
|
1978
3068
|
.catch((e) => {
|
|
1979
3069
|
// ignore
|
|
3070
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1980
3071
|
});
|
|
1981
3072
|
}
|
|
1982
3073
|
}
|
|
@@ -1984,8 +3075,49 @@ class StableBrowser {
|
|
|
1984
3075
|
console.debug(error);
|
|
1985
3076
|
}
|
|
1986
3077
|
}
|
|
3078
|
+
_matcher(text) {
|
|
3079
|
+
if (!text) {
|
|
3080
|
+
return { matcher: "contains", queryText: "" };
|
|
3081
|
+
}
|
|
3082
|
+
if (text.length < 2) {
|
|
3083
|
+
return { matcher: "contains", queryText: text };
|
|
3084
|
+
}
|
|
3085
|
+
const split = text.split(":");
|
|
3086
|
+
const matcher = split[0].toLowerCase();
|
|
3087
|
+
const queryText = split.slice(1).join(":").trim();
|
|
3088
|
+
return { matcher, queryText };
|
|
3089
|
+
}
|
|
3090
|
+
_getDomain(url) {
|
|
3091
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
3092
|
+
return "";
|
|
3093
|
+
}
|
|
3094
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
3095
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
3096
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
3097
|
+
}
|
|
3098
|
+
let n = hostnameFragments.length;
|
|
3099
|
+
let fragments = [...hostnameFragments];
|
|
3100
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
3101
|
+
hostnameFragments.pop();
|
|
3102
|
+
n = hostnameFragments.length;
|
|
3103
|
+
}
|
|
3104
|
+
if (n == 0) {
|
|
3105
|
+
if (fragments[0] === "www")
|
|
3106
|
+
fragments = fragments.slice(1);
|
|
3107
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
3108
|
+
}
|
|
3109
|
+
if (hostnameFragments[0] === "www")
|
|
3110
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
3111
|
+
return hostnameFragments.join(".");
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
3114
|
+
* Verify the page path matches the given path.
|
|
3115
|
+
* @param {string} pathPart - The path to verify.
|
|
3116
|
+
* @param {object} options - Options for verification.
|
|
3117
|
+
* @param {object} world - The world context.
|
|
3118
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3119
|
+
*/
|
|
1987
3120
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
1988
|
-
const startTime = Date.now();
|
|
1989
3121
|
let error = null;
|
|
1990
3122
|
let screenshotId = null;
|
|
1991
3123
|
let screenshotPath = null;
|
|
@@ -1999,51 +3131,242 @@ class StableBrowser {
|
|
|
1999
3131
|
pathPart = newValue;
|
|
2000
3132
|
}
|
|
2001
3133
|
info.pathPart = pathPart;
|
|
3134
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
3135
|
+
const state = {
|
|
3136
|
+
text_search: queryText,
|
|
3137
|
+
options,
|
|
3138
|
+
world,
|
|
3139
|
+
locate: false,
|
|
3140
|
+
scroll: false,
|
|
3141
|
+
highlight: false,
|
|
3142
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
3143
|
+
text: `Verify the page url is ${queryText}`,
|
|
3144
|
+
_text: `Verify the page url is ${queryText}`,
|
|
3145
|
+
operation: "verifyPagePath",
|
|
3146
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
3147
|
+
};
|
|
2002
3148
|
try {
|
|
3149
|
+
await _preCommand(state, this);
|
|
3150
|
+
state.info.text = queryText;
|
|
2003
3151
|
for (let i = 0; i < 30; i++) {
|
|
2004
3152
|
const url = await this.page.url();
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
3153
|
+
switch (matcher) {
|
|
3154
|
+
case "exact":
|
|
3155
|
+
if (url !== queryText) {
|
|
3156
|
+
if (i === 29) {
|
|
3157
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
3158
|
+
}
|
|
3159
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3160
|
+
continue;
|
|
3161
|
+
}
|
|
3162
|
+
break;
|
|
3163
|
+
case "contains":
|
|
3164
|
+
if (!url.includes(queryText)) {
|
|
3165
|
+
if (i === 29) {
|
|
3166
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
3167
|
+
}
|
|
3168
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3169
|
+
continue;
|
|
3170
|
+
}
|
|
3171
|
+
break;
|
|
3172
|
+
case "starts-with":
|
|
3173
|
+
{
|
|
3174
|
+
const domain = this._getDomain(url);
|
|
3175
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
3176
|
+
if (i === 29) {
|
|
3177
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
3178
|
+
}
|
|
3179
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
break;
|
|
3184
|
+
case "ends-with":
|
|
3185
|
+
{
|
|
3186
|
+
const urlObj = new URL(url);
|
|
3187
|
+
let route = "/";
|
|
3188
|
+
if (urlObj.pathname !== "/") {
|
|
3189
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
3190
|
+
}
|
|
3191
|
+
else {
|
|
3192
|
+
route = "/";
|
|
3193
|
+
}
|
|
3194
|
+
if (route !== queryText) {
|
|
3195
|
+
if (i === 29) {
|
|
3196
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
3197
|
+
}
|
|
3198
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3199
|
+
continue;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
break;
|
|
3203
|
+
case "regex":
|
|
3204
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3205
|
+
if (!regex.test(url)) {
|
|
3206
|
+
if (i === 29) {
|
|
3207
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
3208
|
+
}
|
|
3209
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
break;
|
|
3213
|
+
default:
|
|
3214
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3215
|
+
if (!url.includes(pathPart)) {
|
|
3216
|
+
if (i === 29) {
|
|
3217
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
3218
|
+
}
|
|
3219
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3220
|
+
continue;
|
|
3221
|
+
}
|
|
2011
3222
|
}
|
|
2012
|
-
|
|
2013
|
-
return info;
|
|
3223
|
+
await _screenshot(state, this);
|
|
3224
|
+
return state.info;
|
|
2014
3225
|
}
|
|
2015
3226
|
}
|
|
2016
3227
|
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);
|
|
3228
|
+
state.info.failCause.lastError = e.message;
|
|
3229
|
+
state.info.failCause.assertionFailed = true;
|
|
3230
|
+
await _commandError(state, e, this);
|
|
2025
3231
|
}
|
|
2026
3232
|
finally {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
3233
|
+
await _commandFinally(state, this);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* Verify the page title matches the given title.
|
|
3238
|
+
* @param {string} title - The title to verify.
|
|
3239
|
+
* @param {object} options - Options for verification.
|
|
3240
|
+
* @param {object} world - The world context.
|
|
3241
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
3242
|
+
*/
|
|
3243
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
3244
|
+
let error = null;
|
|
3245
|
+
let screenshotId = null;
|
|
3246
|
+
let screenshotPath = null;
|
|
3247
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3248
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
3249
|
+
if (newValue !== title) {
|
|
3250
|
+
this.logger.info(title + "=" + newValue);
|
|
3251
|
+
title = newValue;
|
|
3252
|
+
}
|
|
3253
|
+
const { matcher, queryText } = this._matcher(title);
|
|
3254
|
+
const state = {
|
|
3255
|
+
text_search: queryText,
|
|
3256
|
+
options,
|
|
3257
|
+
world,
|
|
3258
|
+
locate: false,
|
|
3259
|
+
scroll: false,
|
|
3260
|
+
highlight: false,
|
|
3261
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
3262
|
+
text: `Verify the page title is ${queryText}`,
|
|
3263
|
+
_text: `Verify the page title is ${queryText}`,
|
|
3264
|
+
operation: "verifyPageTitle",
|
|
3265
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
3266
|
+
};
|
|
3267
|
+
try {
|
|
3268
|
+
await _preCommand(state, this);
|
|
3269
|
+
state.info.text = queryText;
|
|
3270
|
+
for (let i = 0; i < 30; i++) {
|
|
3271
|
+
const foundTitle = await this.page.title();
|
|
3272
|
+
switch (matcher) {
|
|
3273
|
+
case "exact":
|
|
3274
|
+
if (foundTitle !== queryText) {
|
|
3275
|
+
if (i === 29) {
|
|
3276
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
3277
|
+
}
|
|
3278
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
break;
|
|
3282
|
+
case "contains":
|
|
3283
|
+
if (!foundTitle.includes(queryText)) {
|
|
3284
|
+
if (i === 29) {
|
|
3285
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
3286
|
+
}
|
|
3287
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
break;
|
|
3291
|
+
case "starts-with":
|
|
3292
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
3293
|
+
if (i === 29) {
|
|
3294
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
3295
|
+
}
|
|
3296
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
break;
|
|
3300
|
+
case "ends-with":
|
|
3301
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
3302
|
+
if (i === 29) {
|
|
3303
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
3304
|
+
}
|
|
3305
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
break;
|
|
3309
|
+
case "regex":
|
|
3310
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3311
|
+
if (!regex.test(foundTitle)) {
|
|
3312
|
+
if (i === 29) {
|
|
3313
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
3314
|
+
}
|
|
3315
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
break;
|
|
3319
|
+
default:
|
|
3320
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3321
|
+
if (!foundTitle.includes(title)) {
|
|
3322
|
+
if (i === 29) {
|
|
3323
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
3324
|
+
}
|
|
3325
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
await _screenshot(state, this);
|
|
3330
|
+
return state.info;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
catch (e) {
|
|
3334
|
+
state.info.failCause.lastError = e.message;
|
|
3335
|
+
state.info.failCause.assertionFailed = true;
|
|
3336
|
+
await _commandError(state, e, this);
|
|
3337
|
+
}
|
|
3338
|
+
finally {
|
|
3339
|
+
await _commandFinally(state, this);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
3343
|
+
const frames = this.page.frames();
|
|
3344
|
+
let results = [];
|
|
3345
|
+
// let ignoreCase = false;
|
|
3346
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3347
|
+
if (dateAlternatives.date) {
|
|
3348
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
3349
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3350
|
+
result.frame = frames[i];
|
|
3351
|
+
results.push(result);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
else if (numberAlternatives.number) {
|
|
3355
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
3356
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3357
|
+
result.frame = frames[i];
|
|
3358
|
+
results.push(result);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
else {
|
|
3362
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
3363
|
+
result.frame = frames[i];
|
|
3364
|
+
results.push(result);
|
|
3365
|
+
}
|
|
2046
3366
|
}
|
|
3367
|
+
state.info.results = results;
|
|
3368
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3369
|
+
return resultWithElementsFound;
|
|
2047
3370
|
}
|
|
2048
3371
|
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2049
3372
|
text = unEscapeString(text);
|
|
@@ -2054,13 +3377,26 @@ class StableBrowser {
|
|
|
2054
3377
|
locate: false,
|
|
2055
3378
|
scroll: false,
|
|
2056
3379
|
highlight: false,
|
|
2057
|
-
type: Types.
|
|
2058
|
-
text: `Verify text exists in page`,
|
|
3380
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
3381
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
3382
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2059
3383
|
operation: "verifyTextExistInPage",
|
|
2060
3384
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2061
3385
|
};
|
|
2062
|
-
|
|
2063
|
-
|
|
3386
|
+
if (testForRegex(text)) {
|
|
3387
|
+
text = text.replace(/\\"/g, '"');
|
|
3388
|
+
}
|
|
3389
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3390
|
+
//if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
3391
|
+
let stepFastMode = this.stepTags.includes("fast-mode");
|
|
3392
|
+
if (!stepFastMode) {
|
|
3393
|
+
if (!this.fastMode) {
|
|
3394
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3395
|
+
}
|
|
3396
|
+
else {
|
|
3397
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
2064
3400
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2065
3401
|
if (newValue !== text) {
|
|
2066
3402
|
this.logger.info(text + "=" + newValue);
|
|
@@ -2068,35 +3404,24 @@ class StableBrowser {
|
|
|
2068
3404
|
}
|
|
2069
3405
|
let dateAlternatives = findDateAlternatives(text);
|
|
2070
3406
|
let numberAlternatives = findNumberAlternatives(text);
|
|
3407
|
+
if (stepFastMode) {
|
|
3408
|
+
state.onlyFailuresScreenshot = true;
|
|
3409
|
+
state.scroll = false;
|
|
3410
|
+
state.highlight = false;
|
|
3411
|
+
}
|
|
2071
3412
|
try {
|
|
2072
3413
|
await _preCommand(state, this);
|
|
2073
3414
|
state.info.text = text;
|
|
2074
3415
|
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
|
-
}
|
|
3416
|
+
let resultWithElementsFound = {
|
|
3417
|
+
length: 0,
|
|
3418
|
+
};
|
|
3419
|
+
try {
|
|
3420
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3421
|
+
}
|
|
3422
|
+
catch (error) {
|
|
3423
|
+
// ignore
|
|
2097
3424
|
}
|
|
2098
|
-
state.info.results = results;
|
|
2099
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2100
3425
|
if (resultWithElementsFound.length === 0) {
|
|
2101
3426
|
if (Date.now() - state.startTime > timeout) {
|
|
2102
3427
|
throw new Error(`Text ${text} not found in page`);
|
|
@@ -2104,26 +3429,30 @@ class StableBrowser {
|
|
|
2104
3429
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2105
3430
|
continue;
|
|
2106
3431
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
3432
|
+
try {
|
|
3433
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
3434
|
+
const frame = resultWithElementsFound[0].frame;
|
|
3435
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
3436
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3437
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3438
|
+
if (element) {
|
|
3439
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3440
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3441
|
+
}
|
|
2115
3442
|
}
|
|
3443
|
+
await _screenshot(state, this);
|
|
3444
|
+
return state.info;
|
|
3445
|
+
}
|
|
3446
|
+
catch (error) {
|
|
3447
|
+
console.error(error);
|
|
2116
3448
|
}
|
|
2117
|
-
await _screenshot(state, this);
|
|
2118
|
-
return state.info;
|
|
2119
3449
|
}
|
|
2120
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2121
3450
|
}
|
|
2122
3451
|
catch (e) {
|
|
2123
3452
|
await _commandError(state, e, this);
|
|
2124
3453
|
}
|
|
2125
3454
|
finally {
|
|
2126
|
-
_commandFinally(state, this);
|
|
3455
|
+
await _commandFinally(state, this);
|
|
2127
3456
|
}
|
|
2128
3457
|
}
|
|
2129
3458
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2136,11 +3465,15 @@ class StableBrowser {
|
|
|
2136
3465
|
scroll: false,
|
|
2137
3466
|
highlight: false,
|
|
2138
3467
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2139
|
-
text: `Verify text does not exist in page`,
|
|
3468
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
3469
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2140
3470
|
operation: "verifyTextNotExistInPage",
|
|
2141
3471
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2142
3472
|
};
|
|
2143
|
-
|
|
3473
|
+
if (testForRegex(text)) {
|
|
3474
|
+
text = text.replace(/\\"/g, '"');
|
|
3475
|
+
}
|
|
3476
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2144
3477
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2145
3478
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2146
3479
|
if (newValue !== text) {
|
|
@@ -2152,32 +3485,16 @@ class StableBrowser {
|
|
|
2152
3485
|
try {
|
|
2153
3486
|
await _preCommand(state, this);
|
|
2154
3487
|
state.info.text = text;
|
|
3488
|
+
let resultWithElementsFound = {
|
|
3489
|
+
length: null, // initial cannot be 0
|
|
3490
|
+
};
|
|
2155
3491
|
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
|
-
}
|
|
3492
|
+
try {
|
|
3493
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3494
|
+
}
|
|
3495
|
+
catch (error) {
|
|
3496
|
+
// ignore
|
|
2178
3497
|
}
|
|
2179
|
-
state.info.results = results;
|
|
2180
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2181
3498
|
if (resultWithElementsFound.length === 0) {
|
|
2182
3499
|
await _screenshot(state, this);
|
|
2183
3500
|
return state.info;
|
|
@@ -2192,18 +3509,158 @@ class StableBrowser {
|
|
|
2192
3509
|
await _commandError(state, e, this);
|
|
2193
3510
|
}
|
|
2194
3511
|
finally {
|
|
2195
|
-
_commandFinally(state, this);
|
|
3512
|
+
await _commandFinally(state, this);
|
|
2196
3513
|
}
|
|
2197
3514
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
3515
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
3516
|
+
textAnchor = unEscapeString(textAnchor);
|
|
3517
|
+
textToVerify = unEscapeString(textToVerify);
|
|
3518
|
+
const state = {
|
|
3519
|
+
text_search: textToVerify,
|
|
3520
|
+
options,
|
|
3521
|
+
world,
|
|
3522
|
+
locate: false,
|
|
3523
|
+
scroll: false,
|
|
3524
|
+
highlight: false,
|
|
3525
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
3526
|
+
text: `Verify text with relation to another text`,
|
|
3527
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
3528
|
+
operation: "verify_text_with_relation",
|
|
3529
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
3530
|
+
};
|
|
3531
|
+
const cmdStartTime = Date.now();
|
|
3532
|
+
let cmdEndTime = null;
|
|
3533
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3534
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
3535
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
3536
|
+
if (newValue !== textAnchor) {
|
|
3537
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
3538
|
+
textAnchor = newValue;
|
|
3539
|
+
}
|
|
3540
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
3541
|
+
if (newValue !== textToVerify) {
|
|
3542
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
3543
|
+
textToVerify = newValue;
|
|
3544
|
+
}
|
|
3545
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
3546
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
3547
|
+
let foundAncore = false;
|
|
3548
|
+
try {
|
|
3549
|
+
await _preCommand(state, this);
|
|
3550
|
+
state.info.text = textToVerify;
|
|
3551
|
+
let resultWithElementsFound = {
|
|
3552
|
+
length: 0,
|
|
3553
|
+
};
|
|
3554
|
+
while (true) {
|
|
3555
|
+
try {
|
|
3556
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3557
|
+
}
|
|
3558
|
+
catch (error) {
|
|
3559
|
+
// ignore
|
|
3560
|
+
}
|
|
3561
|
+
if (resultWithElementsFound.length === 0) {
|
|
3562
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3563
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
3564
|
+
}
|
|
3565
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3566
|
+
continue;
|
|
3567
|
+
}
|
|
3568
|
+
else {
|
|
3569
|
+
cmdEndTime = Date.now();
|
|
3570
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3571
|
+
if (foundAncore) {
|
|
3572
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3573
|
+
}
|
|
3574
|
+
else {
|
|
3575
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
try {
|
|
3580
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
3581
|
+
foundAncore = true;
|
|
3582
|
+
const result = resultWithElementsFound[i];
|
|
3583
|
+
const token = result.randomToken;
|
|
3584
|
+
const frame = result.frame;
|
|
3585
|
+
let css = `[data-blinq-id-${token}]`;
|
|
3586
|
+
const climbArray1 = [];
|
|
3587
|
+
for (let i = 0; i < climb; i++) {
|
|
3588
|
+
climbArray1.push("..");
|
|
3589
|
+
}
|
|
3590
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
3591
|
+
if (Number(climb) > 0) {
|
|
3592
|
+
css = css + " >> " + climbXpath;
|
|
3593
|
+
}
|
|
3594
|
+
const count = await frame.locator(css).count();
|
|
3595
|
+
for (let j = 0; j < count; j++) {
|
|
3596
|
+
const continer = await frame.locator(css).nth(j);
|
|
3597
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
3598
|
+
if (result.elementCount > 0) {
|
|
3599
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
3600
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3601
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
3602
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
3603
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
3604
|
+
// this._highlightElements(frame, dataAttribute)
|
|
3605
|
+
// .then(async () => {
|
|
3606
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3607
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
3608
|
+
// () => {}
|
|
3609
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
3610
|
+
// );
|
|
3611
|
+
// })
|
|
3612
|
+
// .catch(e);
|
|
3613
|
+
// }
|
|
3614
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
3615
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3616
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3617
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
3618
|
+
if (element) {
|
|
3619
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3620
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3621
|
+
}
|
|
3622
|
+
await _screenshot(state, this);
|
|
3623
|
+
return state.info;
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
catch (error) {
|
|
3629
|
+
console.error(error);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
3633
|
+
}
|
|
3634
|
+
catch (e) {
|
|
3635
|
+
await _commandError(state, e, this);
|
|
2202
3636
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
3637
|
+
finally {
|
|
3638
|
+
await _commandFinally(state, this);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
3642
|
+
const frames = this.page.frames();
|
|
3643
|
+
let results = [];
|
|
3644
|
+
let ignoreCase = false;
|
|
3645
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3646
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
3647
|
+
result.frame = frames[i];
|
|
3648
|
+
const climbArray = [];
|
|
3649
|
+
for (let i = 0; i < climb; i++) {
|
|
3650
|
+
climbArray.push("..");
|
|
3651
|
+
}
|
|
3652
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
3653
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
3654
|
+
const count = await frames[i].locator(newLocator).count();
|
|
3655
|
+
if (count > 0) {
|
|
3656
|
+
result.elementCount = count;
|
|
3657
|
+
result.locator = newLocator;
|
|
3658
|
+
results.push(result);
|
|
3659
|
+
}
|
|
2205
3660
|
}
|
|
2206
|
-
|
|
3661
|
+
// state.info.results = results;
|
|
3662
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3663
|
+
return resultWithElementsFound;
|
|
2207
3664
|
}
|
|
2208
3665
|
async visualVerification(text, options = {}, world = null) {
|
|
2209
3666
|
const startTime = Date.now();
|
|
@@ -2219,14 +3676,17 @@ class StableBrowser {
|
|
|
2219
3676
|
throw new Error("TOKEN is not set");
|
|
2220
3677
|
}
|
|
2221
3678
|
try {
|
|
2222
|
-
let serviceUrl =
|
|
3679
|
+
let serviceUrl = _getServerUrl();
|
|
2223
3680
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2224
3681
|
info.screenshotPath = screenshotPath;
|
|
2225
3682
|
const screenshot = await this.takeScreenshot();
|
|
2226
|
-
|
|
2227
|
-
method: "
|
|
3683
|
+
let request = {
|
|
3684
|
+
method: "post",
|
|
3685
|
+
maxBodyLength: Infinity,
|
|
2228
3686
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2229
3687
|
headers: {
|
|
3688
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3689
|
+
"x-source": "aaa",
|
|
2230
3690
|
"Content-Type": "application/json",
|
|
2231
3691
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2232
3692
|
},
|
|
@@ -2235,7 +3695,7 @@ class StableBrowser {
|
|
|
2235
3695
|
screenshot: screenshot,
|
|
2236
3696
|
}),
|
|
2237
3697
|
};
|
|
2238
|
-
|
|
3698
|
+
const result = await axios.request(request);
|
|
2239
3699
|
if (result.data.status !== true) {
|
|
2240
3700
|
throw new Error("Visual validation failed");
|
|
2241
3701
|
}
|
|
@@ -2256,13 +3716,14 @@ class StableBrowser {
|
|
|
2256
3716
|
Object.assign(e, { info: info });
|
|
2257
3717
|
error = e;
|
|
2258
3718
|
// throw e;
|
|
2259
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3719
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2260
3720
|
}
|
|
2261
3721
|
finally {
|
|
2262
3722
|
const endTime = Date.now();
|
|
2263
|
-
|
|
3723
|
+
_reportToWorld(world, {
|
|
2264
3724
|
type: Types.VERIFY_VISUAL,
|
|
2265
3725
|
text: "Visual verification",
|
|
3726
|
+
_text: "Visual verification of " + text,
|
|
2266
3727
|
screenshotId,
|
|
2267
3728
|
result: error
|
|
2268
3729
|
? {
|
|
@@ -2308,6 +3769,7 @@ class StableBrowser {
|
|
|
2308
3769
|
let screenshotPath = null;
|
|
2309
3770
|
const info = {};
|
|
2310
3771
|
info.log = "";
|
|
3772
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2311
3773
|
info.operation = "getTableData";
|
|
2312
3774
|
info.selectors = selectors;
|
|
2313
3775
|
try {
|
|
@@ -2328,7 +3790,7 @@ class StableBrowser {
|
|
|
2328
3790
|
}
|
|
2329
3791
|
finally {
|
|
2330
3792
|
const endTime = Date.now();
|
|
2331
|
-
|
|
3793
|
+
_reportToWorld(world, {
|
|
2332
3794
|
element_name: selectors.element_name,
|
|
2333
3795
|
type: Types.GET_TABLE_DATA,
|
|
2334
3796
|
text: "Get table data",
|
|
@@ -2383,7 +3845,7 @@ class StableBrowser {
|
|
|
2383
3845
|
info.operation = "analyzeTable";
|
|
2384
3846
|
info.selectors = selectors;
|
|
2385
3847
|
info.query = query;
|
|
2386
|
-
query =
|
|
3848
|
+
query = _fixUsingParams(query, _params);
|
|
2387
3849
|
info.query_fixed = query;
|
|
2388
3850
|
info.operator = operator;
|
|
2389
3851
|
info.value = value;
|
|
@@ -2494,7 +3956,7 @@ class StableBrowser {
|
|
|
2494
3956
|
}
|
|
2495
3957
|
finally {
|
|
2496
3958
|
const endTime = Date.now();
|
|
2497
|
-
|
|
3959
|
+
_reportToWorld(world, {
|
|
2498
3960
|
element_name: selectors.element_name,
|
|
2499
3961
|
type: Types.ANALYZE_TABLE,
|
|
2500
3962
|
text: "Analyze table",
|
|
@@ -2515,8 +3977,51 @@ class StableBrowser {
|
|
|
2515
3977
|
});
|
|
2516
3978
|
}
|
|
2517
3979
|
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3982
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3983
|
+
* @param options - Optional configuration object
|
|
3984
|
+
* @param world - Optional world context
|
|
3985
|
+
* @returns Promise that resolves after the specified duration
|
|
3986
|
+
*/
|
|
3987
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3988
|
+
const state = {
|
|
3989
|
+
duration,
|
|
3990
|
+
options,
|
|
3991
|
+
world,
|
|
3992
|
+
locate: false,
|
|
3993
|
+
scroll: false,
|
|
3994
|
+
screenshot: false,
|
|
3995
|
+
highlight: false,
|
|
3996
|
+
type: Types.SLEEP,
|
|
3997
|
+
text: `Sleep for ${duration} ms`,
|
|
3998
|
+
_text: `Sleep for ${duration} ms`,
|
|
3999
|
+
operation: "sleep",
|
|
4000
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
4001
|
+
};
|
|
4002
|
+
try {
|
|
4003
|
+
await _preCommand(state, this);
|
|
4004
|
+
if (duration < 0) {
|
|
4005
|
+
throw new Error("Sleep duration cannot be negative");
|
|
4006
|
+
}
|
|
4007
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
4008
|
+
return state.info;
|
|
4009
|
+
}
|
|
4010
|
+
catch (e) {
|
|
4011
|
+
await _commandError(state, e, this);
|
|
4012
|
+
}
|
|
4013
|
+
finally {
|
|
4014
|
+
await _commandFinally(state, this);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
2518
4017
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2519
|
-
|
|
4018
|
+
try {
|
|
4019
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
4020
|
+
}
|
|
4021
|
+
catch (error) {
|
|
4022
|
+
this.logger.debug(error);
|
|
4023
|
+
throw error;
|
|
4024
|
+
}
|
|
2520
4025
|
}
|
|
2521
4026
|
_getLoadTimeout(options) {
|
|
2522
4027
|
let timeout = 15000;
|
|
@@ -2528,7 +4033,54 @@ class StableBrowser {
|
|
|
2528
4033
|
}
|
|
2529
4034
|
return timeout;
|
|
2530
4035
|
}
|
|
4036
|
+
_getFindElementTimeout(options) {
|
|
4037
|
+
if (options && options.timeout) {
|
|
4038
|
+
return options.timeout;
|
|
4039
|
+
}
|
|
4040
|
+
if (this.configuration.find_element_timeout) {
|
|
4041
|
+
return this.configuration.find_element_timeout;
|
|
4042
|
+
}
|
|
4043
|
+
return 30000;
|
|
4044
|
+
}
|
|
4045
|
+
async saveStoreState(path = null, world = null) {
|
|
4046
|
+
const storageState = await this.page.context().storageState();
|
|
4047
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4048
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
4049
|
+
if (path) {
|
|
4050
|
+
// save { storageState: storageState } into the path
|
|
4051
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
4052
|
+
}
|
|
4053
|
+
else {
|
|
4054
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
async restoreSaveState(path = null, world = null) {
|
|
4058
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4059
|
+
await refreshBrowser(this, path, world);
|
|
4060
|
+
this.registerEventListeners(this.context);
|
|
4061
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
4062
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
4063
|
+
if (this.onRestoreSaveState) {
|
|
4064
|
+
await this.onRestoreSaveState(path);
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
2531
4067
|
async waitForPageLoad(options = {}, world = null) {
|
|
4068
|
+
// try {
|
|
4069
|
+
// let currentPagePath = null;
|
|
4070
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4071
|
+
// if (this.latestPagePath) {
|
|
4072
|
+
// // get the currect page path and compare with the latest page path
|
|
4073
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4074
|
+
// // if the page path is the same, do not wait for page load
|
|
4075
|
+
// console.log("No page change: " + currentPagePath);
|
|
4076
|
+
// return;
|
|
4077
|
+
// }
|
|
4078
|
+
// }
|
|
4079
|
+
// this.latestPagePath = currentPagePath;
|
|
4080
|
+
// } catch (e) {
|
|
4081
|
+
// console.debug("Error getting current page path: ", e);
|
|
4082
|
+
// }
|
|
4083
|
+
//console.log("Waiting for page load");
|
|
2532
4084
|
let timeout = this._getLoadTimeout(options);
|
|
2533
4085
|
const promiseArray = [];
|
|
2534
4086
|
// let waitForNetworkIdle = true;
|
|
@@ -2561,13 +4113,15 @@ class StableBrowser {
|
|
|
2561
4113
|
else if (e.label === "domcontentloaded") {
|
|
2562
4114
|
console.log("waited for the domcontent loaded timeout");
|
|
2563
4115
|
}
|
|
2564
|
-
console.log(".");
|
|
2565
4116
|
}
|
|
2566
4117
|
finally {
|
|
2567
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4118
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4119
|
+
if (options && !options.noSleep) {
|
|
4120
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4121
|
+
}
|
|
2568
4122
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2569
4123
|
const endTime = Date.now();
|
|
2570
|
-
|
|
4124
|
+
_reportToWorld(world, {
|
|
2571
4125
|
type: Types.GET_PAGE_STATUS,
|
|
2572
4126
|
text: "Wait for page load",
|
|
2573
4127
|
screenshotId,
|
|
@@ -2595,21 +4149,134 @@ class StableBrowser {
|
|
|
2595
4149
|
highlight: false,
|
|
2596
4150
|
type: Types.CLOSE_PAGE,
|
|
2597
4151
|
text: `Close page`,
|
|
4152
|
+
_text: `Close the page`,
|
|
2598
4153
|
operation: "closePage",
|
|
2599
4154
|
log: "***** close page *****\n",
|
|
2600
|
-
throwError: false,
|
|
4155
|
+
// throwError: false,
|
|
2601
4156
|
};
|
|
2602
4157
|
try {
|
|
2603
4158
|
await _preCommand(state, this);
|
|
2604
4159
|
await this.page.close();
|
|
2605
4160
|
}
|
|
2606
4161
|
catch (e) {
|
|
2607
|
-
console.log(".");
|
|
2608
4162
|
await _commandError(state, e, this);
|
|
2609
4163
|
}
|
|
2610
4164
|
finally {
|
|
2611
|
-
_commandFinally(state, this);
|
|
4165
|
+
await _commandFinally(state, this);
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
4169
|
+
let operation = null;
|
|
4170
|
+
if (!options || !options.operation) {
|
|
4171
|
+
throw new Error("operation is not defined");
|
|
4172
|
+
}
|
|
4173
|
+
operation = options.operation;
|
|
4174
|
+
// validate operation is one of the supported operations
|
|
4175
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
4176
|
+
throw new Error("operation is not supported");
|
|
4177
|
+
}
|
|
4178
|
+
const state = {
|
|
4179
|
+
options,
|
|
4180
|
+
world,
|
|
4181
|
+
locate: false,
|
|
4182
|
+
scroll: false,
|
|
4183
|
+
highlight: false,
|
|
4184
|
+
type: Types.TABLE_OPERATION,
|
|
4185
|
+
text: `Table operation`,
|
|
4186
|
+
_text: `Table ${operation} operation`,
|
|
4187
|
+
operation: operation,
|
|
4188
|
+
log: "***** Table operation *****\n",
|
|
4189
|
+
};
|
|
4190
|
+
const timeout = this._getFindElementTimeout(options);
|
|
4191
|
+
try {
|
|
4192
|
+
await _preCommand(state, this);
|
|
4193
|
+
const start = Date.now();
|
|
4194
|
+
let cellArea = null;
|
|
4195
|
+
while (true) {
|
|
4196
|
+
try {
|
|
4197
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
4198
|
+
if (cellArea) {
|
|
4199
|
+
break;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
catch (e) {
|
|
4203
|
+
// ignore
|
|
4204
|
+
}
|
|
4205
|
+
if (Date.now() - start > timeout) {
|
|
4206
|
+
throw new Error(`Cell not found in table`);
|
|
4207
|
+
}
|
|
4208
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
4209
|
+
}
|
|
4210
|
+
switch (operation) {
|
|
4211
|
+
case "click":
|
|
4212
|
+
if (!options.css) {
|
|
4213
|
+
// will click in the center of the cell
|
|
4214
|
+
let xOffset = 0;
|
|
4215
|
+
let yOffset = 0;
|
|
4216
|
+
if (options.xOffset) {
|
|
4217
|
+
xOffset = options.xOffset;
|
|
4218
|
+
}
|
|
4219
|
+
if (options.yOffset) {
|
|
4220
|
+
yOffset = options.yOffset;
|
|
4221
|
+
}
|
|
4222
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
4223
|
+
}
|
|
4224
|
+
else {
|
|
4225
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4226
|
+
if (results.length === 0) {
|
|
4227
|
+
throw new Error(`Element not found in cell area`);
|
|
4228
|
+
}
|
|
4229
|
+
state.element = results[0];
|
|
4230
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
4231
|
+
}
|
|
4232
|
+
break;
|
|
4233
|
+
case "hover+click":
|
|
4234
|
+
if (!options.css) {
|
|
4235
|
+
throw new Error("css is not defined");
|
|
4236
|
+
}
|
|
4237
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4238
|
+
if (results.length === 0) {
|
|
4239
|
+
throw new Error(`Element not found in cell area`);
|
|
4240
|
+
}
|
|
4241
|
+
state.element = results[0];
|
|
4242
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
4243
|
+
break;
|
|
4244
|
+
case "hover":
|
|
4245
|
+
if (!options.css) {
|
|
4246
|
+
throw new Error("css is not defined");
|
|
4247
|
+
}
|
|
4248
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4249
|
+
if (result1.length === 0) {
|
|
4250
|
+
throw new Error(`Element not found in cell area`);
|
|
4251
|
+
}
|
|
4252
|
+
state.element = result1[0];
|
|
4253
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4254
|
+
break;
|
|
4255
|
+
default:
|
|
4256
|
+
throw new Error("operation is not supported");
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
catch (e) {
|
|
4260
|
+
await _commandError(state, e, this);
|
|
4261
|
+
}
|
|
4262
|
+
finally {
|
|
4263
|
+
await _commandFinally(state, this);
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
saveTestDataAsGlobal(options, world) {
|
|
4267
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
4268
|
+
if (process.env.MODE === "executions") {
|
|
4269
|
+
const globalDataFile = path.join(this.project_path, "global_test_data.json");
|
|
4270
|
+
const dataFileContents = fs.existsSync(dataFile) ? JSON.parse(fs.readFileSync(dataFile)) : {};
|
|
4271
|
+
const globalDataFileContents = fs.existsSync(globalDataFile) ? JSON.parse(fs.readFileSync(globalDataFile)) : {};
|
|
4272
|
+
const mergedData = JSON.stringify(_.merge({}, dataFileContents, globalDataFileContents), null, 2);
|
|
4273
|
+
fs.writeFileSync(dataFile, mergedData);
|
|
4274
|
+
fs.writeFileSync(globalDataFile, mergedData);
|
|
4275
|
+
this.logger.info("Save the scenario test data to " + dataFile + " as global for the following scenarios.");
|
|
4276
|
+
return;
|
|
2612
4277
|
}
|
|
4278
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4279
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2613
4280
|
}
|
|
2614
4281
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2615
4282
|
const startTime = Date.now();
|
|
@@ -2627,16 +4294,16 @@ class StableBrowser {
|
|
|
2627
4294
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2628
4295
|
}
|
|
2629
4296
|
catch (e) {
|
|
2630
|
-
console.log(".");
|
|
2631
4297
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2632
4298
|
}
|
|
2633
4299
|
finally {
|
|
2634
4300
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2635
4301
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2636
4302
|
const endTime = Date.now();
|
|
2637
|
-
|
|
4303
|
+
_reportToWorld(world, {
|
|
2638
4304
|
type: Types.SET_VIEWPORT,
|
|
2639
4305
|
text: "set viewport size to " + width + "x" + hight,
|
|
4306
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2640
4307
|
screenshotId,
|
|
2641
4308
|
result: error
|
|
2642
4309
|
? {
|
|
@@ -2664,14 +4331,13 @@ class StableBrowser {
|
|
|
2664
4331
|
await this.page.reload();
|
|
2665
4332
|
}
|
|
2666
4333
|
catch (e) {
|
|
2667
|
-
console.log(".");
|
|
2668
4334
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2669
4335
|
}
|
|
2670
4336
|
finally {
|
|
2671
4337
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2672
4338
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2673
4339
|
const endTime = Date.now();
|
|
2674
|
-
|
|
4340
|
+
_reportToWorld(world, {
|
|
2675
4341
|
type: Types.GET_PAGE_STATUS,
|
|
2676
4342
|
text: "page relaod",
|
|
2677
4343
|
screenshotId,
|
|
@@ -2707,36 +4373,276 @@ class StableBrowser {
|
|
|
2707
4373
|
console.log("#-#");
|
|
2708
4374
|
}
|
|
2709
4375
|
}
|
|
2710
|
-
|
|
2711
|
-
if (
|
|
2712
|
-
|
|
4376
|
+
async beforeScenario(world, scenario) {
|
|
4377
|
+
if (world && world.attach) {
|
|
4378
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4379
|
+
}
|
|
4380
|
+
this.context.loadedRoutes = null;
|
|
4381
|
+
this.beforeScenarioCalled = true;
|
|
4382
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4383
|
+
this.scenarioName = scenario.pickle.name;
|
|
4384
|
+
}
|
|
4385
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
4386
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
4387
|
+
}
|
|
4388
|
+
if (this.context) {
|
|
4389
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
4390
|
+
}
|
|
4391
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4392
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4393
|
+
// check if @global_test_data tag is present
|
|
4394
|
+
if (this.tags.includes("@global_test_data" /* TAG_CONSTANTS.GLOBAL_TEST_DATA */)) {
|
|
4395
|
+
this.saveTestDataAsGlobal({}, world);
|
|
4396
|
+
}
|
|
4397
|
+
if (this.tags.includes("@fast_mode" /* TAG_CONSTANTS.FAST_MODE */)) {
|
|
4398
|
+
this.fastMode = true;
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
// update test data based on feature/scenario
|
|
4402
|
+
let envName = null;
|
|
4403
|
+
if (this.context && this.context.environment) {
|
|
4404
|
+
envName = this.context.environment.name;
|
|
4405
|
+
}
|
|
4406
|
+
if (!process.env.TEMP_RUN) {
|
|
4407
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4408
|
+
}
|
|
4409
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4410
|
+
if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
|
|
4411
|
+
this.trace = true;
|
|
4412
|
+
const traceFolder = path.join(this.context.reportFolder, "trace");
|
|
4413
|
+
if (!fs.existsSync(traceFolder)) {
|
|
4414
|
+
fs.mkdirSync(traceFolder, { recursive: true });
|
|
4415
|
+
}
|
|
4416
|
+
this.traceFolder = traceFolder;
|
|
4417
|
+
await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
async afterScenario(world, scenario) {
|
|
4421
|
+
const id = scenario.testCaseStartedId;
|
|
4422
|
+
if (this.trace) {
|
|
4423
|
+
await this.context.playContext.tracing.stop({
|
|
4424
|
+
path: path.join(this.traceFolder, `trace-${id}.zip`),
|
|
4425
|
+
});
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
getGherkinKeyword(step) {
|
|
4429
|
+
if (!step?.type) {
|
|
4430
|
+
return "";
|
|
4431
|
+
}
|
|
4432
|
+
switch (step.type) {
|
|
4433
|
+
case "Context":
|
|
4434
|
+
return "Given";
|
|
4435
|
+
case "Action":
|
|
4436
|
+
return "When";
|
|
4437
|
+
case "Outcome":
|
|
4438
|
+
return "Then";
|
|
4439
|
+
case "Conjunction":
|
|
4440
|
+
return "And";
|
|
4441
|
+
default:
|
|
4442
|
+
return "";
|
|
2713
4443
|
}
|
|
2714
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2715
4444
|
}
|
|
2716
4445
|
async beforeStep(world, step) {
|
|
2717
|
-
|
|
2718
|
-
|
|
4446
|
+
if (step?.pickleStep && this.trace) {
|
|
4447
|
+
const keyword = this.getGherkinKeyword(step.pickleStep);
|
|
4448
|
+
this.traceGroupName = `${keyword} ${step.pickleStep.text}`;
|
|
4449
|
+
await this.context.playContext.tracing.group(this.traceGroupName);
|
|
4450
|
+
}
|
|
4451
|
+
this.stepTags = [];
|
|
4452
|
+
if (!this.beforeScenarioCalled) {
|
|
4453
|
+
this.beforeScenario(world, step);
|
|
4454
|
+
this.context.loadedRoutes = null;
|
|
4455
|
+
}
|
|
2719
4456
|
if (this.stepIndex === undefined) {
|
|
2720
4457
|
this.stepIndex = 0;
|
|
2721
4458
|
}
|
|
2722
4459
|
else {
|
|
2723
4460
|
this.stepIndex++;
|
|
2724
4461
|
}
|
|
4462
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4463
|
+
this.stepName = step.pickleStep.text;
|
|
4464
|
+
let printableStepName = this.stepName;
|
|
4465
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4466
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4467
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4468
|
+
});
|
|
4469
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4470
|
+
}
|
|
4471
|
+
else if (step && step.text) {
|
|
4472
|
+
this.stepName = step.text;
|
|
4473
|
+
}
|
|
4474
|
+
else {
|
|
4475
|
+
this.stepName = "step " + this.stepIndex;
|
|
4476
|
+
}
|
|
2725
4477
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2726
4478
|
if (this.context.browserObject.context) {
|
|
2727
4479
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
2728
4480
|
}
|
|
2729
4481
|
}
|
|
4482
|
+
if (this.initSnapshotTaken === false) {
|
|
4483
|
+
this.initSnapshotTaken = true;
|
|
4484
|
+
if (world &&
|
|
4485
|
+
world.attach &&
|
|
4486
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4487
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4488
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4489
|
+
if (snapshot) {
|
|
4490
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
this.context.routeResults = null;
|
|
4495
|
+
this.context.loadedRoutes = null;
|
|
4496
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4497
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4498
|
+
this.inStepReport = false;
|
|
4499
|
+
}
|
|
4500
|
+
setStepTags(tags) {
|
|
4501
|
+
this.stepTags = tags;
|
|
4502
|
+
}
|
|
4503
|
+
async getAriaSnapshot() {
|
|
4504
|
+
try {
|
|
4505
|
+
// find the page url
|
|
4506
|
+
const url = await this.page.url();
|
|
4507
|
+
// extract the path from the url
|
|
4508
|
+
const path = new URL(url).pathname;
|
|
4509
|
+
// get the page title
|
|
4510
|
+
const title = await this.page.title();
|
|
4511
|
+
// go over other frams
|
|
4512
|
+
const frames = this.page.frames();
|
|
4513
|
+
const snapshots = [];
|
|
4514
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4515
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4516
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4517
|
+
const frame = frames[i];
|
|
4518
|
+
try {
|
|
4519
|
+
// Ensure frame is attached and has body
|
|
4520
|
+
const body = frame.locator("body");
|
|
4521
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4522
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4523
|
+
if (!snapshot) {
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4526
|
+
content.push(`- frame: ${i}`);
|
|
4527
|
+
content.push(snapshot);
|
|
4528
|
+
}
|
|
4529
|
+
catch (innerErr) {
|
|
4530
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4531
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
return content.join("\n");
|
|
4535
|
+
}
|
|
4536
|
+
catch (e) {
|
|
4537
|
+
console.log("Error in getAriaSnapshot");
|
|
4538
|
+
//console.debug(e);
|
|
4539
|
+
}
|
|
4540
|
+
return null;
|
|
4541
|
+
}
|
|
4542
|
+
/**
|
|
4543
|
+
* Sends command with custom payload to report.
|
|
4544
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4545
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4546
|
+
* @param content - Content of the command to be shown in the report.
|
|
4547
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4548
|
+
* @param world - Optional world context.
|
|
4549
|
+
* @public
|
|
4550
|
+
*/
|
|
4551
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4552
|
+
const state = {
|
|
4553
|
+
options,
|
|
4554
|
+
world,
|
|
4555
|
+
locate: false,
|
|
4556
|
+
scroll: false,
|
|
4557
|
+
screenshot: options.screenshot ?? false,
|
|
4558
|
+
highlight: options.highlight ?? false,
|
|
4559
|
+
type: Types.REPORT_COMMAND,
|
|
4560
|
+
text: commandText,
|
|
4561
|
+
_text: commandText,
|
|
4562
|
+
operation: "report_command",
|
|
4563
|
+
log: "***** " + commandText + " *****\n",
|
|
4564
|
+
};
|
|
4565
|
+
try {
|
|
4566
|
+
await _preCommand(state, this);
|
|
4567
|
+
const payload = {
|
|
4568
|
+
type: options.type ?? "text",
|
|
4569
|
+
content: content,
|
|
4570
|
+
screenshotId: null,
|
|
4571
|
+
};
|
|
4572
|
+
state.payload = payload;
|
|
4573
|
+
if (commandStatus === "FAILED") {
|
|
4574
|
+
state.throwError = true;
|
|
4575
|
+
throw new Error(commandText);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
catch (e) {
|
|
4579
|
+
await _commandError(state, e, this);
|
|
4580
|
+
}
|
|
4581
|
+
finally {
|
|
4582
|
+
await _commandFinally(state, this);
|
|
4583
|
+
}
|
|
2730
4584
|
}
|
|
2731
|
-
async afterStep(world, step) {
|
|
4585
|
+
async afterStep(world, step, result) {
|
|
2732
4586
|
this.stepName = null;
|
|
2733
|
-
if (this.context
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
4587
|
+
if (this.context) {
|
|
4588
|
+
this.context.examplesRow = null;
|
|
4589
|
+
}
|
|
4590
|
+
if (!this.inStepReport) {
|
|
4591
|
+
// check the step result
|
|
4592
|
+
if (result && result.status === "FAILED" && world && world.attach) {
|
|
4593
|
+
await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
if (world &&
|
|
4597
|
+
world.attach &&
|
|
4598
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4599
|
+
!this.fastMode &&
|
|
4600
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4601
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4602
|
+
if (snapshot) {
|
|
4603
|
+
const obj = {};
|
|
4604
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4608
|
+
if (this.context.routeResults) {
|
|
4609
|
+
if (world && world.attach) {
|
|
4610
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
if (!process.env.TEMP_RUN) {
|
|
4614
|
+
const state = {
|
|
4615
|
+
world,
|
|
4616
|
+
locate: false,
|
|
4617
|
+
scroll: false,
|
|
4618
|
+
screenshot: true,
|
|
4619
|
+
highlight: true,
|
|
4620
|
+
type: Types.STEP_COMPLETE,
|
|
4621
|
+
text: "end of scenario",
|
|
4622
|
+
_text: "end of scenario",
|
|
4623
|
+
operation: "step_complete",
|
|
4624
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4625
|
+
};
|
|
4626
|
+
try {
|
|
4627
|
+
await _preCommand(state, this);
|
|
4628
|
+
}
|
|
4629
|
+
catch (e) {
|
|
4630
|
+
await _commandError(state, e, this);
|
|
4631
|
+
}
|
|
4632
|
+
finally {
|
|
4633
|
+
await _commandFinally(state, this);
|
|
2738
4634
|
}
|
|
2739
4635
|
}
|
|
4636
|
+
networkAfterStep(this.stepName, this.context);
|
|
4637
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4638
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4639
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4640
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
if (this.trace) {
|
|
4644
|
+
await this.context.playContext.tracing.groupEnd();
|
|
4645
|
+
}
|
|
2740
4646
|
}
|
|
2741
4647
|
}
|
|
2742
4648
|
function createTimedPromise(promise, label) {
|
|
@@ -2744,156 +4650,5 @@ function createTimedPromise(promise, label) {
|
|
|
2744
4650
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2745
4651
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2746
4652
|
}
|
|
2747
|
-
const KEYBOARD_EVENTS = [
|
|
2748
|
-
"ALT",
|
|
2749
|
-
"AltGraph",
|
|
2750
|
-
"CapsLock",
|
|
2751
|
-
"Control",
|
|
2752
|
-
"Fn",
|
|
2753
|
-
"FnLock",
|
|
2754
|
-
"Hyper",
|
|
2755
|
-
"Meta",
|
|
2756
|
-
"NumLock",
|
|
2757
|
-
"ScrollLock",
|
|
2758
|
-
"Shift",
|
|
2759
|
-
"Super",
|
|
2760
|
-
"Symbol",
|
|
2761
|
-
"SymbolLock",
|
|
2762
|
-
"Enter",
|
|
2763
|
-
"Tab",
|
|
2764
|
-
"ArrowDown",
|
|
2765
|
-
"ArrowLeft",
|
|
2766
|
-
"ArrowRight",
|
|
2767
|
-
"ArrowUp",
|
|
2768
|
-
"End",
|
|
2769
|
-
"Home",
|
|
2770
|
-
"PageDown",
|
|
2771
|
-
"PageUp",
|
|
2772
|
-
"Backspace",
|
|
2773
|
-
"Clear",
|
|
2774
|
-
"Copy",
|
|
2775
|
-
"CrSel",
|
|
2776
|
-
"Cut",
|
|
2777
|
-
"Delete",
|
|
2778
|
-
"EraseEof",
|
|
2779
|
-
"ExSel",
|
|
2780
|
-
"Insert",
|
|
2781
|
-
"Paste",
|
|
2782
|
-
"Redo",
|
|
2783
|
-
"Undo",
|
|
2784
|
-
"Accept",
|
|
2785
|
-
"Again",
|
|
2786
|
-
"Attn",
|
|
2787
|
-
"Cancel",
|
|
2788
|
-
"ContextMenu",
|
|
2789
|
-
"Escape",
|
|
2790
|
-
"Execute",
|
|
2791
|
-
"Find",
|
|
2792
|
-
"Finish",
|
|
2793
|
-
"Help",
|
|
2794
|
-
"Pause",
|
|
2795
|
-
"Play",
|
|
2796
|
-
"Props",
|
|
2797
|
-
"Select",
|
|
2798
|
-
"ZoomIn",
|
|
2799
|
-
"ZoomOut",
|
|
2800
|
-
"BrightnessDown",
|
|
2801
|
-
"BrightnessUp",
|
|
2802
|
-
"Eject",
|
|
2803
|
-
"LogOff",
|
|
2804
|
-
"Power",
|
|
2805
|
-
"PowerOff",
|
|
2806
|
-
"PrintScreen",
|
|
2807
|
-
"Hibernate",
|
|
2808
|
-
"Standby",
|
|
2809
|
-
"WakeUp",
|
|
2810
|
-
"AllCandidates",
|
|
2811
|
-
"Alphanumeric",
|
|
2812
|
-
"CodeInput",
|
|
2813
|
-
"Compose",
|
|
2814
|
-
"Convert",
|
|
2815
|
-
"Dead",
|
|
2816
|
-
"FinalMode",
|
|
2817
|
-
"GroupFirst",
|
|
2818
|
-
"GroupLast",
|
|
2819
|
-
"GroupNext",
|
|
2820
|
-
"GroupPrevious",
|
|
2821
|
-
"ModeChange",
|
|
2822
|
-
"NextCandidate",
|
|
2823
|
-
"NonConvert",
|
|
2824
|
-
"PreviousCandidate",
|
|
2825
|
-
"Process",
|
|
2826
|
-
"SingleCandidate",
|
|
2827
|
-
"HangulMode",
|
|
2828
|
-
"HanjaMode",
|
|
2829
|
-
"JunjaMode",
|
|
2830
|
-
"Eisu",
|
|
2831
|
-
"Hankaku",
|
|
2832
|
-
"Hiragana",
|
|
2833
|
-
"HiraganaKatakana",
|
|
2834
|
-
"KanaMode",
|
|
2835
|
-
"KanjiMode",
|
|
2836
|
-
"Katakana",
|
|
2837
|
-
"Romaji",
|
|
2838
|
-
"Zenkaku",
|
|
2839
|
-
"ZenkakuHanaku",
|
|
2840
|
-
"F1",
|
|
2841
|
-
"F2",
|
|
2842
|
-
"F3",
|
|
2843
|
-
"F4",
|
|
2844
|
-
"F5",
|
|
2845
|
-
"F6",
|
|
2846
|
-
"F7",
|
|
2847
|
-
"F8",
|
|
2848
|
-
"F9",
|
|
2849
|
-
"F10",
|
|
2850
|
-
"F11",
|
|
2851
|
-
"F12",
|
|
2852
|
-
"Soft1",
|
|
2853
|
-
"Soft2",
|
|
2854
|
-
"Soft3",
|
|
2855
|
-
"Soft4",
|
|
2856
|
-
"ChannelDown",
|
|
2857
|
-
"ChannelUp",
|
|
2858
|
-
"Close",
|
|
2859
|
-
"MailForward",
|
|
2860
|
-
"MailReply",
|
|
2861
|
-
"MailSend",
|
|
2862
|
-
"MediaFastForward",
|
|
2863
|
-
"MediaPause",
|
|
2864
|
-
"MediaPlay",
|
|
2865
|
-
"MediaPlayPause",
|
|
2866
|
-
"MediaRecord",
|
|
2867
|
-
"MediaRewind",
|
|
2868
|
-
"MediaStop",
|
|
2869
|
-
"MediaTrackNext",
|
|
2870
|
-
"MediaTrackPrevious",
|
|
2871
|
-
"AudioBalanceLeft",
|
|
2872
|
-
"AudioBalanceRight",
|
|
2873
|
-
"AudioBassBoostDown",
|
|
2874
|
-
"AudioBassBoostToggle",
|
|
2875
|
-
"AudioBassBoostUp",
|
|
2876
|
-
"AudioFaderFront",
|
|
2877
|
-
"AudioFaderRear",
|
|
2878
|
-
"AudioSurroundModeNext",
|
|
2879
|
-
"AudioTrebleDown",
|
|
2880
|
-
"AudioTrebleUp",
|
|
2881
|
-
"AudioVolumeDown",
|
|
2882
|
-
"AudioVolumeMute",
|
|
2883
|
-
"AudioVolumeUp",
|
|
2884
|
-
"MicrophoneToggle",
|
|
2885
|
-
"MicrophoneVolumeDown",
|
|
2886
|
-
"MicrophoneVolumeMute",
|
|
2887
|
-
"MicrophoneVolumeUp",
|
|
2888
|
-
"TV",
|
|
2889
|
-
"TV3DMode",
|
|
2890
|
-
"TVAntennaCable",
|
|
2891
|
-
"TVAudioDescription",
|
|
2892
|
-
];
|
|
2893
|
-
function unEscapeString(str) {
|
|
2894
|
-
const placeholder = "__NEWLINE__";
|
|
2895
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2896
|
-
return str;
|
|
2897
|
-
}
|
|
2898
4653
|
export { StableBrowser };
|
|
2899
4654
|
//# sourceMappingURL=stable_browser.js.map
|