automation_model 1.0.496-dev → 1.0.496
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 +297 -17
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +6 -3
- package/lib/browser_manager.js +222 -52
- 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 +126 -22
- package/lib/command_common.js.map +1 -1
- package/lib/date_time.js.map +1 -1
- package/lib/drawRect.js.map +1 -1
- package/lib/environment.d.ts +1 -0
- package/lib/environment.js +1 -0
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.d.ts +6 -0
- package/lib/error-messages.js +206 -0
- package/lib/error-messages.js.map +1 -0
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +172 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/find_function.js.map +1 -1
- package/lib/generation_scripts.d.ts +4 -0
- package/lib/generation_scripts.js +2 -0
- package/lib/generation_scripts.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +4 -3
- package/lib/init_browser.js +160 -90
- 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 +4 -2
- package/lib/network.js +435 -69
- 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 +24005 -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 +147 -47
- package/lib/stable_browser.js +2599 -820
- 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 +747 -35
- package/lib/utils.js.map +1 -1
- package/package.json +31 -13
- package/lib/axe/axe.mini.js +0 -12
package/lib/stable_browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
+
import { check_performance } from "./check_performance.js";
|
|
2
3
|
import { expect } from "@playwright/test";
|
|
3
4
|
import dayjs from "dayjs";
|
|
4
5
|
import fs from "fs";
|
|
@@ -10,59 +11,102 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
11
|
import drawRectangle from "./drawRect.js";
|
|
11
12
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
13
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
+
import 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,28 +135,54 @@ class StableBrowser {
|
|
|
91
135
|
context.pages = [this.page];
|
|
92
136
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
93
137
|
this.world = world;
|
|
138
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
139
|
+
this.fastMode = true;
|
|
140
|
+
}
|
|
141
|
+
if (process.env.FAST_MODE === "true") {
|
|
142
|
+
// console.log("Fast mode enabled from environment variable");
|
|
143
|
+
this.fastMode = true;
|
|
144
|
+
}
|
|
145
|
+
if (process.env.FAST_MODE === "false") {
|
|
146
|
+
this.fastMode = false;
|
|
147
|
+
}
|
|
148
|
+
if (this.context) {
|
|
149
|
+
this.context.fastMode = this.fastMode;
|
|
150
|
+
}
|
|
94
151
|
this.registerEventListeners(this.context);
|
|
95
152
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
96
|
-
registerDownloadEvent(this.page);
|
|
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");
|
|
107
167
|
await page.close();
|
|
168
|
+
return;
|
|
108
169
|
}
|
|
109
170
|
context.pageLoading.status = true;
|
|
110
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
|
+
}
|
|
111
180
|
context.page = page;
|
|
112
181
|
context.pages.push(page);
|
|
113
182
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
114
|
-
registerDownloadEvent(this.page);
|
|
183
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
115
184
|
page.on("close", async () => {
|
|
185
|
+
// return if browser context is already closed
|
|
116
186
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
117
187
|
this.context.pages.pop();
|
|
118
188
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -122,7 +192,12 @@ class StableBrowser {
|
|
|
122
192
|
console.log("Switched to page " + title);
|
|
123
193
|
}
|
|
124
194
|
catch (error) {
|
|
125
|
-
|
|
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
|
+
}
|
|
126
201
|
}
|
|
127
202
|
}
|
|
128
203
|
});
|
|
@@ -131,7 +206,12 @@ class StableBrowser {
|
|
|
131
206
|
console.log("Switch page: " + (await page.title()));
|
|
132
207
|
}
|
|
133
208
|
catch (e) {
|
|
134
|
-
|
|
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
|
+
}
|
|
135
215
|
}
|
|
136
216
|
context.pageLoading.status = false;
|
|
137
217
|
}.bind(this));
|
|
@@ -143,7 +223,7 @@ class StableBrowser {
|
|
|
143
223
|
}
|
|
144
224
|
let newContextCreated = false;
|
|
145
225
|
if (!apps[appName]) {
|
|
146
|
-
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);
|
|
147
227
|
newContextCreated = true;
|
|
148
228
|
apps[appName] = {
|
|
149
229
|
context: newContext,
|
|
@@ -152,31 +232,41 @@ class StableBrowser {
|
|
|
152
232
|
};
|
|
153
233
|
}
|
|
154
234
|
const tempContext = {};
|
|
155
|
-
|
|
156
|
-
|
|
235
|
+
_copyContext(this, tempContext);
|
|
236
|
+
_copyContext(apps[appName], this);
|
|
157
237
|
apps[this.appName] = tempContext;
|
|
158
238
|
this.appName = appName;
|
|
159
239
|
if (newContextCreated) {
|
|
160
240
|
this.registerEventListeners(this.context);
|
|
161
241
|
await this.goto(this.context.environment.baseUrl);
|
|
162
|
-
|
|
242
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
243
|
+
await this.waitForPageLoad();
|
|
244
|
+
}
|
|
163
245
|
}
|
|
164
246
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
257
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|
|
177
268
|
}
|
|
178
|
-
|
|
179
|
-
return path.join(logFolder, fileName);
|
|
269
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
180
270
|
}
|
|
181
271
|
registerConsoleLogListener(page, context) {
|
|
182
272
|
if (!this.context.webLogger) {
|
|
@@ -227,7 +317,7 @@ class StableBrowser {
|
|
|
227
317
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
228
318
|
}
|
|
229
319
|
catch (error) {
|
|
230
|
-
console.error("Error in request listener", error);
|
|
320
|
+
// console.error("Error in request listener", error);
|
|
231
321
|
context.networkLogger.push({
|
|
232
322
|
error: "not able to listen",
|
|
233
323
|
message: error.message,
|
|
@@ -241,55 +331,117 @@ class StableBrowser {
|
|
|
241
331
|
// async closeUnexpectedPopups() {
|
|
242
332
|
// await closeUnexpectedPopups(this.page);
|
|
243
333
|
// }
|
|
244
|
-
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);
|
|
245
339
|
if (!url.startsWith("http")) {
|
|
246
340
|
url = "https://" + url;
|
|
247
341
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
358
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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);
|
|
263
375
|
}
|
|
264
|
-
return text;
|
|
265
376
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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);
|
|
270
404
|
}
|
|
271
|
-
// clone the locator
|
|
272
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
273
|
-
this.scanAndManipulate(locator, _params);
|
|
274
|
-
return locator;
|
|
275
405
|
}
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|
|
278
434
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
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);
|
|
288
443
|
}
|
|
289
444
|
}
|
|
290
|
-
}
|
|
291
|
-
_getLocator(locator, scope, _params) {
|
|
292
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
293
445
|
let locatorReturn;
|
|
294
446
|
if (locator.role) {
|
|
295
447
|
if (locator.role[1].nameReg) {
|
|
@@ -297,7 +449,7 @@ class StableBrowser {
|
|
|
297
449
|
delete locator.role[1].nameReg;
|
|
298
450
|
}
|
|
299
451
|
// if (locator.role[1].name) {
|
|
300
|
-
// locator.role[1].name =
|
|
452
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
301
453
|
// }
|
|
302
454
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
303
455
|
}
|
|
@@ -340,213 +492,171 @@ class StableBrowser {
|
|
|
340
492
|
if (css && css.locator) {
|
|
341
493
|
css = css.locator;
|
|
342
494
|
}
|
|
343
|
-
let result = await this._locateElementByText(scope,
|
|
495
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
344
496
|
if (result.elementCount === 0) {
|
|
345
497
|
return;
|
|
346
498
|
}
|
|
347
|
-
let textElementCss = "[data-blinq-id
|
|
499
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
348
500
|
// css climb to parent element
|
|
349
501
|
const climbArray = [];
|
|
350
502
|
for (let i = 0; i < climb; i++) {
|
|
351
503
|
climbArray.push("..");
|
|
352
504
|
}
|
|
353
505
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return new RegExp(pattern, flags);
|
|
377
|
-
}
|
|
378
|
-
document.getRegex = getRegex;
|
|
379
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
380
|
-
// Check and add the element if it has a shadow root
|
|
381
|
-
if (element.shadowRoot) {
|
|
382
|
-
result.push(element);
|
|
383
|
-
// Also search within the shadow root
|
|
384
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
385
|
-
}
|
|
386
|
-
// Iterate over child nodes
|
|
387
|
-
element.childNodes.forEach((child) => {
|
|
388
|
-
// Recursively call the function for each child node
|
|
389
|
-
document.collectAllShadowDomElements(child, result);
|
|
390
|
-
});
|
|
391
|
-
return result;
|
|
392
|
-
}
|
|
393
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
394
|
-
if (!tag) {
|
|
395
|
-
tag = "*";
|
|
396
|
-
}
|
|
397
|
-
let regexpSearch = document.getRegex(text);
|
|
398
|
-
if (regexpSearch) {
|
|
399
|
-
regex = true;
|
|
400
|
-
}
|
|
401
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
402
|
-
let shadowHosts = [];
|
|
403
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
404
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
405
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
406
|
-
if (!shadowElement) {
|
|
407
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
411
|
-
elements = elements.concat(shadowElements);
|
|
412
|
-
}
|
|
413
|
-
let randomToken = null;
|
|
414
|
-
const foundElements = [];
|
|
415
|
-
if (regex) {
|
|
416
|
-
if (!regexpSearch) {
|
|
417
|
-
regexpSearch = new RegExp(text, "im");
|
|
418
|
-
}
|
|
419
|
-
for (let i = 0; i < elements.length; i++) {
|
|
420
|
-
const element = elements[i];
|
|
421
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
422
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
423
|
-
foundElements.push(element);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
text = text.trim();
|
|
429
|
-
for (let i = 0; i < elements.length; i++) {
|
|
430
|
-
const element = elements[i];
|
|
431
|
-
if (partial) {
|
|
432
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
433
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
434
|
-
foundElements.push(element);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
439
|
-
(element.value && element.value === text)) {
|
|
440
|
-
foundElements.push(element);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
let noChildElements = [];
|
|
446
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
447
|
-
let element = foundElements[i];
|
|
448
|
-
let hasChild = false;
|
|
449
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
450
|
-
if (i === j) {
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
if (isParent(element, foundElements[j])) {
|
|
454
|
-
hasChild = true;
|
|
455
|
-
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;
|
|
456
528
|
}
|
|
457
529
|
}
|
|
458
|
-
if (!
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
let elementCount = 0;
|
|
463
|
-
if (noChildElements.length > 0) {
|
|
464
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
465
|
-
if (randomToken === null) {
|
|
466
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
467
|
-
}
|
|
468
|
-
let element = noChildElements[i];
|
|
469
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
470
|
-
elementCount++;
|
|
530
|
+
if (!el.setAttribute) {
|
|
531
|
+
el = el.parentElement;
|
|
471
532
|
}
|
|
533
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
534
|
+
return true;
|
|
535
|
+
}, [tag1, randomToken]))) {
|
|
536
|
+
continue;
|
|
472
537
|
}
|
|
473
|
-
|
|
474
|
-
}
|
|
538
|
+
tagCount++;
|
|
539
|
+
}
|
|
540
|
+
return { elementCount: tagCount, randomToken };
|
|
475
541
|
}
|
|
476
|
-
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
|
+
}
|
|
477
553
|
let locatorSearch = selectorHierarchy[index];
|
|
478
554
|
try {
|
|
479
|
-
locatorSearch =
|
|
555
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
480
556
|
}
|
|
481
557
|
catch (e) {
|
|
482
558
|
console.error(e);
|
|
483
559
|
}
|
|
560
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
484
561
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
485
562
|
let locator = null;
|
|
486
563
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
487
|
-
|
|
564
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
565
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
488
566
|
if (!locatorString) {
|
|
567
|
+
info.failCause.textNotFound = true;
|
|
568
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
489
569
|
return;
|
|
490
570
|
}
|
|
491
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
571
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
492
572
|
}
|
|
493
573
|
else if (locatorSearch.text) {
|
|
494
|
-
let
|
|
574
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
575
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
495
576
|
if (result.elementCount === 0) {
|
|
577
|
+
info.failCause.textNotFound = true;
|
|
578
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
496
579
|
return;
|
|
497
580
|
}
|
|
498
|
-
locatorSearch.css = "[data-blinq-id
|
|
581
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
499
582
|
if (locatorSearch.childCss) {
|
|
500
583
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
501
584
|
}
|
|
502
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
585
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
503
586
|
}
|
|
504
587
|
else {
|
|
505
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
588
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
506
589
|
}
|
|
507
590
|
// let cssHref = false;
|
|
508
591
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
509
592
|
// cssHref = true;
|
|
510
593
|
// }
|
|
511
594
|
let count = await locator.count();
|
|
595
|
+
if (count > 0 && !info.failCause.count) {
|
|
596
|
+
info.failCause.count = count;
|
|
597
|
+
}
|
|
512
598
|
//info.log += "total elements found " + count + "\n";
|
|
513
599
|
//let visibleCount = 0;
|
|
514
600
|
let visibleLocator = null;
|
|
515
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
601
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
516
602
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
603
|
+
if (info.locatorLog) {
|
|
604
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
605
|
+
}
|
|
517
606
|
return;
|
|
518
607
|
}
|
|
608
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
609
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
610
|
+
}
|
|
519
611
|
for (let j = 0; j < count; j++) {
|
|
520
612
|
let visible = await locator.nth(j).isVisible();
|
|
521
613
|
const enabled = await locator.nth(j).isEnabled();
|
|
522
614
|
if (!visibleOnly) {
|
|
523
615
|
visible = true;
|
|
524
616
|
}
|
|
525
|
-
if (visible && enabled) {
|
|
617
|
+
if (visible && (allowDisabled || enabled)) {
|
|
526
618
|
foundLocators.push(locator.nth(j));
|
|
619
|
+
if (info.locatorLog) {
|
|
620
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
621
|
+
}
|
|
527
622
|
}
|
|
528
|
-
else {
|
|
623
|
+
else if (logErrors) {
|
|
624
|
+
info.failCause.visible = visible;
|
|
625
|
+
info.failCause.enabled = enabled;
|
|
529
626
|
if (!info.printMessages) {
|
|
530
627
|
info.printMessages = {};
|
|
531
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
|
+
}
|
|
532
637
|
if (!info.printMessages[j.toString()]) {
|
|
533
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
638
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
534
639
|
info.printMessages[j.toString()] = true;
|
|
535
640
|
}
|
|
536
641
|
}
|
|
537
642
|
}
|
|
538
643
|
}
|
|
539
644
|
async closeUnexpectedPopups(info, _params) {
|
|
645
|
+
if (!info) {
|
|
646
|
+
info = {};
|
|
647
|
+
info.failCause = {};
|
|
648
|
+
info.log = "";
|
|
649
|
+
}
|
|
540
650
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
541
651
|
if (!info) {
|
|
542
652
|
info = {};
|
|
543
653
|
}
|
|
544
|
-
info.log += "scan for popup handlers" + "\n";
|
|
654
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
545
655
|
const handlerGroup = [];
|
|
546
656
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
547
657
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
548
658
|
}
|
|
549
|
-
const scopes =
|
|
659
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
550
660
|
let result = null;
|
|
551
661
|
let scope = null;
|
|
552
662
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -568,33 +678,218 @@ class StableBrowser {
|
|
|
568
678
|
}
|
|
569
679
|
if (result.foundElements.length > 0) {
|
|
570
680
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
}
|
|
574
696
|
return { rerun: true };
|
|
575
697
|
}
|
|
576
698
|
}
|
|
577
699
|
}
|
|
578
700
|
return { rerun: false };
|
|
579
701
|
}
|
|
580
|
-
|
|
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) {
|
|
581
767
|
if (!timeout) {
|
|
582
768
|
timeout = 30000;
|
|
583
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
|
+
}
|
|
584
777
|
for (let i = 0; i < 3; i++) {
|
|
585
778
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
586
779
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
587
780
|
let selector = selectors.locators[j];
|
|
588
781
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
589
782
|
}
|
|
590
|
-
|
|
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
|
+
}
|
|
591
826
|
if (!element.rerun) {
|
|
592
|
-
|
|
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();
|
|
593
882
|
}
|
|
594
883
|
}
|
|
595
884
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
596
885
|
}
|
|
597
|
-
async _findFrameScope(selectors, timeout = 30000) {
|
|
886
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
887
|
+
if (!info) {
|
|
888
|
+
info = {};
|
|
889
|
+
info.failCause = {};
|
|
890
|
+
info.log = "";
|
|
891
|
+
}
|
|
892
|
+
let startTime = Date.now();
|
|
598
893
|
let scope = this.page;
|
|
599
894
|
if (selectors.frame) {
|
|
600
895
|
return selectors.frame;
|
|
@@ -604,7 +899,7 @@ class StableBrowser {
|
|
|
604
899
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
605
900
|
let frameLocator = frame.selectors[i];
|
|
606
901
|
if (frameLocator.css) {
|
|
607
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
902
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
608
903
|
if (frameLocator.index) {
|
|
609
904
|
testframescope = framescope.nth(frameLocator.index);
|
|
610
905
|
}
|
|
@@ -616,7 +911,7 @@ class StableBrowser {
|
|
|
616
911
|
break;
|
|
617
912
|
}
|
|
618
913
|
catch (error) {
|
|
619
|
-
console.error("frame not found " + frameLocator.css);
|
|
914
|
+
// console.error("frame not found " + frameLocator.css);
|
|
620
915
|
}
|
|
621
916
|
}
|
|
622
917
|
}
|
|
@@ -625,9 +920,11 @@ class StableBrowser {
|
|
|
625
920
|
}
|
|
626
921
|
return framescope;
|
|
627
922
|
};
|
|
923
|
+
let fLocator = null;
|
|
628
924
|
while (true) {
|
|
629
925
|
let frameFound = false;
|
|
630
926
|
if (selectors.nestFrmLoc) {
|
|
927
|
+
fLocator = selectors.nestFrmLoc;
|
|
631
928
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
632
929
|
frameFound = true;
|
|
633
930
|
break;
|
|
@@ -636,6 +933,7 @@ class StableBrowser {
|
|
|
636
933
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
637
934
|
let frameLocator = selectors.frameLocators[i];
|
|
638
935
|
if (frameLocator.css) {
|
|
936
|
+
fLocator = frameLocator.css;
|
|
639
937
|
scope = scope.frameLocator(frameLocator.css);
|
|
640
938
|
frameFound = true;
|
|
641
939
|
break;
|
|
@@ -643,16 +941,25 @@ class StableBrowser {
|
|
|
643
941
|
}
|
|
644
942
|
}
|
|
645
943
|
if (!frameFound && selectors.iframe_src) {
|
|
944
|
+
fLocator = selectors.iframe_src;
|
|
646
945
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
647
946
|
}
|
|
648
947
|
if (!scope) {
|
|
649
|
-
info
|
|
650
|
-
|
|
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) {
|
|
953
|
+
info.failCause.iframeNotFound = true;
|
|
954
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
651
955
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
652
956
|
}
|
|
653
957
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
654
958
|
}
|
|
655
959
|
else {
|
|
960
|
+
if (info && info.locatorLog) {
|
|
961
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
962
|
+
}
|
|
656
963
|
break;
|
|
657
964
|
}
|
|
658
965
|
}
|
|
@@ -662,20 +969,35 @@ class StableBrowser {
|
|
|
662
969
|
}
|
|
663
970
|
return scope;
|
|
664
971
|
}
|
|
665
|
-
async _getDocumentBody(selectors, timeout = 30000) {
|
|
666
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
972
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
973
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
667
974
|
return scope.evaluate(() => {
|
|
668
975
|
var bodyContent = document.body.innerHTML;
|
|
669
976
|
return bodyContent;
|
|
670
977
|
});
|
|
671
978
|
}
|
|
672
|
-
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
|
+
}
|
|
989
|
+
if (!info) {
|
|
990
|
+
info = {};
|
|
991
|
+
info.failCause = {};
|
|
992
|
+
info.log = "";
|
|
993
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
994
|
+
}
|
|
673
995
|
let highPriorityTimeout = 5000;
|
|
674
996
|
let visibleOnlyTimeout = 6000;
|
|
675
|
-
let startTime =
|
|
997
|
+
let startTime = Date.now();
|
|
676
998
|
let locatorsCount = 0;
|
|
999
|
+
let lazy_scroll = false;
|
|
677
1000
|
//let arrayMode = Array.isArray(selectors);
|
|
678
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
679
1001
|
let selectorsLocators = null;
|
|
680
1002
|
selectorsLocators = selectors.locators;
|
|
681
1003
|
// group selectors by priority
|
|
@@ -703,6 +1025,7 @@ class StableBrowser {
|
|
|
703
1025
|
let highPriorityOnly = true;
|
|
704
1026
|
let visibleOnly = true;
|
|
705
1027
|
while (true) {
|
|
1028
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
706
1029
|
locatorsCount = 0;
|
|
707
1030
|
let result = [];
|
|
708
1031
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -711,18 +1034,13 @@ class StableBrowser {
|
|
|
711
1034
|
}
|
|
712
1035
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
713
1036
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
714
|
-
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);
|
|
715
1038
|
if (result.foundElements.length === 0) {
|
|
716
1039
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
717
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
718
|
-
}
|
|
719
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
720
|
-
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);
|
|
721
1041
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
725
|
-
}
|
|
1042
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1043
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
726
1044
|
}
|
|
727
1045
|
let foundElements = result.foundElements;
|
|
728
1046
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -762,24 +1080,43 @@ class StableBrowser {
|
|
|
762
1080
|
return maxCountElement.locator;
|
|
763
1081
|
}
|
|
764
1082
|
}
|
|
765
|
-
if (
|
|
1083
|
+
if (Date.now() - startTime > timeout) {
|
|
766
1084
|
break;
|
|
767
1085
|
}
|
|
768
|
-
if (
|
|
769
|
-
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";
|
|
770
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
|
+
}
|
|
771
1093
|
}
|
|
772
|
-
if (
|
|
773
|
-
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";
|
|
774
1096
|
visibleOnly = false;
|
|
775
1097
|
}
|
|
776
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
|
+
}
|
|
777
1104
|
}
|
|
778
1105
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
779
|
-
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";
|
|
1113
|
+
info.failCause.locatorNotFound = true;
|
|
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
|
+
}
|
|
780
1117
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
781
1118
|
}
|
|
782
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1119
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
783
1120
|
let foundElements = [];
|
|
784
1121
|
const result = {
|
|
785
1122
|
foundElements: foundElements,
|
|
@@ -787,31 +1124,88 @@ class StableBrowser {
|
|
|
787
1124
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
788
1125
|
let foundLocators = [];
|
|
789
1126
|
try {
|
|
790
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1127
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
791
1128
|
}
|
|
792
1129
|
catch (e) {
|
|
793
|
-
this
|
|
794
|
-
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);
|
|
795
1133
|
foundLocators = [];
|
|
796
1134
|
try {
|
|
797
|
-
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);
|
|
798
1136
|
}
|
|
799
1137
|
catch (e) {
|
|
800
|
-
|
|
1138
|
+
if (logErrors) {
|
|
1139
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1140
|
+
}
|
|
801
1141
|
}
|
|
802
1142
|
}
|
|
803
1143
|
if (foundLocators.length === 1) {
|
|
1144
|
+
let box = null;
|
|
1145
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1146
|
+
box = await foundLocators[0].boundingBox();
|
|
1147
|
+
}
|
|
804
1148
|
result.foundElements.push({
|
|
805
1149
|
locator: foundLocators[0],
|
|
806
|
-
box:
|
|
1150
|
+
box: box,
|
|
807
1151
|
unique: true,
|
|
808
1152
|
});
|
|
809
1153
|
result.locatorIndex = i;
|
|
810
1154
|
}
|
|
1155
|
+
if (foundLocators.length > 1) {
|
|
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
|
+
}
|
|
1191
|
+
}
|
|
811
1192
|
}
|
|
812
1193
|
return result;
|
|
813
1194
|
}
|
|
814
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);
|
|
815
1209
|
const startTime = Date.now();
|
|
816
1210
|
let timeout = 30000;
|
|
817
1211
|
if (options && options.timeout) {
|
|
@@ -835,13 +1229,32 @@ class StableBrowser {
|
|
|
835
1229
|
}
|
|
836
1230
|
catch (e) {
|
|
837
1231
|
if (performance.now() - startTime > timeout) {
|
|
838
|
-
throw e;
|
|
1232
|
+
// throw e;
|
|
1233
|
+
try {
|
|
1234
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1235
|
+
}
|
|
1236
|
+
finally {
|
|
1237
|
+
await _commandFinally(state, this);
|
|
1238
|
+
}
|
|
839
1239
|
}
|
|
840
1240
|
}
|
|
841
1241
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
842
1242
|
}
|
|
843
1243
|
}
|
|
844
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);
|
|
845
1258
|
const startTime = Date.now();
|
|
846
1259
|
let timeout = 30000;
|
|
847
1260
|
if (options && options.timeout) {
|
|
@@ -865,7 +1278,13 @@ class StableBrowser {
|
|
|
865
1278
|
}
|
|
866
1279
|
catch (e) {
|
|
867
1280
|
if (performance.now() - startTime > timeout) {
|
|
868
|
-
throw e;
|
|
1281
|
+
// throw e;
|
|
1282
|
+
try {
|
|
1283
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
1284
|
+
}
|
|
1285
|
+
finally {
|
|
1286
|
+
await _commandFinally(state, this);
|
|
1287
|
+
}
|
|
869
1288
|
}
|
|
870
1289
|
}
|
|
871
1290
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -878,34 +1297,74 @@ class StableBrowser {
|
|
|
878
1297
|
options,
|
|
879
1298
|
world,
|
|
880
1299
|
text: "Click element",
|
|
1300
|
+
_text: "Click on " + selectors.element_name,
|
|
881
1301
|
type: Types.CLICK,
|
|
882
1302
|
operation: "click",
|
|
883
1303
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
884
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
|
+
}
|
|
885
1312
|
try {
|
|
1313
|
+
check_performance("click_preCommand", this.context, true);
|
|
886
1314
|
await _preCommand(state, this);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
await
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
catch (e) {
|
|
895
|
-
// await this.closeUnexpectedPopups();
|
|
896
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
897
|
-
await state.element.dispatchEvent("click");
|
|
898
|
-
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);
|
|
899
1321
|
}
|
|
900
|
-
await this.waitForPageLoad();
|
|
901
1322
|
return state.info;
|
|
902
1323
|
}
|
|
903
1324
|
catch (e) {
|
|
904
1325
|
await _commandError(state, e, this);
|
|
905
1326
|
}
|
|
906
1327
|
finally {
|
|
907
|
-
|
|
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);
|
|
908
1366
|
}
|
|
1367
|
+
return found;
|
|
909
1368
|
}
|
|
910
1369
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
911
1370
|
const state = {
|
|
@@ -915,6 +1374,7 @@ class StableBrowser {
|
|
|
915
1374
|
world,
|
|
916
1375
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
917
1376
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1377
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
918
1378
|
operation: "setCheck",
|
|
919
1379
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
920
1380
|
};
|
|
@@ -924,30 +1384,53 @@ class StableBrowser {
|
|
|
924
1384
|
// let element = await this._locate(selectors, info, _params);
|
|
925
1385
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
926
1386
|
try {
|
|
927
|
-
//
|
|
928
|
-
|
|
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 });
|
|
929
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);
|
|
930
1396
|
}
|
|
931
1397
|
catch (e) {
|
|
932
1398
|
if (e.message && e.message.includes("did not change its state")) {
|
|
933
1399
|
this.logger.info("element did not change its state, ignoring...");
|
|
934
1400
|
}
|
|
935
1401
|
else {
|
|
1402
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
936
1403
|
//await this.closeUnexpectedPopups();
|
|
937
1404
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
938
|
-
state.
|
|
939
|
-
|
|
940
|
-
|
|
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
|
+
}
|
|
941
1424
|
}
|
|
942
1425
|
}
|
|
943
|
-
await this.waitForPageLoad();
|
|
1426
|
+
//await this.waitForPageLoad();
|
|
944
1427
|
return state.info;
|
|
945
1428
|
}
|
|
946
1429
|
catch (e) {
|
|
947
1430
|
await _commandError(state, e, this);
|
|
948
1431
|
}
|
|
949
1432
|
finally {
|
|
950
|
-
_commandFinally(state, this);
|
|
1433
|
+
await _commandFinally(state, this);
|
|
951
1434
|
}
|
|
952
1435
|
}
|
|
953
1436
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -958,31 +1441,22 @@ class StableBrowser {
|
|
|
958
1441
|
world,
|
|
959
1442
|
type: Types.HOVER,
|
|
960
1443
|
text: `Hover element`,
|
|
1444
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
961
1445
|
operation: "hover",
|
|
962
1446
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
963
1447
|
};
|
|
964
1448
|
try {
|
|
965
1449
|
await _preCommand(state, this);
|
|
966
|
-
|
|
967
|
-
await state.element.hover();
|
|
968
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
969
|
-
}
|
|
970
|
-
catch (e) {
|
|
971
|
-
//await this.closeUnexpectedPopups();
|
|
972
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
973
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
974
|
-
await state.element.hover({ timeout: 10000 });
|
|
975
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
976
|
-
}
|
|
1450
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
977
1451
|
await _screenshot(state, this);
|
|
978
|
-
await this.waitForPageLoad();
|
|
1452
|
+
//await this.waitForPageLoad();
|
|
979
1453
|
return state.info;
|
|
980
1454
|
}
|
|
981
1455
|
catch (e) {
|
|
982
1456
|
await _commandError(state, e, this);
|
|
983
1457
|
}
|
|
984
1458
|
finally {
|
|
985
|
-
_commandFinally(state, this);
|
|
1459
|
+
await _commandFinally(state, this);
|
|
986
1460
|
}
|
|
987
1461
|
}
|
|
988
1462
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -997,6 +1471,7 @@ class StableBrowser {
|
|
|
997
1471
|
value: values.toString(),
|
|
998
1472
|
type: Types.SELECT,
|
|
999
1473
|
text: `Select option: ${values}`,
|
|
1474
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1000
1475
|
operation: "selectOption",
|
|
1001
1476
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1002
1477
|
};
|
|
@@ -1010,14 +1485,14 @@ class StableBrowser {
|
|
|
1010
1485
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1011
1486
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1012
1487
|
}
|
|
1013
|
-
await this.waitForPageLoad();
|
|
1488
|
+
//await this.waitForPageLoad();
|
|
1014
1489
|
return state.info;
|
|
1015
1490
|
}
|
|
1016
1491
|
catch (e) {
|
|
1017
1492
|
await _commandError(state, e, this);
|
|
1018
1493
|
}
|
|
1019
1494
|
finally {
|
|
1020
|
-
_commandFinally(state, this);
|
|
1495
|
+
await _commandFinally(state, this);
|
|
1021
1496
|
}
|
|
1022
1497
|
}
|
|
1023
1498
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1031,6 +1506,7 @@ class StableBrowser {
|
|
|
1031
1506
|
highlight: false,
|
|
1032
1507
|
type: Types.TYPE_PRESS,
|
|
1033
1508
|
text: `Type value: ${_value}`,
|
|
1509
|
+
_text: `Type value: ${_value}`,
|
|
1034
1510
|
operation: "type",
|
|
1035
1511
|
log: "",
|
|
1036
1512
|
};
|
|
@@ -1062,7 +1538,7 @@ class StableBrowser {
|
|
|
1062
1538
|
await _commandError(state, e, this);
|
|
1063
1539
|
}
|
|
1064
1540
|
finally {
|
|
1065
|
-
_commandFinally(state, this);
|
|
1541
|
+
await _commandFinally(state, this);
|
|
1066
1542
|
}
|
|
1067
1543
|
}
|
|
1068
1544
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1098,37 +1574,35 @@ class StableBrowser {
|
|
|
1098
1574
|
await _commandError(state, e, this);
|
|
1099
1575
|
}
|
|
1100
1576
|
finally {
|
|
1101
|
-
_commandFinally(state, this);
|
|
1577
|
+
await _commandFinally(state, this);
|
|
1102
1578
|
}
|
|
1103
1579
|
}
|
|
1104
1580
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
+
};
|
|
1115
1594
|
try {
|
|
1116
|
-
|
|
1117
|
-
let element = await this._locate(selectors, info, _params);
|
|
1118
|
-
//insert red border around the element
|
|
1119
|
-
await this.scrollIfNeeded(element, info);
|
|
1120
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1121
|
-
await this._highlightElements(element);
|
|
1595
|
+
await _preCommand(state, this);
|
|
1122
1596
|
try {
|
|
1123
|
-
await element
|
|
1597
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1124
1598
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1125
1599
|
if (format) {
|
|
1126
|
-
value = dayjs(value).format(format);
|
|
1127
|
-
await element.fill(value);
|
|
1600
|
+
state.value = dayjs(state.value).format(format);
|
|
1601
|
+
await state.element.fill(state.value);
|
|
1128
1602
|
}
|
|
1129
1603
|
else {
|
|
1130
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1131
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1604
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1605
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1132
1606
|
el.value = ""; // clear input
|
|
1133
1607
|
el.value = dateTimeValue;
|
|
1134
1608
|
}, dateTimeValue);
|
|
@@ -1141,20 +1615,19 @@ class StableBrowser {
|
|
|
1141
1615
|
}
|
|
1142
1616
|
catch (err) {
|
|
1143
1617
|
//await this.closeUnexpectedPopups();
|
|
1144
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1618
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1145
1619
|
this.logger.info("Trying again");
|
|
1146
|
-
|
|
1147
|
-
info.
|
|
1148
|
-
Object.assign(err, { info: info });
|
|
1620
|
+
await _screenshot(state, this);
|
|
1621
|
+
Object.assign(err, { info: state.info });
|
|
1149
1622
|
await element.click();
|
|
1150
1623
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1151
1624
|
if (format) {
|
|
1152
|
-
value = dayjs(value).format(format);
|
|
1153
|
-
await element.fill(value);
|
|
1625
|
+
state.value = dayjs(state.value).format(format);
|
|
1626
|
+
await state.element.fill(state.value);
|
|
1154
1627
|
}
|
|
1155
1628
|
else {
|
|
1156
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1157
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1629
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1630
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1158
1631
|
el.value = ""; // clear input
|
|
1159
1632
|
el.value = dateTimeValue;
|
|
1160
1633
|
}, dateTimeValue);
|
|
@@ -1167,31 +1640,10 @@ class StableBrowser {
|
|
|
1167
1640
|
}
|
|
1168
1641
|
}
|
|
1169
1642
|
catch (e) {
|
|
1170
|
-
|
|
1171
|
-
throw e;
|
|
1643
|
+
await _commandError(state, e, this);
|
|
1172
1644
|
}
|
|
1173
1645
|
finally {
|
|
1174
|
-
|
|
1175
|
-
this._reportToWorld(world, {
|
|
1176
|
-
element_name: selectors.element_name,
|
|
1177
|
-
type: Types.SET_DATE_TIME,
|
|
1178
|
-
screenshotId,
|
|
1179
|
-
value: value,
|
|
1180
|
-
text: `setDateTime input with value: ${value}`,
|
|
1181
|
-
result: error
|
|
1182
|
-
? {
|
|
1183
|
-
status: "FAILED",
|
|
1184
|
-
startTime,
|
|
1185
|
-
endTime,
|
|
1186
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1187
|
-
}
|
|
1188
|
-
: {
|
|
1189
|
-
status: "PASSED",
|
|
1190
|
-
startTime,
|
|
1191
|
-
endTime,
|
|
1192
|
-
},
|
|
1193
|
-
info: info,
|
|
1194
|
-
});
|
|
1646
|
+
await _commandFinally(state, this);
|
|
1195
1647
|
}
|
|
1196
1648
|
}
|
|
1197
1649
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1206,17 +1658,29 @@ class StableBrowser {
|
|
|
1206
1658
|
world,
|
|
1207
1659
|
type: Types.FILL,
|
|
1208
1660
|
text: `Click type input with value: ${_value}`,
|
|
1661
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1209
1662
|
operation: "clickType",
|
|
1210
1663
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1211
1664
|
};
|
|
1665
|
+
if (!options) {
|
|
1666
|
+
options = {};
|
|
1667
|
+
}
|
|
1212
1668
|
if (newValue !== _value) {
|
|
1213
1669
|
//this.logger.info(_value + "=" + newValue);
|
|
1214
1670
|
_value = newValue;
|
|
1215
1671
|
}
|
|
1216
1672
|
try {
|
|
1217
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);
|
|
1218
1682
|
state.info.value = _value;
|
|
1219
|
-
if (
|
|
1683
|
+
if (!options.press) {
|
|
1220
1684
|
try {
|
|
1221
1685
|
let currentValue = await state.element.inputValue();
|
|
1222
1686
|
if (currentValue) {
|
|
@@ -1227,13 +1691,9 @@ class StableBrowser {
|
|
|
1227
1691
|
this.logger.info("unable to clear input value");
|
|
1228
1692
|
}
|
|
1229
1693
|
}
|
|
1230
|
-
if (options
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
catch (e) {
|
|
1235
|
-
await state.element.dispatchEvent("click");
|
|
1236
|
-
}
|
|
1694
|
+
if (options.press) {
|
|
1695
|
+
options.timeout = 5000;
|
|
1696
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1237
1697
|
}
|
|
1238
1698
|
else {
|
|
1239
1699
|
try {
|
|
@@ -1244,6 +1704,25 @@ class StableBrowser {
|
|
|
1244
1704
|
}
|
|
1245
1705
|
}
|
|
1246
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
|
+
}
|
|
1247
1726
|
const valueSegment = state.value.split("&&");
|
|
1248
1727
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1249
1728
|
if (i > 0) {
|
|
@@ -1264,14 +1743,21 @@ class StableBrowser {
|
|
|
1264
1743
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1265
1744
|
}
|
|
1266
1745
|
}
|
|
1746
|
+
//if (!this.fastMode) {
|
|
1267
1747
|
await _screenshot(state, this);
|
|
1748
|
+
//}
|
|
1268
1749
|
if (enter === true) {
|
|
1269
1750
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1270
1751
|
await this.page.keyboard.press("Enter");
|
|
1271
1752
|
await this.waitForPageLoad();
|
|
1272
1753
|
}
|
|
1273
1754
|
else if (enter === false) {
|
|
1274
|
-
|
|
1755
|
+
try {
|
|
1756
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1757
|
+
}
|
|
1758
|
+
catch (e) {
|
|
1759
|
+
// ignore
|
|
1760
|
+
}
|
|
1275
1761
|
//await this.page.keyboard.press("Tab");
|
|
1276
1762
|
}
|
|
1277
1763
|
else {
|
|
@@ -1286,7 +1772,7 @@ class StableBrowser {
|
|
|
1286
1772
|
await _commandError(state, e, this);
|
|
1287
1773
|
}
|
|
1288
1774
|
finally {
|
|
1289
|
-
_commandFinally(state, this);
|
|
1775
|
+
await _commandFinally(state, this);
|
|
1290
1776
|
}
|
|
1291
1777
|
}
|
|
1292
1778
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1308,30 +1794,67 @@ class StableBrowser {
|
|
|
1308
1794
|
if (enter) {
|
|
1309
1795
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1310
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;
|
|
1311
1832
|
}
|
|
1312
|
-
await
|
|
1833
|
+
await state.element.setInputFiles(files);
|
|
1313
1834
|
return state.info;
|
|
1314
1835
|
}
|
|
1315
1836
|
catch (e) {
|
|
1316
1837
|
await _commandError(state, e, this);
|
|
1317
1838
|
}
|
|
1318
1839
|
finally {
|
|
1319
|
-
_commandFinally(state, this);
|
|
1840
|
+
await _commandFinally(state, this);
|
|
1320
1841
|
}
|
|
1321
1842
|
}
|
|
1322
1843
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1323
1844
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1324
1845
|
}
|
|
1325
1846
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1847
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1326
1848
|
_validateSelectors(selectors);
|
|
1327
1849
|
let screenshotId = null;
|
|
1328
1850
|
let screenshotPath = null;
|
|
1329
1851
|
if (!info.log) {
|
|
1330
1852
|
info.log = "";
|
|
1853
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1331
1854
|
}
|
|
1332
1855
|
info.operation = "getText";
|
|
1333
1856
|
info.selectors = selectors;
|
|
1334
|
-
let element = await this._locate(selectors, info, _params);
|
|
1857
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1335
1858
|
if (climb > 0) {
|
|
1336
1859
|
const climbArray = [];
|
|
1337
1860
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1350,6 +1873,18 @@ class StableBrowser {
|
|
|
1350
1873
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1351
1874
|
try {
|
|
1352
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
|
+
// }
|
|
1353
1888
|
const elementText = await element.innerText();
|
|
1354
1889
|
return {
|
|
1355
1890
|
text: elementText,
|
|
@@ -1361,7 +1896,7 @@ class StableBrowser {
|
|
|
1361
1896
|
}
|
|
1362
1897
|
catch (e) {
|
|
1363
1898
|
//await this.closeUnexpectedPopups();
|
|
1364
|
-
this.logger.info("no innerText will use textContent");
|
|
1899
|
+
this.logger.info("no innerText, will use textContent");
|
|
1365
1900
|
const elementText = await element.textContent();
|
|
1366
1901
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1367
1902
|
}
|
|
@@ -1386,6 +1921,7 @@ class StableBrowser {
|
|
|
1386
1921
|
highlight: false,
|
|
1387
1922
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1388
1923
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1924
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1389
1925
|
operation: "containsPattern",
|
|
1390
1926
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1391
1927
|
};
|
|
@@ -1417,10 +1953,12 @@ class StableBrowser {
|
|
|
1417
1953
|
await _commandError(state, e, this);
|
|
1418
1954
|
}
|
|
1419
1955
|
finally {
|
|
1420
|
-
_commandFinally(state, this);
|
|
1956
|
+
await _commandFinally(state, this);
|
|
1421
1957
|
}
|
|
1422
1958
|
}
|
|
1423
1959
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1960
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1961
|
+
const startTime = Date.now();
|
|
1424
1962
|
const state = {
|
|
1425
1963
|
selectors,
|
|
1426
1964
|
_params,
|
|
@@ -1447,61 +1985,137 @@ class StableBrowser {
|
|
|
1447
1985
|
}
|
|
1448
1986
|
let foundObj = null;
|
|
1449
1987
|
try {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1457
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1458
|
-
if (dateAlternatives.date) {
|
|
1459
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1460
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1461
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1462
|
-
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);
|
|
1463
1994
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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)) {
|
|
1471
2015
|
return state.info;
|
|
1472
2016
|
}
|
|
1473
2017
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
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
|
|
1480
2023
|
}
|
|
1481
|
-
|
|
2024
|
+
state.info.foundText = foundObj?.text;
|
|
2025
|
+
state.info.value = foundObj?.value;
|
|
2026
|
+
throw new Error("element doesn't contain text " + text);
|
|
1482
2027
|
}
|
|
1483
2028
|
catch (e) {
|
|
1484
2029
|
await _commandError(state, e, this);
|
|
2030
|
+
throw e;
|
|
1485
2031
|
}
|
|
1486
2032
|
finally {
|
|
1487
|
-
_commandFinally(state, this);
|
|
2033
|
+
await _commandFinally(state, this);
|
|
1488
2034
|
}
|
|
1489
2035
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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");
|
|
1494
2055
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
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");
|
|
1497
2060
|
}
|
|
1498
|
-
else if (
|
|
1499
|
-
|
|
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);
|
|
1500
2066
|
}
|
|
1501
2067
|
else {
|
|
1502
|
-
|
|
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);
|
|
1503
2118
|
}
|
|
1504
|
-
return dataFile;
|
|
1505
2119
|
}
|
|
1506
2120
|
async waitForUserInput(message, world = null) {
|
|
1507
2121
|
if (!message) {
|
|
@@ -1531,13 +2145,22 @@ class StableBrowser {
|
|
|
1531
2145
|
return;
|
|
1532
2146
|
}
|
|
1533
2147
|
// if data file exists, load it
|
|
1534
|
-
const dataFile =
|
|
2148
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1535
2149
|
let data = this.getTestData(world);
|
|
1536
2150
|
// merge the testData with the existing data
|
|
1537
2151
|
Object.assign(data, testData);
|
|
1538
2152
|
// save the data to the file
|
|
1539
2153
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1540
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
|
+
}
|
|
1541
2164
|
_getDataFilePath(fileName) {
|
|
1542
2165
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1543
2166
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1634,14 +2257,12 @@ class StableBrowser {
|
|
|
1634
2257
|
}
|
|
1635
2258
|
}
|
|
1636
2259
|
getTestData(world = null) {
|
|
1637
|
-
|
|
1638
|
-
let data = {};
|
|
1639
|
-
if (fs.existsSync(dataFile)) {
|
|
1640
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1641
|
-
}
|
|
1642
|
-
return data;
|
|
2260
|
+
return _getTestData(world, this.context, this);
|
|
1643
2261
|
}
|
|
1644
2262
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2263
|
+
if (!options) {
|
|
2264
|
+
options = {};
|
|
2265
|
+
}
|
|
1645
2266
|
// collect url/path/title
|
|
1646
2267
|
if (info) {
|
|
1647
2268
|
if (!info.title) {
|
|
@@ -1666,13 +2287,11 @@ class StableBrowser {
|
|
|
1666
2287
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1667
2288
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1668
2289
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
}
|
|
1673
|
-
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");
|
|
1674
2293
|
try {
|
|
1675
|
-
await this.takeScreenshot(screenshotPath);
|
|
2294
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
1676
2295
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1677
2296
|
// // save the buffer to the screenshot path asynchrously
|
|
1678
2297
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -1680,20 +2299,20 @@ class StableBrowser {
|
|
|
1680
2299
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1681
2300
|
// }
|
|
1682
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
|
+
}
|
|
1683
2307
|
}
|
|
1684
2308
|
catch (e) {
|
|
1685
2309
|
this.logger.info("unable to take screenshot, ignored");
|
|
1686
2310
|
}
|
|
1687
|
-
result.screenshotId = nextIndex;
|
|
1688
|
-
result.screenshotPath = screenshotPath;
|
|
1689
|
-
if (info && info.box) {
|
|
1690
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1691
|
-
}
|
|
1692
2311
|
}
|
|
1693
2312
|
else if (options && options.screenshot) {
|
|
1694
2313
|
result.screenshotPath = options.screenshotPath;
|
|
1695
2314
|
try {
|
|
1696
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2315
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
1697
2316
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1698
2317
|
// // save the buffer to the screenshot path asynchrously
|
|
1699
2318
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -1711,7 +2330,7 @@ class StableBrowser {
|
|
|
1711
2330
|
}
|
|
1712
2331
|
return result;
|
|
1713
2332
|
}
|
|
1714
|
-
async takeScreenshot(screenshotPath) {
|
|
2333
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
1715
2334
|
const playContext = this.context.playContext;
|
|
1716
2335
|
// Using CDP to capture the screenshot
|
|
1717
2336
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -1723,17 +2342,20 @@ class StableBrowser {
|
|
|
1723
2342
|
document.documentElement.clientWidth,
|
|
1724
2343
|
])));
|
|
1725
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
|
+
// }
|
|
1726
2354
|
if (this.context.browserName === "chromium") {
|
|
1727
2355
|
const client = await playContext.newCDPSession(this.page);
|
|
1728
2356
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1729
2357
|
format: "png",
|
|
1730
|
-
|
|
1731
|
-
// x: 0,
|
|
1732
|
-
// y: 0,
|
|
1733
|
-
// width: viewportWidth,
|
|
1734
|
-
// height: viewportHeight,
|
|
1735
|
-
// scale: 1,
|
|
1736
|
-
// },
|
|
2358
|
+
captureBeyondViewport: fullPage,
|
|
1737
2359
|
});
|
|
1738
2360
|
await client.detach();
|
|
1739
2361
|
if (!screenshotPath) {
|
|
@@ -1742,8 +2364,12 @@ class StableBrowser {
|
|
|
1742
2364
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
1743
2365
|
}
|
|
1744
2366
|
else {
|
|
1745
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2367
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
1746
2368
|
}
|
|
2369
|
+
// if (focusedElement) {
|
|
2370
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2371
|
+
// await this._unhighlightElements(focusedElement);
|
|
2372
|
+
// }
|
|
1747
2373
|
let image = await Jimp.read(screenshotBuffer);
|
|
1748
2374
|
// Get the image dimensions
|
|
1749
2375
|
const { width, height } = image.bitmap;
|
|
@@ -1756,6 +2382,7 @@ class StableBrowser {
|
|
|
1756
2382
|
else {
|
|
1757
2383
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1758
2384
|
}
|
|
2385
|
+
return screenshotBuffer;
|
|
1759
2386
|
}
|
|
1760
2387
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1761
2388
|
const state = {
|
|
@@ -1778,111 +2405,542 @@ class StableBrowser {
|
|
|
1778
2405
|
await _commandError(state, e, this);
|
|
1779
2406
|
}
|
|
1780
2407
|
finally {
|
|
1781
|
-
_commandFinally(state, this);
|
|
2408
|
+
await _commandFinally(state, this);
|
|
1782
2409
|
}
|
|
1783
2410
|
}
|
|
1784
2411
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
+
};
|
|
1790
2426
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1791
|
-
const info = {};
|
|
1792
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1793
|
-
info.operation = "extract";
|
|
1794
|
-
info.selectors = selectors;
|
|
1795
2427
|
try {
|
|
1796
|
-
|
|
1797
|
-
await this._highlightElements(element);
|
|
1798
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2428
|
+
await _preCommand(state, this);
|
|
1799
2429
|
switch (attribute) {
|
|
1800
2430
|
case "inner_text":
|
|
1801
|
-
|
|
2431
|
+
state.value = await state.element.innerText();
|
|
1802
2432
|
break;
|
|
1803
2433
|
case "href":
|
|
1804
|
-
|
|
2434
|
+
state.value = await state.element.getAttribute("href");
|
|
1805
2435
|
break;
|
|
1806
2436
|
case "value":
|
|
1807
|
-
|
|
2437
|
+
state.value = await state.element.inputValue();
|
|
2438
|
+
break;
|
|
2439
|
+
case "text":
|
|
2440
|
+
state.value = await state.element.textContent();
|
|
1808
2441
|
break;
|
|
1809
2442
|
default:
|
|
1810
|
-
|
|
2443
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1811
2444
|
break;
|
|
1812
2445
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2446
|
+
if (options !== null) {
|
|
2447
|
+
if (options.regex && options.regex !== "") {
|
|
2448
|
+
// Construct a regex pattern from the provided string
|
|
2449
|
+
const regex = options.regex.slice(1, -1);
|
|
2450
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2451
|
+
const matches = state.value.match(regexPattern);
|
|
2452
|
+
if (matches) {
|
|
2453
|
+
let newValue = "";
|
|
2454
|
+
for (const match of matches) {
|
|
2455
|
+
newValue += match;
|
|
2456
|
+
}
|
|
2457
|
+
state.value = newValue;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2461
|
+
state.value = state.value.trim();
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
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
|
+
}
|
|
1816
2472
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
return info;
|
|
2473
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2474
|
+
return state.info;
|
|
1820
2475
|
}
|
|
1821
2476
|
catch (e) {
|
|
1822
|
-
|
|
1823
|
-
this.logger.error("extract failed " + info.log);
|
|
1824
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1825
|
-
info.screenshotPath = screenshotPath;
|
|
1826
|
-
Object.assign(e, { info: info });
|
|
1827
|
-
error = e;
|
|
1828
|
-
throw e;
|
|
2477
|
+
await _commandError(state, e, this);
|
|
1829
2478
|
}
|
|
1830
2479
|
finally {
|
|
1831
|
-
|
|
1832
|
-
this._reportToWorld(world, {
|
|
1833
|
-
element_name: selectors.element_name,
|
|
1834
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1835
|
-
variable: variable,
|
|
1836
|
-
value: info.value,
|
|
1837
|
-
text: "Extract attribute from element",
|
|
1838
|
-
screenshotId,
|
|
1839
|
-
result: error
|
|
1840
|
-
? {
|
|
1841
|
-
status: "FAILED",
|
|
1842
|
-
startTime,
|
|
1843
|
-
endTime,
|
|
1844
|
-
message: error?.message,
|
|
1845
|
-
}
|
|
1846
|
-
: {
|
|
1847
|
-
status: "PASSED",
|
|
1848
|
-
startTime,
|
|
1849
|
-
endTime,
|
|
1850
|
-
},
|
|
1851
|
-
info: info,
|
|
1852
|
-
});
|
|
2480
|
+
await _commandFinally(state, this);
|
|
1853
2481
|
}
|
|
1854
2482
|
}
|
|
1855
|
-
async
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
1866
2549
|
}
|
|
2550
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2551
|
+
return state.info;
|
|
1867
2552
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
if (options && options.timeout) {
|
|
1871
|
-
timeout = options.timeout;
|
|
2553
|
+
catch (e) {
|
|
2554
|
+
await _commandError(state, e, this);
|
|
1872
2555
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
+
}),
|
|
2942
|
+
};
|
|
2943
|
+
let errorCount = 0;
|
|
1886
2944
|
while (true) {
|
|
1887
2945
|
try {
|
|
1888
2946
|
let result = await this.context.api.request(request);
|
|
@@ -1915,6 +2973,12 @@ class StableBrowser {
|
|
|
1915
2973
|
emailUrl = url;
|
|
1916
2974
|
codeOrUrlFound = true;
|
|
1917
2975
|
}
|
|
2976
|
+
if (process.env.MODE === "executions") {
|
|
2977
|
+
const globalDataFile = "global_test_data.json";
|
|
2978
|
+
if (existsSync(globalDataFile)) {
|
|
2979
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
1918
2982
|
if (codeOrUrlFound) {
|
|
1919
2983
|
return { emailUrl, emailCode };
|
|
1920
2984
|
}
|
|
@@ -1926,7 +2990,8 @@ class StableBrowser {
|
|
|
1926
2990
|
catch (e) {
|
|
1927
2991
|
errorCount++;
|
|
1928
2992
|
if (errorCount > 3) {
|
|
1929
|
-
throw e;
|
|
2993
|
+
// throw e;
|
|
2994
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1930
2995
|
}
|
|
1931
2996
|
// ignore
|
|
1932
2997
|
}
|
|
@@ -1940,27 +3005,32 @@ class StableBrowser {
|
|
|
1940
3005
|
async _highlightElements(scope, css) {
|
|
1941
3006
|
try {
|
|
1942
3007
|
if (!scope) {
|
|
3008
|
+
// console.log(`Scope is not defined`);
|
|
1943
3009
|
return;
|
|
1944
3010
|
}
|
|
1945
3011
|
if (!css) {
|
|
1946
3012
|
scope
|
|
1947
3013
|
.evaluate((node) => {
|
|
1948
3014
|
if (node && node.style) {
|
|
1949
|
-
let
|
|
1950
|
-
|
|
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}`);
|
|
1951
3020
|
if (window) {
|
|
1952
3021
|
window.addEventListener("beforeunload", function (e) {
|
|
1953
|
-
node.style.
|
|
3022
|
+
node.style.outline = originalOutline;
|
|
1954
3023
|
});
|
|
1955
3024
|
}
|
|
1956
3025
|
setTimeout(function () {
|
|
1957
|
-
node.style.
|
|
3026
|
+
node.style.outline = originalOutline;
|
|
1958
3027
|
}, 2000);
|
|
1959
3028
|
}
|
|
1960
3029
|
})
|
|
1961
3030
|
.then(() => { })
|
|
1962
3031
|
.catch((e) => {
|
|
1963
3032
|
// ignore
|
|
3033
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1964
3034
|
});
|
|
1965
3035
|
}
|
|
1966
3036
|
else {
|
|
@@ -1976,17 +3046,18 @@ class StableBrowser {
|
|
|
1976
3046
|
if (!element.style) {
|
|
1977
3047
|
return;
|
|
1978
3048
|
}
|
|
1979
|
-
|
|
3049
|
+
let originalOutline = element.style.outline;
|
|
3050
|
+
element.__previousOutline = originalOutline;
|
|
1980
3051
|
// Set the new border to be red and 2px solid
|
|
1981
|
-
element.style.
|
|
3052
|
+
element.style.outline = "2px solid red";
|
|
1982
3053
|
if (window) {
|
|
1983
3054
|
window.addEventListener("beforeunload", function (e) {
|
|
1984
|
-
element.style.
|
|
3055
|
+
element.style.outline = originalOutline;
|
|
1985
3056
|
});
|
|
1986
3057
|
}
|
|
1987
3058
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1988
3059
|
setTimeout(function () {
|
|
1989
|
-
element.style.
|
|
3060
|
+
element.style.outline = originalOutline;
|
|
1990
3061
|
}, 2000);
|
|
1991
3062
|
}
|
|
1992
3063
|
return;
|
|
@@ -1994,6 +3065,7 @@ class StableBrowser {
|
|
|
1994
3065
|
.then(() => { })
|
|
1995
3066
|
.catch((e) => {
|
|
1996
3067
|
// ignore
|
|
3068
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1997
3069
|
});
|
|
1998
3070
|
}
|
|
1999
3071
|
}
|
|
@@ -2001,8 +3073,49 @@ class StableBrowser {
|
|
|
2001
3073
|
console.debug(error);
|
|
2002
3074
|
}
|
|
2003
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
|
+
*/
|
|
2004
3118
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2005
|
-
const startTime = Date.now();
|
|
2006
3119
|
let error = null;
|
|
2007
3120
|
let screenshotId = null;
|
|
2008
3121
|
let screenshotPath = null;
|
|
@@ -2016,159 +3129,534 @@ class StableBrowser {
|
|
|
2016
3129
|
pathPart = newValue;
|
|
2017
3130
|
}
|
|
2018
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
|
+
};
|
|
2019
3146
|
try {
|
|
3147
|
+
await _preCommand(state, this);
|
|
3148
|
+
state.info.text = queryText;
|
|
2020
3149
|
for (let i = 0; i < 30; i++) {
|
|
2021
3150
|
const url = await this.page.url();
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
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`);
|
|
2025
3426
|
}
|
|
2026
3427
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2027
3428
|
continue;
|
|
2028
3429
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
+
}
|
|
2031
3447
|
}
|
|
2032
3448
|
}
|
|
2033
3449
|
catch (e) {
|
|
2034
|
-
|
|
2035
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2036
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2037
|
-
info.screenshotPath = screenshotPath;
|
|
2038
|
-
Object.assign(e, { info: info });
|
|
2039
|
-
error = e;
|
|
2040
|
-
throw e;
|
|
3450
|
+
await _commandError(state, e, this);
|
|
2041
3451
|
}
|
|
2042
3452
|
finally {
|
|
2043
|
-
|
|
2044
|
-
this._reportToWorld(world, {
|
|
2045
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2046
|
-
text: "Verify page path",
|
|
2047
|
-
screenshotId,
|
|
2048
|
-
result: error
|
|
2049
|
-
? {
|
|
2050
|
-
status: "FAILED",
|
|
2051
|
-
startTime,
|
|
2052
|
-
endTime,
|
|
2053
|
-
message: error?.message,
|
|
2054
|
-
}
|
|
2055
|
-
: {
|
|
2056
|
-
status: "PASSED",
|
|
2057
|
-
startTime,
|
|
2058
|
-
endTime,
|
|
2059
|
-
},
|
|
2060
|
-
info: info,
|
|
2061
|
-
});
|
|
3453
|
+
await _commandFinally(state, this);
|
|
2062
3454
|
}
|
|
2063
3455
|
}
|
|
2064
|
-
async
|
|
3456
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2065
3457
|
text = unEscapeString(text);
|
|
2066
|
-
const
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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);
|
|
2071
3475
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2072
|
-
const info = {};
|
|
2073
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2074
|
-
info.operation = "verifyTextExistInPage";
|
|
2075
3476
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2076
3477
|
if (newValue !== text) {
|
|
2077
3478
|
this.logger.info(text + "=" + newValue);
|
|
2078
3479
|
text = newValue;
|
|
2079
3480
|
}
|
|
2080
|
-
|
|
2081
|
-
let
|
|
2082
|
-
|
|
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;
|
|
2083
3546
|
try {
|
|
3547
|
+
await _preCommand(state, this);
|
|
3548
|
+
state.info.text = textToVerify;
|
|
3549
|
+
let resultWithElementsFound = {
|
|
3550
|
+
length: 0,
|
|
3551
|
+
};
|
|
2084
3552
|
while (true) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
|
|
2091
|
-
result.frame = frames[i];
|
|
2092
|
-
results.push(result);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
else if (numberAlternatives.number) {
|
|
2096
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2097
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
|
|
2098
|
-
result.frame = frames[i];
|
|
2099
|
-
results.push(result);
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
else {
|
|
2103
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
|
|
2104
|
-
result.frame = frames[i];
|
|
2105
|
-
results.push(result);
|
|
2106
|
-
}
|
|
3553
|
+
try {
|
|
3554
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3555
|
+
}
|
|
3556
|
+
catch (error) {
|
|
3557
|
+
// ignore
|
|
2107
3558
|
}
|
|
2108
|
-
info.results = results;
|
|
2109
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2110
3559
|
if (resultWithElementsFound.length === 0) {
|
|
2111
|
-
if (Date.now() - startTime > timeout) {
|
|
2112
|
-
throw new Error(`Text ${
|
|
3560
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3561
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2113
3562
|
}
|
|
2114
3563
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2115
3564
|
continue;
|
|
2116
3565
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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
|
+
css = css + " >> " + climbXpath;
|
|
3590
|
+
const count = await frame.locator(css).count();
|
|
3591
|
+
for (let j = 0; j < count; j++) {
|
|
3592
|
+
const continer = await frame.locator(css).nth(j);
|
|
3593
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
3594
|
+
if (result.elementCount > 0) {
|
|
3595
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
3596
|
+
await this._highlightElements(frame, dataAttribute);
|
|
3597
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
3598
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
3599
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
3600
|
+
// this._highlightElements(frame, dataAttribute)
|
|
3601
|
+
// .then(async () => {
|
|
3602
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3603
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
3604
|
+
// () => {}
|
|
3605
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
3606
|
+
// );
|
|
3607
|
+
// })
|
|
3608
|
+
// .catch(e);
|
|
3609
|
+
// }
|
|
3610
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
3611
|
+
const element = await frame.locator(dataAttribute).first();
|
|
3612
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3613
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
3614
|
+
if (element) {
|
|
3615
|
+
await this.scrollIfNeeded(element, state.info);
|
|
3616
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
3617
|
+
}
|
|
3618
|
+
await _screenshot(state, this);
|
|
3619
|
+
return state.info;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
2125
3622
|
}
|
|
2126
3623
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
3624
|
+
catch (error) {
|
|
3625
|
+
console.error(error);
|
|
3626
|
+
}
|
|
2129
3627
|
}
|
|
2130
3628
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2131
3629
|
}
|
|
2132
3630
|
catch (e) {
|
|
2133
|
-
|
|
2134
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2135
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2136
|
-
info.screenshotPath = screenshotPath;
|
|
2137
|
-
Object.assign(e, { info: info });
|
|
2138
|
-
error = e;
|
|
2139
|
-
throw e;
|
|
3631
|
+
await _commandError(state, e, this);
|
|
2140
3632
|
}
|
|
2141
3633
|
finally {
|
|
2142
|
-
|
|
2143
|
-
this._reportToWorld(world, {
|
|
2144
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2145
|
-
text: "Verify text exists in page",
|
|
2146
|
-
screenshotId,
|
|
2147
|
-
result: error
|
|
2148
|
-
? {
|
|
2149
|
-
status: "FAILED",
|
|
2150
|
-
startTime,
|
|
2151
|
-
endTime,
|
|
2152
|
-
message: error?.message,
|
|
2153
|
-
}
|
|
2154
|
-
: {
|
|
2155
|
-
status: "PASSED",
|
|
2156
|
-
startTime,
|
|
2157
|
-
endTime,
|
|
2158
|
-
},
|
|
2159
|
-
info: info,
|
|
2160
|
-
});
|
|
3634
|
+
await _commandFinally(state, this);
|
|
2161
3635
|
}
|
|
2162
3636
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
3637
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
3638
|
+
const frames = this.page.frames();
|
|
3639
|
+
let results = [];
|
|
3640
|
+
let ignoreCase = false;
|
|
3641
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3642
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
3643
|
+
result.frame = frames[i];
|
|
3644
|
+
const climbArray = [];
|
|
3645
|
+
for (let i = 0; i < climb; i++) {
|
|
3646
|
+
climbArray.push("..");
|
|
3647
|
+
}
|
|
3648
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
3649
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
3650
|
+
const count = await frames[i].locator(newLocator).count();
|
|
3651
|
+
if (count > 0) {
|
|
3652
|
+
result.elementCount = count;
|
|
3653
|
+
result.locator = newLocator;
|
|
3654
|
+
results.push(result);
|
|
3655
|
+
}
|
|
2170
3656
|
}
|
|
2171
|
-
|
|
3657
|
+
// state.info.results = results;
|
|
3658
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3659
|
+
return resultWithElementsFound;
|
|
2172
3660
|
}
|
|
2173
3661
|
async visualVerification(text, options = {}, world = null) {
|
|
2174
3662
|
const startTime = Date.now();
|
|
@@ -2184,14 +3672,17 @@ class StableBrowser {
|
|
|
2184
3672
|
throw new Error("TOKEN is not set");
|
|
2185
3673
|
}
|
|
2186
3674
|
try {
|
|
2187
|
-
let serviceUrl =
|
|
3675
|
+
let serviceUrl = _getServerUrl();
|
|
2188
3676
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2189
3677
|
info.screenshotPath = screenshotPath;
|
|
2190
3678
|
const screenshot = await this.takeScreenshot();
|
|
2191
|
-
|
|
2192
|
-
method: "
|
|
3679
|
+
let request = {
|
|
3680
|
+
method: "post",
|
|
3681
|
+
maxBodyLength: Infinity,
|
|
2193
3682
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2194
3683
|
headers: {
|
|
3684
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3685
|
+
"x-source": "aaa",
|
|
2195
3686
|
"Content-Type": "application/json",
|
|
2196
3687
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2197
3688
|
},
|
|
@@ -2200,7 +3691,7 @@ class StableBrowser {
|
|
|
2200
3691
|
screenshot: screenshot,
|
|
2201
3692
|
}),
|
|
2202
3693
|
};
|
|
2203
|
-
|
|
3694
|
+
const result = await axios.request(request);
|
|
2204
3695
|
if (result.data.status !== true) {
|
|
2205
3696
|
throw new Error("Visual validation failed");
|
|
2206
3697
|
}
|
|
@@ -2220,13 +3711,15 @@ class StableBrowser {
|
|
|
2220
3711
|
info.screenshotPath = screenshotPath;
|
|
2221
3712
|
Object.assign(e, { info: info });
|
|
2222
3713
|
error = e;
|
|
2223
|
-
throw e;
|
|
3714
|
+
// throw e;
|
|
3715
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2224
3716
|
}
|
|
2225
3717
|
finally {
|
|
2226
3718
|
const endTime = Date.now();
|
|
2227
|
-
|
|
3719
|
+
_reportToWorld(world, {
|
|
2228
3720
|
type: Types.VERIFY_VISUAL,
|
|
2229
3721
|
text: "Visual verification",
|
|
3722
|
+
_text: "Visual verification of " + text,
|
|
2230
3723
|
screenshotId,
|
|
2231
3724
|
result: error
|
|
2232
3725
|
? {
|
|
@@ -2272,6 +3765,7 @@ class StableBrowser {
|
|
|
2272
3765
|
let screenshotPath = null;
|
|
2273
3766
|
const info = {};
|
|
2274
3767
|
info.log = "";
|
|
3768
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2275
3769
|
info.operation = "getTableData";
|
|
2276
3770
|
info.selectors = selectors;
|
|
2277
3771
|
try {
|
|
@@ -2287,11 +3781,12 @@ class StableBrowser {
|
|
|
2287
3781
|
info.screenshotPath = screenshotPath;
|
|
2288
3782
|
Object.assign(e, { info: info });
|
|
2289
3783
|
error = e;
|
|
2290
|
-
throw e;
|
|
3784
|
+
// throw e;
|
|
3785
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2291
3786
|
}
|
|
2292
3787
|
finally {
|
|
2293
3788
|
const endTime = Date.now();
|
|
2294
|
-
|
|
3789
|
+
_reportToWorld(world, {
|
|
2295
3790
|
element_name: selectors.element_name,
|
|
2296
3791
|
type: Types.GET_TABLE_DATA,
|
|
2297
3792
|
text: "Get table data",
|
|
@@ -2346,7 +3841,7 @@ class StableBrowser {
|
|
|
2346
3841
|
info.operation = "analyzeTable";
|
|
2347
3842
|
info.selectors = selectors;
|
|
2348
3843
|
info.query = query;
|
|
2349
|
-
query =
|
|
3844
|
+
query = _fixUsingParams(query, _params);
|
|
2350
3845
|
info.query_fixed = query;
|
|
2351
3846
|
info.operator = operator;
|
|
2352
3847
|
info.value = value;
|
|
@@ -2452,11 +3947,12 @@ class StableBrowser {
|
|
|
2452
3947
|
info.screenshotPath = screenshotPath;
|
|
2453
3948
|
Object.assign(e, { info: info });
|
|
2454
3949
|
error = e;
|
|
2455
|
-
throw e;
|
|
3950
|
+
// throw e;
|
|
3951
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2456
3952
|
}
|
|
2457
3953
|
finally {
|
|
2458
3954
|
const endTime = Date.now();
|
|
2459
|
-
|
|
3955
|
+
_reportToWorld(world, {
|
|
2460
3956
|
element_name: selectors.element_name,
|
|
2461
3957
|
type: Types.ANALYZE_TABLE,
|
|
2462
3958
|
text: "Analyze table",
|
|
@@ -2477,8 +3973,51 @@ class StableBrowser {
|
|
|
2477
3973
|
});
|
|
2478
3974
|
}
|
|
2479
3975
|
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3978
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3979
|
+
* @param options - Optional configuration object
|
|
3980
|
+
* @param world - Optional world context
|
|
3981
|
+
* @returns Promise that resolves after the specified duration
|
|
3982
|
+
*/
|
|
3983
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3984
|
+
const state = {
|
|
3985
|
+
duration,
|
|
3986
|
+
options,
|
|
3987
|
+
world,
|
|
3988
|
+
locate: false,
|
|
3989
|
+
scroll: false,
|
|
3990
|
+
screenshot: false,
|
|
3991
|
+
highlight: false,
|
|
3992
|
+
type: Types.SLEEP,
|
|
3993
|
+
text: `Sleep for ${duration} ms`,
|
|
3994
|
+
_text: `Sleep for ${duration} ms`,
|
|
3995
|
+
operation: "sleep",
|
|
3996
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3997
|
+
};
|
|
3998
|
+
try {
|
|
3999
|
+
await _preCommand(state, this);
|
|
4000
|
+
if (duration < 0) {
|
|
4001
|
+
throw new Error("Sleep duration cannot be negative");
|
|
4002
|
+
}
|
|
4003
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
4004
|
+
return state.info;
|
|
4005
|
+
}
|
|
4006
|
+
catch (e) {
|
|
4007
|
+
await _commandError(state, e, this);
|
|
4008
|
+
}
|
|
4009
|
+
finally {
|
|
4010
|
+
await _commandFinally(state, this);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
2480
4013
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2481
|
-
|
|
4014
|
+
try {
|
|
4015
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
4016
|
+
}
|
|
4017
|
+
catch (error) {
|
|
4018
|
+
this.logger.debug(error);
|
|
4019
|
+
throw error;
|
|
4020
|
+
}
|
|
2482
4021
|
}
|
|
2483
4022
|
_getLoadTimeout(options) {
|
|
2484
4023
|
let timeout = 15000;
|
|
@@ -2490,7 +4029,54 @@ class StableBrowser {
|
|
|
2490
4029
|
}
|
|
2491
4030
|
return timeout;
|
|
2492
4031
|
}
|
|
4032
|
+
_getFindElementTimeout(options) {
|
|
4033
|
+
if (options && options.timeout) {
|
|
4034
|
+
return options.timeout;
|
|
4035
|
+
}
|
|
4036
|
+
if (this.configuration.find_element_timeout) {
|
|
4037
|
+
return this.configuration.find_element_timeout;
|
|
4038
|
+
}
|
|
4039
|
+
return 30000;
|
|
4040
|
+
}
|
|
4041
|
+
async saveStoreState(path = null, world = null) {
|
|
4042
|
+
const storageState = await this.page.context().storageState();
|
|
4043
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4044
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
4045
|
+
if (path) {
|
|
4046
|
+
// save { storageState: storageState } into the path
|
|
4047
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
4048
|
+
}
|
|
4049
|
+
else {
|
|
4050
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
async restoreSaveState(path = null, world = null) {
|
|
4054
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
4055
|
+
await refreshBrowser(this, path, world);
|
|
4056
|
+
this.registerEventListeners(this.context);
|
|
4057
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
4058
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
4059
|
+
if (this.onRestoreSaveState) {
|
|
4060
|
+
this.onRestoreSaveState(path);
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
2493
4063
|
async waitForPageLoad(options = {}, world = null) {
|
|
4064
|
+
// try {
|
|
4065
|
+
// let currentPagePath = null;
|
|
4066
|
+
// currentPagePath = new URL(this.page.url()).pathname;
|
|
4067
|
+
// if (this.latestPagePath) {
|
|
4068
|
+
// // get the currect page path and compare with the latest page path
|
|
4069
|
+
// if (this.latestPagePath === currentPagePath) {
|
|
4070
|
+
// // if the page path is the same, do not wait for page load
|
|
4071
|
+
// console.log("No page change: " + currentPagePath);
|
|
4072
|
+
// return;
|
|
4073
|
+
// }
|
|
4074
|
+
// }
|
|
4075
|
+
// this.latestPagePath = currentPagePath;
|
|
4076
|
+
// } catch (e) {
|
|
4077
|
+
// console.debug("Error getting current page path: ", e);
|
|
4078
|
+
// }
|
|
4079
|
+
//console.log("Waiting for page load");
|
|
2494
4080
|
let timeout = this._getLoadTimeout(options);
|
|
2495
4081
|
const promiseArray = [];
|
|
2496
4082
|
// let waitForNetworkIdle = true;
|
|
@@ -2523,13 +4109,15 @@ class StableBrowser {
|
|
|
2523
4109
|
else if (e.label === "domcontentloaded") {
|
|
2524
4110
|
console.log("waited for the domcontent loaded timeout");
|
|
2525
4111
|
}
|
|
2526
|
-
console.log(".");
|
|
2527
4112
|
}
|
|
2528
4113
|
finally {
|
|
2529
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4114
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4115
|
+
if (options && !options.noSleep) {
|
|
4116
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4117
|
+
}
|
|
2530
4118
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2531
4119
|
const endTime = Date.now();
|
|
2532
|
-
|
|
4120
|
+
_reportToWorld(world, {
|
|
2533
4121
|
type: Types.GET_PAGE_STATUS,
|
|
2534
4122
|
text: "Wait for page load",
|
|
2535
4123
|
screenshotId,
|
|
@@ -2549,40 +4137,138 @@ class StableBrowser {
|
|
|
2549
4137
|
}
|
|
2550
4138
|
}
|
|
2551
4139
|
async closePage(options = {}, world = null) {
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
4140
|
+
const state = {
|
|
4141
|
+
options,
|
|
4142
|
+
world,
|
|
4143
|
+
locate: false,
|
|
4144
|
+
scroll: false,
|
|
4145
|
+
highlight: false,
|
|
4146
|
+
type: Types.CLOSE_PAGE,
|
|
4147
|
+
text: `Close page`,
|
|
4148
|
+
_text: `Close the page`,
|
|
4149
|
+
operation: "closePage",
|
|
4150
|
+
log: "***** close page *****\n",
|
|
4151
|
+
throwError: false,
|
|
4152
|
+
};
|
|
2557
4153
|
try {
|
|
4154
|
+
await _preCommand(state, this);
|
|
2558
4155
|
await this.page.close();
|
|
2559
4156
|
}
|
|
2560
4157
|
catch (e) {
|
|
2561
|
-
|
|
4158
|
+
await _commandError(state, e, this);
|
|
2562
4159
|
}
|
|
2563
4160
|
finally {
|
|
2564
|
-
await
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
4161
|
+
await _commandFinally(state, this);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
4165
|
+
let operation = null;
|
|
4166
|
+
if (!options || !options.operation) {
|
|
4167
|
+
throw new Error("operation is not defined");
|
|
4168
|
+
}
|
|
4169
|
+
operation = options.operation;
|
|
4170
|
+
// validate operation is one of the supported operations
|
|
4171
|
+
if (operation != "click" && operation != "hover+click" && operation != "hover") {
|
|
4172
|
+
throw new Error("operation is not supported");
|
|
4173
|
+
}
|
|
4174
|
+
const state = {
|
|
4175
|
+
options,
|
|
4176
|
+
world,
|
|
4177
|
+
locate: false,
|
|
4178
|
+
scroll: false,
|
|
4179
|
+
highlight: false,
|
|
4180
|
+
type: Types.TABLE_OPERATION,
|
|
4181
|
+
text: `Table operation`,
|
|
4182
|
+
_text: `Table ${operation} operation`,
|
|
4183
|
+
operation: operation,
|
|
4184
|
+
log: "***** Table operation *****\n",
|
|
4185
|
+
};
|
|
4186
|
+
const timeout = this._getFindElementTimeout(options);
|
|
4187
|
+
try {
|
|
4188
|
+
await _preCommand(state, this);
|
|
4189
|
+
const start = Date.now();
|
|
4190
|
+
let cellArea = null;
|
|
4191
|
+
while (true) {
|
|
4192
|
+
try {
|
|
4193
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
4194
|
+
if (cellArea) {
|
|
4195
|
+
break;
|
|
2577
4196
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
4197
|
+
}
|
|
4198
|
+
catch (e) {
|
|
4199
|
+
// ignore
|
|
4200
|
+
}
|
|
4201
|
+
if (Date.now() - start > timeout) {
|
|
4202
|
+
throw new Error(`Cell not found in table`);
|
|
4203
|
+
}
|
|
4204
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
4205
|
+
}
|
|
4206
|
+
switch (operation) {
|
|
4207
|
+
case "click":
|
|
4208
|
+
if (!options.css) {
|
|
4209
|
+
// will click in the center of the cell
|
|
4210
|
+
let xOffset = 0;
|
|
4211
|
+
let yOffset = 0;
|
|
4212
|
+
if (options.xOffset) {
|
|
4213
|
+
xOffset = options.xOffset;
|
|
4214
|
+
}
|
|
4215
|
+
if (options.yOffset) {
|
|
4216
|
+
yOffset = options.yOffset;
|
|
4217
|
+
}
|
|
4218
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
4219
|
+
}
|
|
4220
|
+
else {
|
|
4221
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4222
|
+
if (results.length === 0) {
|
|
4223
|
+
throw new Error(`Element not found in cell area`);
|
|
4224
|
+
}
|
|
4225
|
+
state.element = results[0];
|
|
4226
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
4227
|
+
}
|
|
4228
|
+
break;
|
|
4229
|
+
case "hover+click":
|
|
4230
|
+
if (!options.css) {
|
|
4231
|
+
throw new Error("css is not defined");
|
|
4232
|
+
}
|
|
4233
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
4234
|
+
if (results.length === 0) {
|
|
4235
|
+
throw new Error(`Element not found in cell area`);
|
|
4236
|
+
}
|
|
4237
|
+
state.element = results[0];
|
|
4238
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
4239
|
+
break;
|
|
4240
|
+
case "hover":
|
|
4241
|
+
if (!options.css) {
|
|
4242
|
+
throw new Error("css is not defined");
|
|
4243
|
+
}
|
|
4244
|
+
const result1 = await findElementsInArea(options.css, cellArea, this, options);
|
|
4245
|
+
if (result1.length === 0) {
|
|
4246
|
+
throw new Error(`Element not found in cell area`);
|
|
4247
|
+
}
|
|
4248
|
+
state.element = result1[0];
|
|
4249
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
4250
|
+
break;
|
|
4251
|
+
default:
|
|
4252
|
+
throw new Error("operation is not supported");
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
catch (e) {
|
|
4256
|
+
await _commandError(state, e, this);
|
|
4257
|
+
}
|
|
4258
|
+
finally {
|
|
4259
|
+
await _commandFinally(state, this);
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
saveTestDataAsGlobal(options, world) {
|
|
4263
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
4264
|
+
if (process.env.MODE === "executions") {
|
|
4265
|
+
const globalDataFile = path.join(this.project_path, "global_test_data.json");
|
|
4266
|
+
fs.copyFileSync(dataFile, globalDataFile);
|
|
4267
|
+
this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
|
|
4268
|
+
return;
|
|
2585
4269
|
}
|
|
4270
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4271
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2586
4272
|
}
|
|
2587
4273
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2588
4274
|
const startTime = Date.now();
|
|
@@ -2600,15 +4286,16 @@ class StableBrowser {
|
|
|
2600
4286
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2601
4287
|
}
|
|
2602
4288
|
catch (e) {
|
|
2603
|
-
|
|
4289
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2604
4290
|
}
|
|
2605
4291
|
finally {
|
|
2606
4292
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2607
4293
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2608
4294
|
const endTime = Date.now();
|
|
2609
|
-
|
|
4295
|
+
_reportToWorld(world, {
|
|
2610
4296
|
type: Types.SET_VIEWPORT,
|
|
2611
4297
|
text: "set viewport size to " + width + "x" + hight,
|
|
4298
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2612
4299
|
screenshotId,
|
|
2613
4300
|
result: error
|
|
2614
4301
|
? {
|
|
@@ -2636,13 +4323,13 @@ class StableBrowser {
|
|
|
2636
4323
|
await this.page.reload();
|
|
2637
4324
|
}
|
|
2638
4325
|
catch (e) {
|
|
2639
|
-
|
|
4326
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2640
4327
|
}
|
|
2641
4328
|
finally {
|
|
2642
4329
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2643
4330
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2644
4331
|
const endTime = Date.now();
|
|
2645
|
-
|
|
4332
|
+
_reportToWorld(world, {
|
|
2646
4333
|
type: Types.GET_PAGE_STATUS,
|
|
2647
4334
|
text: "page relaod",
|
|
2648
4335
|
screenshotId,
|
|
@@ -2678,11 +4365,254 @@ class StableBrowser {
|
|
|
2678
4365
|
console.log("#-#");
|
|
2679
4366
|
}
|
|
2680
4367
|
}
|
|
2681
|
-
|
|
2682
|
-
if (
|
|
2683
|
-
|
|
4368
|
+
async beforeScenario(world, scenario) {
|
|
4369
|
+
if (world && world.attach) {
|
|
4370
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4371
|
+
}
|
|
4372
|
+
this.context.loadedRoutes = null;
|
|
4373
|
+
this.beforeScenarioCalled = true;
|
|
4374
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
4375
|
+
this.scenarioName = scenario.pickle.name;
|
|
4376
|
+
}
|
|
4377
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
4378
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
4379
|
+
}
|
|
4380
|
+
if (this.context) {
|
|
4381
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
4382
|
+
}
|
|
4383
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
4384
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
4385
|
+
// check if @global_test_data tag is present
|
|
4386
|
+
if (this.tags.includes("@global_test_data")) {
|
|
4387
|
+
this.saveTestDataAsGlobal({}, world);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
// update test data based on feature/scenario
|
|
4391
|
+
let envName = null;
|
|
4392
|
+
if (this.context && this.context.environment) {
|
|
4393
|
+
envName = this.context.environment.name;
|
|
4394
|
+
}
|
|
4395
|
+
if (!process.env.TEMP_RUN) {
|
|
4396
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
4397
|
+
}
|
|
4398
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
4399
|
+
if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
|
|
4400
|
+
this.trace = true;
|
|
4401
|
+
const traceFolder = path.join(this.context.reportFolder, "trace");
|
|
4402
|
+
if (!fs.existsSync(traceFolder)) {
|
|
4403
|
+
fs.mkdirSync(traceFolder, { recursive: true });
|
|
4404
|
+
}
|
|
4405
|
+
this.traceFolder = traceFolder;
|
|
4406
|
+
await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
async afterScenario(world, scenario) {
|
|
4410
|
+
const id = scenario.testCaseStartedId;
|
|
4411
|
+
if (this.trace) {
|
|
4412
|
+
await this.context.playContext.tracing.stop({
|
|
4413
|
+
path: path.join(this.traceFolder, `trace-${id}.zip`),
|
|
4414
|
+
});
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
async beforeStep(world, step) {
|
|
4418
|
+
if (step?.pickleStep && this.trace) {
|
|
4419
|
+
await this.context.playContext.tracing.group(`Step: ${step.pickleStep.text}`);
|
|
4420
|
+
}
|
|
4421
|
+
this.stepTags = [];
|
|
4422
|
+
if (!this.beforeScenarioCalled) {
|
|
4423
|
+
this.beforeScenario(world, step);
|
|
4424
|
+
this.context.loadedRoutes = null;
|
|
4425
|
+
}
|
|
4426
|
+
if (this.stepIndex === undefined) {
|
|
4427
|
+
this.stepIndex = 0;
|
|
4428
|
+
}
|
|
4429
|
+
else {
|
|
4430
|
+
this.stepIndex++;
|
|
4431
|
+
}
|
|
4432
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4433
|
+
this.stepName = step.pickleStep.text;
|
|
4434
|
+
let printableStepName = this.stepName;
|
|
4435
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4436
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4437
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4438
|
+
});
|
|
4439
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4440
|
+
}
|
|
4441
|
+
else if (step && step.text) {
|
|
4442
|
+
this.stepName = step.text;
|
|
4443
|
+
}
|
|
4444
|
+
else {
|
|
4445
|
+
this.stepName = "step " + this.stepIndex;
|
|
4446
|
+
}
|
|
4447
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4448
|
+
if (this.context.browserObject.context) {
|
|
4449
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
if (this.initSnapshotTaken === false) {
|
|
4453
|
+
this.initSnapshotTaken = true;
|
|
4454
|
+
if (world &&
|
|
4455
|
+
world.attach &&
|
|
4456
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4457
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4458
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4459
|
+
if (snapshot) {
|
|
4460
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
this.context.routeResults = null;
|
|
4465
|
+
this.context.loadedRoutes = null;
|
|
4466
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4467
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4468
|
+
this.inStepReport = false;
|
|
4469
|
+
}
|
|
4470
|
+
setStepTags(tags) {
|
|
4471
|
+
this.stepTags = tags;
|
|
4472
|
+
}
|
|
4473
|
+
async getAriaSnapshot() {
|
|
4474
|
+
try {
|
|
4475
|
+
// find the page url
|
|
4476
|
+
const url = await this.page.url();
|
|
4477
|
+
// extract the path from the url
|
|
4478
|
+
const path = new URL(url).pathname;
|
|
4479
|
+
// get the page title
|
|
4480
|
+
const title = await this.page.title();
|
|
4481
|
+
// go over other frams
|
|
4482
|
+
const frames = this.page.frames();
|
|
4483
|
+
const snapshots = [];
|
|
4484
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4485
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4486
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4487
|
+
const frame = frames[i];
|
|
4488
|
+
try {
|
|
4489
|
+
// Ensure frame is attached and has body
|
|
4490
|
+
const body = frame.locator("body");
|
|
4491
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4492
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4493
|
+
if (!snapshot) {
|
|
4494
|
+
continue;
|
|
4495
|
+
}
|
|
4496
|
+
content.push(`- frame: ${i}`);
|
|
4497
|
+
content.push(snapshot);
|
|
4498
|
+
}
|
|
4499
|
+
catch (innerErr) {
|
|
4500
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4501
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
return content.join("\n");
|
|
4505
|
+
}
|
|
4506
|
+
catch (e) {
|
|
4507
|
+
console.log("Error in getAriaSnapshot");
|
|
4508
|
+
//console.debug(e);
|
|
4509
|
+
}
|
|
4510
|
+
return null;
|
|
4511
|
+
}
|
|
4512
|
+
/**
|
|
4513
|
+
* Sends command with custom payload to report.
|
|
4514
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4515
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4516
|
+
* @param content - Content of the command to be shown in the report.
|
|
4517
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4518
|
+
* @param world - Optional world context.
|
|
4519
|
+
* @public
|
|
4520
|
+
*/
|
|
4521
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4522
|
+
const state = {
|
|
4523
|
+
options,
|
|
4524
|
+
world,
|
|
4525
|
+
locate: false,
|
|
4526
|
+
scroll: false,
|
|
4527
|
+
screenshot: options.screenshot ?? false,
|
|
4528
|
+
highlight: options.highlight ?? false,
|
|
4529
|
+
type: Types.REPORT_COMMAND,
|
|
4530
|
+
text: commandText,
|
|
4531
|
+
_text: commandText,
|
|
4532
|
+
operation: "report_command",
|
|
4533
|
+
log: "***** " + commandText + " *****\n",
|
|
4534
|
+
};
|
|
4535
|
+
try {
|
|
4536
|
+
await _preCommand(state, this);
|
|
4537
|
+
const payload = {
|
|
4538
|
+
type: options.type ?? "text",
|
|
4539
|
+
content: content,
|
|
4540
|
+
screenshotId: null,
|
|
4541
|
+
};
|
|
4542
|
+
state.payload = payload;
|
|
4543
|
+
if (commandStatus === "FAILED") {
|
|
4544
|
+
state.throwError = true;
|
|
4545
|
+
throw new Error(commandText);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
catch (e) {
|
|
4549
|
+
await _commandError(state, e, this);
|
|
4550
|
+
}
|
|
4551
|
+
finally {
|
|
4552
|
+
await _commandFinally(state, this);
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
async afterStep(world, step, result) {
|
|
4556
|
+
this.stepName = null;
|
|
4557
|
+
if (this.context) {
|
|
4558
|
+
this.context.examplesRow = null;
|
|
4559
|
+
}
|
|
4560
|
+
if (!this.inStepReport) {
|
|
4561
|
+
// check the step result
|
|
4562
|
+
if (result && result.status === "FAILED" && world && world.attach) {
|
|
4563
|
+
await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
if (world &&
|
|
4567
|
+
world.attach &&
|
|
4568
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4569
|
+
!this.fastMode &&
|
|
4570
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4571
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4572
|
+
if (snapshot) {
|
|
4573
|
+
const obj = {};
|
|
4574
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4578
|
+
if (this.context.routeResults) {
|
|
4579
|
+
if (world && world.attach) {
|
|
4580
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
if (!process.env.TEMP_RUN) {
|
|
4584
|
+
const state = {
|
|
4585
|
+
world,
|
|
4586
|
+
locate: false,
|
|
4587
|
+
scroll: false,
|
|
4588
|
+
screenshot: true,
|
|
4589
|
+
highlight: true,
|
|
4590
|
+
type: Types.STEP_COMPLETE,
|
|
4591
|
+
text: "end of scenario",
|
|
4592
|
+
_text: "end of scenario",
|
|
4593
|
+
operation: "step_complete",
|
|
4594
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4595
|
+
};
|
|
4596
|
+
try {
|
|
4597
|
+
await _preCommand(state, this);
|
|
4598
|
+
}
|
|
4599
|
+
catch (e) {
|
|
4600
|
+
await _commandError(state, e, this);
|
|
4601
|
+
}
|
|
4602
|
+
finally {
|
|
4603
|
+
await _commandFinally(state, this);
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
networkAfterStep(this.stepName, this.context);
|
|
4607
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4608
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4609
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4610
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
if (this.trace) {
|
|
4614
|
+
await this.context.playContext.tracing.groupEnd();
|
|
2684
4615
|
}
|
|
2685
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2686
4616
|
}
|
|
2687
4617
|
}
|
|
2688
4618
|
function createTimedPromise(promise, label) {
|
|
@@ -2690,156 +4620,5 @@ function createTimedPromise(promise, label) {
|
|
|
2690
4620
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2691
4621
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2692
4622
|
}
|
|
2693
|
-
const KEYBOARD_EVENTS = [
|
|
2694
|
-
"ALT",
|
|
2695
|
-
"AltGraph",
|
|
2696
|
-
"CapsLock",
|
|
2697
|
-
"Control",
|
|
2698
|
-
"Fn",
|
|
2699
|
-
"FnLock",
|
|
2700
|
-
"Hyper",
|
|
2701
|
-
"Meta",
|
|
2702
|
-
"NumLock",
|
|
2703
|
-
"ScrollLock",
|
|
2704
|
-
"Shift",
|
|
2705
|
-
"Super",
|
|
2706
|
-
"Symbol",
|
|
2707
|
-
"SymbolLock",
|
|
2708
|
-
"Enter",
|
|
2709
|
-
"Tab",
|
|
2710
|
-
"ArrowDown",
|
|
2711
|
-
"ArrowLeft",
|
|
2712
|
-
"ArrowRight",
|
|
2713
|
-
"ArrowUp",
|
|
2714
|
-
"End",
|
|
2715
|
-
"Home",
|
|
2716
|
-
"PageDown",
|
|
2717
|
-
"PageUp",
|
|
2718
|
-
"Backspace",
|
|
2719
|
-
"Clear",
|
|
2720
|
-
"Copy",
|
|
2721
|
-
"CrSel",
|
|
2722
|
-
"Cut",
|
|
2723
|
-
"Delete",
|
|
2724
|
-
"EraseEof",
|
|
2725
|
-
"ExSel",
|
|
2726
|
-
"Insert",
|
|
2727
|
-
"Paste",
|
|
2728
|
-
"Redo",
|
|
2729
|
-
"Undo",
|
|
2730
|
-
"Accept",
|
|
2731
|
-
"Again",
|
|
2732
|
-
"Attn",
|
|
2733
|
-
"Cancel",
|
|
2734
|
-
"ContextMenu",
|
|
2735
|
-
"Escape",
|
|
2736
|
-
"Execute",
|
|
2737
|
-
"Find",
|
|
2738
|
-
"Finish",
|
|
2739
|
-
"Help",
|
|
2740
|
-
"Pause",
|
|
2741
|
-
"Play",
|
|
2742
|
-
"Props",
|
|
2743
|
-
"Select",
|
|
2744
|
-
"ZoomIn",
|
|
2745
|
-
"ZoomOut",
|
|
2746
|
-
"BrightnessDown",
|
|
2747
|
-
"BrightnessUp",
|
|
2748
|
-
"Eject",
|
|
2749
|
-
"LogOff",
|
|
2750
|
-
"Power",
|
|
2751
|
-
"PowerOff",
|
|
2752
|
-
"PrintScreen",
|
|
2753
|
-
"Hibernate",
|
|
2754
|
-
"Standby",
|
|
2755
|
-
"WakeUp",
|
|
2756
|
-
"AllCandidates",
|
|
2757
|
-
"Alphanumeric",
|
|
2758
|
-
"CodeInput",
|
|
2759
|
-
"Compose",
|
|
2760
|
-
"Convert",
|
|
2761
|
-
"Dead",
|
|
2762
|
-
"FinalMode",
|
|
2763
|
-
"GroupFirst",
|
|
2764
|
-
"GroupLast",
|
|
2765
|
-
"GroupNext",
|
|
2766
|
-
"GroupPrevious",
|
|
2767
|
-
"ModeChange",
|
|
2768
|
-
"NextCandidate",
|
|
2769
|
-
"NonConvert",
|
|
2770
|
-
"PreviousCandidate",
|
|
2771
|
-
"Process",
|
|
2772
|
-
"SingleCandidate",
|
|
2773
|
-
"HangulMode",
|
|
2774
|
-
"HanjaMode",
|
|
2775
|
-
"JunjaMode",
|
|
2776
|
-
"Eisu",
|
|
2777
|
-
"Hankaku",
|
|
2778
|
-
"Hiragana",
|
|
2779
|
-
"HiraganaKatakana",
|
|
2780
|
-
"KanaMode",
|
|
2781
|
-
"KanjiMode",
|
|
2782
|
-
"Katakana",
|
|
2783
|
-
"Romaji",
|
|
2784
|
-
"Zenkaku",
|
|
2785
|
-
"ZenkakuHanaku",
|
|
2786
|
-
"F1",
|
|
2787
|
-
"F2",
|
|
2788
|
-
"F3",
|
|
2789
|
-
"F4",
|
|
2790
|
-
"F5",
|
|
2791
|
-
"F6",
|
|
2792
|
-
"F7",
|
|
2793
|
-
"F8",
|
|
2794
|
-
"F9",
|
|
2795
|
-
"F10",
|
|
2796
|
-
"F11",
|
|
2797
|
-
"F12",
|
|
2798
|
-
"Soft1",
|
|
2799
|
-
"Soft2",
|
|
2800
|
-
"Soft3",
|
|
2801
|
-
"Soft4",
|
|
2802
|
-
"ChannelDown",
|
|
2803
|
-
"ChannelUp",
|
|
2804
|
-
"Close",
|
|
2805
|
-
"MailForward",
|
|
2806
|
-
"MailReply",
|
|
2807
|
-
"MailSend",
|
|
2808
|
-
"MediaFastForward",
|
|
2809
|
-
"MediaPause",
|
|
2810
|
-
"MediaPlay",
|
|
2811
|
-
"MediaPlayPause",
|
|
2812
|
-
"MediaRecord",
|
|
2813
|
-
"MediaRewind",
|
|
2814
|
-
"MediaStop",
|
|
2815
|
-
"MediaTrackNext",
|
|
2816
|
-
"MediaTrackPrevious",
|
|
2817
|
-
"AudioBalanceLeft",
|
|
2818
|
-
"AudioBalanceRight",
|
|
2819
|
-
"AudioBassBoostDown",
|
|
2820
|
-
"AudioBassBoostToggle",
|
|
2821
|
-
"AudioBassBoostUp",
|
|
2822
|
-
"AudioFaderFront",
|
|
2823
|
-
"AudioFaderRear",
|
|
2824
|
-
"AudioSurroundModeNext",
|
|
2825
|
-
"AudioTrebleDown",
|
|
2826
|
-
"AudioTrebleUp",
|
|
2827
|
-
"AudioVolumeDown",
|
|
2828
|
-
"AudioVolumeMute",
|
|
2829
|
-
"AudioVolumeUp",
|
|
2830
|
-
"MicrophoneToggle",
|
|
2831
|
-
"MicrophoneVolumeDown",
|
|
2832
|
-
"MicrophoneVolumeMute",
|
|
2833
|
-
"MicrophoneVolumeUp",
|
|
2834
|
-
"TV",
|
|
2835
|
-
"TV3DMode",
|
|
2836
|
-
"TVAntennaCable",
|
|
2837
|
-
"TVAudioDescription",
|
|
2838
|
-
];
|
|
2839
|
-
function unEscapeString(str) {
|
|
2840
|
-
const placeholder = "__NEWLINE__";
|
|
2841
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2842
|
-
return str;
|
|
2843
|
-
}
|
|
2844
4623
|
export { StableBrowser };
|
|
2845
4624
|
//# sourceMappingURL=stable_browser.js.map
|