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