automation_model 1.0.516-dev → 1.0.516
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 +2425 -679
- 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,140 +493,54 @@ 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) {
|
|
479
544
|
if (!info) {
|
|
480
545
|
info = {};
|
|
481
546
|
}
|
|
@@ -484,41 +549,44 @@ class StableBrowser {
|
|
|
484
549
|
}
|
|
485
550
|
if (!info.log) {
|
|
486
551
|
info.log = "";
|
|
552
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
487
553
|
}
|
|
488
554
|
let locatorSearch = selectorHierarchy[index];
|
|
489
555
|
try {
|
|
490
|
-
locatorSearch =
|
|
556
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
491
557
|
}
|
|
492
558
|
catch (e) {
|
|
493
559
|
console.error(e);
|
|
494
560
|
}
|
|
561
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
495
562
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
496
563
|
let locator = null;
|
|
497
564
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
498
|
-
|
|
565
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
566
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
499
567
|
if (!locatorString) {
|
|
500
568
|
info.failCause.textNotFound = true;
|
|
501
|
-
info.failCause.lastError =
|
|
569
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
502
570
|
return;
|
|
503
571
|
}
|
|
504
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
572
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
505
573
|
}
|
|
506
574
|
else if (locatorSearch.text) {
|
|
507
|
-
let text =
|
|
508
|
-
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);
|
|
509
577
|
if (result.elementCount === 0) {
|
|
510
578
|
info.failCause.textNotFound = true;
|
|
511
|
-
info.failCause.lastError =
|
|
579
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
512
580
|
return;
|
|
513
581
|
}
|
|
514
|
-
locatorSearch.css = "[data-blinq-id
|
|
582
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
515
583
|
if (locatorSearch.childCss) {
|
|
516
584
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
517
585
|
}
|
|
518
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
586
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
519
587
|
}
|
|
520
588
|
else {
|
|
521
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
589
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
522
590
|
}
|
|
523
591
|
// let cssHref = false;
|
|
524
592
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
@@ -531,27 +599,44 @@ class StableBrowser {
|
|
|
531
599
|
//info.log += "total elements found " + count + "\n";
|
|
532
600
|
//let visibleCount = 0;
|
|
533
601
|
let visibleLocator = null;
|
|
534
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
602
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
535
603
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
604
|
+
if (info.locatorLog) {
|
|
605
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
606
|
+
}
|
|
536
607
|
return;
|
|
537
608
|
}
|
|
609
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
610
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
611
|
+
}
|
|
538
612
|
for (let j = 0; j < count; j++) {
|
|
539
613
|
let visible = await locator.nth(j).isVisible();
|
|
540
614
|
const enabled = await locator.nth(j).isEnabled();
|
|
541
615
|
if (!visibleOnly) {
|
|
542
616
|
visible = true;
|
|
543
617
|
}
|
|
544
|
-
if (visible && enabled) {
|
|
618
|
+
if (visible && (allowDisabled || enabled)) {
|
|
545
619
|
foundLocators.push(locator.nth(j));
|
|
620
|
+
if (info.locatorLog) {
|
|
621
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
622
|
+
}
|
|
546
623
|
}
|
|
547
|
-
else {
|
|
624
|
+
else if (logErrors) {
|
|
548
625
|
info.failCause.visible = visible;
|
|
549
626
|
info.failCause.enabled = enabled;
|
|
550
627
|
if (!info.printMessages) {
|
|
551
628
|
info.printMessages = {};
|
|
552
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
|
+
}
|
|
553
638
|
if (!info.printMessages[j.toString()]) {
|
|
554
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
639
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
555
640
|
info.printMessages[j.toString()] = true;
|
|
556
641
|
}
|
|
557
642
|
}
|
|
@@ -567,12 +652,12 @@ class StableBrowser {
|
|
|
567
652
|
if (!info) {
|
|
568
653
|
info = {};
|
|
569
654
|
}
|
|
570
|
-
info.log += "scan for popup handlers" + "\n";
|
|
655
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
571
656
|
const handlerGroup = [];
|
|
572
657
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
573
658
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
574
659
|
}
|
|
575
|
-
const scopes =
|
|
660
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
576
661
|
let result = null;
|
|
577
662
|
let scope = null;
|
|
578
663
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -594,28 +679,207 @@ class StableBrowser {
|
|
|
594
679
|
}
|
|
595
680
|
if (result.foundElements.length > 0) {
|
|
596
681
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
+
}
|
|
600
697
|
return { rerun: true };
|
|
601
698
|
}
|
|
602
699
|
}
|
|
603
700
|
}
|
|
604
701
|
return { rerun: false };
|
|
605
702
|
}
|
|
606
|
-
|
|
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) {
|
|
607
768
|
if (!timeout) {
|
|
608
769
|
timeout = 30000;
|
|
609
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
|
+
}
|
|
610
778
|
for (let i = 0; i < 3; i++) {
|
|
611
779
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
612
780
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
613
781
|
let selector = selectors.locators[j];
|
|
614
782
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
615
783
|
}
|
|
616
|
-
|
|
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
|
+
}
|
|
617
827
|
if (!element.rerun) {
|
|
618
|
-
|
|
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();
|
|
619
883
|
}
|
|
620
884
|
}
|
|
621
885
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -626,6 +890,7 @@ class StableBrowser {
|
|
|
626
890
|
info.failCause = {};
|
|
627
891
|
info.log = "";
|
|
628
892
|
}
|
|
893
|
+
let startTime = Date.now();
|
|
629
894
|
let scope = this.page;
|
|
630
895
|
if (selectors.frame) {
|
|
631
896
|
return selectors.frame;
|
|
@@ -635,7 +900,7 @@ class StableBrowser {
|
|
|
635
900
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
636
901
|
let frameLocator = frame.selectors[i];
|
|
637
902
|
if (frameLocator.css) {
|
|
638
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
903
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
639
904
|
if (frameLocator.index) {
|
|
640
905
|
testframescope = framescope.nth(frameLocator.index);
|
|
641
906
|
}
|
|
@@ -647,7 +912,7 @@ class StableBrowser {
|
|
|
647
912
|
break;
|
|
648
913
|
}
|
|
649
914
|
catch (error) {
|
|
650
|
-
console.error("frame not found " + frameLocator.css);
|
|
915
|
+
// console.error("frame not found " + frameLocator.css);
|
|
651
916
|
}
|
|
652
917
|
}
|
|
653
918
|
}
|
|
@@ -656,9 +921,11 @@ class StableBrowser {
|
|
|
656
921
|
}
|
|
657
922
|
return framescope;
|
|
658
923
|
};
|
|
924
|
+
let fLocator = null;
|
|
659
925
|
while (true) {
|
|
660
926
|
let frameFound = false;
|
|
661
927
|
if (selectors.nestFrmLoc) {
|
|
928
|
+
fLocator = selectors.nestFrmLoc;
|
|
662
929
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
663
930
|
frameFound = true;
|
|
664
931
|
break;
|
|
@@ -667,6 +934,7 @@ class StableBrowser {
|
|
|
667
934
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
668
935
|
let frameLocator = selectors.frameLocators[i];
|
|
669
936
|
if (frameLocator.css) {
|
|
937
|
+
fLocator = frameLocator.css;
|
|
670
938
|
scope = scope.frameLocator(frameLocator.css);
|
|
671
939
|
frameFound = true;
|
|
672
940
|
break;
|
|
@@ -674,18 +942,25 @@ class StableBrowser {
|
|
|
674
942
|
}
|
|
675
943
|
}
|
|
676
944
|
if (!frameFound && selectors.iframe_src) {
|
|
945
|
+
fLocator = selectors.iframe_src;
|
|
677
946
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
678
947
|
}
|
|
679
948
|
if (!scope) {
|
|
680
|
-
info
|
|
681
|
-
|
|
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) {
|
|
682
954
|
info.failCause.iframeNotFound = true;
|
|
683
|
-
info.failCause.lastError =
|
|
955
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
684
956
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
685
957
|
}
|
|
686
958
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
687
959
|
}
|
|
688
960
|
else {
|
|
961
|
+
if (info && info.locatorLog) {
|
|
962
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
963
|
+
}
|
|
689
964
|
break;
|
|
690
965
|
}
|
|
691
966
|
}
|
|
@@ -702,18 +977,28 @@ class StableBrowser {
|
|
|
702
977
|
return bodyContent;
|
|
703
978
|
});
|
|
704
979
|
}
|
|
705
|
-
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
|
+
}
|
|
706
990
|
if (!info) {
|
|
707
991
|
info = {};
|
|
708
992
|
info.failCause = {};
|
|
709
993
|
info.log = "";
|
|
994
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
710
995
|
}
|
|
711
996
|
let highPriorityTimeout = 5000;
|
|
712
997
|
let visibleOnlyTimeout = 6000;
|
|
713
|
-
let startTime =
|
|
998
|
+
let startTime = Date.now();
|
|
714
999
|
let locatorsCount = 0;
|
|
1000
|
+
let lazy_scroll = false;
|
|
715
1001
|
//let arrayMode = Array.isArray(selectors);
|
|
716
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
717
1002
|
let selectorsLocators = null;
|
|
718
1003
|
selectorsLocators = selectors.locators;
|
|
719
1004
|
// group selectors by priority
|
|
@@ -741,6 +1026,7 @@ class StableBrowser {
|
|
|
741
1026
|
let highPriorityOnly = true;
|
|
742
1027
|
let visibleOnly = true;
|
|
743
1028
|
while (true) {
|
|
1029
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
744
1030
|
locatorsCount = 0;
|
|
745
1031
|
let result = [];
|
|
746
1032
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -749,18 +1035,13 @@ class StableBrowser {
|
|
|
749
1035
|
}
|
|
750
1036
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
751
1037
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
752
|
-
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);
|
|
753
1039
|
if (result.foundElements.length === 0) {
|
|
754
1040
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
755
|
-
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);
|
|
756
1042
|
}
|
|
757
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
758
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
759
|
-
}
|
|
760
|
-
else {
|
|
761
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
762
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
763
|
-
}
|
|
1043
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1044
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
764
1045
|
}
|
|
765
1046
|
let foundElements = result.foundElements;
|
|
766
1047
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -800,26 +1081,43 @@ class StableBrowser {
|
|
|
800
1081
|
return maxCountElement.locator;
|
|
801
1082
|
}
|
|
802
1083
|
}
|
|
803
|
-
if (
|
|
1084
|
+
if (Date.now() - startTime > timeout) {
|
|
804
1085
|
break;
|
|
805
1086
|
}
|
|
806
|
-
if (
|
|
807
|
-
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";
|
|
808
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
|
+
}
|
|
809
1094
|
}
|
|
810
|
-
if (
|
|
811
|
-
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";
|
|
812
1097
|
visibleOnly = false;
|
|
813
1098
|
}
|
|
814
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
|
+
}
|
|
815
1105
|
}
|
|
816
1106
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
817
|
-
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";
|
|
818
1114
|
info.failCause.locatorNotFound = true;
|
|
819
|
-
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
|
+
}
|
|
820
1118
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
821
1119
|
}
|
|
822
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1120
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
823
1121
|
let foundElements = [];
|
|
824
1122
|
const result = {
|
|
825
1123
|
foundElements: foundElements,
|
|
@@ -827,34 +1125,88 @@ class StableBrowser {
|
|
|
827
1125
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
828
1126
|
let foundLocators = [];
|
|
829
1127
|
try {
|
|
830
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1128
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
831
1129
|
}
|
|
832
1130
|
catch (e) {
|
|
833
|
-
this
|
|
834
|
-
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);
|
|
835
1134
|
foundLocators = [];
|
|
836
1135
|
try {
|
|
837
|
-
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);
|
|
838
1137
|
}
|
|
839
1138
|
catch (e) {
|
|
840
|
-
|
|
1139
|
+
if (logErrors) {
|
|
1140
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1141
|
+
}
|
|
841
1142
|
}
|
|
842
1143
|
}
|
|
843
1144
|
if (foundLocators.length === 1) {
|
|
1145
|
+
let box = null;
|
|
1146
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1147
|
+
box = await foundLocators[0].boundingBox();
|
|
1148
|
+
}
|
|
844
1149
|
result.foundElements.push({
|
|
845
1150
|
locator: foundLocators[0],
|
|
846
|
-
box:
|
|
1151
|
+
box: box,
|
|
847
1152
|
unique: true,
|
|
848
1153
|
});
|
|
849
1154
|
result.locatorIndex = i;
|
|
850
1155
|
}
|
|
851
1156
|
if (foundLocators.length > 1) {
|
|
852
|
-
|
|
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
|
+
}
|
|
853
1192
|
}
|
|
854
1193
|
}
|
|
855
1194
|
return result;
|
|
856
1195
|
}
|
|
857
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);
|
|
858
1210
|
const startTime = Date.now();
|
|
859
1211
|
let timeout = 30000;
|
|
860
1212
|
if (options && options.timeout) {
|
|
@@ -879,13 +1231,31 @@ class StableBrowser {
|
|
|
879
1231
|
catch (e) {
|
|
880
1232
|
if (performance.now() - startTime > timeout) {
|
|
881
1233
|
// throw e;
|
|
882
|
-
|
|
1234
|
+
try {
|
|
1235
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1236
|
+
}
|
|
1237
|
+
finally {
|
|
1238
|
+
await _commandFinally(state, this);
|
|
1239
|
+
}
|
|
883
1240
|
}
|
|
884
1241
|
}
|
|
885
1242
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
886
1243
|
}
|
|
887
1244
|
}
|
|
888
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);
|
|
889
1259
|
const startTime = Date.now();
|
|
890
1260
|
let timeout = 30000;
|
|
891
1261
|
if (options && options.timeout) {
|
|
@@ -910,7 +1280,12 @@ class StableBrowser {
|
|
|
910
1280
|
catch (e) {
|
|
911
1281
|
if (performance.now() - startTime > timeout) {
|
|
912
1282
|
// throw e;
|
|
913
|
-
|
|
1283
|
+
try {
|
|
1284
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1285
|
+
}
|
|
1286
|
+
finally {
|
|
1287
|
+
await _commandFinally(state, this);
|
|
1288
|
+
}
|
|
914
1289
|
}
|
|
915
1290
|
}
|
|
916
1291
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -923,34 +1298,74 @@ class StableBrowser {
|
|
|
923
1298
|
options,
|
|
924
1299
|
world,
|
|
925
1300
|
text: "Click element",
|
|
1301
|
+
_text: "Click on " + selectors.element_name,
|
|
926
1302
|
type: Types.CLICK,
|
|
927
1303
|
operation: "click",
|
|
928
1304
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
929
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
|
+
}
|
|
930
1313
|
try {
|
|
1314
|
+
check_performance("click_preCommand", this.context, true);
|
|
931
1315
|
await _preCommand(state, this);
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
await
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
catch (e) {
|
|
940
|
-
// await this.closeUnexpectedPopups();
|
|
941
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
942
|
-
await state.element.dispatchEvent("click");
|
|
943
|
-
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);
|
|
944
1322
|
}
|
|
945
|
-
await this.waitForPageLoad();
|
|
946
1323
|
return state.info;
|
|
947
1324
|
}
|
|
948
1325
|
catch (e) {
|
|
949
1326
|
await _commandError(state, e, this);
|
|
950
1327
|
}
|
|
951
1328
|
finally {
|
|
952
|
-
|
|
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);
|
|
953
1367
|
}
|
|
1368
|
+
return found;
|
|
954
1369
|
}
|
|
955
1370
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
956
1371
|
const state = {
|
|
@@ -960,6 +1375,7 @@ class StableBrowser {
|
|
|
960
1375
|
world,
|
|
961
1376
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
962
1377
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1378
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
963
1379
|
operation: "setCheck",
|
|
964
1380
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
965
1381
|
};
|
|
@@ -969,30 +1385,53 @@ class StableBrowser {
|
|
|
969
1385
|
// let element = await this._locate(selectors, info, _params);
|
|
970
1386
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
971
1387
|
try {
|
|
972
|
-
//
|
|
973
|
-
|
|
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 });
|
|
974
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);
|
|
975
1397
|
}
|
|
976
1398
|
catch (e) {
|
|
977
1399
|
if (e.message && e.message.includes("did not change its state")) {
|
|
978
1400
|
this.logger.info("element did not change its state, ignoring...");
|
|
979
1401
|
}
|
|
980
1402
|
else {
|
|
1403
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
981
1404
|
//await this.closeUnexpectedPopups();
|
|
982
1405
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
983
|
-
state.
|
|
984
|
-
|
|
985
|
-
|
|
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
|
+
}
|
|
986
1425
|
}
|
|
987
1426
|
}
|
|
988
|
-
await this.waitForPageLoad();
|
|
1427
|
+
//await this.waitForPageLoad();
|
|
989
1428
|
return state.info;
|
|
990
1429
|
}
|
|
991
1430
|
catch (e) {
|
|
992
1431
|
await _commandError(state, e, this);
|
|
993
1432
|
}
|
|
994
1433
|
finally {
|
|
995
|
-
_commandFinally(state, this);
|
|
1434
|
+
await _commandFinally(state, this);
|
|
996
1435
|
}
|
|
997
1436
|
}
|
|
998
1437
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1003,37 +1442,29 @@ class StableBrowser {
|
|
|
1003
1442
|
world,
|
|
1004
1443
|
type: Types.HOVER,
|
|
1005
1444
|
text: `Hover element`,
|
|
1445
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1006
1446
|
operation: "hover",
|
|
1007
1447
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1008
1448
|
};
|
|
1009
1449
|
try {
|
|
1010
1450
|
await _preCommand(state, this);
|
|
1011
|
-
|
|
1012
|
-
await state.element.hover();
|
|
1013
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1014
|
-
}
|
|
1015
|
-
catch (e) {
|
|
1016
|
-
//await this.closeUnexpectedPopups();
|
|
1017
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1018
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1019
|
-
await state.element.hover({ timeout: 10000 });
|
|
1020
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1021
|
-
}
|
|
1451
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1022
1452
|
await _screenshot(state, this);
|
|
1023
|
-
await this.waitForPageLoad();
|
|
1453
|
+
//await this.waitForPageLoad();
|
|
1024
1454
|
return state.info;
|
|
1025
1455
|
}
|
|
1026
1456
|
catch (e) {
|
|
1027
1457
|
await _commandError(state, e, this);
|
|
1028
1458
|
}
|
|
1029
1459
|
finally {
|
|
1030
|
-
_commandFinally(state, this);
|
|
1460
|
+
await _commandFinally(state, this);
|
|
1031
1461
|
}
|
|
1032
1462
|
}
|
|
1033
1463
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
+
// }
|
|
1037
1468
|
const state = {
|
|
1038
1469
|
selectors,
|
|
1039
1470
|
_params,
|
|
@@ -1042,6 +1473,7 @@ class StableBrowser {
|
|
|
1042
1473
|
value: values.toString(),
|
|
1043
1474
|
type: Types.SELECT,
|
|
1044
1475
|
text: `Select option: ${values}`,
|
|
1476
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1045
1477
|
operation: "selectOption",
|
|
1046
1478
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1047
1479
|
};
|
|
@@ -1055,14 +1487,14 @@ class StableBrowser {
|
|
|
1055
1487
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1056
1488
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1057
1489
|
}
|
|
1058
|
-
await this.waitForPageLoad();
|
|
1490
|
+
//await this.waitForPageLoad();
|
|
1059
1491
|
return state.info;
|
|
1060
1492
|
}
|
|
1061
1493
|
catch (e) {
|
|
1062
1494
|
await _commandError(state, e, this);
|
|
1063
1495
|
}
|
|
1064
1496
|
finally {
|
|
1065
|
-
_commandFinally(state, this);
|
|
1497
|
+
await _commandFinally(state, this);
|
|
1066
1498
|
}
|
|
1067
1499
|
}
|
|
1068
1500
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1076,6 +1508,7 @@ class StableBrowser {
|
|
|
1076
1508
|
highlight: false,
|
|
1077
1509
|
type: Types.TYPE_PRESS,
|
|
1078
1510
|
text: `Type value: ${_value}`,
|
|
1511
|
+
_text: `Type value: ${_value}`,
|
|
1079
1512
|
operation: "type",
|
|
1080
1513
|
log: "",
|
|
1081
1514
|
};
|
|
@@ -1107,7 +1540,7 @@ class StableBrowser {
|
|
|
1107
1540
|
await _commandError(state, e, this);
|
|
1108
1541
|
}
|
|
1109
1542
|
finally {
|
|
1110
|
-
_commandFinally(state, this);
|
|
1543
|
+
await _commandFinally(state, this);
|
|
1111
1544
|
}
|
|
1112
1545
|
}
|
|
1113
1546
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1143,7 +1576,7 @@ class StableBrowser {
|
|
|
1143
1576
|
await _commandError(state, e, this);
|
|
1144
1577
|
}
|
|
1145
1578
|
finally {
|
|
1146
|
-
_commandFinally(state, this);
|
|
1579
|
+
await _commandFinally(state, this);
|
|
1147
1580
|
}
|
|
1148
1581
|
}
|
|
1149
1582
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1155,14 +1588,15 @@ class StableBrowser {
|
|
|
1155
1588
|
world,
|
|
1156
1589
|
type: Types.SET_DATE_TIME,
|
|
1157
1590
|
text: `Set date time value: ${value}`,
|
|
1591
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1158
1592
|
operation: "setDateTime",
|
|
1159
1593
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1160
|
-
throwError: false,
|
|
1594
|
+
// throwError: false,
|
|
1161
1595
|
};
|
|
1162
1596
|
try {
|
|
1163
1597
|
await _preCommand(state, this);
|
|
1164
1598
|
try {
|
|
1165
|
-
await state.element
|
|
1599
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1166
1600
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1167
1601
|
if (format) {
|
|
1168
1602
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1211,7 +1645,7 @@ class StableBrowser {
|
|
|
1211
1645
|
await _commandError(state, e, this);
|
|
1212
1646
|
}
|
|
1213
1647
|
finally {
|
|
1214
|
-
_commandFinally(state, this);
|
|
1648
|
+
await _commandFinally(state, this);
|
|
1215
1649
|
}
|
|
1216
1650
|
}
|
|
1217
1651
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1226,17 +1660,29 @@ class StableBrowser {
|
|
|
1226
1660
|
world,
|
|
1227
1661
|
type: Types.FILL,
|
|
1228
1662
|
text: `Click type input with value: ${_value}`,
|
|
1663
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1229
1664
|
operation: "clickType",
|
|
1230
1665
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1231
1666
|
};
|
|
1667
|
+
if (!options) {
|
|
1668
|
+
options = {};
|
|
1669
|
+
}
|
|
1232
1670
|
if (newValue !== _value) {
|
|
1233
1671
|
//this.logger.info(_value + "=" + newValue);
|
|
1234
1672
|
_value = newValue;
|
|
1235
1673
|
}
|
|
1236
1674
|
try {
|
|
1237
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);
|
|
1238
1684
|
state.info.value = _value;
|
|
1239
|
-
if (
|
|
1685
|
+
if (!options.press) {
|
|
1240
1686
|
try {
|
|
1241
1687
|
let currentValue = await state.element.inputValue();
|
|
1242
1688
|
if (currentValue) {
|
|
@@ -1247,13 +1693,9 @@ class StableBrowser {
|
|
|
1247
1693
|
this.logger.info("unable to clear input value");
|
|
1248
1694
|
}
|
|
1249
1695
|
}
|
|
1250
|
-
if (options
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
catch (e) {
|
|
1255
|
-
await state.element.dispatchEvent("click");
|
|
1256
|
-
}
|
|
1696
|
+
if (options.press) {
|
|
1697
|
+
options.timeout = 5000;
|
|
1698
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1257
1699
|
}
|
|
1258
1700
|
else {
|
|
1259
1701
|
try {
|
|
@@ -1264,6 +1706,25 @@ class StableBrowser {
|
|
|
1264
1706
|
}
|
|
1265
1707
|
}
|
|
1266
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
|
+
}
|
|
1267
1728
|
const valueSegment = state.value.split("&&");
|
|
1268
1729
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1269
1730
|
if (i > 0) {
|
|
@@ -1284,17 +1745,24 @@ class StableBrowser {
|
|
|
1284
1745
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1285
1746
|
}
|
|
1286
1747
|
}
|
|
1748
|
+
//if (!this.fastMode) {
|
|
1287
1749
|
await _screenshot(state, this);
|
|
1750
|
+
//}
|
|
1288
1751
|
if (enter === true) {
|
|
1289
1752
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1290
1753
|
await this.page.keyboard.press("Enter");
|
|
1291
1754
|
await this.waitForPageLoad();
|
|
1292
1755
|
}
|
|
1293
1756
|
else if (enter === false) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1757
|
+
try {
|
|
1758
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1759
|
+
}
|
|
1760
|
+
catch (e) {
|
|
1761
|
+
// ignore
|
|
1762
|
+
}
|
|
1763
|
+
//await this.page.keyboard.press("Tab");
|
|
1764
|
+
}
|
|
1765
|
+
else {
|
|
1298
1766
|
if (enter !== "" && enter !== null && enter !== undefined) {
|
|
1299
1767
|
await this.page.keyboard.press(enter);
|
|
1300
1768
|
await this.waitForPageLoad();
|
|
@@ -1306,7 +1774,7 @@ class StableBrowser {
|
|
|
1306
1774
|
await _commandError(state, e, this);
|
|
1307
1775
|
}
|
|
1308
1776
|
finally {
|
|
1309
|
-
_commandFinally(state, this);
|
|
1777
|
+
await _commandFinally(state, this);
|
|
1310
1778
|
}
|
|
1311
1779
|
}
|
|
1312
1780
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1328,30 +1796,67 @@ class StableBrowser {
|
|
|
1328
1796
|
if (enter) {
|
|
1329
1797
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1330
1798
|
await this.page.keyboard.press("Enter");
|
|
1799
|
+
await this.waitForPageLoad();
|
|
1331
1800
|
}
|
|
1332
|
-
await this.waitForPageLoad();
|
|
1333
1801
|
return state.info;
|
|
1334
1802
|
}
|
|
1335
1803
|
catch (e) {
|
|
1336
1804
|
await _commandError(state, e, this);
|
|
1337
1805
|
}
|
|
1338
1806
|
finally {
|
|
1339
|
-
_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);
|
|
1340
1843
|
}
|
|
1341
1844
|
}
|
|
1342
1845
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1343
1846
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1344
1847
|
}
|
|
1345
1848
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1849
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1346
1850
|
_validateSelectors(selectors);
|
|
1347
1851
|
let screenshotId = null;
|
|
1348
1852
|
let screenshotPath = null;
|
|
1349
1853
|
if (!info.log) {
|
|
1350
1854
|
info.log = "";
|
|
1855
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1351
1856
|
}
|
|
1352
1857
|
info.operation = "getText";
|
|
1353
1858
|
info.selectors = selectors;
|
|
1354
|
-
let element = await this._locate(selectors, info, _params);
|
|
1859
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1355
1860
|
if (climb > 0) {
|
|
1356
1861
|
const climbArray = [];
|
|
1357
1862
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1370,6 +1875,18 @@ class StableBrowser {
|
|
|
1370
1875
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1371
1876
|
try {
|
|
1372
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
|
+
// }
|
|
1373
1890
|
const elementText = await element.innerText();
|
|
1374
1891
|
return {
|
|
1375
1892
|
text: elementText,
|
|
@@ -1381,7 +1898,7 @@ class StableBrowser {
|
|
|
1381
1898
|
}
|
|
1382
1899
|
catch (e) {
|
|
1383
1900
|
//await this.closeUnexpectedPopups();
|
|
1384
|
-
this.logger.info("no innerText will use textContent");
|
|
1901
|
+
this.logger.info("no innerText, will use textContent");
|
|
1385
1902
|
const elementText = await element.textContent();
|
|
1386
1903
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1387
1904
|
}
|
|
@@ -1406,6 +1923,7 @@ class StableBrowser {
|
|
|
1406
1923
|
highlight: false,
|
|
1407
1924
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1408
1925
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1926
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1409
1927
|
operation: "containsPattern",
|
|
1410
1928
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1411
1929
|
};
|
|
@@ -1437,10 +1955,12 @@ class StableBrowser {
|
|
|
1437
1955
|
await _commandError(state, e, this);
|
|
1438
1956
|
}
|
|
1439
1957
|
finally {
|
|
1440
|
-
_commandFinally(state, this);
|
|
1958
|
+
await _commandFinally(state, this);
|
|
1441
1959
|
}
|
|
1442
1960
|
}
|
|
1443
1961
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1962
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1963
|
+
const startTime = Date.now();
|
|
1444
1964
|
const state = {
|
|
1445
1965
|
selectors,
|
|
1446
1966
|
_params,
|
|
@@ -1467,61 +1987,137 @@ class StableBrowser {
|
|
|
1467
1987
|
}
|
|
1468
1988
|
let foundObj = null;
|
|
1469
1989
|
try {
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1477
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1478
|
-
if (dateAlternatives.date) {
|
|
1479
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1480
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1481
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1482
|
-
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);
|
|
1483
1996
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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)) {
|
|
1491
2017
|
return state.info;
|
|
1492
2018
|
}
|
|
1493
2019
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
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
|
|
1500
2025
|
}
|
|
1501
|
-
|
|
2026
|
+
state.info.foundText = foundObj?.text;
|
|
2027
|
+
state.info.value = foundObj?.value;
|
|
2028
|
+
throw new Error("element doesn't contain text " + text);
|
|
1502
2029
|
}
|
|
1503
2030
|
catch (e) {
|
|
1504
2031
|
await _commandError(state, e, this);
|
|
2032
|
+
throw e;
|
|
1505
2033
|
}
|
|
1506
2034
|
finally {
|
|
1507
|
-
_commandFinally(state, this);
|
|
2035
|
+
await _commandFinally(state, this);
|
|
1508
2036
|
}
|
|
1509
2037
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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");
|
|
1514
2062
|
}
|
|
1515
|
-
else if (
|
|
1516
|
-
|
|
2063
|
+
else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
|
|
2064
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
|
|
1517
2065
|
}
|
|
1518
|
-
else if (
|
|
1519
|
-
|
|
2066
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2067
|
+
text = referanceSnapshot.substring(5);
|
|
1520
2068
|
}
|
|
1521
2069
|
else {
|
|
1522
|
-
|
|
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);
|
|
1523
2120
|
}
|
|
1524
|
-
return dataFile;
|
|
1525
2121
|
}
|
|
1526
2122
|
async waitForUserInput(message, world = null) {
|
|
1527
2123
|
if (!message) {
|
|
@@ -1551,13 +2147,22 @@ class StableBrowser {
|
|
|
1551
2147
|
return;
|
|
1552
2148
|
}
|
|
1553
2149
|
// if data file exists, load it
|
|
1554
|
-
const dataFile =
|
|
2150
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1555
2151
|
let data = this.getTestData(world);
|
|
1556
2152
|
// merge the testData with the existing data
|
|
1557
2153
|
Object.assign(data, testData);
|
|
1558
2154
|
// save the data to the file
|
|
1559
2155
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1560
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
|
+
}
|
|
1561
2166
|
_getDataFilePath(fileName) {
|
|
1562
2167
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1563
2168
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1654,14 +2259,12 @@ class StableBrowser {
|
|
|
1654
2259
|
}
|
|
1655
2260
|
}
|
|
1656
2261
|
getTestData(world = null) {
|
|
1657
|
-
|
|
1658
|
-
let data = {};
|
|
1659
|
-
if (fs.existsSync(dataFile)) {
|
|
1660
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1661
|
-
}
|
|
1662
|
-
return data;
|
|
2262
|
+
return _getTestData(world, this.context, this);
|
|
1663
2263
|
}
|
|
1664
2264
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2265
|
+
if (!options) {
|
|
2266
|
+
options = {};
|
|
2267
|
+
}
|
|
1665
2268
|
// collect url/path/title
|
|
1666
2269
|
if (info) {
|
|
1667
2270
|
if (!info.title) {
|
|
@@ -1686,13 +2289,11 @@ class StableBrowser {
|
|
|
1686
2289
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1687
2290
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1688
2291
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
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");
|
|
1694
2295
|
try {
|
|
1695
|
-
await this.takeScreenshot(screenshotPath);
|
|
2296
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
1696
2297
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1697
2298
|
// // save the buffer to the screenshot path asynchrously
|
|
1698
2299
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -1700,20 +2301,20 @@ class StableBrowser {
|
|
|
1700
2301
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1701
2302
|
// }
|
|
1702
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
|
+
}
|
|
1703
2309
|
}
|
|
1704
2310
|
catch (e) {
|
|
1705
2311
|
this.logger.info("unable to take screenshot, ignored");
|
|
1706
2312
|
}
|
|
1707
|
-
result.screenshotId = nextIndex;
|
|
1708
|
-
result.screenshotPath = screenshotPath;
|
|
1709
|
-
if (info && info.box) {
|
|
1710
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1711
|
-
}
|
|
1712
2313
|
}
|
|
1713
2314
|
else if (options && options.screenshot) {
|
|
1714
2315
|
result.screenshotPath = options.screenshotPath;
|
|
1715
2316
|
try {
|
|
1716
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2317
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
1717
2318
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1718
2319
|
// // save the buffer to the screenshot path asynchrously
|
|
1719
2320
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -1731,7 +2332,7 @@ class StableBrowser {
|
|
|
1731
2332
|
}
|
|
1732
2333
|
return result;
|
|
1733
2334
|
}
|
|
1734
|
-
async takeScreenshot(screenshotPath) {
|
|
2335
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
1735
2336
|
const playContext = this.context.playContext;
|
|
1736
2337
|
// Using CDP to capture the screenshot
|
|
1737
2338
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -1743,17 +2344,20 @@ class StableBrowser {
|
|
|
1743
2344
|
document.documentElement.clientWidth,
|
|
1744
2345
|
])));
|
|
1745
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
|
+
// }
|
|
1746
2356
|
if (this.context.browserName === "chromium") {
|
|
1747
2357
|
const client = await playContext.newCDPSession(this.page);
|
|
1748
2358
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1749
2359
|
format: "png",
|
|
1750
|
-
|
|
1751
|
-
// x: 0,
|
|
1752
|
-
// y: 0,
|
|
1753
|
-
// width: viewportWidth,
|
|
1754
|
-
// height: viewportHeight,
|
|
1755
|
-
// scale: 1,
|
|
1756
|
-
// },
|
|
2360
|
+
captureBeyondViewport: fullPage,
|
|
1757
2361
|
});
|
|
1758
2362
|
await client.detach();
|
|
1759
2363
|
if (!screenshotPath) {
|
|
@@ -1762,8 +2366,12 @@ class StableBrowser {
|
|
|
1762
2366
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
1763
2367
|
}
|
|
1764
2368
|
else {
|
|
1765
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2369
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
1766
2370
|
}
|
|
2371
|
+
// if (focusedElement) {
|
|
2372
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2373
|
+
// await this._unhighlightElements(focusedElement);
|
|
2374
|
+
// }
|
|
1767
2375
|
let image = await Jimp.read(screenshotBuffer);
|
|
1768
2376
|
// Get the image dimensions
|
|
1769
2377
|
const { width, height } = image.bitmap;
|
|
@@ -1776,6 +2384,7 @@ class StableBrowser {
|
|
|
1776
2384
|
else {
|
|
1777
2385
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1778
2386
|
}
|
|
2387
|
+
return screenshotBuffer;
|
|
1779
2388
|
}
|
|
1780
2389
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1781
2390
|
const state = {
|
|
@@ -1798,7 +2407,7 @@ class StableBrowser {
|
|
|
1798
2407
|
await _commandError(state, e, this);
|
|
1799
2408
|
}
|
|
1800
2409
|
finally {
|
|
1801
|
-
_commandFinally(state, this);
|
|
2410
|
+
await _commandFinally(state, this);
|
|
1802
2411
|
}
|
|
1803
2412
|
}
|
|
1804
2413
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1811,8 +2420,10 @@ class StableBrowser {
|
|
|
1811
2420
|
world,
|
|
1812
2421
|
type: Types.EXTRACT,
|
|
1813
2422
|
text: `Extract attribute from element`,
|
|
2423
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1814
2424
|
operation: "extractAttribute",
|
|
1815
2425
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
2426
|
+
allowDisabled: true,
|
|
1816
2427
|
};
|
|
1817
2428
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1818
2429
|
try {
|
|
@@ -1827,22 +2438,480 @@ class StableBrowser {
|
|
|
1827
2438
|
case "value":
|
|
1828
2439
|
state.value = await state.element.inputValue();
|
|
1829
2440
|
break;
|
|
2441
|
+
case "text":
|
|
2442
|
+
state.value = await state.element.textContent();
|
|
2443
|
+
break;
|
|
1830
2444
|
default:
|
|
1831
2445
|
state.value = await state.element.getAttribute(attribute);
|
|
1832
2446
|
break;
|
|
1833
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
|
+
}
|
|
2466
|
+
state.info.value = state.value;
|
|
2467
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
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));
|
|
2476
|
+
return state.info;
|
|
2477
|
+
}
|
|
2478
|
+
catch (e) {
|
|
2479
|
+
await _commandError(state, e, this);
|
|
2480
|
+
}
|
|
2481
|
+
finally {
|
|
2482
|
+
await _commandFinally(state, this);
|
|
2483
|
+
}
|
|
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
|
+
}
|
|
1834
2543
|
state.info.value = state.value;
|
|
1835
2544
|
this.setTestData({ [variable]: state.value }, world);
|
|
1836
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));
|
|
1837
2553
|
return state.info;
|
|
1838
2554
|
}
|
|
1839
2555
|
catch (e) {
|
|
1840
2556
|
await _commandError(state, e, this);
|
|
1841
2557
|
}
|
|
1842
2558
|
finally {
|
|
1843
|
-
_commandFinally(state, this);
|
|
2559
|
+
await _commandFinally(state, this);
|
|
1844
2560
|
}
|
|
1845
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
|
+
}
|
|
1846
2915
|
async extractEmailData(emailAddress, options, world) {
|
|
1847
2916
|
if (!emailAddress) {
|
|
1848
2917
|
throw new Error("email address is null");
|
|
@@ -1861,7 +2930,7 @@ class StableBrowser {
|
|
|
1861
2930
|
if (options && options.timeout) {
|
|
1862
2931
|
timeout = options.timeout;
|
|
1863
2932
|
}
|
|
1864
|
-
const serviceUrl =
|
|
2933
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1865
2934
|
const request = {
|
|
1866
2935
|
method: "POST",
|
|
1867
2936
|
url: serviceUrl,
|
|
@@ -1906,6 +2975,12 @@ class StableBrowser {
|
|
|
1906
2975
|
emailUrl = url;
|
|
1907
2976
|
codeOrUrlFound = true;
|
|
1908
2977
|
}
|
|
2978
|
+
if (process.env.MODE === "executions") {
|
|
2979
|
+
const globalDataFile = "global_test_data.json";
|
|
2980
|
+
if (existsSync(globalDataFile)) {
|
|
2981
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
1909
2984
|
if (codeOrUrlFound) {
|
|
1910
2985
|
return { emailUrl, emailCode };
|
|
1911
2986
|
}
|
|
@@ -1932,27 +3007,32 @@ class StableBrowser {
|
|
|
1932
3007
|
async _highlightElements(scope, css) {
|
|
1933
3008
|
try {
|
|
1934
3009
|
if (!scope) {
|
|
3010
|
+
// console.log(`Scope is not defined`);
|
|
1935
3011
|
return;
|
|
1936
3012
|
}
|
|
1937
3013
|
if (!css) {
|
|
1938
3014
|
scope
|
|
1939
3015
|
.evaluate((node) => {
|
|
1940
3016
|
if (node && node.style) {
|
|
1941
|
-
let
|
|
1942
|
-
|
|
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}`);
|
|
1943
3022
|
if (window) {
|
|
1944
3023
|
window.addEventListener("beforeunload", function (e) {
|
|
1945
|
-
node.style.
|
|
3024
|
+
node.style.outline = originalOutline;
|
|
1946
3025
|
});
|
|
1947
3026
|
}
|
|
1948
3027
|
setTimeout(function () {
|
|
1949
|
-
node.style.
|
|
3028
|
+
node.style.outline = originalOutline;
|
|
1950
3029
|
}, 2000);
|
|
1951
3030
|
}
|
|
1952
3031
|
})
|
|
1953
3032
|
.then(() => { })
|
|
1954
3033
|
.catch((e) => {
|
|
1955
3034
|
// ignore
|
|
3035
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1956
3036
|
});
|
|
1957
3037
|
}
|
|
1958
3038
|
else {
|
|
@@ -1968,17 +3048,18 @@ class StableBrowser {
|
|
|
1968
3048
|
if (!element.style) {
|
|
1969
3049
|
return;
|
|
1970
3050
|
}
|
|
1971
|
-
|
|
3051
|
+
let originalOutline = element.style.outline;
|
|
3052
|
+
element.__previousOutline = originalOutline;
|
|
1972
3053
|
// Set the new border to be red and 2px solid
|
|
1973
|
-
element.style.
|
|
3054
|
+
element.style.outline = "2px solid red";
|
|
1974
3055
|
if (window) {
|
|
1975
3056
|
window.addEventListener("beforeunload", function (e) {
|
|
1976
|
-
element.style.
|
|
3057
|
+
element.style.outline = originalOutline;
|
|
1977
3058
|
});
|
|
1978
3059
|
}
|
|
1979
3060
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1980
3061
|
setTimeout(function () {
|
|
1981
|
-
element.style.
|
|
3062
|
+
element.style.outline = originalOutline;
|
|
1982
3063
|
}, 2000);
|
|
1983
3064
|
}
|
|
1984
3065
|
return;
|
|
@@ -1986,6 +3067,7 @@ class StableBrowser {
|
|
|
1986
3067
|
.then(() => { })
|
|
1987
3068
|
.catch((e) => {
|
|
1988
3069
|
// ignore
|
|
3070
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1989
3071
|
});
|
|
1990
3072
|
}
|
|
1991
3073
|
}
|
|
@@ -1993,8 +3075,49 @@ class StableBrowser {
|
|
|
1993
3075
|
console.debug(error);
|
|
1994
3076
|
}
|
|
1995
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
|
+
*/
|
|
1996
3120
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
1997
|
-
const startTime = Date.now();
|
|
1998
3121
|
let error = null;
|
|
1999
3122
|
let screenshotId = null;
|
|
2000
3123
|
let screenshotPath = null;
|
|
@@ -2008,51 +3131,242 @@ class StableBrowser {
|
|
|
2008
3131
|
pathPart = newValue;
|
|
2009
3132
|
}
|
|
2010
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
|
+
};
|
|
2011
3148
|
try {
|
|
3149
|
+
await _preCommand(state, this);
|
|
3150
|
+
state.info.text = queryText;
|
|
2012
3151
|
for (let i = 0; i < 30; i++) {
|
|
2013
3152
|
const url = await this.page.url();
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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
|
+
}
|
|
2020
3222
|
}
|
|
2021
|
-
|
|
2022
|
-
return info;
|
|
3223
|
+
await _screenshot(state, this);
|
|
3224
|
+
return state.info;
|
|
2023
3225
|
}
|
|
2024
3226
|
}
|
|
2025
3227
|
catch (e) {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
info.screenshotPath = screenshotPath;
|
|
2030
|
-
Object.assign(e, { info: info });
|
|
2031
|
-
error = e;
|
|
2032
|
-
// throw e;
|
|
2033
|
-
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);
|
|
2034
3231
|
}
|
|
2035
3232
|
finally {
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
+
}
|
|
2055
3366
|
}
|
|
3367
|
+
state.info.results = results;
|
|
3368
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3369
|
+
return resultWithElementsFound;
|
|
2056
3370
|
}
|
|
2057
3371
|
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2058
3372
|
text = unEscapeString(text);
|
|
@@ -2063,13 +3377,26 @@ class StableBrowser {
|
|
|
2063
3377
|
locate: false,
|
|
2064
3378
|
scroll: false,
|
|
2065
3379
|
highlight: false,
|
|
2066
|
-
type: Types.
|
|
2067
|
-
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`,
|
|
2068
3383
|
operation: "verifyTextExistInPage",
|
|
2069
3384
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2070
3385
|
};
|
|
2071
|
-
|
|
2072
|
-
|
|
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
|
+
}
|
|
2073
3400
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2074
3401
|
if (newValue !== text) {
|
|
2075
3402
|
this.logger.info(text + "=" + newValue);
|
|
@@ -2077,35 +3404,24 @@ class StableBrowser {
|
|
|
2077
3404
|
}
|
|
2078
3405
|
let dateAlternatives = findDateAlternatives(text);
|
|
2079
3406
|
let numberAlternatives = findNumberAlternatives(text);
|
|
3407
|
+
if (stepFastMode) {
|
|
3408
|
+
state.onlyFailuresScreenshot = true;
|
|
3409
|
+
state.scroll = false;
|
|
3410
|
+
state.highlight = false;
|
|
3411
|
+
}
|
|
2080
3412
|
try {
|
|
2081
3413
|
await _preCommand(state, this);
|
|
2082
3414
|
state.info.text = text;
|
|
2083
3415
|
while (true) {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
else if (numberAlternatives.number) {
|
|
2095
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2096
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
|
|
2097
|
-
result.frame = frames[i];
|
|
2098
|
-
results.push(result);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
else {
|
|
2102
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
|
|
2103
|
-
result.frame = frames[i];
|
|
2104
|
-
results.push(result);
|
|
2105
|
-
}
|
|
3416
|
+
let resultWithElementsFound = {
|
|
3417
|
+
length: 0,
|
|
3418
|
+
};
|
|
3419
|
+
try {
|
|
3420
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3421
|
+
}
|
|
3422
|
+
catch (error) {
|
|
3423
|
+
// ignore
|
|
2106
3424
|
}
|
|
2107
|
-
state.info.results = results;
|
|
2108
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2109
3425
|
if (resultWithElementsFound.length === 0) {
|
|
2110
3426
|
if (Date.now() - state.startTime > timeout) {
|
|
2111
3427
|
throw new Error(`Text ${text} not found in page`);
|
|
@@ -2113,26 +3429,30 @@ class StableBrowser {
|
|
|
2113
3429
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2114
3430
|
continue;
|
|
2115
3431
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
+
}
|
|
2124
3442
|
}
|
|
3443
|
+
await _screenshot(state, this);
|
|
3444
|
+
return state.info;
|
|
3445
|
+
}
|
|
3446
|
+
catch (error) {
|
|
3447
|
+
console.error(error);
|
|
2125
3448
|
}
|
|
2126
|
-
await _screenshot(state, this);
|
|
2127
|
-
return state.info;
|
|
2128
3449
|
}
|
|
2129
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2130
3450
|
}
|
|
2131
3451
|
catch (e) {
|
|
2132
3452
|
await _commandError(state, e, this);
|
|
2133
3453
|
}
|
|
2134
3454
|
finally {
|
|
2135
|
-
_commandFinally(state, this);
|
|
3455
|
+
await _commandFinally(state, this);
|
|
2136
3456
|
}
|
|
2137
3457
|
}
|
|
2138
3458
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2145,11 +3465,15 @@ class StableBrowser {
|
|
|
2145
3465
|
scroll: false,
|
|
2146
3466
|
highlight: false,
|
|
2147
3467
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2148
|
-
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`,
|
|
2149
3470
|
operation: "verifyTextNotExistInPage",
|
|
2150
3471
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2151
3472
|
};
|
|
2152
|
-
|
|
3473
|
+
if (testForRegex(text)) {
|
|
3474
|
+
text = text.replace(/\\"/g, '"');
|
|
3475
|
+
}
|
|
3476
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2153
3477
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2154
3478
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2155
3479
|
if (newValue !== text) {
|
|
@@ -2161,32 +3485,16 @@ class StableBrowser {
|
|
|
2161
3485
|
try {
|
|
2162
3486
|
await _preCommand(state, this);
|
|
2163
3487
|
state.info.text = text;
|
|
3488
|
+
let resultWithElementsFound = {
|
|
3489
|
+
length: null, // initial cannot be 0
|
|
3490
|
+
};
|
|
2164
3491
|
while (true) {
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", true, true, {});
|
|
2171
|
-
result.frame = frames[i];
|
|
2172
|
-
results.push(result);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
else if (numberAlternatives.number) {
|
|
2176
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2177
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", true, true, {});
|
|
2178
|
-
result.frame = frames[i];
|
|
2179
|
-
results.push(result);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
else {
|
|
2183
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", true, true, {});
|
|
2184
|
-
result.frame = frames[i];
|
|
2185
|
-
results.push(result);
|
|
2186
|
-
}
|
|
3492
|
+
try {
|
|
3493
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
3494
|
+
}
|
|
3495
|
+
catch (error) {
|
|
3496
|
+
// ignore
|
|
2187
3497
|
}
|
|
2188
|
-
state.info.results = results;
|
|
2189
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2190
3498
|
if (resultWithElementsFound.length === 0) {
|
|
2191
3499
|
await _screenshot(state, this);
|
|
2192
3500
|
return state.info;
|
|
@@ -2201,18 +3509,158 @@ class StableBrowser {
|
|
|
2201
3509
|
await _commandError(state, e, this);
|
|
2202
3510
|
}
|
|
2203
3511
|
finally {
|
|
2204
|
-
_commandFinally(state, this);
|
|
3512
|
+
await _commandFinally(state, this);
|
|
2205
3513
|
}
|
|
2206
3514
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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);
|
|
2211
3636
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
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
|
+
}
|
|
2214
3660
|
}
|
|
2215
|
-
|
|
3661
|
+
// state.info.results = results;
|
|
3662
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3663
|
+
return resultWithElementsFound;
|
|
2216
3664
|
}
|
|
2217
3665
|
async visualVerification(text, options = {}, world = null) {
|
|
2218
3666
|
const startTime = Date.now();
|
|
@@ -2228,14 +3676,17 @@ class StableBrowser {
|
|
|
2228
3676
|
throw new Error("TOKEN is not set");
|
|
2229
3677
|
}
|
|
2230
3678
|
try {
|
|
2231
|
-
let serviceUrl =
|
|
3679
|
+
let serviceUrl = _getServerUrl();
|
|
2232
3680
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2233
3681
|
info.screenshotPath = screenshotPath;
|
|
2234
3682
|
const screenshot = await this.takeScreenshot();
|
|
2235
|
-
|
|
2236
|
-
method: "
|
|
3683
|
+
let request = {
|
|
3684
|
+
method: "post",
|
|
3685
|
+
maxBodyLength: Infinity,
|
|
2237
3686
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2238
3687
|
headers: {
|
|
3688
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3689
|
+
"x-source": "aaa",
|
|
2239
3690
|
"Content-Type": "application/json",
|
|
2240
3691
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2241
3692
|
},
|
|
@@ -2244,7 +3695,7 @@ class StableBrowser {
|
|
|
2244
3695
|
screenshot: screenshot,
|
|
2245
3696
|
}),
|
|
2246
3697
|
};
|
|
2247
|
-
|
|
3698
|
+
const result = await axios.request(request);
|
|
2248
3699
|
if (result.data.status !== true) {
|
|
2249
3700
|
throw new Error("Visual validation failed");
|
|
2250
3701
|
}
|
|
@@ -2265,13 +3716,14 @@ class StableBrowser {
|
|
|
2265
3716
|
Object.assign(e, { info: info });
|
|
2266
3717
|
error = e;
|
|
2267
3718
|
// throw e;
|
|
2268
|
-
await _commandError({ text: "visualVerification", operation: "visualVerification",
|
|
3719
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2269
3720
|
}
|
|
2270
3721
|
finally {
|
|
2271
3722
|
const endTime = Date.now();
|
|
2272
|
-
|
|
3723
|
+
_reportToWorld(world, {
|
|
2273
3724
|
type: Types.VERIFY_VISUAL,
|
|
2274
3725
|
text: "Visual verification",
|
|
3726
|
+
_text: "Visual verification of " + text,
|
|
2275
3727
|
screenshotId,
|
|
2276
3728
|
result: error
|
|
2277
3729
|
? {
|
|
@@ -2317,6 +3769,7 @@ class StableBrowser {
|
|
|
2317
3769
|
let screenshotPath = null;
|
|
2318
3770
|
const info = {};
|
|
2319
3771
|
info.log = "";
|
|
3772
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2320
3773
|
info.operation = "getTableData";
|
|
2321
3774
|
info.selectors = selectors;
|
|
2322
3775
|
try {
|
|
@@ -2337,7 +3790,7 @@ class StableBrowser {
|
|
|
2337
3790
|
}
|
|
2338
3791
|
finally {
|
|
2339
3792
|
const endTime = Date.now();
|
|
2340
|
-
|
|
3793
|
+
_reportToWorld(world, {
|
|
2341
3794
|
element_name: selectors.element_name,
|
|
2342
3795
|
type: Types.GET_TABLE_DATA,
|
|
2343
3796
|
text: "Get table data",
|
|
@@ -2392,7 +3845,7 @@ class StableBrowser {
|
|
|
2392
3845
|
info.operation = "analyzeTable";
|
|
2393
3846
|
info.selectors = selectors;
|
|
2394
3847
|
info.query = query;
|
|
2395
|
-
query =
|
|
3848
|
+
query = _fixUsingParams(query, _params);
|
|
2396
3849
|
info.query_fixed = query;
|
|
2397
3850
|
info.operator = operator;
|
|
2398
3851
|
info.value = value;
|
|
@@ -2503,7 +3956,7 @@ class StableBrowser {
|
|
|
2503
3956
|
}
|
|
2504
3957
|
finally {
|
|
2505
3958
|
const endTime = Date.now();
|
|
2506
|
-
|
|
3959
|
+
_reportToWorld(world, {
|
|
2507
3960
|
element_name: selectors.element_name,
|
|
2508
3961
|
type: Types.ANALYZE_TABLE,
|
|
2509
3962
|
text: "Analyze table",
|
|
@@ -2524,8 +3977,51 @@ class StableBrowser {
|
|
|
2524
3977
|
});
|
|
2525
3978
|
}
|
|
2526
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
|
+
}
|
|
2527
4017
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2528
|
-
|
|
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
|
+
}
|
|
2529
4025
|
}
|
|
2530
4026
|
_getLoadTimeout(options) {
|
|
2531
4027
|
let timeout = 15000;
|
|
@@ -2537,7 +4033,54 @@ class StableBrowser {
|
|
|
2537
4033
|
}
|
|
2538
4034
|
return timeout;
|
|
2539
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
|
+
}
|
|
2540
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");
|
|
2541
4084
|
let timeout = this._getLoadTimeout(options);
|
|
2542
4085
|
const promiseArray = [];
|
|
2543
4086
|
// let waitForNetworkIdle = true;
|
|
@@ -2570,13 +4113,15 @@ class StableBrowser {
|
|
|
2570
4113
|
else if (e.label === "domcontentloaded") {
|
|
2571
4114
|
console.log("waited for the domcontent loaded timeout");
|
|
2572
4115
|
}
|
|
2573
|
-
console.log(".");
|
|
2574
4116
|
}
|
|
2575
4117
|
finally {
|
|
2576
|
-
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
|
+
}
|
|
2577
4122
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2578
4123
|
const endTime = Date.now();
|
|
2579
|
-
|
|
4124
|
+
_reportToWorld(world, {
|
|
2580
4125
|
type: Types.GET_PAGE_STATUS,
|
|
2581
4126
|
text: "Wait for page load",
|
|
2582
4127
|
screenshotId,
|
|
@@ -2604,21 +4149,134 @@ class StableBrowser {
|
|
|
2604
4149
|
highlight: false,
|
|
2605
4150
|
type: Types.CLOSE_PAGE,
|
|
2606
4151
|
text: `Close page`,
|
|
4152
|
+
_text: `Close the page`,
|
|
2607
4153
|
operation: "closePage",
|
|
2608
4154
|
log: "***** close page *****\n",
|
|
2609
|
-
throwError: false,
|
|
4155
|
+
// throwError: false,
|
|
2610
4156
|
};
|
|
2611
4157
|
try {
|
|
2612
4158
|
await _preCommand(state, this);
|
|
2613
4159
|
await this.page.close();
|
|
2614
4160
|
}
|
|
2615
4161
|
catch (e) {
|
|
2616
|
-
console.log(".");
|
|
2617
4162
|
await _commandError(state, e, this);
|
|
2618
4163
|
}
|
|
2619
4164
|
finally {
|
|
2620
|
-
_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;
|
|
2621
4277
|
}
|
|
4278
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4279
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2622
4280
|
}
|
|
2623
4281
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2624
4282
|
const startTime = Date.now();
|
|
@@ -2636,16 +4294,16 @@ class StableBrowser {
|
|
|
2636
4294
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2637
4295
|
}
|
|
2638
4296
|
catch (e) {
|
|
2639
|
-
console.log(".");
|
|
2640
4297
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2641
4298
|
}
|
|
2642
4299
|
finally {
|
|
2643
4300
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2644
4301
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2645
4302
|
const endTime = Date.now();
|
|
2646
|
-
|
|
4303
|
+
_reportToWorld(world, {
|
|
2647
4304
|
type: Types.SET_VIEWPORT,
|
|
2648
4305
|
text: "set viewport size to " + width + "x" + hight,
|
|
4306
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2649
4307
|
screenshotId,
|
|
2650
4308
|
result: error
|
|
2651
4309
|
? {
|
|
@@ -2673,14 +4331,13 @@ class StableBrowser {
|
|
|
2673
4331
|
await this.page.reload();
|
|
2674
4332
|
}
|
|
2675
4333
|
catch (e) {
|
|
2676
|
-
console.log(".");
|
|
2677
4334
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2678
4335
|
}
|
|
2679
4336
|
finally {
|
|
2680
4337
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2681
4338
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2682
4339
|
const endTime = Date.now();
|
|
2683
|
-
|
|
4340
|
+
_reportToWorld(world, {
|
|
2684
4341
|
type: Types.GET_PAGE_STATUS,
|
|
2685
4342
|
text: "page relaod",
|
|
2686
4343
|
screenshotId,
|
|
@@ -2716,36 +4373,276 @@ class StableBrowser {
|
|
|
2716
4373
|
console.log("#-#");
|
|
2717
4374
|
}
|
|
2718
4375
|
}
|
|
2719
|
-
|
|
2720
|
-
if (
|
|
2721
|
-
|
|
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 "";
|
|
2722
4443
|
}
|
|
2723
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2724
4444
|
}
|
|
2725
4445
|
async beforeStep(world, step) {
|
|
2726
|
-
|
|
2727
|
-
|
|
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
|
+
}
|
|
2728
4456
|
if (this.stepIndex === undefined) {
|
|
2729
4457
|
this.stepIndex = 0;
|
|
2730
4458
|
}
|
|
2731
4459
|
else {
|
|
2732
4460
|
this.stepIndex++;
|
|
2733
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
|
+
}
|
|
2734
4477
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2735
4478
|
if (this.context.browserObject.context) {
|
|
2736
4479
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
2737
4480
|
}
|
|
2738
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
|
+
}
|
|
2739
4584
|
}
|
|
2740
|
-
async afterStep(world, step) {
|
|
4585
|
+
async afterStep(world, step, result) {
|
|
2741
4586
|
this.stepName = null;
|
|
2742
|
-
if (this.context
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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);
|
|
2747
4634
|
}
|
|
2748
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
|
+
}
|
|
2749
4646
|
}
|
|
2750
4647
|
}
|
|
2751
4648
|
function createTimedPromise(promise, label) {
|
|
@@ -2753,156 +4650,5 @@ function createTimedPromise(promise, label) {
|
|
|
2753
4650
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2754
4651
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2755
4652
|
}
|
|
2756
|
-
const KEYBOARD_EVENTS = [
|
|
2757
|
-
"ALT",
|
|
2758
|
-
"AltGraph",
|
|
2759
|
-
"CapsLock",
|
|
2760
|
-
"Control",
|
|
2761
|
-
"Fn",
|
|
2762
|
-
"FnLock",
|
|
2763
|
-
"Hyper",
|
|
2764
|
-
"Meta",
|
|
2765
|
-
"NumLock",
|
|
2766
|
-
"ScrollLock",
|
|
2767
|
-
"Shift",
|
|
2768
|
-
"Super",
|
|
2769
|
-
"Symbol",
|
|
2770
|
-
"SymbolLock",
|
|
2771
|
-
"Enter",
|
|
2772
|
-
"Tab",
|
|
2773
|
-
"ArrowDown",
|
|
2774
|
-
"ArrowLeft",
|
|
2775
|
-
"ArrowRight",
|
|
2776
|
-
"ArrowUp",
|
|
2777
|
-
"End",
|
|
2778
|
-
"Home",
|
|
2779
|
-
"PageDown",
|
|
2780
|
-
"PageUp",
|
|
2781
|
-
"Backspace",
|
|
2782
|
-
"Clear",
|
|
2783
|
-
"Copy",
|
|
2784
|
-
"CrSel",
|
|
2785
|
-
"Cut",
|
|
2786
|
-
"Delete",
|
|
2787
|
-
"EraseEof",
|
|
2788
|
-
"ExSel",
|
|
2789
|
-
"Insert",
|
|
2790
|
-
"Paste",
|
|
2791
|
-
"Redo",
|
|
2792
|
-
"Undo",
|
|
2793
|
-
"Accept",
|
|
2794
|
-
"Again",
|
|
2795
|
-
"Attn",
|
|
2796
|
-
"Cancel",
|
|
2797
|
-
"ContextMenu",
|
|
2798
|
-
"Escape",
|
|
2799
|
-
"Execute",
|
|
2800
|
-
"Find",
|
|
2801
|
-
"Finish",
|
|
2802
|
-
"Help",
|
|
2803
|
-
"Pause",
|
|
2804
|
-
"Play",
|
|
2805
|
-
"Props",
|
|
2806
|
-
"Select",
|
|
2807
|
-
"ZoomIn",
|
|
2808
|
-
"ZoomOut",
|
|
2809
|
-
"BrightnessDown",
|
|
2810
|
-
"BrightnessUp",
|
|
2811
|
-
"Eject",
|
|
2812
|
-
"LogOff",
|
|
2813
|
-
"Power",
|
|
2814
|
-
"PowerOff",
|
|
2815
|
-
"PrintScreen",
|
|
2816
|
-
"Hibernate",
|
|
2817
|
-
"Standby",
|
|
2818
|
-
"WakeUp",
|
|
2819
|
-
"AllCandidates",
|
|
2820
|
-
"Alphanumeric",
|
|
2821
|
-
"CodeInput",
|
|
2822
|
-
"Compose",
|
|
2823
|
-
"Convert",
|
|
2824
|
-
"Dead",
|
|
2825
|
-
"FinalMode",
|
|
2826
|
-
"GroupFirst",
|
|
2827
|
-
"GroupLast",
|
|
2828
|
-
"GroupNext",
|
|
2829
|
-
"GroupPrevious",
|
|
2830
|
-
"ModeChange",
|
|
2831
|
-
"NextCandidate",
|
|
2832
|
-
"NonConvert",
|
|
2833
|
-
"PreviousCandidate",
|
|
2834
|
-
"Process",
|
|
2835
|
-
"SingleCandidate",
|
|
2836
|
-
"HangulMode",
|
|
2837
|
-
"HanjaMode",
|
|
2838
|
-
"JunjaMode",
|
|
2839
|
-
"Eisu",
|
|
2840
|
-
"Hankaku",
|
|
2841
|
-
"Hiragana",
|
|
2842
|
-
"HiraganaKatakana",
|
|
2843
|
-
"KanaMode",
|
|
2844
|
-
"KanjiMode",
|
|
2845
|
-
"Katakana",
|
|
2846
|
-
"Romaji",
|
|
2847
|
-
"Zenkaku",
|
|
2848
|
-
"ZenkakuHanaku",
|
|
2849
|
-
"F1",
|
|
2850
|
-
"F2",
|
|
2851
|
-
"F3",
|
|
2852
|
-
"F4",
|
|
2853
|
-
"F5",
|
|
2854
|
-
"F6",
|
|
2855
|
-
"F7",
|
|
2856
|
-
"F8",
|
|
2857
|
-
"F9",
|
|
2858
|
-
"F10",
|
|
2859
|
-
"F11",
|
|
2860
|
-
"F12",
|
|
2861
|
-
"Soft1",
|
|
2862
|
-
"Soft2",
|
|
2863
|
-
"Soft3",
|
|
2864
|
-
"Soft4",
|
|
2865
|
-
"ChannelDown",
|
|
2866
|
-
"ChannelUp",
|
|
2867
|
-
"Close",
|
|
2868
|
-
"MailForward",
|
|
2869
|
-
"MailReply",
|
|
2870
|
-
"MailSend",
|
|
2871
|
-
"MediaFastForward",
|
|
2872
|
-
"MediaPause",
|
|
2873
|
-
"MediaPlay",
|
|
2874
|
-
"MediaPlayPause",
|
|
2875
|
-
"MediaRecord",
|
|
2876
|
-
"MediaRewind",
|
|
2877
|
-
"MediaStop",
|
|
2878
|
-
"MediaTrackNext",
|
|
2879
|
-
"MediaTrackPrevious",
|
|
2880
|
-
"AudioBalanceLeft",
|
|
2881
|
-
"AudioBalanceRight",
|
|
2882
|
-
"AudioBassBoostDown",
|
|
2883
|
-
"AudioBassBoostToggle",
|
|
2884
|
-
"AudioBassBoostUp",
|
|
2885
|
-
"AudioFaderFront",
|
|
2886
|
-
"AudioFaderRear",
|
|
2887
|
-
"AudioSurroundModeNext",
|
|
2888
|
-
"AudioTrebleDown",
|
|
2889
|
-
"AudioTrebleUp",
|
|
2890
|
-
"AudioVolumeDown",
|
|
2891
|
-
"AudioVolumeMute",
|
|
2892
|
-
"AudioVolumeUp",
|
|
2893
|
-
"MicrophoneToggle",
|
|
2894
|
-
"MicrophoneVolumeDown",
|
|
2895
|
-
"MicrophoneVolumeMute",
|
|
2896
|
-
"MicrophoneVolumeUp",
|
|
2897
|
-
"TV",
|
|
2898
|
-
"TV3DMode",
|
|
2899
|
-
"TVAntennaCable",
|
|
2900
|
-
"TVAudioDescription",
|
|
2901
|
-
];
|
|
2902
|
-
function unEscapeString(str) {
|
|
2903
|
-
const placeholder = "__NEWLINE__";
|
|
2904
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2905
|
-
return str;
|
|
2906
|
-
}
|
|
2907
4653
|
export { StableBrowser };
|
|
2908
4654
|
//# sourceMappingURL=stable_browser.js.map
|