automation_model 1.0.485-dev → 1.0.485
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 +306 -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 +146 -46
- package/lib/stable_browser.js +2569 -817
- 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 -3
- package/lib/utils.js +763 -36
- 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,58 +11,102 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
11
|
import drawRectangle from "./drawRect.js";
|
|
11
12
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
13
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
+
import errorStackParser from "error-stack-parser";
|
|
15
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
14
16
|
import csv from "csv-parser";
|
|
15
17
|
import { Readable } from "node:stream";
|
|
16
18
|
import readline from "readline";
|
|
17
|
-
import { getContext } from "./init_browser.js";
|
|
19
|
+
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
20
|
+
import { getTestData } from "./auto_page.js";
|
|
18
21
|
import { locate_element } from "./locate_element.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
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 = {
|
|
21
33
|
CLICK: "click_element",
|
|
34
|
+
WAIT_ELEMENT: "wait_element",
|
|
22
35
|
NAVIGATE: "navigate",
|
|
36
|
+
GO_BACK: "go_back",
|
|
37
|
+
GO_FORWARD: "go_forward",
|
|
23
38
|
FILL: "fill_element",
|
|
24
|
-
EXECUTE: "execute_page_method",
|
|
25
|
-
OPEN: "open_environment",
|
|
39
|
+
EXECUTE: "execute_page_method", //
|
|
40
|
+
OPEN: "open_environment", //
|
|
26
41
|
COMPLETE: "step_complete",
|
|
27
42
|
ASK: "information_needed",
|
|
28
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
29
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
43
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
44
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
30
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",
|
|
31
48
|
ANALYZE_TABLE: "analyze_table",
|
|
32
|
-
SELECT: "select_combobox",
|
|
49
|
+
SELECT: "select_combobox", //
|
|
50
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
33
51
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
52
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
34
53
|
TYPE_PRESS: "type_press",
|
|
35
54
|
PRESS: "press_key",
|
|
36
55
|
HOVER: "hover_element",
|
|
37
56
|
CHECK: "check_element",
|
|
38
57
|
UNCHECK: "uncheck_element",
|
|
39
58
|
EXTRACT: "extract_attribute",
|
|
59
|
+
EXTRACT_PROPERTY: "extract_property",
|
|
40
60
|
CLOSE_PAGE: "close_page",
|
|
61
|
+
TABLE_OPERATION: "table_operation",
|
|
41
62
|
SET_DATE_TIME: "set_date_time",
|
|
42
63
|
SET_VIEWPORT: "set_viewport",
|
|
43
64
|
VERIFY_VISUAL: "verify_visual",
|
|
44
65
|
LOAD_DATA: "load_data",
|
|
45
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",
|
|
46
78
|
};
|
|
47
79
|
export const apps = {};
|
|
80
|
+
const formatElementName = (elementName) => {
|
|
81
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
82
|
+
};
|
|
48
83
|
class StableBrowser {
|
|
49
84
|
browser;
|
|
50
85
|
page;
|
|
51
86
|
logger;
|
|
52
87
|
context;
|
|
53
88
|
world;
|
|
89
|
+
fastMode;
|
|
90
|
+
stepTags;
|
|
54
91
|
project_path = null;
|
|
55
92
|
webLogFile = null;
|
|
56
93
|
networkLogger = null;
|
|
57
94
|
configuration = null;
|
|
58
95
|
appName = "main";
|
|
59
|
-
|
|
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 = []) {
|
|
60
103
|
this.browser = browser;
|
|
61
104
|
this.page = page;
|
|
62
105
|
this.logger = logger;
|
|
63
106
|
this.context = context;
|
|
64
107
|
this.world = world;
|
|
108
|
+
this.fastMode = fastMode;
|
|
109
|
+
this.stepTags = stepTags;
|
|
65
110
|
if (!this.logger) {
|
|
66
111
|
this.logger = console;
|
|
67
112
|
}
|
|
@@ -90,20 +135,54 @@ class StableBrowser {
|
|
|
90
135
|
context.pages = [this.page];
|
|
91
136
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
92
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
|
+
}
|
|
93
151
|
this.registerEventListeners(this.context);
|
|
152
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
153
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
94
154
|
}
|
|
95
155
|
registerEventListeners(context) {
|
|
96
156
|
this.registerConsoleLogListener(this.page, context);
|
|
97
|
-
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
157
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
98
158
|
if (!context.pageLoading) {
|
|
99
159
|
context.pageLoading = { status: false };
|
|
100
160
|
}
|
|
161
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
162
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
163
|
+
}
|
|
101
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
|
+
}
|
|
102
170
|
context.pageLoading.status = true;
|
|
103
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
|
+
}
|
|
104
180
|
context.page = page;
|
|
105
181
|
context.pages.push(page);
|
|
182
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
183
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
106
184
|
page.on("close", async () => {
|
|
185
|
+
// return if browser context is already closed
|
|
107
186
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
108
187
|
this.context.pages.pop();
|
|
109
188
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -113,7 +192,12 @@ class StableBrowser {
|
|
|
113
192
|
console.log("Switched to page " + title);
|
|
114
193
|
}
|
|
115
194
|
catch (error) {
|
|
116
|
-
|
|
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
|
+
}
|
|
117
201
|
}
|
|
118
202
|
}
|
|
119
203
|
});
|
|
@@ -122,7 +206,12 @@ class StableBrowser {
|
|
|
122
206
|
console.log("Switch page: " + (await page.title()));
|
|
123
207
|
}
|
|
124
208
|
catch (e) {
|
|
125
|
-
|
|
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
|
+
}
|
|
126
215
|
}
|
|
127
216
|
context.pageLoading.status = false;
|
|
128
217
|
}.bind(this));
|
|
@@ -134,7 +223,7 @@ class StableBrowser {
|
|
|
134
223
|
}
|
|
135
224
|
let newContextCreated = false;
|
|
136
225
|
if (!apps[appName]) {
|
|
137
|
-
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);
|
|
138
227
|
newContextCreated = true;
|
|
139
228
|
apps[appName] = {
|
|
140
229
|
context: newContext,
|
|
@@ -143,31 +232,41 @@ class StableBrowser {
|
|
|
143
232
|
};
|
|
144
233
|
}
|
|
145
234
|
const tempContext = {};
|
|
146
|
-
|
|
147
|
-
|
|
235
|
+
_copyContext(this, tempContext);
|
|
236
|
+
_copyContext(apps[appName], this);
|
|
148
237
|
apps[this.appName] = tempContext;
|
|
149
238
|
this.appName = appName;
|
|
150
239
|
if (newContextCreated) {
|
|
151
240
|
this.registerEventListeners(this.context);
|
|
152
241
|
await this.goto(this.context.environment.baseUrl);
|
|
153
|
-
|
|
242
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
243
|
+
await this.waitForPageLoad();
|
|
244
|
+
}
|
|
154
245
|
}
|
|
155
246
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
|
164
257
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
}
|
|
168
268
|
}
|
|
169
|
-
|
|
170
|
-
return path.join(logFolder, fileName);
|
|
269
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
171
270
|
}
|
|
172
271
|
registerConsoleLogListener(page, context) {
|
|
173
272
|
if (!this.context.webLogger) {
|
|
@@ -218,7 +317,7 @@ class StableBrowser {
|
|
|
218
317
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
219
318
|
}
|
|
220
319
|
catch (error) {
|
|
221
|
-
console.error("Error in request listener", error);
|
|
320
|
+
// console.error("Error in request listener", error);
|
|
222
321
|
context.networkLogger.push({
|
|
223
322
|
error: "not able to listen",
|
|
224
323
|
message: error.message,
|
|
@@ -232,55 +331,110 @@ class StableBrowser {
|
|
|
232
331
|
// async closeUnexpectedPopups() {
|
|
233
332
|
// await closeUnexpectedPopups(this.page);
|
|
234
333
|
// }
|
|
235
|
-
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);
|
|
236
339
|
if (!url.startsWith("http")) {
|
|
237
340
|
url = "https://" + url;
|
|
238
341
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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);
|
|
246
361
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
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);
|
|
254
368
|
}
|
|
255
|
-
return text;
|
|
256
369
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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);
|
|
261
397
|
}
|
|
262
|
-
// clone the locator
|
|
263
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
264
|
-
this.scanAndManipulate(locator, _params);
|
|
265
|
-
return locator;
|
|
266
398
|
}
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
}
|
|
269
427
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
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);
|
|
279
436
|
}
|
|
280
437
|
}
|
|
281
|
-
}
|
|
282
|
-
_getLocator(locator, scope, _params) {
|
|
283
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
284
438
|
let locatorReturn;
|
|
285
439
|
if (locator.role) {
|
|
286
440
|
if (locator.role[1].nameReg) {
|
|
@@ -288,7 +442,7 @@ class StableBrowser {
|
|
|
288
442
|
delete locator.role[1].nameReg;
|
|
289
443
|
}
|
|
290
444
|
// if (locator.role[1].name) {
|
|
291
|
-
// locator.role[1].name =
|
|
445
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
292
446
|
// }
|
|
293
447
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
294
448
|
}
|
|
@@ -331,213 +485,171 @@ class StableBrowser {
|
|
|
331
485
|
if (css && css.locator) {
|
|
332
486
|
css = css.locator;
|
|
333
487
|
}
|
|
334
|
-
let result = await this._locateElementByText(scope,
|
|
488
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
335
489
|
if (result.elementCount === 0) {
|
|
336
490
|
return;
|
|
337
491
|
}
|
|
338
|
-
let textElementCss = "[data-blinq-id
|
|
492
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
339
493
|
// css climb to parent element
|
|
340
494
|
const climbArray = [];
|
|
341
495
|
for (let i = 0; i < climb; i++) {
|
|
342
496
|
climbArray.push("..");
|
|
343
497
|
}
|
|
344
498
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return new RegExp(pattern, flags);
|
|
368
|
-
}
|
|
369
|
-
document.getRegex = getRegex;
|
|
370
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
371
|
-
// Check and add the element if it has a shadow root
|
|
372
|
-
if (element.shadowRoot) {
|
|
373
|
-
result.push(element);
|
|
374
|
-
// Also search within the shadow root
|
|
375
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
376
|
-
}
|
|
377
|
-
// Iterate over child nodes
|
|
378
|
-
element.childNodes.forEach((child) => {
|
|
379
|
-
// Recursively call the function for each child node
|
|
380
|
-
document.collectAllShadowDomElements(child, result);
|
|
381
|
-
});
|
|
382
|
-
return result;
|
|
383
|
-
}
|
|
384
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
385
|
-
if (!tag) {
|
|
386
|
-
tag = "*";
|
|
387
|
-
}
|
|
388
|
-
let regexpSearch = document.getRegex(text);
|
|
389
|
-
if (regexpSearch) {
|
|
390
|
-
regex = true;
|
|
391
|
-
}
|
|
392
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
393
|
-
let shadowHosts = [];
|
|
394
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
395
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
396
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
397
|
-
if (!shadowElement) {
|
|
398
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
402
|
-
elements = elements.concat(shadowElements);
|
|
403
|
-
}
|
|
404
|
-
let randomToken = null;
|
|
405
|
-
const foundElements = [];
|
|
406
|
-
if (regex) {
|
|
407
|
-
if (!regexpSearch) {
|
|
408
|
-
regexpSearch = new RegExp(text, "im");
|
|
409
|
-
}
|
|
410
|
-
for (let i = 0; i < elements.length; i++) {
|
|
411
|
-
const element = elements[i];
|
|
412
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
413
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
414
|
-
foundElements.push(element);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
text = text.trim();
|
|
420
|
-
for (let i = 0; i < elements.length; i++) {
|
|
421
|
-
const element = elements[i];
|
|
422
|
-
if (partial) {
|
|
423
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
424
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
425
|
-
foundElements.push(element);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
430
|
-
(element.value && element.value === text)) {
|
|
431
|
-
foundElements.push(element);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
let noChildElements = [];
|
|
437
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
438
|
-
let element = foundElements[i];
|
|
439
|
-
let hasChild = false;
|
|
440
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
441
|
-
if (i === j) {
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (isParent(element, foundElements[j])) {
|
|
445
|
-
hasChild = true;
|
|
446
|
-
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;
|
|
447
521
|
}
|
|
448
522
|
}
|
|
449
|
-
if (!
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
let elementCount = 0;
|
|
454
|
-
if (noChildElements.length > 0) {
|
|
455
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
456
|
-
if (randomToken === null) {
|
|
457
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
458
|
-
}
|
|
459
|
-
let element = noChildElements[i];
|
|
460
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
461
|
-
elementCount++;
|
|
523
|
+
if (!el.setAttribute) {
|
|
524
|
+
el = el.parentElement;
|
|
462
525
|
}
|
|
526
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
527
|
+
return true;
|
|
528
|
+
}, [tag1, randomToken]))) {
|
|
529
|
+
continue;
|
|
463
530
|
}
|
|
464
|
-
|
|
465
|
-
}
|
|
531
|
+
tagCount++;
|
|
532
|
+
}
|
|
533
|
+
return { elementCount: tagCount, randomToken };
|
|
466
534
|
}
|
|
467
|
-
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
|
+
}
|
|
468
546
|
let locatorSearch = selectorHierarchy[index];
|
|
469
547
|
try {
|
|
470
|
-
locatorSearch =
|
|
548
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
471
549
|
}
|
|
472
550
|
catch (e) {
|
|
473
551
|
console.error(e);
|
|
474
552
|
}
|
|
553
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
475
554
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
476
555
|
let locator = null;
|
|
477
556
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
478
|
-
|
|
557
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
558
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
479
559
|
if (!locatorString) {
|
|
560
|
+
info.failCause.textNotFound = true;
|
|
561
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
480
562
|
return;
|
|
481
563
|
}
|
|
482
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
564
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
483
565
|
}
|
|
484
566
|
else if (locatorSearch.text) {
|
|
485
|
-
let
|
|
567
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
568
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
486
569
|
if (result.elementCount === 0) {
|
|
570
|
+
info.failCause.textNotFound = true;
|
|
571
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
487
572
|
return;
|
|
488
573
|
}
|
|
489
|
-
locatorSearch.css = "[data-blinq-id
|
|
574
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
490
575
|
if (locatorSearch.childCss) {
|
|
491
576
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
492
577
|
}
|
|
493
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
578
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
494
579
|
}
|
|
495
580
|
else {
|
|
496
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
581
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
497
582
|
}
|
|
498
583
|
// let cssHref = false;
|
|
499
584
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
500
585
|
// cssHref = true;
|
|
501
586
|
// }
|
|
502
587
|
let count = await locator.count();
|
|
588
|
+
if (count > 0 && !info.failCause.count) {
|
|
589
|
+
info.failCause.count = count;
|
|
590
|
+
}
|
|
503
591
|
//info.log += "total elements found " + count + "\n";
|
|
504
592
|
//let visibleCount = 0;
|
|
505
593
|
let visibleLocator = null;
|
|
506
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
594
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
507
595
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
596
|
+
if (info.locatorLog) {
|
|
597
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
598
|
+
}
|
|
508
599
|
return;
|
|
509
600
|
}
|
|
601
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
602
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
603
|
+
}
|
|
510
604
|
for (let j = 0; j < count; j++) {
|
|
511
605
|
let visible = await locator.nth(j).isVisible();
|
|
512
606
|
const enabled = await locator.nth(j).isEnabled();
|
|
513
607
|
if (!visibleOnly) {
|
|
514
608
|
visible = true;
|
|
515
609
|
}
|
|
516
|
-
if (visible && enabled) {
|
|
610
|
+
if (visible && (allowDisabled || enabled)) {
|
|
517
611
|
foundLocators.push(locator.nth(j));
|
|
612
|
+
if (info.locatorLog) {
|
|
613
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
614
|
+
}
|
|
518
615
|
}
|
|
519
|
-
else {
|
|
616
|
+
else if (logErrors) {
|
|
617
|
+
info.failCause.visible = visible;
|
|
618
|
+
info.failCause.enabled = enabled;
|
|
520
619
|
if (!info.printMessages) {
|
|
521
620
|
info.printMessages = {};
|
|
522
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
|
+
}
|
|
523
630
|
if (!info.printMessages[j.toString()]) {
|
|
524
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
631
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
525
632
|
info.printMessages[j.toString()] = true;
|
|
526
633
|
}
|
|
527
634
|
}
|
|
528
635
|
}
|
|
529
636
|
}
|
|
530
637
|
async closeUnexpectedPopups(info, _params) {
|
|
638
|
+
if (!info) {
|
|
639
|
+
info = {};
|
|
640
|
+
info.failCause = {};
|
|
641
|
+
info.log = "";
|
|
642
|
+
}
|
|
531
643
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
532
644
|
if (!info) {
|
|
533
645
|
info = {};
|
|
534
646
|
}
|
|
535
|
-
info.log += "scan for popup handlers" + "\n";
|
|
647
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
536
648
|
const handlerGroup = [];
|
|
537
649
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
538
650
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
539
651
|
}
|
|
540
|
-
const scopes =
|
|
652
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
541
653
|
let result = null;
|
|
542
654
|
let scope = null;
|
|
543
655
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -559,33 +671,218 @@ class StableBrowser {
|
|
|
559
671
|
}
|
|
560
672
|
if (result.foundElements.length > 0) {
|
|
561
673
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
}
|
|
565
689
|
return { rerun: true };
|
|
566
690
|
}
|
|
567
691
|
}
|
|
568
692
|
}
|
|
569
693
|
return { rerun: false };
|
|
570
694
|
}
|
|
571
|
-
|
|
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) {
|
|
572
760
|
if (!timeout) {
|
|
573
761
|
timeout = 30000;
|
|
574
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
|
+
}
|
|
575
770
|
for (let i = 0; i < 3; i++) {
|
|
576
771
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
577
772
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
578
773
|
let selector = selectors.locators[j];
|
|
579
774
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
580
775
|
}
|
|
581
|
-
|
|
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
|
+
}
|
|
582
819
|
if (!element.rerun) {
|
|
583
|
-
|
|
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();
|
|
584
875
|
}
|
|
585
876
|
}
|
|
586
877
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
587
878
|
}
|
|
588
|
-
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();
|
|
589
886
|
let scope = this.page;
|
|
590
887
|
if (selectors.frame) {
|
|
591
888
|
return selectors.frame;
|
|
@@ -595,7 +892,7 @@ class StableBrowser {
|
|
|
595
892
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
596
893
|
let frameLocator = frame.selectors[i];
|
|
597
894
|
if (frameLocator.css) {
|
|
598
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
895
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
599
896
|
if (frameLocator.index) {
|
|
600
897
|
testframescope = framescope.nth(frameLocator.index);
|
|
601
898
|
}
|
|
@@ -607,7 +904,7 @@ class StableBrowser {
|
|
|
607
904
|
break;
|
|
608
905
|
}
|
|
609
906
|
catch (error) {
|
|
610
|
-
console.error("frame not found " + frameLocator.css);
|
|
907
|
+
// console.error("frame not found " + frameLocator.css);
|
|
611
908
|
}
|
|
612
909
|
}
|
|
613
910
|
}
|
|
@@ -616,9 +913,11 @@ class StableBrowser {
|
|
|
616
913
|
}
|
|
617
914
|
return framescope;
|
|
618
915
|
};
|
|
916
|
+
let fLocator = null;
|
|
619
917
|
while (true) {
|
|
620
918
|
let frameFound = false;
|
|
621
919
|
if (selectors.nestFrmLoc) {
|
|
920
|
+
fLocator = selectors.nestFrmLoc;
|
|
622
921
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
623
922
|
frameFound = true;
|
|
624
923
|
break;
|
|
@@ -627,6 +926,7 @@ class StableBrowser {
|
|
|
627
926
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
628
927
|
let frameLocator = selectors.frameLocators[i];
|
|
629
928
|
if (frameLocator.css) {
|
|
929
|
+
fLocator = frameLocator.css;
|
|
630
930
|
scope = scope.frameLocator(frameLocator.css);
|
|
631
931
|
frameFound = true;
|
|
632
932
|
break;
|
|
@@ -634,16 +934,25 @@ class StableBrowser {
|
|
|
634
934
|
}
|
|
635
935
|
}
|
|
636
936
|
if (!frameFound && selectors.iframe_src) {
|
|
937
|
+
fLocator = selectors.iframe_src;
|
|
637
938
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
638
939
|
}
|
|
639
940
|
if (!scope) {
|
|
640
|
-
info
|
|
641
|
-
|
|
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}"`;
|
|
642
948
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
643
949
|
}
|
|
644
950
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
645
951
|
}
|
|
646
952
|
else {
|
|
953
|
+
if (info && info.locatorLog) {
|
|
954
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
955
|
+
}
|
|
647
956
|
break;
|
|
648
957
|
}
|
|
649
958
|
}
|
|
@@ -653,20 +962,35 @@ class StableBrowser {
|
|
|
653
962
|
}
|
|
654
963
|
return scope;
|
|
655
964
|
}
|
|
656
|
-
async _getDocumentBody(selectors, timeout = 30000) {
|
|
657
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
965
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
966
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
658
967
|
return scope.evaluate(() => {
|
|
659
968
|
var bodyContent = document.body.innerHTML;
|
|
660
969
|
return bodyContent;
|
|
661
970
|
});
|
|
662
971
|
}
|
|
663
|
-
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
|
+
}
|
|
664
988
|
let highPriorityTimeout = 5000;
|
|
665
989
|
let visibleOnlyTimeout = 6000;
|
|
666
|
-
let startTime =
|
|
990
|
+
let startTime = Date.now();
|
|
667
991
|
let locatorsCount = 0;
|
|
992
|
+
let lazy_scroll = false;
|
|
668
993
|
//let arrayMode = Array.isArray(selectors);
|
|
669
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
670
994
|
let selectorsLocators = null;
|
|
671
995
|
selectorsLocators = selectors.locators;
|
|
672
996
|
// group selectors by priority
|
|
@@ -694,6 +1018,7 @@ class StableBrowser {
|
|
|
694
1018
|
let highPriorityOnly = true;
|
|
695
1019
|
let visibleOnly = true;
|
|
696
1020
|
while (true) {
|
|
1021
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
697
1022
|
locatorsCount = 0;
|
|
698
1023
|
let result = [];
|
|
699
1024
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -702,18 +1027,13 @@ class StableBrowser {
|
|
|
702
1027
|
}
|
|
703
1028
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
704
1029
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
705
|
-
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);
|
|
706
1031
|
if (result.foundElements.length === 0) {
|
|
707
1032
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
708
|
-
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);
|
|
709
1034
|
}
|
|
710
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
711
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
712
|
-
}
|
|
713
|
-
else {
|
|
714
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
715
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
716
|
-
}
|
|
1035
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1036
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
717
1037
|
}
|
|
718
1038
|
let foundElements = result.foundElements;
|
|
719
1039
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -753,24 +1073,43 @@ class StableBrowser {
|
|
|
753
1073
|
return maxCountElement.locator;
|
|
754
1074
|
}
|
|
755
1075
|
}
|
|
756
|
-
if (
|
|
1076
|
+
if (Date.now() - startTime > timeout) {
|
|
757
1077
|
break;
|
|
758
1078
|
}
|
|
759
|
-
if (
|
|
760
|
-
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";
|
|
761
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
|
+
}
|
|
762
1086
|
}
|
|
763
|
-
if (
|
|
764
|
-
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";
|
|
765
1089
|
visibleOnly = false;
|
|
766
1090
|
}
|
|
767
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
|
+
}
|
|
768
1097
|
}
|
|
769
1098
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
770
|
-
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
|
+
}
|
|
771
1110
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
772
1111
|
}
|
|
773
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1112
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
774
1113
|
let foundElements = [];
|
|
775
1114
|
const result = {
|
|
776
1115
|
foundElements: foundElements,
|
|
@@ -778,31 +1117,88 @@ class StableBrowser {
|
|
|
778
1117
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
779
1118
|
let foundLocators = [];
|
|
780
1119
|
try {
|
|
781
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1120
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
782
1121
|
}
|
|
783
1122
|
catch (e) {
|
|
784
|
-
this
|
|
785
|
-
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);
|
|
786
1126
|
foundLocators = [];
|
|
787
1127
|
try {
|
|
788
|
-
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);
|
|
789
1129
|
}
|
|
790
1130
|
catch (e) {
|
|
791
|
-
|
|
1131
|
+
if (logErrors) {
|
|
1132
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1133
|
+
}
|
|
792
1134
|
}
|
|
793
1135
|
}
|
|
794
1136
|
if (foundLocators.length === 1) {
|
|
1137
|
+
let box = null;
|
|
1138
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1139
|
+
box = await foundLocators[0].boundingBox();
|
|
1140
|
+
}
|
|
795
1141
|
result.foundElements.push({
|
|
796
1142
|
locator: foundLocators[0],
|
|
797
|
-
box:
|
|
1143
|
+
box: box,
|
|
798
1144
|
unique: true,
|
|
799
1145
|
});
|
|
800
1146
|
result.locatorIndex = i;
|
|
801
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
|
+
}
|
|
802
1185
|
}
|
|
803
1186
|
return result;
|
|
804
1187
|
}
|
|
805
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);
|
|
806
1202
|
const startTime = Date.now();
|
|
807
1203
|
let timeout = 30000;
|
|
808
1204
|
if (options && options.timeout) {
|
|
@@ -826,13 +1222,32 @@ class StableBrowser {
|
|
|
826
1222
|
}
|
|
827
1223
|
catch (e) {
|
|
828
1224
|
if (performance.now() - startTime > timeout) {
|
|
829
|
-
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
|
+
}
|
|
830
1232
|
}
|
|
831
1233
|
}
|
|
832
1234
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
833
1235
|
}
|
|
834
1236
|
}
|
|
835
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);
|
|
836
1251
|
const startTime = Date.now();
|
|
837
1252
|
let timeout = 30000;
|
|
838
1253
|
if (options && options.timeout) {
|
|
@@ -856,7 +1271,13 @@ class StableBrowser {
|
|
|
856
1271
|
}
|
|
857
1272
|
catch (e) {
|
|
858
1273
|
if (performance.now() - startTime > timeout) {
|
|
859
|
-
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
|
+
}
|
|
860
1281
|
}
|
|
861
1282
|
}
|
|
862
1283
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -869,34 +1290,74 @@ class StableBrowser {
|
|
|
869
1290
|
options,
|
|
870
1291
|
world,
|
|
871
1292
|
text: "Click element",
|
|
1293
|
+
_text: "Click on " + selectors.element_name,
|
|
872
1294
|
type: Types.CLICK,
|
|
873
1295
|
operation: "click",
|
|
874
1296
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
875
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
|
+
}
|
|
876
1305
|
try {
|
|
1306
|
+
check_performance("click_preCommand", this.context, true);
|
|
877
1307
|
await _preCommand(state, this);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
await
|
|
883
|
-
|
|
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);
|
|
884
1314
|
}
|
|
885
|
-
catch (e) {
|
|
886
|
-
// await this.closeUnexpectedPopups();
|
|
887
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
888
|
-
await state.element.dispatchEvent("click");
|
|
889
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
890
|
-
}
|
|
891
|
-
await this.waitForPageLoad();
|
|
892
1315
|
return state.info;
|
|
893
1316
|
}
|
|
894
1317
|
catch (e) {
|
|
895
1318
|
await _commandError(state, e, this);
|
|
896
1319
|
}
|
|
897
1320
|
finally {
|
|
898
|
-
|
|
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);
|
|
899
1359
|
}
|
|
1360
|
+
return found;
|
|
900
1361
|
}
|
|
901
1362
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
902
1363
|
const state = {
|
|
@@ -906,6 +1367,7 @@ class StableBrowser {
|
|
|
906
1367
|
world,
|
|
907
1368
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
908
1369
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1370
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
909
1371
|
operation: "setCheck",
|
|
910
1372
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
911
1373
|
};
|
|
@@ -915,30 +1377,53 @@ class StableBrowser {
|
|
|
915
1377
|
// let element = await this._locate(selectors, info, _params);
|
|
916
1378
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
917
1379
|
try {
|
|
918
|
-
//
|
|
919
|
-
|
|
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 });
|
|
920
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);
|
|
921
1389
|
}
|
|
922
1390
|
catch (e) {
|
|
923
1391
|
if (e.message && e.message.includes("did not change its state")) {
|
|
924
1392
|
this.logger.info("element did not change its state, ignoring...");
|
|
925
1393
|
}
|
|
926
1394
|
else {
|
|
1395
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
927
1396
|
//await this.closeUnexpectedPopups();
|
|
928
1397
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
929
|
-
state.
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
}
|
|
932
1417
|
}
|
|
933
1418
|
}
|
|
934
|
-
await this.waitForPageLoad();
|
|
1419
|
+
//await this.waitForPageLoad();
|
|
935
1420
|
return state.info;
|
|
936
1421
|
}
|
|
937
1422
|
catch (e) {
|
|
938
1423
|
await _commandError(state, e, this);
|
|
939
1424
|
}
|
|
940
1425
|
finally {
|
|
941
|
-
_commandFinally(state, this);
|
|
1426
|
+
await _commandFinally(state, this);
|
|
942
1427
|
}
|
|
943
1428
|
}
|
|
944
1429
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -949,31 +1434,22 @@ class StableBrowser {
|
|
|
949
1434
|
world,
|
|
950
1435
|
type: Types.HOVER,
|
|
951
1436
|
text: `Hover element`,
|
|
1437
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
952
1438
|
operation: "hover",
|
|
953
1439
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
954
1440
|
};
|
|
955
1441
|
try {
|
|
956
1442
|
await _preCommand(state, this);
|
|
957
|
-
|
|
958
|
-
await state.element.hover();
|
|
959
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
960
|
-
}
|
|
961
|
-
catch (e) {
|
|
962
|
-
//await this.closeUnexpectedPopups();
|
|
963
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
964
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
965
|
-
await state.element.hover({ timeout: 10000 });
|
|
966
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
967
|
-
}
|
|
1443
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
968
1444
|
await _screenshot(state, this);
|
|
969
|
-
await this.waitForPageLoad();
|
|
1445
|
+
//await this.waitForPageLoad();
|
|
970
1446
|
return state.info;
|
|
971
1447
|
}
|
|
972
1448
|
catch (e) {
|
|
973
1449
|
await _commandError(state, e, this);
|
|
974
1450
|
}
|
|
975
1451
|
finally {
|
|
976
|
-
_commandFinally(state, this);
|
|
1452
|
+
await _commandFinally(state, this);
|
|
977
1453
|
}
|
|
978
1454
|
}
|
|
979
1455
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -988,6 +1464,7 @@ class StableBrowser {
|
|
|
988
1464
|
value: values.toString(),
|
|
989
1465
|
type: Types.SELECT,
|
|
990
1466
|
text: `Select option: ${values}`,
|
|
1467
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
991
1468
|
operation: "selectOption",
|
|
992
1469
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
993
1470
|
};
|
|
@@ -1001,14 +1478,14 @@ class StableBrowser {
|
|
|
1001
1478
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1002
1479
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1003
1480
|
}
|
|
1004
|
-
await this.waitForPageLoad();
|
|
1481
|
+
//await this.waitForPageLoad();
|
|
1005
1482
|
return state.info;
|
|
1006
1483
|
}
|
|
1007
1484
|
catch (e) {
|
|
1008
1485
|
await _commandError(state, e, this);
|
|
1009
1486
|
}
|
|
1010
1487
|
finally {
|
|
1011
|
-
_commandFinally(state, this);
|
|
1488
|
+
await _commandFinally(state, this);
|
|
1012
1489
|
}
|
|
1013
1490
|
}
|
|
1014
1491
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1022,6 +1499,7 @@ class StableBrowser {
|
|
|
1022
1499
|
highlight: false,
|
|
1023
1500
|
type: Types.TYPE_PRESS,
|
|
1024
1501
|
text: `Type value: ${_value}`,
|
|
1502
|
+
_text: `Type value: ${_value}`,
|
|
1025
1503
|
operation: "type",
|
|
1026
1504
|
log: "",
|
|
1027
1505
|
};
|
|
@@ -1053,7 +1531,7 @@ class StableBrowser {
|
|
|
1053
1531
|
await _commandError(state, e, this);
|
|
1054
1532
|
}
|
|
1055
1533
|
finally {
|
|
1056
|
-
_commandFinally(state, this);
|
|
1534
|
+
await _commandFinally(state, this);
|
|
1057
1535
|
}
|
|
1058
1536
|
}
|
|
1059
1537
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1089,37 +1567,35 @@ class StableBrowser {
|
|
|
1089
1567
|
await _commandError(state, e, this);
|
|
1090
1568
|
}
|
|
1091
1569
|
finally {
|
|
1092
|
-
_commandFinally(state, this);
|
|
1570
|
+
await _commandFinally(state, this);
|
|
1093
1571
|
}
|
|
1094
1572
|
}
|
|
1095
1573
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
+
};
|
|
1106
1587
|
try {
|
|
1107
|
-
|
|
1108
|
-
let element = await this._locate(selectors, info, _params);
|
|
1109
|
-
//insert red border around the element
|
|
1110
|
-
await this.scrollIfNeeded(element, info);
|
|
1111
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1112
|
-
await this._highlightElements(element);
|
|
1588
|
+
await _preCommand(state, this);
|
|
1113
1589
|
try {
|
|
1114
|
-
await element
|
|
1590
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1115
1591
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1116
1592
|
if (format) {
|
|
1117
|
-
value = dayjs(value).format(format);
|
|
1118
|
-
await element.fill(value);
|
|
1593
|
+
state.value = dayjs(state.value).format(format);
|
|
1594
|
+
await state.element.fill(state.value);
|
|
1119
1595
|
}
|
|
1120
1596
|
else {
|
|
1121
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1122
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1597
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1598
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1123
1599
|
el.value = ""; // clear input
|
|
1124
1600
|
el.value = dateTimeValue;
|
|
1125
1601
|
}, dateTimeValue);
|
|
@@ -1132,20 +1608,19 @@ class StableBrowser {
|
|
|
1132
1608
|
}
|
|
1133
1609
|
catch (err) {
|
|
1134
1610
|
//await this.closeUnexpectedPopups();
|
|
1135
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1611
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1136
1612
|
this.logger.info("Trying again");
|
|
1137
|
-
|
|
1138
|
-
info.
|
|
1139
|
-
Object.assign(err, { info: info });
|
|
1613
|
+
await _screenshot(state, this);
|
|
1614
|
+
Object.assign(err, { info: state.info });
|
|
1140
1615
|
await element.click();
|
|
1141
1616
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1142
1617
|
if (format) {
|
|
1143
|
-
value = dayjs(value).format(format);
|
|
1144
|
-
await element.fill(value);
|
|
1618
|
+
state.value = dayjs(state.value).format(format);
|
|
1619
|
+
await state.element.fill(state.value);
|
|
1145
1620
|
}
|
|
1146
1621
|
else {
|
|
1147
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1148
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1622
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1623
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1149
1624
|
el.value = ""; // clear input
|
|
1150
1625
|
el.value = dateTimeValue;
|
|
1151
1626
|
}, dateTimeValue);
|
|
@@ -1158,55 +1633,47 @@ class StableBrowser {
|
|
|
1158
1633
|
}
|
|
1159
1634
|
}
|
|
1160
1635
|
catch (e) {
|
|
1161
|
-
|
|
1162
|
-
throw e;
|
|
1636
|
+
await _commandError(state, e, this);
|
|
1163
1637
|
}
|
|
1164
1638
|
finally {
|
|
1165
|
-
|
|
1166
|
-
this._reportToWorld(world, {
|
|
1167
|
-
element_name: selectors.element_name,
|
|
1168
|
-
type: Types.SET_DATE_TIME,
|
|
1169
|
-
screenshotId,
|
|
1170
|
-
value: value,
|
|
1171
|
-
text: `setDateTime input with value: ${value}`,
|
|
1172
|
-
result: error
|
|
1173
|
-
? {
|
|
1174
|
-
status: "FAILED",
|
|
1175
|
-
startTime,
|
|
1176
|
-
endTime,
|
|
1177
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1178
|
-
}
|
|
1179
|
-
: {
|
|
1180
|
-
status: "PASSED",
|
|
1181
|
-
startTime,
|
|
1182
|
-
endTime,
|
|
1183
|
-
},
|
|
1184
|
-
info: info,
|
|
1185
|
-
});
|
|
1639
|
+
await _commandFinally(state, this);
|
|
1186
1640
|
}
|
|
1187
1641
|
}
|
|
1188
1642
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1643
|
+
_value = unEscapeString(_value);
|
|
1644
|
+
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1189
1645
|
const state = {
|
|
1190
1646
|
selectors,
|
|
1191
1647
|
_params,
|
|
1192
|
-
value:
|
|
1648
|
+
value: newValue,
|
|
1649
|
+
originalValue: _value,
|
|
1193
1650
|
options,
|
|
1194
1651
|
world,
|
|
1195
1652
|
type: Types.FILL,
|
|
1196
1653
|
text: `Click type input with value: ${_value}`,
|
|
1654
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1197
1655
|
operation: "clickType",
|
|
1198
|
-
log: "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n",
|
|
1656
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1199
1657
|
};
|
|
1200
|
-
|
|
1658
|
+
if (!options) {
|
|
1659
|
+
options = {};
|
|
1660
|
+
}
|
|
1201
1661
|
if (newValue !== _value) {
|
|
1202
1662
|
//this.logger.info(_value + "=" + newValue);
|
|
1203
1663
|
_value = newValue;
|
|
1204
|
-
state.value = newValue;
|
|
1205
1664
|
}
|
|
1206
1665
|
try {
|
|
1207
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);
|
|
1208
1675
|
state.info.value = _value;
|
|
1209
|
-
if (
|
|
1676
|
+
if (!options.press) {
|
|
1210
1677
|
try {
|
|
1211
1678
|
let currentValue = await state.element.inputValue();
|
|
1212
1679
|
if (currentValue) {
|
|
@@ -1217,13 +1684,9 @@ class StableBrowser {
|
|
|
1217
1684
|
this.logger.info("unable to clear input value");
|
|
1218
1685
|
}
|
|
1219
1686
|
}
|
|
1220
|
-
if (options
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}
|
|
1224
|
-
catch (e) {
|
|
1225
|
-
await state.element.dispatchEvent("click");
|
|
1226
|
-
}
|
|
1687
|
+
if (options.press) {
|
|
1688
|
+
options.timeout = 5000;
|
|
1689
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1227
1690
|
}
|
|
1228
1691
|
else {
|
|
1229
1692
|
try {
|
|
@@ -1234,6 +1697,25 @@ class StableBrowser {
|
|
|
1234
1697
|
}
|
|
1235
1698
|
}
|
|
1236
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
|
+
}
|
|
1237
1719
|
const valueSegment = state.value.split("&&");
|
|
1238
1720
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1239
1721
|
if (i > 0) {
|
|
@@ -1254,14 +1736,21 @@ class StableBrowser {
|
|
|
1254
1736
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1255
1737
|
}
|
|
1256
1738
|
}
|
|
1739
|
+
//if (!this.fastMode) {
|
|
1257
1740
|
await _screenshot(state, this);
|
|
1741
|
+
//}
|
|
1258
1742
|
if (enter === true) {
|
|
1259
1743
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1260
1744
|
await this.page.keyboard.press("Enter");
|
|
1261
1745
|
await this.waitForPageLoad();
|
|
1262
1746
|
}
|
|
1263
1747
|
else if (enter === false) {
|
|
1264
|
-
|
|
1748
|
+
try {
|
|
1749
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1750
|
+
}
|
|
1751
|
+
catch (e) {
|
|
1752
|
+
// ignore
|
|
1753
|
+
}
|
|
1265
1754
|
//await this.page.keyboard.press("Tab");
|
|
1266
1755
|
}
|
|
1267
1756
|
else {
|
|
@@ -1276,7 +1765,7 @@ class StableBrowser {
|
|
|
1276
1765
|
await _commandError(state, e, this);
|
|
1277
1766
|
}
|
|
1278
1767
|
finally {
|
|
1279
|
-
_commandFinally(state, this);
|
|
1768
|
+
await _commandFinally(state, this);
|
|
1280
1769
|
}
|
|
1281
1770
|
}
|
|
1282
1771
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1298,30 +1787,67 @@ class StableBrowser {
|
|
|
1298
1787
|
if (enter) {
|
|
1299
1788
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1300
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;
|
|
1301
1825
|
}
|
|
1302
|
-
await
|
|
1826
|
+
await state.element.setInputFiles(files);
|
|
1303
1827
|
return state.info;
|
|
1304
1828
|
}
|
|
1305
1829
|
catch (e) {
|
|
1306
1830
|
await _commandError(state, e, this);
|
|
1307
1831
|
}
|
|
1308
1832
|
finally {
|
|
1309
|
-
_commandFinally(state, this);
|
|
1833
|
+
await _commandFinally(state, this);
|
|
1310
1834
|
}
|
|
1311
1835
|
}
|
|
1312
1836
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1313
1837
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1314
1838
|
}
|
|
1315
1839
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1840
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1316
1841
|
_validateSelectors(selectors);
|
|
1317
1842
|
let screenshotId = null;
|
|
1318
1843
|
let screenshotPath = null;
|
|
1319
1844
|
if (!info.log) {
|
|
1320
1845
|
info.log = "";
|
|
1846
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1321
1847
|
}
|
|
1322
1848
|
info.operation = "getText";
|
|
1323
1849
|
info.selectors = selectors;
|
|
1324
|
-
let element = await this._locate(selectors, info, _params);
|
|
1850
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1325
1851
|
if (climb > 0) {
|
|
1326
1852
|
const climbArray = [];
|
|
1327
1853
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1340,6 +1866,18 @@ class StableBrowser {
|
|
|
1340
1866
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1341
1867
|
try {
|
|
1342
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
|
+
// }
|
|
1343
1881
|
const elementText = await element.innerText();
|
|
1344
1882
|
return {
|
|
1345
1883
|
text: elementText,
|
|
@@ -1351,7 +1889,7 @@ class StableBrowser {
|
|
|
1351
1889
|
}
|
|
1352
1890
|
catch (e) {
|
|
1353
1891
|
//await this.closeUnexpectedPopups();
|
|
1354
|
-
this.logger.info("no innerText will use textContent");
|
|
1892
|
+
this.logger.info("no innerText, will use textContent");
|
|
1355
1893
|
const elementText = await element.textContent();
|
|
1356
1894
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1357
1895
|
}
|
|
@@ -1376,6 +1914,7 @@ class StableBrowser {
|
|
|
1376
1914
|
highlight: false,
|
|
1377
1915
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1378
1916
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1917
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1379
1918
|
operation: "containsPattern",
|
|
1380
1919
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1381
1920
|
};
|
|
@@ -1407,10 +1946,12 @@ class StableBrowser {
|
|
|
1407
1946
|
await _commandError(state, e, this);
|
|
1408
1947
|
}
|
|
1409
1948
|
finally {
|
|
1410
|
-
_commandFinally(state, this);
|
|
1949
|
+
await _commandFinally(state, this);
|
|
1411
1950
|
}
|
|
1412
1951
|
}
|
|
1413
1952
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1953
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1954
|
+
const startTime = Date.now();
|
|
1414
1955
|
const state = {
|
|
1415
1956
|
selectors,
|
|
1416
1957
|
_params,
|
|
@@ -1437,61 +1978,130 @@ class StableBrowser {
|
|
|
1437
1978
|
}
|
|
1438
1979
|
let foundObj = null;
|
|
1439
1980
|
try {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1447
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1448
|
-
if (dateAlternatives.date) {
|
|
1449
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1450
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1451
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1452
|
-
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);
|
|
1453
1987
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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)) {
|
|
1461
2008
|
return state.info;
|
|
1462
2009
|
}
|
|
1463
2010
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
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
|
|
1470
2016
|
}
|
|
1471
|
-
|
|
2017
|
+
state.info.foundText = foundObj?.text;
|
|
2018
|
+
state.info.value = foundObj?.value;
|
|
2019
|
+
throw new Error("element doesn't contain text " + text);
|
|
1472
2020
|
}
|
|
1473
2021
|
catch (e) {
|
|
1474
2022
|
await _commandError(state, e, this);
|
|
2023
|
+
throw e;
|
|
1475
2024
|
}
|
|
1476
2025
|
finally {
|
|
1477
|
-
_commandFinally(state, this);
|
|
2026
|
+
await _commandFinally(state, this);
|
|
1478
2027
|
}
|
|
1479
2028
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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");
|
|
1484
2052
|
}
|
|
1485
|
-
else if (this.
|
|
1486
|
-
|
|
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");
|
|
1487
2055
|
}
|
|
1488
|
-
else if (
|
|
1489
|
-
|
|
2056
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2057
|
+
text = referanceSnapshot.substring(5);
|
|
1490
2058
|
}
|
|
1491
2059
|
else {
|
|
1492
|
-
|
|
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);
|
|
1493
2104
|
}
|
|
1494
|
-
return dataFile;
|
|
1495
2105
|
}
|
|
1496
2106
|
async waitForUserInput(message, world = null) {
|
|
1497
2107
|
if (!message) {
|
|
@@ -1521,13 +2131,22 @@ class StableBrowser {
|
|
|
1521
2131
|
return;
|
|
1522
2132
|
}
|
|
1523
2133
|
// if data file exists, load it
|
|
1524
|
-
const dataFile =
|
|
2134
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1525
2135
|
let data = this.getTestData(world);
|
|
1526
2136
|
// merge the testData with the existing data
|
|
1527
2137
|
Object.assign(data, testData);
|
|
1528
2138
|
// save the data to the file
|
|
1529
2139
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1530
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
|
+
}
|
|
1531
2150
|
_getDataFilePath(fileName) {
|
|
1532
2151
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1533
2152
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1624,12 +2243,7 @@ class StableBrowser {
|
|
|
1624
2243
|
}
|
|
1625
2244
|
}
|
|
1626
2245
|
getTestData(world = null) {
|
|
1627
|
-
|
|
1628
|
-
let data = {};
|
|
1629
|
-
if (fs.existsSync(dataFile)) {
|
|
1630
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1631
|
-
}
|
|
1632
|
-
return data;
|
|
2246
|
+
return _getTestData(world, this.context, this);
|
|
1633
2247
|
}
|
|
1634
2248
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1635
2249
|
// collect url/path/title
|
|
@@ -1656,11 +2270,9 @@ class StableBrowser {
|
|
|
1656
2270
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1657
2271
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1658
2272
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1663
|
-
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");
|
|
1664
2276
|
try {
|
|
1665
2277
|
await this.takeScreenshot(screenshotPath);
|
|
1666
2278
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1670,15 +2282,15 @@ class StableBrowser {
|
|
|
1670
2282
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1671
2283
|
// }
|
|
1672
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
|
+
}
|
|
1673
2290
|
}
|
|
1674
2291
|
catch (e) {
|
|
1675
2292
|
this.logger.info("unable to take screenshot, ignored");
|
|
1676
2293
|
}
|
|
1677
|
-
result.screenshotId = nextIndex;
|
|
1678
|
-
result.screenshotPath = screenshotPath;
|
|
1679
|
-
if (info && info.box) {
|
|
1680
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1681
|
-
}
|
|
1682
2294
|
}
|
|
1683
2295
|
else if (options && options.screenshot) {
|
|
1684
2296
|
result.screenshotPath = options.screenshotPath;
|
|
@@ -1713,6 +2325,15 @@ class StableBrowser {
|
|
|
1713
2325
|
document.documentElement.clientWidth,
|
|
1714
2326
|
])));
|
|
1715
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
|
+
// }
|
|
1716
2337
|
if (this.context.browserName === "chromium") {
|
|
1717
2338
|
const client = await playContext.newCDPSession(this.page);
|
|
1718
2339
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
@@ -1734,6 +2355,10 @@ class StableBrowser {
|
|
|
1734
2355
|
else {
|
|
1735
2356
|
screenshotBuffer = await this.page.screenshot();
|
|
1736
2357
|
}
|
|
2358
|
+
// if (focusedElement) {
|
|
2359
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2360
|
+
// await this._unhighlightElements(focusedElement);
|
|
2361
|
+
// }
|
|
1737
2362
|
let image = await Jimp.read(screenshotBuffer);
|
|
1738
2363
|
// Get the image dimensions
|
|
1739
2364
|
const { width, height } = image.bitmap;
|
|
@@ -1746,6 +2371,7 @@ class StableBrowser {
|
|
|
1746
2371
|
else {
|
|
1747
2372
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1748
2373
|
}
|
|
2374
|
+
return screenshotBuffer;
|
|
1749
2375
|
}
|
|
1750
2376
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1751
2377
|
const state = {
|
|
@@ -1768,113 +2394,532 @@ class StableBrowser {
|
|
|
1768
2394
|
await _commandError(state, e, this);
|
|
1769
2395
|
}
|
|
1770
2396
|
finally {
|
|
1771
|
-
_commandFinally(state, this);
|
|
2397
|
+
await _commandFinally(state, this);
|
|
1772
2398
|
}
|
|
1773
2399
|
}
|
|
1774
2400
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
+
};
|
|
1780
2415
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1781
|
-
const info = {};
|
|
1782
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1783
|
-
info.operation = "extract";
|
|
1784
|
-
info.selectors = selectors;
|
|
1785
2416
|
try {
|
|
1786
|
-
|
|
1787
|
-
await this._highlightElements(element);
|
|
1788
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2417
|
+
await _preCommand(state, this);
|
|
1789
2418
|
switch (attribute) {
|
|
1790
2419
|
case "inner_text":
|
|
1791
|
-
|
|
2420
|
+
state.value = await state.element.innerText();
|
|
1792
2421
|
break;
|
|
1793
2422
|
case "href":
|
|
1794
|
-
|
|
2423
|
+
state.value = await state.element.getAttribute("href");
|
|
1795
2424
|
break;
|
|
1796
2425
|
case "value":
|
|
1797
|
-
|
|
2426
|
+
state.value = await state.element.inputValue();
|
|
2427
|
+
break;
|
|
2428
|
+
case "text":
|
|
2429
|
+
state.value = await state.element.textContent();
|
|
1798
2430
|
break;
|
|
1799
2431
|
default:
|
|
1800
|
-
|
|
2432
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1801
2433
|
break;
|
|
1802
2434
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
+
}
|
|
1806
2452
|
}
|
|
1807
|
-
|
|
1808
|
-
this.
|
|
1809
|
-
|
|
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;
|
|
1810
2458
|
}
|
|
1811
2459
|
catch (e) {
|
|
1812
|
-
|
|
1813
|
-
this.logger.error("extract failed " + info.log);
|
|
1814
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1815
|
-
info.screenshotPath = screenshotPath;
|
|
1816
|
-
Object.assign(e, { info: info });
|
|
1817
|
-
error = e;
|
|
1818
|
-
throw e;
|
|
2460
|
+
await _commandError(state, e, this);
|
|
1819
2461
|
}
|
|
1820
2462
|
finally {
|
|
1821
|
-
|
|
1822
|
-
this._reportToWorld(world, {
|
|
1823
|
-
element_name: selectors.element_name,
|
|
1824
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1825
|
-
variable: variable,
|
|
1826
|
-
value: info.value,
|
|
1827
|
-
text: "Extract attribute from element",
|
|
1828
|
-
screenshotId,
|
|
1829
|
-
result: error
|
|
1830
|
-
? {
|
|
1831
|
-
status: "FAILED",
|
|
1832
|
-
startTime,
|
|
1833
|
-
endTime,
|
|
1834
|
-
message: error?.message,
|
|
1835
|
-
}
|
|
1836
|
-
: {
|
|
1837
|
-
status: "PASSED",
|
|
1838
|
-
startTime,
|
|
1839
|
-
endTime,
|
|
1840
|
-
},
|
|
1841
|
-
info: info,
|
|
1842
|
-
});
|
|
2463
|
+
await _commandFinally(state, this);
|
|
1843
2464
|
}
|
|
1844
2465
|
}
|
|
1845
|
-
async
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
+
}
|
|
1856
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;
|
|
1857
2529
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
if (options && options.timeout) {
|
|
1861
|
-
timeout = options.timeout;
|
|
2530
|
+
catch (e) {
|
|
2531
|
+
await _commandError(state, e, this);
|
|
1862
2532
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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,
|
|
1874
2553
|
};
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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 {
|
|
1878
2923
|
let result = await this.context.api.request(request);
|
|
1879
2924
|
// the response body expected to be the following:
|
|
1880
2925
|
// {
|
|
@@ -1916,7 +2961,8 @@ class StableBrowser {
|
|
|
1916
2961
|
catch (e) {
|
|
1917
2962
|
errorCount++;
|
|
1918
2963
|
if (errorCount > 3) {
|
|
1919
|
-
throw e;
|
|
2964
|
+
// throw e;
|
|
2965
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1920
2966
|
}
|
|
1921
2967
|
// ignore
|
|
1922
2968
|
}
|
|
@@ -1930,27 +2976,32 @@ class StableBrowser {
|
|
|
1930
2976
|
async _highlightElements(scope, css) {
|
|
1931
2977
|
try {
|
|
1932
2978
|
if (!scope) {
|
|
2979
|
+
// console.log(`Scope is not defined`);
|
|
1933
2980
|
return;
|
|
1934
2981
|
}
|
|
1935
2982
|
if (!css) {
|
|
1936
2983
|
scope
|
|
1937
2984
|
.evaluate((node) => {
|
|
1938
2985
|
if (node && node.style) {
|
|
1939
|
-
let
|
|
1940
|
-
|
|
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}`);
|
|
1941
2991
|
if (window) {
|
|
1942
2992
|
window.addEventListener("beforeunload", function (e) {
|
|
1943
|
-
node.style.
|
|
2993
|
+
node.style.outline = originalOutline;
|
|
1944
2994
|
});
|
|
1945
2995
|
}
|
|
1946
2996
|
setTimeout(function () {
|
|
1947
|
-
node.style.
|
|
2997
|
+
node.style.outline = originalOutline;
|
|
1948
2998
|
}, 2000);
|
|
1949
2999
|
}
|
|
1950
3000
|
})
|
|
1951
3001
|
.then(() => { })
|
|
1952
3002
|
.catch((e) => {
|
|
1953
3003
|
// ignore
|
|
3004
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1954
3005
|
});
|
|
1955
3006
|
}
|
|
1956
3007
|
else {
|
|
@@ -1966,17 +3017,18 @@ class StableBrowser {
|
|
|
1966
3017
|
if (!element.style) {
|
|
1967
3018
|
return;
|
|
1968
3019
|
}
|
|
1969
|
-
|
|
3020
|
+
let originalOutline = element.style.outline;
|
|
3021
|
+
element.__previousOutline = originalOutline;
|
|
1970
3022
|
// Set the new border to be red and 2px solid
|
|
1971
|
-
element.style.
|
|
3023
|
+
element.style.outline = "2px solid red";
|
|
1972
3024
|
if (window) {
|
|
1973
3025
|
window.addEventListener("beforeunload", function (e) {
|
|
1974
|
-
element.style.
|
|
3026
|
+
element.style.outline = originalOutline;
|
|
1975
3027
|
});
|
|
1976
3028
|
}
|
|
1977
3029
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1978
3030
|
setTimeout(function () {
|
|
1979
|
-
element.style.
|
|
3031
|
+
element.style.outline = originalOutline;
|
|
1980
3032
|
}, 2000);
|
|
1981
3033
|
}
|
|
1982
3034
|
return;
|
|
@@ -1984,6 +3036,7 @@ class StableBrowser {
|
|
|
1984
3036
|
.then(() => { })
|
|
1985
3037
|
.catch((e) => {
|
|
1986
3038
|
// ignore
|
|
3039
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1987
3040
|
});
|
|
1988
3041
|
}
|
|
1989
3042
|
}
|
|
@@ -1991,8 +3044,49 @@ class StableBrowser {
|
|
|
1991
3044
|
console.debug(error);
|
|
1992
3045
|
}
|
|
1993
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
|
+
*/
|
|
1994
3089
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
1995
|
-
const startTime = Date.now();
|
|
1996
3090
|
let error = null;
|
|
1997
3091
|
let screenshotId = null;
|
|
1998
3092
|
let screenshotPath = null;
|
|
@@ -2006,159 +3100,534 @@ class StableBrowser {
|
|
|
2006
3100
|
pathPart = newValue;
|
|
2007
3101
|
}
|
|
2008
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
|
+
};
|
|
2009
3117
|
try {
|
|
3118
|
+
await _preCommand(state, this);
|
|
3119
|
+
state.info.text = queryText;
|
|
2010
3120
|
for (let i = 0; i < 30; i++) {
|
|
2011
3121
|
const url = await this.page.url();
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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`);
|
|
2015
3397
|
}
|
|
2016
3398
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2017
3399
|
continue;
|
|
2018
3400
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
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
|
+
}
|
|
2021
3418
|
}
|
|
2022
3419
|
}
|
|
2023
3420
|
catch (e) {
|
|
2024
|
-
|
|
2025
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2026
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2027
|
-
info.screenshotPath = screenshotPath;
|
|
2028
|
-
Object.assign(e, { info: info });
|
|
2029
|
-
error = e;
|
|
2030
|
-
throw e;
|
|
3421
|
+
await _commandError(state, e, this);
|
|
2031
3422
|
}
|
|
2032
3423
|
finally {
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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);
|
|
2052
3482
|
}
|
|
2053
3483
|
}
|
|
2054
|
-
async
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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);
|
|
2061
3503
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
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;
|
|
2073
3517
|
try {
|
|
3518
|
+
await _preCommand(state, this);
|
|
3519
|
+
state.info.text = textToVerify;
|
|
3520
|
+
let resultWithElementsFound = {
|
|
3521
|
+
length: 0,
|
|
3522
|
+
};
|
|
2074
3523
|
while (true) {
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
|
|
2081
|
-
result.frame = frames[i];
|
|
2082
|
-
results.push(result);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
else if (numberAlternatives.number) {
|
|
2086
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2087
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
|
|
2088
|
-
result.frame = frames[i];
|
|
2089
|
-
results.push(result);
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
else {
|
|
2093
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
|
|
2094
|
-
result.frame = frames[i];
|
|
2095
|
-
results.push(result);
|
|
2096
|
-
}
|
|
3524
|
+
try {
|
|
3525
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3526
|
+
}
|
|
3527
|
+
catch (error) {
|
|
3528
|
+
// ignore
|
|
2097
3529
|
}
|
|
2098
|
-
info.results = results;
|
|
2099
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2100
3530
|
if (resultWithElementsFound.length === 0) {
|
|
2101
|
-
if (Date.now() - startTime > timeout) {
|
|
2102
|
-
throw new Error(`Text ${
|
|
3531
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3532
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2103
3533
|
}
|
|
2104
3534
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2105
3535
|
continue;
|
|
2106
3536
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
+
}
|
|
2115
3593
|
}
|
|
2116
3594
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
3595
|
+
catch (error) {
|
|
3596
|
+
console.error(error);
|
|
3597
|
+
}
|
|
2119
3598
|
}
|
|
2120
3599
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2121
3600
|
}
|
|
2122
3601
|
catch (e) {
|
|
2123
|
-
|
|
2124
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2125
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2126
|
-
info.screenshotPath = screenshotPath;
|
|
2127
|
-
Object.assign(e, { info: info });
|
|
2128
|
-
error = e;
|
|
2129
|
-
throw e;
|
|
3602
|
+
await _commandError(state, e, this);
|
|
2130
3603
|
}
|
|
2131
3604
|
finally {
|
|
2132
|
-
|
|
2133
|
-
this._reportToWorld(world, {
|
|
2134
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2135
|
-
text: "Verify text exists in page",
|
|
2136
|
-
screenshotId,
|
|
2137
|
-
result: error
|
|
2138
|
-
? {
|
|
2139
|
-
status: "FAILED",
|
|
2140
|
-
startTime,
|
|
2141
|
-
endTime,
|
|
2142
|
-
message: error?.message,
|
|
2143
|
-
}
|
|
2144
|
-
: {
|
|
2145
|
-
status: "PASSED",
|
|
2146
|
-
startTime,
|
|
2147
|
-
endTime,
|
|
2148
|
-
},
|
|
2149
|
-
info: info,
|
|
2150
|
-
});
|
|
3605
|
+
await _commandFinally(state, this);
|
|
2151
3606
|
}
|
|
2152
3607
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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
|
+
}
|
|
2160
3627
|
}
|
|
2161
|
-
|
|
3628
|
+
// state.info.results = results;
|
|
3629
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3630
|
+
return resultWithElementsFound;
|
|
2162
3631
|
}
|
|
2163
3632
|
async visualVerification(text, options = {}, world = null) {
|
|
2164
3633
|
const startTime = Date.now();
|
|
@@ -2174,14 +3643,17 @@ class StableBrowser {
|
|
|
2174
3643
|
throw new Error("TOKEN is not set");
|
|
2175
3644
|
}
|
|
2176
3645
|
try {
|
|
2177
|
-
let serviceUrl =
|
|
3646
|
+
let serviceUrl = _getServerUrl();
|
|
2178
3647
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2179
3648
|
info.screenshotPath = screenshotPath;
|
|
2180
3649
|
const screenshot = await this.takeScreenshot();
|
|
2181
|
-
|
|
2182
|
-
method: "
|
|
3650
|
+
let request = {
|
|
3651
|
+
method: "post",
|
|
3652
|
+
maxBodyLength: Infinity,
|
|
2183
3653
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2184
3654
|
headers: {
|
|
3655
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3656
|
+
"x-source": "aaa",
|
|
2185
3657
|
"Content-Type": "application/json",
|
|
2186
3658
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2187
3659
|
},
|
|
@@ -2190,7 +3662,7 @@ class StableBrowser {
|
|
|
2190
3662
|
screenshot: screenshot,
|
|
2191
3663
|
}),
|
|
2192
3664
|
};
|
|
2193
|
-
|
|
3665
|
+
const result = await axios.request(request);
|
|
2194
3666
|
if (result.data.status !== true) {
|
|
2195
3667
|
throw new Error("Visual validation failed");
|
|
2196
3668
|
}
|
|
@@ -2210,13 +3682,15 @@ class StableBrowser {
|
|
|
2210
3682
|
info.screenshotPath = screenshotPath;
|
|
2211
3683
|
Object.assign(e, { info: info });
|
|
2212
3684
|
error = e;
|
|
2213
|
-
throw e;
|
|
3685
|
+
// throw e;
|
|
3686
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2214
3687
|
}
|
|
2215
3688
|
finally {
|
|
2216
3689
|
const endTime = Date.now();
|
|
2217
|
-
|
|
3690
|
+
_reportToWorld(world, {
|
|
2218
3691
|
type: Types.VERIFY_VISUAL,
|
|
2219
3692
|
text: "Visual verification",
|
|
3693
|
+
_text: "Visual verification of " + text,
|
|
2220
3694
|
screenshotId,
|
|
2221
3695
|
result: error
|
|
2222
3696
|
? {
|
|
@@ -2262,6 +3736,7 @@ class StableBrowser {
|
|
|
2262
3736
|
let screenshotPath = null;
|
|
2263
3737
|
const info = {};
|
|
2264
3738
|
info.log = "";
|
|
3739
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2265
3740
|
info.operation = "getTableData";
|
|
2266
3741
|
info.selectors = selectors;
|
|
2267
3742
|
try {
|
|
@@ -2277,11 +3752,12 @@ class StableBrowser {
|
|
|
2277
3752
|
info.screenshotPath = screenshotPath;
|
|
2278
3753
|
Object.assign(e, { info: info });
|
|
2279
3754
|
error = e;
|
|
2280
|
-
throw e;
|
|
3755
|
+
// throw e;
|
|
3756
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2281
3757
|
}
|
|
2282
3758
|
finally {
|
|
2283
3759
|
const endTime = Date.now();
|
|
2284
|
-
|
|
3760
|
+
_reportToWorld(world, {
|
|
2285
3761
|
element_name: selectors.element_name,
|
|
2286
3762
|
type: Types.GET_TABLE_DATA,
|
|
2287
3763
|
text: "Get table data",
|
|
@@ -2336,7 +3812,7 @@ class StableBrowser {
|
|
|
2336
3812
|
info.operation = "analyzeTable";
|
|
2337
3813
|
info.selectors = selectors;
|
|
2338
3814
|
info.query = query;
|
|
2339
|
-
query =
|
|
3815
|
+
query = _fixUsingParams(query, _params);
|
|
2340
3816
|
info.query_fixed = query;
|
|
2341
3817
|
info.operator = operator;
|
|
2342
3818
|
info.value = value;
|
|
@@ -2442,11 +3918,12 @@ class StableBrowser {
|
|
|
2442
3918
|
info.screenshotPath = screenshotPath;
|
|
2443
3919
|
Object.assign(e, { info: info });
|
|
2444
3920
|
error = e;
|
|
2445
|
-
throw e;
|
|
3921
|
+
// throw e;
|
|
3922
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2446
3923
|
}
|
|
2447
3924
|
finally {
|
|
2448
3925
|
const endTime = Date.now();
|
|
2449
|
-
|
|
3926
|
+
_reportToWorld(world, {
|
|
2450
3927
|
element_name: selectors.element_name,
|
|
2451
3928
|
type: Types.ANALYZE_TABLE,
|
|
2452
3929
|
text: "Analyze table",
|
|
@@ -2467,8 +3944,51 @@ class StableBrowser {
|
|
|
2467
3944
|
});
|
|
2468
3945
|
}
|
|
2469
3946
|
}
|
|
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");
|
|
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
|
+
}
|
|
2470
3984
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2471
|
-
|
|
3985
|
+
try {
|
|
3986
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3987
|
+
}
|
|
3988
|
+
catch (error) {
|
|
3989
|
+
this.logger.debug(error);
|
|
3990
|
+
throw error;
|
|
3991
|
+
}
|
|
2472
3992
|
}
|
|
2473
3993
|
_getLoadTimeout(options) {
|
|
2474
3994
|
let timeout = 15000;
|
|
@@ -2480,7 +4000,54 @@ class StableBrowser {
|
|
|
2480
4000
|
}
|
|
2481
4001
|
return timeout;
|
|
2482
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
|
+
}
|
|
2483
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");
|
|
2484
4051
|
let timeout = this._getLoadTimeout(options);
|
|
2485
4052
|
const promiseArray = [];
|
|
2486
4053
|
// let waitForNetworkIdle = true;
|
|
@@ -2513,13 +4080,15 @@ class StableBrowser {
|
|
|
2513
4080
|
else if (e.label === "domcontentloaded") {
|
|
2514
4081
|
console.log("waited for the domcontent loaded timeout");
|
|
2515
4082
|
}
|
|
2516
|
-
console.log(".");
|
|
2517
4083
|
}
|
|
2518
4084
|
finally {
|
|
2519
|
-
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
|
+
}
|
|
2520
4089
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2521
4090
|
const endTime = Date.now();
|
|
2522
|
-
|
|
4091
|
+
_reportToWorld(world, {
|
|
2523
4092
|
type: Types.GET_PAGE_STATUS,
|
|
2524
4093
|
text: "Wait for page load",
|
|
2525
4094
|
screenshotId,
|
|
@@ -2539,41 +4108,139 @@ class StableBrowser {
|
|
|
2539
4108
|
}
|
|
2540
4109
|
}
|
|
2541
4110
|
async closePage(options = {}, world = null) {
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
+
};
|
|
2547
4124
|
try {
|
|
4125
|
+
await _preCommand(state, this);
|
|
2548
4126
|
await this.page.close();
|
|
2549
4127
|
}
|
|
2550
4128
|
catch (e) {
|
|
2551
|
-
|
|
4129
|
+
await _commandError(state, e, this);
|
|
2552
4130
|
}
|
|
2553
4131
|
finally {
|
|
2554
|
-
await
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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;
|
|
2567
4167
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
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);
|
|
2575
4231
|
}
|
|
2576
4232
|
}
|
|
4233
|
+
saveTestDataAsGlobal(options, world) {
|
|
4234
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
4235
|
+
if (process.env.MODE === "executions") {
|
|
4236
|
+
const globalDataFile = path.join(this.project_path, "global_test_data.json");
|
|
4237
|
+
fs.copyFileSync(dataFile, globalDataFile);
|
|
4238
|
+
this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4242
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
4243
|
+
}
|
|
2577
4244
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2578
4245
|
const startTime = Date.now();
|
|
2579
4246
|
let error = null;
|
|
@@ -2590,15 +4257,16 @@ class StableBrowser {
|
|
|
2590
4257
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2591
4258
|
}
|
|
2592
4259
|
catch (e) {
|
|
2593
|
-
|
|
4260
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2594
4261
|
}
|
|
2595
4262
|
finally {
|
|
2596
4263
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2597
4264
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2598
4265
|
const endTime = Date.now();
|
|
2599
|
-
|
|
4266
|
+
_reportToWorld(world, {
|
|
2600
4267
|
type: Types.SET_VIEWPORT,
|
|
2601
4268
|
text: "set viewport size to " + width + "x" + hight,
|
|
4269
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2602
4270
|
screenshotId,
|
|
2603
4271
|
result: error
|
|
2604
4272
|
? {
|
|
@@ -2626,13 +4294,13 @@ class StableBrowser {
|
|
|
2626
4294
|
await this.page.reload();
|
|
2627
4295
|
}
|
|
2628
4296
|
catch (e) {
|
|
2629
|
-
|
|
4297
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2630
4298
|
}
|
|
2631
4299
|
finally {
|
|
2632
4300
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2633
4301
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2634
4302
|
const endTime = Date.now();
|
|
2635
|
-
|
|
4303
|
+
_reportToWorld(world, {
|
|
2636
4304
|
type: Types.GET_PAGE_STATUS,
|
|
2637
4305
|
text: "page relaod",
|
|
2638
4306
|
screenshotId,
|
|
@@ -2668,11 +4336,246 @@ class StableBrowser {
|
|
|
2668
4336
|
console.log("#-#");
|
|
2669
4337
|
}
|
|
2670
4338
|
}
|
|
2671
|
-
|
|
2672
|
-
if (
|
|
2673
|
-
|
|
4339
|
+
async beforeScenario(world, scenario) {
|
|
4340
|
+
if (world && world.attach) {
|
|
4341
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4342
|
+
}
|
|
4343
|
+
this.context.loadedRoutes = null;
|
|
4344
|
+
this.beforeScenarioCalled = true;
|
|
4345
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4346
|
+
this.scenarioName = scenario.pickle.name;
|
|
4347
|
+
}
|
|
4348
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
4349
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
4350
|
+
}
|
|
4351
|
+
if (this.context) {
|
|
4352
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
4353
|
+
}
|
|
4354
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4355
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4356
|
+
// check if @global_test_data tag is present
|
|
4357
|
+
if (this.tags.includes("@global_test_data")) {
|
|
4358
|
+
this.saveTestDataAsGlobal({}, world);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
// update test data based on feature/scenario
|
|
4362
|
+
let envName = null;
|
|
4363
|
+
if (this.context && this.context.environment) {
|
|
4364
|
+
envName = this.context.environment.name;
|
|
4365
|
+
}
|
|
4366
|
+
if (!process.env.TEMP_RUN) {
|
|
4367
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4368
|
+
}
|
|
4369
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4370
|
+
}
|
|
4371
|
+
async afterScenario(world, scenario) { }
|
|
4372
|
+
async beforeStep(world, step) {
|
|
4373
|
+
this.stepTags = [];
|
|
4374
|
+
if (!this.beforeScenarioCalled) {
|
|
4375
|
+
this.beforeScenario(world, step);
|
|
4376
|
+
this.context.loadedRoutes = null;
|
|
4377
|
+
}
|
|
4378
|
+
if (this.stepIndex === undefined) {
|
|
4379
|
+
this.stepIndex = 0;
|
|
4380
|
+
}
|
|
4381
|
+
else {
|
|
4382
|
+
this.stepIndex++;
|
|
4383
|
+
}
|
|
4384
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4385
|
+
this.stepName = step.pickleStep.text;
|
|
4386
|
+
let printableStepName = this.stepName;
|
|
4387
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4388
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4389
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4390
|
+
});
|
|
4391
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4392
|
+
}
|
|
4393
|
+
else if (step && step.text) {
|
|
4394
|
+
this.stepName = step.text;
|
|
4395
|
+
}
|
|
4396
|
+
else {
|
|
4397
|
+
this.stepName = "step " + this.stepIndex;
|
|
4398
|
+
}
|
|
4399
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4400
|
+
if (this.context.browserObject.context) {
|
|
4401
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
if (this.initSnapshotTaken === false) {
|
|
4405
|
+
this.initSnapshotTaken = true;
|
|
4406
|
+
if (world &&
|
|
4407
|
+
world.attach &&
|
|
4408
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4409
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4410
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4411
|
+
if (snapshot) {
|
|
4412
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
this.context.routeResults = null;
|
|
4417
|
+
this.context.loadedRoutes = null;
|
|
4418
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4419
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4420
|
+
this.inStepReport = false;
|
|
4421
|
+
}
|
|
4422
|
+
setStepTags(tags) {
|
|
4423
|
+
this.stepTags = tags;
|
|
4424
|
+
}
|
|
4425
|
+
async getAriaSnapshot() {
|
|
4426
|
+
try {
|
|
4427
|
+
// find the page url
|
|
4428
|
+
const url = await this.page.url();
|
|
4429
|
+
// extract the path from the url
|
|
4430
|
+
const path = new URL(url).pathname;
|
|
4431
|
+
// get the page title
|
|
4432
|
+
const title = await this.page.title();
|
|
4433
|
+
// go over other frams
|
|
4434
|
+
const frames = this.page.frames();
|
|
4435
|
+
const snapshots = [];
|
|
4436
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4437
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4438
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4439
|
+
const frame = frames[i];
|
|
4440
|
+
try {
|
|
4441
|
+
// Ensure frame is attached and has body
|
|
4442
|
+
const body = frame.locator("body");
|
|
4443
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4444
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4445
|
+
if (!snapshot) {
|
|
4446
|
+
continue;
|
|
4447
|
+
}
|
|
4448
|
+
content.push(`- frame: ${i}`);
|
|
4449
|
+
content.push(snapshot);
|
|
4450
|
+
}
|
|
4451
|
+
catch (innerErr) {
|
|
4452
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4453
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
return content.join("\n");
|
|
4457
|
+
}
|
|
4458
|
+
catch (e) {
|
|
4459
|
+
console.log("Error in getAriaSnapshot");
|
|
4460
|
+
//console.debug(e);
|
|
4461
|
+
}
|
|
4462
|
+
return null;
|
|
4463
|
+
}
|
|
4464
|
+
/**
|
|
4465
|
+
* Sends command with custom payload to report.
|
|
4466
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4467
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4468
|
+
* @param content - Content of the command to be shown in the report.
|
|
4469
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4470
|
+
* @param world - Optional world context.
|
|
4471
|
+
* @public
|
|
4472
|
+
*/
|
|
4473
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4474
|
+
const state = {
|
|
4475
|
+
options,
|
|
4476
|
+
world,
|
|
4477
|
+
locate: false,
|
|
4478
|
+
scroll: false,
|
|
4479
|
+
screenshot: options.screenshot ?? false,
|
|
4480
|
+
highlight: options.highlight ?? false,
|
|
4481
|
+
type: Types.REPORT_COMMAND,
|
|
4482
|
+
text: commandText,
|
|
4483
|
+
_text: commandText,
|
|
4484
|
+
operation: "report_command",
|
|
4485
|
+
log: "***** " + commandText + " *****\n",
|
|
4486
|
+
};
|
|
4487
|
+
try {
|
|
4488
|
+
await _preCommand(state, this);
|
|
4489
|
+
const payload = {
|
|
4490
|
+
type: options.type ?? "text",
|
|
4491
|
+
content: content,
|
|
4492
|
+
screenshotId: null,
|
|
4493
|
+
};
|
|
4494
|
+
state.payload = payload;
|
|
4495
|
+
if (commandStatus === "FAILED") {
|
|
4496
|
+
state.throwError = true;
|
|
4497
|
+
throw new Error(commandText);
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
catch (e) {
|
|
4501
|
+
await _commandError(state, e, this);
|
|
4502
|
+
}
|
|
4503
|
+
finally {
|
|
4504
|
+
await _commandFinally(state, this);
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
async afterStep(world, step, result) {
|
|
4508
|
+
this.stepName = null;
|
|
4509
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4510
|
+
if (this.context.browserObject.context) {
|
|
4511
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
4512
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
4513
|
+
});
|
|
4514
|
+
if (world && world.attach) {
|
|
4515
|
+
await world.attach(JSON.stringify({
|
|
4516
|
+
type: "trace",
|
|
4517
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4518
|
+
}), "application/json+trace");
|
|
4519
|
+
}
|
|
4520
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
if (this.context) {
|
|
4524
|
+
this.context.examplesRow = null;
|
|
4525
|
+
}
|
|
4526
|
+
if (!this.inStepReport) {
|
|
4527
|
+
// check the step result
|
|
4528
|
+
if (result && result.status === "FAILED" && world && world.attach) {
|
|
4529
|
+
await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
if (world &&
|
|
4533
|
+
world.attach &&
|
|
4534
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4535
|
+
!this.fastMode &&
|
|
4536
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4537
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4538
|
+
if (snapshot) {
|
|
4539
|
+
const obj = {};
|
|
4540
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4544
|
+
if (this.context.routeResults) {
|
|
4545
|
+
if (world && world.attach) {
|
|
4546
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
if (!process.env.TEMP_RUN) {
|
|
4550
|
+
const state = {
|
|
4551
|
+
world,
|
|
4552
|
+
locate: false,
|
|
4553
|
+
scroll: false,
|
|
4554
|
+
screenshot: true,
|
|
4555
|
+
highlight: true,
|
|
4556
|
+
type: Types.STEP_COMPLETE,
|
|
4557
|
+
text: "end of scenario",
|
|
4558
|
+
_text: "end of scenario",
|
|
4559
|
+
operation: "step_complete",
|
|
4560
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4561
|
+
};
|
|
4562
|
+
try {
|
|
4563
|
+
await _preCommand(state, this);
|
|
4564
|
+
}
|
|
4565
|
+
catch (e) {
|
|
4566
|
+
await _commandError(state, e, this);
|
|
4567
|
+
}
|
|
4568
|
+
finally {
|
|
4569
|
+
await _commandFinally(state, this);
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
networkAfterStep(this.stepName, this.context);
|
|
4573
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4574
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4575
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4576
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4577
|
+
}
|
|
2674
4578
|
}
|
|
2675
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2676
4579
|
}
|
|
2677
4580
|
}
|
|
2678
4581
|
function createTimedPromise(promise, label) {
|
|
@@ -2680,156 +4583,5 @@ function createTimedPromise(promise, label) {
|
|
|
2680
4583
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2681
4584
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2682
4585
|
}
|
|
2683
|
-
const KEYBOARD_EVENTS = [
|
|
2684
|
-
"ALT",
|
|
2685
|
-
"AltGraph",
|
|
2686
|
-
"CapsLock",
|
|
2687
|
-
"Control",
|
|
2688
|
-
"Fn",
|
|
2689
|
-
"FnLock",
|
|
2690
|
-
"Hyper",
|
|
2691
|
-
"Meta",
|
|
2692
|
-
"NumLock",
|
|
2693
|
-
"ScrollLock",
|
|
2694
|
-
"Shift",
|
|
2695
|
-
"Super",
|
|
2696
|
-
"Symbol",
|
|
2697
|
-
"SymbolLock",
|
|
2698
|
-
"Enter",
|
|
2699
|
-
"Tab",
|
|
2700
|
-
"ArrowDown",
|
|
2701
|
-
"ArrowLeft",
|
|
2702
|
-
"ArrowRight",
|
|
2703
|
-
"ArrowUp",
|
|
2704
|
-
"End",
|
|
2705
|
-
"Home",
|
|
2706
|
-
"PageDown",
|
|
2707
|
-
"PageUp",
|
|
2708
|
-
"Backspace",
|
|
2709
|
-
"Clear",
|
|
2710
|
-
"Copy",
|
|
2711
|
-
"CrSel",
|
|
2712
|
-
"Cut",
|
|
2713
|
-
"Delete",
|
|
2714
|
-
"EraseEof",
|
|
2715
|
-
"ExSel",
|
|
2716
|
-
"Insert",
|
|
2717
|
-
"Paste",
|
|
2718
|
-
"Redo",
|
|
2719
|
-
"Undo",
|
|
2720
|
-
"Accept",
|
|
2721
|
-
"Again",
|
|
2722
|
-
"Attn",
|
|
2723
|
-
"Cancel",
|
|
2724
|
-
"ContextMenu",
|
|
2725
|
-
"Escape",
|
|
2726
|
-
"Execute",
|
|
2727
|
-
"Find",
|
|
2728
|
-
"Finish",
|
|
2729
|
-
"Help",
|
|
2730
|
-
"Pause",
|
|
2731
|
-
"Play",
|
|
2732
|
-
"Props",
|
|
2733
|
-
"Select",
|
|
2734
|
-
"ZoomIn",
|
|
2735
|
-
"ZoomOut",
|
|
2736
|
-
"BrightnessDown",
|
|
2737
|
-
"BrightnessUp",
|
|
2738
|
-
"Eject",
|
|
2739
|
-
"LogOff",
|
|
2740
|
-
"Power",
|
|
2741
|
-
"PowerOff",
|
|
2742
|
-
"PrintScreen",
|
|
2743
|
-
"Hibernate",
|
|
2744
|
-
"Standby",
|
|
2745
|
-
"WakeUp",
|
|
2746
|
-
"AllCandidates",
|
|
2747
|
-
"Alphanumeric",
|
|
2748
|
-
"CodeInput",
|
|
2749
|
-
"Compose",
|
|
2750
|
-
"Convert",
|
|
2751
|
-
"Dead",
|
|
2752
|
-
"FinalMode",
|
|
2753
|
-
"GroupFirst",
|
|
2754
|
-
"GroupLast",
|
|
2755
|
-
"GroupNext",
|
|
2756
|
-
"GroupPrevious",
|
|
2757
|
-
"ModeChange",
|
|
2758
|
-
"NextCandidate",
|
|
2759
|
-
"NonConvert",
|
|
2760
|
-
"PreviousCandidate",
|
|
2761
|
-
"Process",
|
|
2762
|
-
"SingleCandidate",
|
|
2763
|
-
"HangulMode",
|
|
2764
|
-
"HanjaMode",
|
|
2765
|
-
"JunjaMode",
|
|
2766
|
-
"Eisu",
|
|
2767
|
-
"Hankaku",
|
|
2768
|
-
"Hiragana",
|
|
2769
|
-
"HiraganaKatakana",
|
|
2770
|
-
"KanaMode",
|
|
2771
|
-
"KanjiMode",
|
|
2772
|
-
"Katakana",
|
|
2773
|
-
"Romaji",
|
|
2774
|
-
"Zenkaku",
|
|
2775
|
-
"ZenkakuHanaku",
|
|
2776
|
-
"F1",
|
|
2777
|
-
"F2",
|
|
2778
|
-
"F3",
|
|
2779
|
-
"F4",
|
|
2780
|
-
"F5",
|
|
2781
|
-
"F6",
|
|
2782
|
-
"F7",
|
|
2783
|
-
"F8",
|
|
2784
|
-
"F9",
|
|
2785
|
-
"F10",
|
|
2786
|
-
"F11",
|
|
2787
|
-
"F12",
|
|
2788
|
-
"Soft1",
|
|
2789
|
-
"Soft2",
|
|
2790
|
-
"Soft3",
|
|
2791
|
-
"Soft4",
|
|
2792
|
-
"ChannelDown",
|
|
2793
|
-
"ChannelUp",
|
|
2794
|
-
"Close",
|
|
2795
|
-
"MailForward",
|
|
2796
|
-
"MailReply",
|
|
2797
|
-
"MailSend",
|
|
2798
|
-
"MediaFastForward",
|
|
2799
|
-
"MediaPause",
|
|
2800
|
-
"MediaPlay",
|
|
2801
|
-
"MediaPlayPause",
|
|
2802
|
-
"MediaRecord",
|
|
2803
|
-
"MediaRewind",
|
|
2804
|
-
"MediaStop",
|
|
2805
|
-
"MediaTrackNext",
|
|
2806
|
-
"MediaTrackPrevious",
|
|
2807
|
-
"AudioBalanceLeft",
|
|
2808
|
-
"AudioBalanceRight",
|
|
2809
|
-
"AudioBassBoostDown",
|
|
2810
|
-
"AudioBassBoostToggle",
|
|
2811
|
-
"AudioBassBoostUp",
|
|
2812
|
-
"AudioFaderFront",
|
|
2813
|
-
"AudioFaderRear",
|
|
2814
|
-
"AudioSurroundModeNext",
|
|
2815
|
-
"AudioTrebleDown",
|
|
2816
|
-
"AudioTrebleUp",
|
|
2817
|
-
"AudioVolumeDown",
|
|
2818
|
-
"AudioVolumeMute",
|
|
2819
|
-
"AudioVolumeUp",
|
|
2820
|
-
"MicrophoneToggle",
|
|
2821
|
-
"MicrophoneVolumeDown",
|
|
2822
|
-
"MicrophoneVolumeMute",
|
|
2823
|
-
"MicrophoneVolumeUp",
|
|
2824
|
-
"TV",
|
|
2825
|
-
"TV3DMode",
|
|
2826
|
-
"TVAntennaCable",
|
|
2827
|
-
"TVAudioDescription",
|
|
2828
|
-
];
|
|
2829
|
-
function unEscapeString(str) {
|
|
2830
|
-
const placeholder = "__NEWLINE__";
|
|
2831
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2832
|
-
return str;
|
|
2833
|
-
}
|
|
2834
4586
|
export { StableBrowser };
|
|
2835
4587
|
//# sourceMappingURL=stable_browser.js.map
|