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