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