automation_model 1.0.495-dev → 1.0.495
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/lib/analyze_helper.js.map +1 -1
- package/lib/api.d.ts +2 -2
- package/lib/api.js +151 -120
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +7 -2
- package/lib/auto_page.js +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 +2602 -819
- 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,24 +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) {
|
|
165
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
166
|
+
console.log("close unexpected popups");
|
|
167
|
+
await page.close();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
105
170
|
context.pageLoading.status = true;
|
|
106
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
|
+
}
|
|
107
180
|
context.page = page;
|
|
108
181
|
context.pages.push(page);
|
|
109
182
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
110
|
-
registerDownloadEvent(this.page);
|
|
183
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
111
184
|
page.on("close", async () => {
|
|
185
|
+
// return if browser context is already closed
|
|
112
186
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
113
187
|
this.context.pages.pop();
|
|
114
188
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -118,7 +192,12 @@ class StableBrowser {
|
|
|
118
192
|
console.log("Switched to page " + title);
|
|
119
193
|
}
|
|
120
194
|
catch (error) {
|
|
121
|
-
|
|
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
|
+
}
|
|
122
201
|
}
|
|
123
202
|
}
|
|
124
203
|
});
|
|
@@ -127,7 +206,12 @@ class StableBrowser {
|
|
|
127
206
|
console.log("Switch page: " + (await page.title()));
|
|
128
207
|
}
|
|
129
208
|
catch (e) {
|
|
130
|
-
|
|
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
|
+
}
|
|
131
215
|
}
|
|
132
216
|
context.pageLoading.status = false;
|
|
133
217
|
}.bind(this));
|
|
@@ -139,7 +223,7 @@ class StableBrowser {
|
|
|
139
223
|
}
|
|
140
224
|
let newContextCreated = false;
|
|
141
225
|
if (!apps[appName]) {
|
|
142
|
-
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);
|
|
143
227
|
newContextCreated = true;
|
|
144
228
|
apps[appName] = {
|
|
145
229
|
context: newContext,
|
|
@@ -148,31 +232,41 @@ class StableBrowser {
|
|
|
148
232
|
};
|
|
149
233
|
}
|
|
150
234
|
const tempContext = {};
|
|
151
|
-
|
|
152
|
-
|
|
235
|
+
_copyContext(this, tempContext);
|
|
236
|
+
_copyContext(apps[appName], this);
|
|
153
237
|
apps[this.appName] = tempContext;
|
|
154
238
|
this.appName = appName;
|
|
155
239
|
if (newContextCreated) {
|
|
156
240
|
this.registerEventListeners(this.context);
|
|
157
241
|
await this.goto(this.context.environment.baseUrl);
|
|
158
|
-
|
|
242
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
243
|
+
await this.waitForPageLoad();
|
|
244
|
+
}
|
|
159
245
|
}
|
|
160
246
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
257
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
268
|
}
|
|
174
|
-
|
|
175
|
-
return path.join(logFolder, fileName);
|
|
269
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
176
270
|
}
|
|
177
271
|
registerConsoleLogListener(page, context) {
|
|
178
272
|
if (!this.context.webLogger) {
|
|
@@ -223,7 +317,7 @@ class StableBrowser {
|
|
|
223
317
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
224
318
|
}
|
|
225
319
|
catch (error) {
|
|
226
|
-
console.error("Error in request listener", error);
|
|
320
|
+
// console.error("Error in request listener", error);
|
|
227
321
|
context.networkLogger.push({
|
|
228
322
|
error: "not able to listen",
|
|
229
323
|
message: error.message,
|
|
@@ -237,55 +331,117 @@ class StableBrowser {
|
|
|
237
331
|
// async closeUnexpectedPopups() {
|
|
238
332
|
// await closeUnexpectedPopups(this.page);
|
|
239
333
|
// }
|
|
240
|
-
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);
|
|
241
339
|
if (!url.startsWith("http")) {
|
|
242
340
|
url = "https://" + url;
|
|
243
341
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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;
|
|
251
358
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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);
|
|
259
375
|
}
|
|
260
|
-
return text;
|
|
261
376
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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);
|
|
266
404
|
}
|
|
267
|
-
// clone the locator
|
|
268
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
269
|
-
this.scanAndManipulate(locator, _params);
|
|
270
|
-
return locator;
|
|
271
405
|
}
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
434
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
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);
|
|
284
443
|
}
|
|
285
444
|
}
|
|
286
|
-
}
|
|
287
|
-
_getLocator(locator, scope, _params) {
|
|
288
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
289
445
|
let locatorReturn;
|
|
290
446
|
if (locator.role) {
|
|
291
447
|
if (locator.role[1].nameReg) {
|
|
@@ -293,7 +449,7 @@ class StableBrowser {
|
|
|
293
449
|
delete locator.role[1].nameReg;
|
|
294
450
|
}
|
|
295
451
|
// if (locator.role[1].name) {
|
|
296
|
-
// locator.role[1].name =
|
|
452
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
297
453
|
// }
|
|
298
454
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
299
455
|
}
|
|
@@ -336,213 +492,171 @@ class StableBrowser {
|
|
|
336
492
|
if (css && css.locator) {
|
|
337
493
|
css = css.locator;
|
|
338
494
|
}
|
|
339
|
-
let result = await this._locateElementByText(scope,
|
|
495
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
340
496
|
if (result.elementCount === 0) {
|
|
341
497
|
return;
|
|
342
498
|
}
|
|
343
|
-
let textElementCss = "[data-blinq-id
|
|
499
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
344
500
|
// css climb to parent element
|
|
345
501
|
const climbArray = [];
|
|
346
502
|
for (let i = 0; i < climb; i++) {
|
|
347
503
|
climbArray.push("..");
|
|
348
504
|
}
|
|
349
505
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return new RegExp(pattern, flags);
|
|
373
|
-
}
|
|
374
|
-
document.getRegex = getRegex;
|
|
375
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
376
|
-
// Check and add the element if it has a shadow root
|
|
377
|
-
if (element.shadowRoot) {
|
|
378
|
-
result.push(element);
|
|
379
|
-
// Also search within the shadow root
|
|
380
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
381
|
-
}
|
|
382
|
-
// Iterate over child nodes
|
|
383
|
-
element.childNodes.forEach((child) => {
|
|
384
|
-
// Recursively call the function for each child node
|
|
385
|
-
document.collectAllShadowDomElements(child, result);
|
|
386
|
-
});
|
|
387
|
-
return result;
|
|
388
|
-
}
|
|
389
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
390
|
-
if (!tag) {
|
|
391
|
-
tag = "*";
|
|
392
|
-
}
|
|
393
|
-
let regexpSearch = document.getRegex(text);
|
|
394
|
-
if (regexpSearch) {
|
|
395
|
-
regex = true;
|
|
396
|
-
}
|
|
397
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
398
|
-
let shadowHosts = [];
|
|
399
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
400
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
401
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
402
|
-
if (!shadowElement) {
|
|
403
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
407
|
-
elements = elements.concat(shadowElements);
|
|
408
|
-
}
|
|
409
|
-
let randomToken = null;
|
|
410
|
-
const foundElements = [];
|
|
411
|
-
if (regex) {
|
|
412
|
-
if (!regexpSearch) {
|
|
413
|
-
regexpSearch = new RegExp(text, "im");
|
|
414
|
-
}
|
|
415
|
-
for (let i = 0; i < elements.length; i++) {
|
|
416
|
-
const element = elements[i];
|
|
417
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
418
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
419
|
-
foundElements.push(element);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
text = text.trim();
|
|
425
|
-
for (let i = 0; i < elements.length; i++) {
|
|
426
|
-
const element = elements[i];
|
|
427
|
-
if (partial) {
|
|
428
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
429
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
430
|
-
foundElements.push(element);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
435
|
-
(element.value && element.value === text)) {
|
|
436
|
-
foundElements.push(element);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
let noChildElements = [];
|
|
442
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
443
|
-
let element = foundElements[i];
|
|
444
|
-
let hasChild = false;
|
|
445
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
446
|
-
if (i === j) {
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
if (isParent(element, foundElements[j])) {
|
|
450
|
-
hasChild = true;
|
|
451
|
-
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;
|
|
452
528
|
}
|
|
453
529
|
}
|
|
454
|
-
if (!
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
let elementCount = 0;
|
|
459
|
-
if (noChildElements.length > 0) {
|
|
460
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
461
|
-
if (randomToken === null) {
|
|
462
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
463
|
-
}
|
|
464
|
-
let element = noChildElements[i];
|
|
465
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
466
|
-
elementCount++;
|
|
530
|
+
if (!el.setAttribute) {
|
|
531
|
+
el = el.parentElement;
|
|
467
532
|
}
|
|
533
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
534
|
+
return true;
|
|
535
|
+
}, [tag1, randomToken]))) {
|
|
536
|
+
continue;
|
|
468
537
|
}
|
|
469
|
-
|
|
470
|
-
}
|
|
538
|
+
tagCount++;
|
|
539
|
+
}
|
|
540
|
+
return { elementCount: tagCount, randomToken };
|
|
471
541
|
}
|
|
472
|
-
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
|
+
}
|
|
473
553
|
let locatorSearch = selectorHierarchy[index];
|
|
474
554
|
try {
|
|
475
|
-
locatorSearch =
|
|
555
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
476
556
|
}
|
|
477
557
|
catch (e) {
|
|
478
558
|
console.error(e);
|
|
479
559
|
}
|
|
560
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
480
561
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
481
562
|
let locator = null;
|
|
482
563
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
483
|
-
|
|
564
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
565
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
484
566
|
if (!locatorString) {
|
|
567
|
+
info.failCause.textNotFound = true;
|
|
568
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
485
569
|
return;
|
|
486
570
|
}
|
|
487
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
571
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
488
572
|
}
|
|
489
573
|
else if (locatorSearch.text) {
|
|
490
|
-
let
|
|
574
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
575
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
491
576
|
if (result.elementCount === 0) {
|
|
577
|
+
info.failCause.textNotFound = true;
|
|
578
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
492
579
|
return;
|
|
493
580
|
}
|
|
494
|
-
locatorSearch.css = "[data-blinq-id
|
|
581
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
495
582
|
if (locatorSearch.childCss) {
|
|
496
583
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
497
584
|
}
|
|
498
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
585
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
499
586
|
}
|
|
500
587
|
else {
|
|
501
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
588
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
502
589
|
}
|
|
503
590
|
// let cssHref = false;
|
|
504
591
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
505
592
|
// cssHref = true;
|
|
506
593
|
// }
|
|
507
594
|
let count = await locator.count();
|
|
595
|
+
if (count > 0 && !info.failCause.count) {
|
|
596
|
+
info.failCause.count = count;
|
|
597
|
+
}
|
|
508
598
|
//info.log += "total elements found " + count + "\n";
|
|
509
599
|
//let visibleCount = 0;
|
|
510
600
|
let visibleLocator = null;
|
|
511
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
601
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
512
602
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
603
|
+
if (info.locatorLog) {
|
|
604
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
605
|
+
}
|
|
513
606
|
return;
|
|
514
607
|
}
|
|
608
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
609
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
610
|
+
}
|
|
515
611
|
for (let j = 0; j < count; j++) {
|
|
516
612
|
let visible = await locator.nth(j).isVisible();
|
|
517
613
|
const enabled = await locator.nth(j).isEnabled();
|
|
518
614
|
if (!visibleOnly) {
|
|
519
615
|
visible = true;
|
|
520
616
|
}
|
|
521
|
-
if (visible && enabled) {
|
|
617
|
+
if (visible && (allowDisabled || enabled)) {
|
|
522
618
|
foundLocators.push(locator.nth(j));
|
|
619
|
+
if (info.locatorLog) {
|
|
620
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
621
|
+
}
|
|
523
622
|
}
|
|
524
|
-
else {
|
|
623
|
+
else if (logErrors) {
|
|
624
|
+
info.failCause.visible = visible;
|
|
625
|
+
info.failCause.enabled = enabled;
|
|
525
626
|
if (!info.printMessages) {
|
|
526
627
|
info.printMessages = {};
|
|
527
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
|
+
}
|
|
528
637
|
if (!info.printMessages[j.toString()]) {
|
|
529
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
638
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
530
639
|
info.printMessages[j.toString()] = true;
|
|
531
640
|
}
|
|
532
641
|
}
|
|
533
642
|
}
|
|
534
643
|
}
|
|
535
644
|
async closeUnexpectedPopups(info, _params) {
|
|
645
|
+
if (!info) {
|
|
646
|
+
info = {};
|
|
647
|
+
info.failCause = {};
|
|
648
|
+
info.log = "";
|
|
649
|
+
}
|
|
536
650
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
537
651
|
if (!info) {
|
|
538
652
|
info = {};
|
|
539
653
|
}
|
|
540
|
-
info.log += "scan for popup handlers" + "\n";
|
|
654
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
541
655
|
const handlerGroup = [];
|
|
542
656
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
543
657
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
544
658
|
}
|
|
545
|
-
const scopes =
|
|
659
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
546
660
|
let result = null;
|
|
547
661
|
let scope = null;
|
|
548
662
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -564,33 +678,218 @@ class StableBrowser {
|
|
|
564
678
|
}
|
|
565
679
|
if (result.foundElements.length > 0) {
|
|
566
680
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
}
|
|
570
696
|
return { rerun: true };
|
|
571
697
|
}
|
|
572
698
|
}
|
|
573
699
|
}
|
|
574
700
|
return { rerun: false };
|
|
575
701
|
}
|
|
576
|
-
|
|
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) {
|
|
577
767
|
if (!timeout) {
|
|
578
768
|
timeout = 30000;
|
|
579
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
|
+
}
|
|
580
777
|
for (let i = 0; i < 3; i++) {
|
|
581
778
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
582
779
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
583
780
|
let selector = selectors.locators[j];
|
|
584
781
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
585
782
|
}
|
|
586
|
-
|
|
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
|
+
}
|
|
587
826
|
if (!element.rerun) {
|
|
588
|
-
|
|
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();
|
|
589
882
|
}
|
|
590
883
|
}
|
|
591
884
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
592
885
|
}
|
|
593
|
-
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();
|
|
594
893
|
let scope = this.page;
|
|
595
894
|
if (selectors.frame) {
|
|
596
895
|
return selectors.frame;
|
|
@@ -600,7 +899,7 @@ class StableBrowser {
|
|
|
600
899
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
601
900
|
let frameLocator = frame.selectors[i];
|
|
602
901
|
if (frameLocator.css) {
|
|
603
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
902
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
604
903
|
if (frameLocator.index) {
|
|
605
904
|
testframescope = framescope.nth(frameLocator.index);
|
|
606
905
|
}
|
|
@@ -612,7 +911,7 @@ class StableBrowser {
|
|
|
612
911
|
break;
|
|
613
912
|
}
|
|
614
913
|
catch (error) {
|
|
615
|
-
console.error("frame not found " + frameLocator.css);
|
|
914
|
+
// console.error("frame not found " + frameLocator.css);
|
|
616
915
|
}
|
|
617
916
|
}
|
|
618
917
|
}
|
|
@@ -621,9 +920,11 @@ class StableBrowser {
|
|
|
621
920
|
}
|
|
622
921
|
return framescope;
|
|
623
922
|
};
|
|
923
|
+
let fLocator = null;
|
|
624
924
|
while (true) {
|
|
625
925
|
let frameFound = false;
|
|
626
926
|
if (selectors.nestFrmLoc) {
|
|
927
|
+
fLocator = selectors.nestFrmLoc;
|
|
627
928
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
628
929
|
frameFound = true;
|
|
629
930
|
break;
|
|
@@ -632,6 +933,7 @@ class StableBrowser {
|
|
|
632
933
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
633
934
|
let frameLocator = selectors.frameLocators[i];
|
|
634
935
|
if (frameLocator.css) {
|
|
936
|
+
fLocator = frameLocator.css;
|
|
635
937
|
scope = scope.frameLocator(frameLocator.css);
|
|
636
938
|
frameFound = true;
|
|
637
939
|
break;
|
|
@@ -639,16 +941,25 @@ class StableBrowser {
|
|
|
639
941
|
}
|
|
640
942
|
}
|
|
641
943
|
if (!frameFound && selectors.iframe_src) {
|
|
944
|
+
fLocator = selectors.iframe_src;
|
|
642
945
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
643
946
|
}
|
|
644
947
|
if (!scope) {
|
|
645
|
-
info
|
|
646
|
-
|
|
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}"`;
|
|
647
955
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
648
956
|
}
|
|
649
957
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
650
958
|
}
|
|
651
959
|
else {
|
|
960
|
+
if (info && info.locatorLog) {
|
|
961
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
962
|
+
}
|
|
652
963
|
break;
|
|
653
964
|
}
|
|
654
965
|
}
|
|
@@ -658,20 +969,35 @@ class StableBrowser {
|
|
|
658
969
|
}
|
|
659
970
|
return scope;
|
|
660
971
|
}
|
|
661
|
-
async _getDocumentBody(selectors, timeout = 30000) {
|
|
662
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
972
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
973
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
663
974
|
return scope.evaluate(() => {
|
|
664
975
|
var bodyContent = document.body.innerHTML;
|
|
665
976
|
return bodyContent;
|
|
666
977
|
});
|
|
667
978
|
}
|
|
668
|
-
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
|
+
}
|
|
669
995
|
let highPriorityTimeout = 5000;
|
|
670
996
|
let visibleOnlyTimeout = 6000;
|
|
671
|
-
let startTime =
|
|
997
|
+
let startTime = Date.now();
|
|
672
998
|
let locatorsCount = 0;
|
|
999
|
+
let lazy_scroll = false;
|
|
673
1000
|
//let arrayMode = Array.isArray(selectors);
|
|
674
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
675
1001
|
let selectorsLocators = null;
|
|
676
1002
|
selectorsLocators = selectors.locators;
|
|
677
1003
|
// group selectors by priority
|
|
@@ -699,6 +1025,7 @@ class StableBrowser {
|
|
|
699
1025
|
let highPriorityOnly = true;
|
|
700
1026
|
let visibleOnly = true;
|
|
701
1027
|
while (true) {
|
|
1028
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
702
1029
|
locatorsCount = 0;
|
|
703
1030
|
let result = [];
|
|
704
1031
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -707,18 +1034,13 @@ class StableBrowser {
|
|
|
707
1034
|
}
|
|
708
1035
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
709
1036
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
710
|
-
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);
|
|
711
1038
|
if (result.foundElements.length === 0) {
|
|
712
1039
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
713
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
1040
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
714
1041
|
}
|
|
715
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
716
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
720
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
721
|
-
}
|
|
1042
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1043
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
722
1044
|
}
|
|
723
1045
|
let foundElements = result.foundElements;
|
|
724
1046
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -758,24 +1080,43 @@ class StableBrowser {
|
|
|
758
1080
|
return maxCountElement.locator;
|
|
759
1081
|
}
|
|
760
1082
|
}
|
|
761
|
-
if (
|
|
1083
|
+
if (Date.now() - startTime > timeout) {
|
|
762
1084
|
break;
|
|
763
1085
|
}
|
|
764
|
-
if (
|
|
765
|
-
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";
|
|
766
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
|
+
}
|
|
767
1093
|
}
|
|
768
|
-
if (
|
|
769
|
-
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";
|
|
770
1096
|
visibleOnly = false;
|
|
771
1097
|
}
|
|
772
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
|
+
}
|
|
773
1104
|
}
|
|
774
1105
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
775
|
-
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
|
+
}
|
|
776
1117
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
777
1118
|
}
|
|
778
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1119
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
779
1120
|
let foundElements = [];
|
|
780
1121
|
const result = {
|
|
781
1122
|
foundElements: foundElements,
|
|
@@ -783,31 +1124,88 @@ class StableBrowser {
|
|
|
783
1124
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
784
1125
|
let foundLocators = [];
|
|
785
1126
|
try {
|
|
786
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1127
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
787
1128
|
}
|
|
788
1129
|
catch (e) {
|
|
789
|
-
this
|
|
790
|
-
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);
|
|
791
1133
|
foundLocators = [];
|
|
792
1134
|
try {
|
|
793
|
-
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);
|
|
794
1136
|
}
|
|
795
1137
|
catch (e) {
|
|
796
|
-
|
|
1138
|
+
if (logErrors) {
|
|
1139
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1140
|
+
}
|
|
797
1141
|
}
|
|
798
1142
|
}
|
|
799
1143
|
if (foundLocators.length === 1) {
|
|
1144
|
+
let box = null;
|
|
1145
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1146
|
+
box = await foundLocators[0].boundingBox();
|
|
1147
|
+
}
|
|
800
1148
|
result.foundElements.push({
|
|
801
1149
|
locator: foundLocators[0],
|
|
802
|
-
box:
|
|
1150
|
+
box: box,
|
|
803
1151
|
unique: true,
|
|
804
1152
|
});
|
|
805
1153
|
result.locatorIndex = i;
|
|
806
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
|
+
}
|
|
807
1192
|
}
|
|
808
1193
|
return result;
|
|
809
1194
|
}
|
|
810
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);
|
|
811
1209
|
const startTime = Date.now();
|
|
812
1210
|
let timeout = 30000;
|
|
813
1211
|
if (options && options.timeout) {
|
|
@@ -831,13 +1229,32 @@ class StableBrowser {
|
|
|
831
1229
|
}
|
|
832
1230
|
catch (e) {
|
|
833
1231
|
if (performance.now() - startTime > timeout) {
|
|
834
|
-
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
|
+
}
|
|
835
1239
|
}
|
|
836
1240
|
}
|
|
837
1241
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
838
1242
|
}
|
|
839
1243
|
}
|
|
840
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);
|
|
841
1258
|
const startTime = Date.now();
|
|
842
1259
|
let timeout = 30000;
|
|
843
1260
|
if (options && options.timeout) {
|
|
@@ -861,7 +1278,13 @@ class StableBrowser {
|
|
|
861
1278
|
}
|
|
862
1279
|
catch (e) {
|
|
863
1280
|
if (performance.now() - startTime > timeout) {
|
|
864
|
-
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
|
+
}
|
|
865
1288
|
}
|
|
866
1289
|
}
|
|
867
1290
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -874,34 +1297,74 @@ class StableBrowser {
|
|
|
874
1297
|
options,
|
|
875
1298
|
world,
|
|
876
1299
|
text: "Click element",
|
|
1300
|
+
_text: "Click on " + selectors.element_name,
|
|
877
1301
|
type: Types.CLICK,
|
|
878
1302
|
operation: "click",
|
|
879
1303
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
880
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
|
+
}
|
|
881
1312
|
try {
|
|
1313
|
+
check_performance("click_preCommand", this.context, true);
|
|
882
1314
|
await _preCommand(state, this);
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
await
|
|
888
|
-
|
|
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);
|
|
889
1321
|
}
|
|
890
|
-
catch (e) {
|
|
891
|
-
// await this.closeUnexpectedPopups();
|
|
892
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
893
|
-
await state.element.dispatchEvent("click");
|
|
894
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
895
|
-
}
|
|
896
|
-
await this.waitForPageLoad();
|
|
897
1322
|
return state.info;
|
|
898
1323
|
}
|
|
899
1324
|
catch (e) {
|
|
900
1325
|
await _commandError(state, e, this);
|
|
901
1326
|
}
|
|
902
1327
|
finally {
|
|
903
|
-
|
|
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);
|
|
904
1366
|
}
|
|
1367
|
+
return found;
|
|
905
1368
|
}
|
|
906
1369
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
907
1370
|
const state = {
|
|
@@ -911,6 +1374,7 @@ class StableBrowser {
|
|
|
911
1374
|
world,
|
|
912
1375
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
913
1376
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1377
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
914
1378
|
operation: "setCheck",
|
|
915
1379
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
916
1380
|
};
|
|
@@ -920,30 +1384,53 @@ class StableBrowser {
|
|
|
920
1384
|
// let element = await this._locate(selectors, info, _params);
|
|
921
1385
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
922
1386
|
try {
|
|
923
|
-
//
|
|
924
|
-
|
|
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 });
|
|
925
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);
|
|
926
1396
|
}
|
|
927
1397
|
catch (e) {
|
|
928
1398
|
if (e.message && e.message.includes("did not change its state")) {
|
|
929
1399
|
this.logger.info("element did not change its state, ignoring...");
|
|
930
1400
|
}
|
|
931
1401
|
else {
|
|
1402
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
932
1403
|
//await this.closeUnexpectedPopups();
|
|
933
1404
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
934
|
-
state.
|
|
935
|
-
|
|
936
|
-
|
|
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
|
+
}
|
|
937
1424
|
}
|
|
938
1425
|
}
|
|
939
|
-
await this.waitForPageLoad();
|
|
1426
|
+
//await this.waitForPageLoad();
|
|
940
1427
|
return state.info;
|
|
941
1428
|
}
|
|
942
1429
|
catch (e) {
|
|
943
1430
|
await _commandError(state, e, this);
|
|
944
1431
|
}
|
|
945
1432
|
finally {
|
|
946
|
-
_commandFinally(state, this);
|
|
1433
|
+
await _commandFinally(state, this);
|
|
947
1434
|
}
|
|
948
1435
|
}
|
|
949
1436
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -954,31 +1441,22 @@ class StableBrowser {
|
|
|
954
1441
|
world,
|
|
955
1442
|
type: Types.HOVER,
|
|
956
1443
|
text: `Hover element`,
|
|
1444
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
957
1445
|
operation: "hover",
|
|
958
1446
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
959
1447
|
};
|
|
960
1448
|
try {
|
|
961
1449
|
await _preCommand(state, this);
|
|
962
|
-
|
|
963
|
-
await state.element.hover();
|
|
964
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
965
|
-
}
|
|
966
|
-
catch (e) {
|
|
967
|
-
//await this.closeUnexpectedPopups();
|
|
968
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
969
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
970
|
-
await state.element.hover({ timeout: 10000 });
|
|
971
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
972
|
-
}
|
|
1450
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
973
1451
|
await _screenshot(state, this);
|
|
974
|
-
await this.waitForPageLoad();
|
|
1452
|
+
//await this.waitForPageLoad();
|
|
975
1453
|
return state.info;
|
|
976
1454
|
}
|
|
977
1455
|
catch (e) {
|
|
978
1456
|
await _commandError(state, e, this);
|
|
979
1457
|
}
|
|
980
1458
|
finally {
|
|
981
|
-
_commandFinally(state, this);
|
|
1459
|
+
await _commandFinally(state, this);
|
|
982
1460
|
}
|
|
983
1461
|
}
|
|
984
1462
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -993,6 +1471,7 @@ class StableBrowser {
|
|
|
993
1471
|
value: values.toString(),
|
|
994
1472
|
type: Types.SELECT,
|
|
995
1473
|
text: `Select option: ${values}`,
|
|
1474
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
996
1475
|
operation: "selectOption",
|
|
997
1476
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
998
1477
|
};
|
|
@@ -1006,14 +1485,14 @@ class StableBrowser {
|
|
|
1006
1485
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1007
1486
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1008
1487
|
}
|
|
1009
|
-
await this.waitForPageLoad();
|
|
1488
|
+
//await this.waitForPageLoad();
|
|
1010
1489
|
return state.info;
|
|
1011
1490
|
}
|
|
1012
1491
|
catch (e) {
|
|
1013
1492
|
await _commandError(state, e, this);
|
|
1014
1493
|
}
|
|
1015
1494
|
finally {
|
|
1016
|
-
_commandFinally(state, this);
|
|
1495
|
+
await _commandFinally(state, this);
|
|
1017
1496
|
}
|
|
1018
1497
|
}
|
|
1019
1498
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1027,6 +1506,7 @@ class StableBrowser {
|
|
|
1027
1506
|
highlight: false,
|
|
1028
1507
|
type: Types.TYPE_PRESS,
|
|
1029
1508
|
text: `Type value: ${_value}`,
|
|
1509
|
+
_text: `Type value: ${_value}`,
|
|
1030
1510
|
operation: "type",
|
|
1031
1511
|
log: "",
|
|
1032
1512
|
};
|
|
@@ -1058,7 +1538,7 @@ class StableBrowser {
|
|
|
1058
1538
|
await _commandError(state, e, this);
|
|
1059
1539
|
}
|
|
1060
1540
|
finally {
|
|
1061
|
-
_commandFinally(state, this);
|
|
1541
|
+
await _commandFinally(state, this);
|
|
1062
1542
|
}
|
|
1063
1543
|
}
|
|
1064
1544
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1094,37 +1574,35 @@ class StableBrowser {
|
|
|
1094
1574
|
await _commandError(state, e, this);
|
|
1095
1575
|
}
|
|
1096
1576
|
finally {
|
|
1097
|
-
_commandFinally(state, this);
|
|
1577
|
+
await _commandFinally(state, this);
|
|
1098
1578
|
}
|
|
1099
1579
|
}
|
|
1100
1580
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
+
};
|
|
1111
1594
|
try {
|
|
1112
|
-
|
|
1113
|
-
let element = await this._locate(selectors, info, _params);
|
|
1114
|
-
//insert red border around the element
|
|
1115
|
-
await this.scrollIfNeeded(element, info);
|
|
1116
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1117
|
-
await this._highlightElements(element);
|
|
1595
|
+
await _preCommand(state, this);
|
|
1118
1596
|
try {
|
|
1119
|
-
await element
|
|
1597
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1120
1598
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1121
1599
|
if (format) {
|
|
1122
|
-
value = dayjs(value).format(format);
|
|
1123
|
-
await element.fill(value);
|
|
1600
|
+
state.value = dayjs(state.value).format(format);
|
|
1601
|
+
await state.element.fill(state.value);
|
|
1124
1602
|
}
|
|
1125
1603
|
else {
|
|
1126
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1127
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1604
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1605
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1128
1606
|
el.value = ""; // clear input
|
|
1129
1607
|
el.value = dateTimeValue;
|
|
1130
1608
|
}, dateTimeValue);
|
|
@@ -1137,20 +1615,19 @@ class StableBrowser {
|
|
|
1137
1615
|
}
|
|
1138
1616
|
catch (err) {
|
|
1139
1617
|
//await this.closeUnexpectedPopups();
|
|
1140
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1618
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1141
1619
|
this.logger.info("Trying again");
|
|
1142
|
-
|
|
1143
|
-
info.
|
|
1144
|
-
Object.assign(err, { info: info });
|
|
1620
|
+
await _screenshot(state, this);
|
|
1621
|
+
Object.assign(err, { info: state.info });
|
|
1145
1622
|
await element.click();
|
|
1146
1623
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1147
1624
|
if (format) {
|
|
1148
|
-
value = dayjs(value).format(format);
|
|
1149
|
-
await element.fill(value);
|
|
1625
|
+
state.value = dayjs(state.value).format(format);
|
|
1626
|
+
await state.element.fill(state.value);
|
|
1150
1627
|
}
|
|
1151
1628
|
else {
|
|
1152
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1153
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1629
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1630
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1154
1631
|
el.value = ""; // clear input
|
|
1155
1632
|
el.value = dateTimeValue;
|
|
1156
1633
|
}, dateTimeValue);
|
|
@@ -1163,31 +1640,10 @@ class StableBrowser {
|
|
|
1163
1640
|
}
|
|
1164
1641
|
}
|
|
1165
1642
|
catch (e) {
|
|
1166
|
-
|
|
1167
|
-
throw e;
|
|
1643
|
+
await _commandError(state, e, this);
|
|
1168
1644
|
}
|
|
1169
1645
|
finally {
|
|
1170
|
-
|
|
1171
|
-
this._reportToWorld(world, {
|
|
1172
|
-
element_name: selectors.element_name,
|
|
1173
|
-
type: Types.SET_DATE_TIME,
|
|
1174
|
-
screenshotId,
|
|
1175
|
-
value: value,
|
|
1176
|
-
text: `setDateTime input with value: ${value}`,
|
|
1177
|
-
result: error
|
|
1178
|
-
? {
|
|
1179
|
-
status: "FAILED",
|
|
1180
|
-
startTime,
|
|
1181
|
-
endTime,
|
|
1182
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1183
|
-
}
|
|
1184
|
-
: {
|
|
1185
|
-
status: "PASSED",
|
|
1186
|
-
startTime,
|
|
1187
|
-
endTime,
|
|
1188
|
-
},
|
|
1189
|
-
info: info,
|
|
1190
|
-
});
|
|
1646
|
+
await _commandFinally(state, this);
|
|
1191
1647
|
}
|
|
1192
1648
|
}
|
|
1193
1649
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1202,17 +1658,29 @@ class StableBrowser {
|
|
|
1202
1658
|
world,
|
|
1203
1659
|
type: Types.FILL,
|
|
1204
1660
|
text: `Click type input with value: ${_value}`,
|
|
1661
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1205
1662
|
operation: "clickType",
|
|
1206
1663
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1207
1664
|
};
|
|
1665
|
+
if (!options) {
|
|
1666
|
+
options = {};
|
|
1667
|
+
}
|
|
1208
1668
|
if (newValue !== _value) {
|
|
1209
1669
|
//this.logger.info(_value + "=" + newValue);
|
|
1210
1670
|
_value = newValue;
|
|
1211
1671
|
}
|
|
1212
1672
|
try {
|
|
1213
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);
|
|
1214
1682
|
state.info.value = _value;
|
|
1215
|
-
if (
|
|
1683
|
+
if (!options.press) {
|
|
1216
1684
|
try {
|
|
1217
1685
|
let currentValue = await state.element.inputValue();
|
|
1218
1686
|
if (currentValue) {
|
|
@@ -1223,13 +1691,9 @@ class StableBrowser {
|
|
|
1223
1691
|
this.logger.info("unable to clear input value");
|
|
1224
1692
|
}
|
|
1225
1693
|
}
|
|
1226
|
-
if (options
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
catch (e) {
|
|
1231
|
-
await state.element.dispatchEvent("click");
|
|
1232
|
-
}
|
|
1694
|
+
if (options.press) {
|
|
1695
|
+
options.timeout = 5000;
|
|
1696
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1233
1697
|
}
|
|
1234
1698
|
else {
|
|
1235
1699
|
try {
|
|
@@ -1240,6 +1704,25 @@ class StableBrowser {
|
|
|
1240
1704
|
}
|
|
1241
1705
|
}
|
|
1242
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
|
+
}
|
|
1243
1726
|
const valueSegment = state.value.split("&&");
|
|
1244
1727
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1245
1728
|
if (i > 0) {
|
|
@@ -1260,14 +1743,21 @@ class StableBrowser {
|
|
|
1260
1743
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1261
1744
|
}
|
|
1262
1745
|
}
|
|
1746
|
+
//if (!this.fastMode) {
|
|
1263
1747
|
await _screenshot(state, this);
|
|
1748
|
+
//}
|
|
1264
1749
|
if (enter === true) {
|
|
1265
1750
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1266
1751
|
await this.page.keyboard.press("Enter");
|
|
1267
1752
|
await this.waitForPageLoad();
|
|
1268
1753
|
}
|
|
1269
1754
|
else if (enter === false) {
|
|
1270
|
-
|
|
1755
|
+
try {
|
|
1756
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1757
|
+
}
|
|
1758
|
+
catch (e) {
|
|
1759
|
+
// ignore
|
|
1760
|
+
}
|
|
1271
1761
|
//await this.page.keyboard.press("Tab");
|
|
1272
1762
|
}
|
|
1273
1763
|
else {
|
|
@@ -1282,7 +1772,7 @@ class StableBrowser {
|
|
|
1282
1772
|
await _commandError(state, e, this);
|
|
1283
1773
|
}
|
|
1284
1774
|
finally {
|
|
1285
|
-
_commandFinally(state, this);
|
|
1775
|
+
await _commandFinally(state, this);
|
|
1286
1776
|
}
|
|
1287
1777
|
}
|
|
1288
1778
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1304,30 +1794,67 @@ class StableBrowser {
|
|
|
1304
1794
|
if (enter) {
|
|
1305
1795
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1306
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;
|
|
1307
1832
|
}
|
|
1308
|
-
await
|
|
1833
|
+
await state.element.setInputFiles(files);
|
|
1309
1834
|
return state.info;
|
|
1310
1835
|
}
|
|
1311
1836
|
catch (e) {
|
|
1312
1837
|
await _commandError(state, e, this);
|
|
1313
1838
|
}
|
|
1314
1839
|
finally {
|
|
1315
|
-
_commandFinally(state, this);
|
|
1840
|
+
await _commandFinally(state, this);
|
|
1316
1841
|
}
|
|
1317
1842
|
}
|
|
1318
1843
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1319
1844
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1320
1845
|
}
|
|
1321
1846
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1847
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1322
1848
|
_validateSelectors(selectors);
|
|
1323
1849
|
let screenshotId = null;
|
|
1324
1850
|
let screenshotPath = null;
|
|
1325
1851
|
if (!info.log) {
|
|
1326
1852
|
info.log = "";
|
|
1853
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1327
1854
|
}
|
|
1328
1855
|
info.operation = "getText";
|
|
1329
1856
|
info.selectors = selectors;
|
|
1330
|
-
let element = await this._locate(selectors, info, _params);
|
|
1857
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1331
1858
|
if (climb > 0) {
|
|
1332
1859
|
const climbArray = [];
|
|
1333
1860
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1346,6 +1873,18 @@ class StableBrowser {
|
|
|
1346
1873
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1347
1874
|
try {
|
|
1348
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
|
+
// }
|
|
1349
1888
|
const elementText = await element.innerText();
|
|
1350
1889
|
return {
|
|
1351
1890
|
text: elementText,
|
|
@@ -1357,7 +1896,7 @@ class StableBrowser {
|
|
|
1357
1896
|
}
|
|
1358
1897
|
catch (e) {
|
|
1359
1898
|
//await this.closeUnexpectedPopups();
|
|
1360
|
-
this.logger.info("no innerText will use textContent");
|
|
1899
|
+
this.logger.info("no innerText, will use textContent");
|
|
1361
1900
|
const elementText = await element.textContent();
|
|
1362
1901
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1363
1902
|
}
|
|
@@ -1382,6 +1921,7 @@ class StableBrowser {
|
|
|
1382
1921
|
highlight: false,
|
|
1383
1922
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1384
1923
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1924
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1385
1925
|
operation: "containsPattern",
|
|
1386
1926
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1387
1927
|
};
|
|
@@ -1413,10 +1953,12 @@ class StableBrowser {
|
|
|
1413
1953
|
await _commandError(state, e, this);
|
|
1414
1954
|
}
|
|
1415
1955
|
finally {
|
|
1416
|
-
_commandFinally(state, this);
|
|
1956
|
+
await _commandFinally(state, this);
|
|
1417
1957
|
}
|
|
1418
1958
|
}
|
|
1419
1959
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1960
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1961
|
+
const startTime = Date.now();
|
|
1420
1962
|
const state = {
|
|
1421
1963
|
selectors,
|
|
1422
1964
|
_params,
|
|
@@ -1443,61 +1985,137 @@ class StableBrowser {
|
|
|
1443
1985
|
}
|
|
1444
1986
|
let foundObj = null;
|
|
1445
1987
|
try {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1453
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1454
|
-
if (dateAlternatives.date) {
|
|
1455
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1456
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1457
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1458
|
-
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);
|
|
1459
1994
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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)) {
|
|
1467
2015
|
return state.info;
|
|
1468
2016
|
}
|
|
1469
2017
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
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
|
|
1476
2023
|
}
|
|
1477
|
-
|
|
2024
|
+
state.info.foundText = foundObj?.text;
|
|
2025
|
+
state.info.value = foundObj?.value;
|
|
2026
|
+
throw new Error("element doesn't contain text " + text);
|
|
1478
2027
|
}
|
|
1479
2028
|
catch (e) {
|
|
1480
2029
|
await _commandError(state, e, this);
|
|
2030
|
+
throw e;
|
|
1481
2031
|
}
|
|
1482
2032
|
finally {
|
|
1483
|
-
_commandFinally(state, this);
|
|
2033
|
+
await _commandFinally(state, this);
|
|
1484
2034
|
}
|
|
1485
2035
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
2036
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
2037
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2038
|
+
const startTime = Date.now();
|
|
2039
|
+
const state = {
|
|
2040
|
+
_params,
|
|
2041
|
+
value: referanceSnapshot,
|
|
2042
|
+
options,
|
|
2043
|
+
world,
|
|
2044
|
+
locate: false,
|
|
2045
|
+
scroll: false,
|
|
2046
|
+
screenshot: true,
|
|
2047
|
+
highlight: false,
|
|
2048
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
2049
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
2050
|
+
operation: "snapshotValidation",
|
|
2051
|
+
log: "***** verify snapshot *****\n",
|
|
2052
|
+
};
|
|
2053
|
+
if (!referanceSnapshot) {
|
|
2054
|
+
throw new Error("referanceSnapshot is null");
|
|
2055
|
+
}
|
|
2056
|
+
let text = null;
|
|
2057
|
+
const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
|
|
2058
|
+
if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
|
|
2059
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
|
|
1490
2060
|
}
|
|
1491
|
-
else if (
|
|
1492
|
-
|
|
2061
|
+
else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
|
|
2062
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
|
|
1493
2063
|
}
|
|
1494
|
-
else if (
|
|
1495
|
-
|
|
2064
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2065
|
+
text = referanceSnapshot.substring(5);
|
|
1496
2066
|
}
|
|
1497
2067
|
else {
|
|
1498
|
-
|
|
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);
|
|
1499
2118
|
}
|
|
1500
|
-
return dataFile;
|
|
1501
2119
|
}
|
|
1502
2120
|
async waitForUserInput(message, world = null) {
|
|
1503
2121
|
if (!message) {
|
|
@@ -1527,13 +2145,22 @@ class StableBrowser {
|
|
|
1527
2145
|
return;
|
|
1528
2146
|
}
|
|
1529
2147
|
// if data file exists, load it
|
|
1530
|
-
const dataFile =
|
|
2148
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1531
2149
|
let data = this.getTestData(world);
|
|
1532
2150
|
// merge the testData with the existing data
|
|
1533
2151
|
Object.assign(data, testData);
|
|
1534
2152
|
// save the data to the file
|
|
1535
2153
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1536
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
|
+
}
|
|
1537
2164
|
_getDataFilePath(fileName) {
|
|
1538
2165
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1539
2166
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1630,14 +2257,12 @@ class StableBrowser {
|
|
|
1630
2257
|
}
|
|
1631
2258
|
}
|
|
1632
2259
|
getTestData(world = null) {
|
|
1633
|
-
|
|
1634
|
-
let data = {};
|
|
1635
|
-
if (fs.existsSync(dataFile)) {
|
|
1636
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1637
|
-
}
|
|
1638
|
-
return data;
|
|
2260
|
+
return _getTestData(world, this.context, this);
|
|
1639
2261
|
}
|
|
1640
2262
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2263
|
+
if (!options) {
|
|
2264
|
+
options = {};
|
|
2265
|
+
}
|
|
1641
2266
|
// collect url/path/title
|
|
1642
2267
|
if (info) {
|
|
1643
2268
|
if (!info.title) {
|
|
@@ -1662,13 +2287,11 @@ class StableBrowser {
|
|
|
1662
2287
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1663
2288
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1664
2289
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
}
|
|
1669
|
-
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");
|
|
1670
2293
|
try {
|
|
1671
|
-
await this.takeScreenshot(screenshotPath);
|
|
2294
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
1672
2295
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1673
2296
|
// // save the buffer to the screenshot path asynchrously
|
|
1674
2297
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -1676,20 +2299,20 @@ class StableBrowser {
|
|
|
1676
2299
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1677
2300
|
// }
|
|
1678
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
|
+
}
|
|
1679
2307
|
}
|
|
1680
2308
|
catch (e) {
|
|
1681
2309
|
this.logger.info("unable to take screenshot, ignored");
|
|
1682
2310
|
}
|
|
1683
|
-
result.screenshotId = nextIndex;
|
|
1684
|
-
result.screenshotPath = screenshotPath;
|
|
1685
|
-
if (info && info.box) {
|
|
1686
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1687
|
-
}
|
|
1688
2311
|
}
|
|
1689
2312
|
else if (options && options.screenshot) {
|
|
1690
2313
|
result.screenshotPath = options.screenshotPath;
|
|
1691
2314
|
try {
|
|
1692
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2315
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
1693
2316
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1694
2317
|
// // save the buffer to the screenshot path asynchrously
|
|
1695
2318
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -1707,7 +2330,7 @@ class StableBrowser {
|
|
|
1707
2330
|
}
|
|
1708
2331
|
return result;
|
|
1709
2332
|
}
|
|
1710
|
-
async takeScreenshot(screenshotPath) {
|
|
2333
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
1711
2334
|
const playContext = this.context.playContext;
|
|
1712
2335
|
// Using CDP to capture the screenshot
|
|
1713
2336
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -1719,17 +2342,20 @@ class StableBrowser {
|
|
|
1719
2342
|
document.documentElement.clientWidth,
|
|
1720
2343
|
])));
|
|
1721
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
|
+
// }
|
|
1722
2354
|
if (this.context.browserName === "chromium") {
|
|
1723
2355
|
const client = await playContext.newCDPSession(this.page);
|
|
1724
2356
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1725
2357
|
format: "png",
|
|
1726
|
-
|
|
1727
|
-
// x: 0,
|
|
1728
|
-
// y: 0,
|
|
1729
|
-
// width: viewportWidth,
|
|
1730
|
-
// height: viewportHeight,
|
|
1731
|
-
// scale: 1,
|
|
1732
|
-
// },
|
|
2358
|
+
captureBeyondViewport: fullPage,
|
|
1733
2359
|
});
|
|
1734
2360
|
await client.detach();
|
|
1735
2361
|
if (!screenshotPath) {
|
|
@@ -1738,8 +2364,12 @@ class StableBrowser {
|
|
|
1738
2364
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
1739
2365
|
}
|
|
1740
2366
|
else {
|
|
1741
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2367
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
1742
2368
|
}
|
|
2369
|
+
// if (focusedElement) {
|
|
2370
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2371
|
+
// await this._unhighlightElements(focusedElement);
|
|
2372
|
+
// }
|
|
1743
2373
|
let image = await Jimp.read(screenshotBuffer);
|
|
1744
2374
|
// Get the image dimensions
|
|
1745
2375
|
const { width, height } = image.bitmap;
|
|
@@ -1752,6 +2382,7 @@ class StableBrowser {
|
|
|
1752
2382
|
else {
|
|
1753
2383
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1754
2384
|
}
|
|
2385
|
+
return screenshotBuffer;
|
|
1755
2386
|
}
|
|
1756
2387
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1757
2388
|
const state = {
|
|
@@ -1774,110 +2405,541 @@ class StableBrowser {
|
|
|
1774
2405
|
await _commandError(state, e, this);
|
|
1775
2406
|
}
|
|
1776
2407
|
finally {
|
|
1777
|
-
_commandFinally(state, this);
|
|
2408
|
+
await _commandFinally(state, this);
|
|
1778
2409
|
}
|
|
1779
2410
|
}
|
|
1780
2411
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
+
};
|
|
1786
2426
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1787
|
-
const info = {};
|
|
1788
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1789
|
-
info.operation = "extract";
|
|
1790
|
-
info.selectors = selectors;
|
|
1791
2427
|
try {
|
|
1792
|
-
|
|
1793
|
-
await this._highlightElements(element);
|
|
1794
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2428
|
+
await _preCommand(state, this);
|
|
1795
2429
|
switch (attribute) {
|
|
1796
2430
|
case "inner_text":
|
|
1797
|
-
|
|
2431
|
+
state.value = await state.element.innerText();
|
|
1798
2432
|
break;
|
|
1799
2433
|
case "href":
|
|
1800
|
-
|
|
2434
|
+
state.value = await state.element.getAttribute("href");
|
|
1801
2435
|
break;
|
|
1802
2436
|
case "value":
|
|
1803
|
-
|
|
2437
|
+
state.value = await state.element.inputValue();
|
|
2438
|
+
break;
|
|
2439
|
+
case "text":
|
|
2440
|
+
state.value = await state.element.textContent();
|
|
1804
2441
|
break;
|
|
1805
2442
|
default:
|
|
1806
|
-
|
|
2443
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1807
2444
|
break;
|
|
1808
2445
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
+
}
|
|
1812
2463
|
}
|
|
1813
|
-
|
|
1814
|
-
this.
|
|
1815
|
-
|
|
2464
|
+
state.info.value = state.value;
|
|
2465
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2466
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2467
|
+
if (process.env.MODE === "executions") {
|
|
2468
|
+
const globalDataFile = "global_test_data.json";
|
|
2469
|
+
if (existsSync(globalDataFile)) {
|
|
2470
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2474
|
+
return state.info;
|
|
1816
2475
|
}
|
|
1817
2476
|
catch (e) {
|
|
1818
|
-
|
|
1819
|
-
this.logger.error("extract failed " + info.log);
|
|
1820
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1821
|
-
info.screenshotPath = screenshotPath;
|
|
1822
|
-
Object.assign(e, { info: info });
|
|
1823
|
-
error = e;
|
|
1824
|
-
throw e;
|
|
2477
|
+
await _commandError(state, e, this);
|
|
1825
2478
|
}
|
|
1826
2479
|
finally {
|
|
1827
|
-
|
|
1828
|
-
this._reportToWorld(world, {
|
|
1829
|
-
element_name: selectors.element_name,
|
|
1830
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1831
|
-
variable: variable,
|
|
1832
|
-
value: info.value,
|
|
1833
|
-
text: "Extract attribute from element",
|
|
1834
|
-
screenshotId,
|
|
1835
|
-
result: error
|
|
1836
|
-
? {
|
|
1837
|
-
status: "FAILED",
|
|
1838
|
-
startTime,
|
|
1839
|
-
endTime,
|
|
1840
|
-
message: error?.message,
|
|
1841
|
-
}
|
|
1842
|
-
: {
|
|
1843
|
-
status: "PASSED",
|
|
1844
|
-
startTime,
|
|
1845
|
-
endTime,
|
|
1846
|
-
},
|
|
1847
|
-
info: info,
|
|
1848
|
-
});
|
|
2480
|
+
await _commandFinally(state, this);
|
|
1849
2481
|
}
|
|
1850
2482
|
}
|
|
1851
|
-
async
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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
|
+
}
|
|
1862
2540
|
}
|
|
2541
|
+
state.info.value = state.value;
|
|
2542
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2543
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2544
|
+
if (process.env.MODE === "executions") {
|
|
2545
|
+
const globalDataFile = "global_test_data.json";
|
|
2546
|
+
if (existsSync(globalDataFile)) {
|
|
2547
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2551
|
+
return state.info;
|
|
1863
2552
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
if (options && options.timeout) {
|
|
1867
|
-
timeout = options.timeout;
|
|
2553
|
+
catch (e) {
|
|
2554
|
+
await _commandError(state, e, this);
|
|
1868
2555
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
+
};
|
|
1881
2943
|
let errorCount = 0;
|
|
1882
2944
|
while (true) {
|
|
1883
2945
|
try {
|
|
@@ -1911,6 +2973,12 @@ class StableBrowser {
|
|
|
1911
2973
|
emailUrl = url;
|
|
1912
2974
|
codeOrUrlFound = true;
|
|
1913
2975
|
}
|
|
2976
|
+
if (process.env.MODE === "executions") {
|
|
2977
|
+
const globalDataFile = "global_test_data.json";
|
|
2978
|
+
if (existsSync(globalDataFile)) {
|
|
2979
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
1914
2982
|
if (codeOrUrlFound) {
|
|
1915
2983
|
return { emailUrl, emailCode };
|
|
1916
2984
|
}
|
|
@@ -1922,7 +2990,8 @@ class StableBrowser {
|
|
|
1922
2990
|
catch (e) {
|
|
1923
2991
|
errorCount++;
|
|
1924
2992
|
if (errorCount > 3) {
|
|
1925
|
-
throw e;
|
|
2993
|
+
// throw e;
|
|
2994
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1926
2995
|
}
|
|
1927
2996
|
// ignore
|
|
1928
2997
|
}
|
|
@@ -1936,27 +3005,32 @@ class StableBrowser {
|
|
|
1936
3005
|
async _highlightElements(scope, css) {
|
|
1937
3006
|
try {
|
|
1938
3007
|
if (!scope) {
|
|
3008
|
+
// console.log(`Scope is not defined`);
|
|
1939
3009
|
return;
|
|
1940
3010
|
}
|
|
1941
3011
|
if (!css) {
|
|
1942
3012
|
scope
|
|
1943
3013
|
.evaluate((node) => {
|
|
1944
3014
|
if (node && node.style) {
|
|
1945
|
-
let
|
|
1946
|
-
|
|
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}`);
|
|
1947
3020
|
if (window) {
|
|
1948
3021
|
window.addEventListener("beforeunload", function (e) {
|
|
1949
|
-
node.style.
|
|
3022
|
+
node.style.outline = originalOutline;
|
|
1950
3023
|
});
|
|
1951
3024
|
}
|
|
1952
3025
|
setTimeout(function () {
|
|
1953
|
-
node.style.
|
|
3026
|
+
node.style.outline = originalOutline;
|
|
1954
3027
|
}, 2000);
|
|
1955
3028
|
}
|
|
1956
3029
|
})
|
|
1957
3030
|
.then(() => { })
|
|
1958
3031
|
.catch((e) => {
|
|
1959
3032
|
// ignore
|
|
3033
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1960
3034
|
});
|
|
1961
3035
|
}
|
|
1962
3036
|
else {
|
|
@@ -1972,17 +3046,18 @@ class StableBrowser {
|
|
|
1972
3046
|
if (!element.style) {
|
|
1973
3047
|
return;
|
|
1974
3048
|
}
|
|
1975
|
-
|
|
3049
|
+
let originalOutline = element.style.outline;
|
|
3050
|
+
element.__previousOutline = originalOutline;
|
|
1976
3051
|
// Set the new border to be red and 2px solid
|
|
1977
|
-
element.style.
|
|
3052
|
+
element.style.outline = "2px solid red";
|
|
1978
3053
|
if (window) {
|
|
1979
3054
|
window.addEventListener("beforeunload", function (e) {
|
|
1980
|
-
element.style.
|
|
3055
|
+
element.style.outline = originalOutline;
|
|
1981
3056
|
});
|
|
1982
3057
|
}
|
|
1983
3058
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1984
3059
|
setTimeout(function () {
|
|
1985
|
-
element.style.
|
|
3060
|
+
element.style.outline = originalOutline;
|
|
1986
3061
|
}, 2000);
|
|
1987
3062
|
}
|
|
1988
3063
|
return;
|
|
@@ -1990,6 +3065,7 @@ class StableBrowser {
|
|
|
1990
3065
|
.then(() => { })
|
|
1991
3066
|
.catch((e) => {
|
|
1992
3067
|
// ignore
|
|
3068
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1993
3069
|
});
|
|
1994
3070
|
}
|
|
1995
3071
|
}
|
|
@@ -1997,8 +3073,49 @@ class StableBrowser {
|
|
|
1997
3073
|
console.debug(error);
|
|
1998
3074
|
}
|
|
1999
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
|
+
*/
|
|
2000
3118
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2001
|
-
const startTime = Date.now();
|
|
2002
3119
|
let error = null;
|
|
2003
3120
|
let screenshotId = null;
|
|
2004
3121
|
let screenshotPath = null;
|
|
@@ -2012,159 +3129,534 @@ class StableBrowser {
|
|
|
2012
3129
|
pathPart = newValue;
|
|
2013
3130
|
}
|
|
2014
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
|
+
};
|
|
2015
3146
|
try {
|
|
3147
|
+
await _preCommand(state, this);
|
|
3148
|
+
state.info.text = queryText;
|
|
2016
3149
|
for (let i = 0; i < 30; i++) {
|
|
2017
3150
|
const url = await this.page.url();
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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`);
|
|
2021
3426
|
}
|
|
2022
3427
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2023
3428
|
continue;
|
|
2024
3429
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
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
|
+
}
|
|
2027
3447
|
}
|
|
2028
3448
|
}
|
|
2029
3449
|
catch (e) {
|
|
2030
|
-
|
|
2031
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2032
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2033
|
-
info.screenshotPath = screenshotPath;
|
|
2034
|
-
Object.assign(e, { info: info });
|
|
2035
|
-
error = e;
|
|
2036
|
-
throw e;
|
|
3450
|
+
await _commandError(state, e, this);
|
|
2037
3451
|
}
|
|
2038
3452
|
finally {
|
|
2039
|
-
|
|
2040
|
-
this._reportToWorld(world, {
|
|
2041
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2042
|
-
text: "Verify page path",
|
|
2043
|
-
screenshotId,
|
|
2044
|
-
result: error
|
|
2045
|
-
? {
|
|
2046
|
-
status: "FAILED",
|
|
2047
|
-
startTime,
|
|
2048
|
-
endTime,
|
|
2049
|
-
message: error?.message,
|
|
2050
|
-
}
|
|
2051
|
-
: {
|
|
2052
|
-
status: "PASSED",
|
|
2053
|
-
startTime,
|
|
2054
|
-
endTime,
|
|
2055
|
-
},
|
|
2056
|
-
info: info,
|
|
2057
|
-
});
|
|
3453
|
+
await _commandFinally(state, this);
|
|
2058
3454
|
}
|
|
2059
3455
|
}
|
|
2060
|
-
async
|
|
3456
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2061
3457
|
text = unEscapeString(text);
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
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);
|
|
2067
3475
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2068
|
-
const info = {};
|
|
2069
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2070
|
-
info.operation = "verifyTextExistInPage";
|
|
2071
3476
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2072
3477
|
if (newValue !== text) {
|
|
2073
3478
|
this.logger.info(text + "=" + newValue);
|
|
2074
3479
|
text = newValue;
|
|
2075
3480
|
}
|
|
2076
|
-
|
|
2077
|
-
let
|
|
2078
|
-
|
|
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;
|
|
2079
3546
|
try {
|
|
3547
|
+
await _preCommand(state, this);
|
|
3548
|
+
state.info.text = textToVerify;
|
|
3549
|
+
let resultWithElementsFound = {
|
|
3550
|
+
length: 0,
|
|
3551
|
+
};
|
|
2080
3552
|
while (true) {
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
|
|
2087
|
-
result.frame = frames[i];
|
|
2088
|
-
results.push(result);
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
else if (numberAlternatives.number) {
|
|
2092
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2093
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
|
|
2094
|
-
result.frame = frames[i];
|
|
2095
|
-
results.push(result);
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
else {
|
|
2099
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
|
|
2100
|
-
result.frame = frames[i];
|
|
2101
|
-
results.push(result);
|
|
2102
|
-
}
|
|
3553
|
+
try {
|
|
3554
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3555
|
+
}
|
|
3556
|
+
catch (error) {
|
|
3557
|
+
// ignore
|
|
2103
3558
|
}
|
|
2104
|
-
info.results = results;
|
|
2105
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2106
3559
|
if (resultWithElementsFound.length === 0) {
|
|
2107
|
-
if (Date.now() - startTime > timeout) {
|
|
2108
|
-
throw new Error(`Text ${
|
|
3560
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3561
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2109
3562
|
}
|
|
2110
3563
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2111
3564
|
continue;
|
|
2112
3565
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
+
}
|
|
2121
3622
|
}
|
|
2122
3623
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
3624
|
+
catch (error) {
|
|
3625
|
+
console.error(error);
|
|
3626
|
+
}
|
|
2125
3627
|
}
|
|
2126
3628
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2127
3629
|
}
|
|
2128
3630
|
catch (e) {
|
|
2129
|
-
|
|
2130
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2131
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2132
|
-
info.screenshotPath = screenshotPath;
|
|
2133
|
-
Object.assign(e, { info: info });
|
|
2134
|
-
error = e;
|
|
2135
|
-
throw e;
|
|
3631
|
+
await _commandError(state, e, this);
|
|
2136
3632
|
}
|
|
2137
3633
|
finally {
|
|
2138
|
-
|
|
2139
|
-
this._reportToWorld(world, {
|
|
2140
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2141
|
-
text: "Verify text exists in page",
|
|
2142
|
-
screenshotId,
|
|
2143
|
-
result: error
|
|
2144
|
-
? {
|
|
2145
|
-
status: "FAILED",
|
|
2146
|
-
startTime,
|
|
2147
|
-
endTime,
|
|
2148
|
-
message: error?.message,
|
|
2149
|
-
}
|
|
2150
|
-
: {
|
|
2151
|
-
status: "PASSED",
|
|
2152
|
-
startTime,
|
|
2153
|
-
endTime,
|
|
2154
|
-
},
|
|
2155
|
-
info: info,
|
|
2156
|
-
});
|
|
3634
|
+
await _commandFinally(state, this);
|
|
2157
3635
|
}
|
|
2158
3636
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
+
}
|
|
2166
3656
|
}
|
|
2167
|
-
|
|
3657
|
+
// state.info.results = results;
|
|
3658
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3659
|
+
return resultWithElementsFound;
|
|
2168
3660
|
}
|
|
2169
3661
|
async visualVerification(text, options = {}, world = null) {
|
|
2170
3662
|
const startTime = Date.now();
|
|
@@ -2180,14 +3672,17 @@ class StableBrowser {
|
|
|
2180
3672
|
throw new Error("TOKEN is not set");
|
|
2181
3673
|
}
|
|
2182
3674
|
try {
|
|
2183
|
-
let serviceUrl =
|
|
3675
|
+
let serviceUrl = _getServerUrl();
|
|
2184
3676
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2185
3677
|
info.screenshotPath = screenshotPath;
|
|
2186
3678
|
const screenshot = await this.takeScreenshot();
|
|
2187
|
-
|
|
2188
|
-
method: "
|
|
3679
|
+
let request = {
|
|
3680
|
+
method: "post",
|
|
3681
|
+
maxBodyLength: Infinity,
|
|
2189
3682
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2190
3683
|
headers: {
|
|
3684
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3685
|
+
"x-source": "aaa",
|
|
2191
3686
|
"Content-Type": "application/json",
|
|
2192
3687
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2193
3688
|
},
|
|
@@ -2196,7 +3691,7 @@ class StableBrowser {
|
|
|
2196
3691
|
screenshot: screenshot,
|
|
2197
3692
|
}),
|
|
2198
3693
|
};
|
|
2199
|
-
|
|
3694
|
+
const result = await axios.request(request);
|
|
2200
3695
|
if (result.data.status !== true) {
|
|
2201
3696
|
throw new Error("Visual validation failed");
|
|
2202
3697
|
}
|
|
@@ -2216,13 +3711,15 @@ class StableBrowser {
|
|
|
2216
3711
|
info.screenshotPath = screenshotPath;
|
|
2217
3712
|
Object.assign(e, { info: info });
|
|
2218
3713
|
error = e;
|
|
2219
|
-
throw e;
|
|
3714
|
+
// throw e;
|
|
3715
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2220
3716
|
}
|
|
2221
3717
|
finally {
|
|
2222
3718
|
const endTime = Date.now();
|
|
2223
|
-
|
|
3719
|
+
_reportToWorld(world, {
|
|
2224
3720
|
type: Types.VERIFY_VISUAL,
|
|
2225
3721
|
text: "Visual verification",
|
|
3722
|
+
_text: "Visual verification of " + text,
|
|
2226
3723
|
screenshotId,
|
|
2227
3724
|
result: error
|
|
2228
3725
|
? {
|
|
@@ -2268,6 +3765,7 @@ class StableBrowser {
|
|
|
2268
3765
|
let screenshotPath = null;
|
|
2269
3766
|
const info = {};
|
|
2270
3767
|
info.log = "";
|
|
3768
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2271
3769
|
info.operation = "getTableData";
|
|
2272
3770
|
info.selectors = selectors;
|
|
2273
3771
|
try {
|
|
@@ -2283,11 +3781,12 @@ class StableBrowser {
|
|
|
2283
3781
|
info.screenshotPath = screenshotPath;
|
|
2284
3782
|
Object.assign(e, { info: info });
|
|
2285
3783
|
error = e;
|
|
2286
|
-
throw e;
|
|
3784
|
+
// throw e;
|
|
3785
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2287
3786
|
}
|
|
2288
3787
|
finally {
|
|
2289
3788
|
const endTime = Date.now();
|
|
2290
|
-
|
|
3789
|
+
_reportToWorld(world, {
|
|
2291
3790
|
element_name: selectors.element_name,
|
|
2292
3791
|
type: Types.GET_TABLE_DATA,
|
|
2293
3792
|
text: "Get table data",
|
|
@@ -2342,7 +3841,7 @@ class StableBrowser {
|
|
|
2342
3841
|
info.operation = "analyzeTable";
|
|
2343
3842
|
info.selectors = selectors;
|
|
2344
3843
|
info.query = query;
|
|
2345
|
-
query =
|
|
3844
|
+
query = _fixUsingParams(query, _params);
|
|
2346
3845
|
info.query_fixed = query;
|
|
2347
3846
|
info.operator = operator;
|
|
2348
3847
|
info.value = value;
|
|
@@ -2448,11 +3947,12 @@ class StableBrowser {
|
|
|
2448
3947
|
info.screenshotPath = screenshotPath;
|
|
2449
3948
|
Object.assign(e, { info: info });
|
|
2450
3949
|
error = e;
|
|
2451
|
-
throw e;
|
|
3950
|
+
// throw e;
|
|
3951
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2452
3952
|
}
|
|
2453
3953
|
finally {
|
|
2454
3954
|
const endTime = Date.now();
|
|
2455
|
-
|
|
3955
|
+
_reportToWorld(world, {
|
|
2456
3956
|
element_name: selectors.element_name,
|
|
2457
3957
|
type: Types.ANALYZE_TABLE,
|
|
2458
3958
|
text: "Analyze table",
|
|
@@ -2473,8 +3973,51 @@ class StableBrowser {
|
|
|
2473
3973
|
});
|
|
2474
3974
|
}
|
|
2475
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
|
+
}
|
|
2476
4013
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2477
|
-
|
|
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
|
+
}
|
|
2478
4021
|
}
|
|
2479
4022
|
_getLoadTimeout(options) {
|
|
2480
4023
|
let timeout = 15000;
|
|
@@ -2486,7 +4029,54 @@ class StableBrowser {
|
|
|
2486
4029
|
}
|
|
2487
4030
|
return timeout;
|
|
2488
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
|
+
}
|
|
2489
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");
|
|
2490
4080
|
let timeout = this._getLoadTimeout(options);
|
|
2491
4081
|
const promiseArray = [];
|
|
2492
4082
|
// let waitForNetworkIdle = true;
|
|
@@ -2519,13 +4109,15 @@ class StableBrowser {
|
|
|
2519
4109
|
else if (e.label === "domcontentloaded") {
|
|
2520
4110
|
console.log("waited for the domcontent loaded timeout");
|
|
2521
4111
|
}
|
|
2522
|
-
console.log(".");
|
|
2523
4112
|
}
|
|
2524
4113
|
finally {
|
|
2525
|
-
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
|
+
}
|
|
2526
4118
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2527
4119
|
const endTime = Date.now();
|
|
2528
|
-
|
|
4120
|
+
_reportToWorld(world, {
|
|
2529
4121
|
type: Types.GET_PAGE_STATUS,
|
|
2530
4122
|
text: "Wait for page load",
|
|
2531
4123
|
screenshotId,
|
|
@@ -2545,40 +4137,138 @@ class StableBrowser {
|
|
|
2545
4137
|
}
|
|
2546
4138
|
}
|
|
2547
4139
|
async closePage(options = {}, world = null) {
|
|
2548
|
-
const
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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
|
+
};
|
|
2553
4153
|
try {
|
|
4154
|
+
await _preCommand(state, this);
|
|
2554
4155
|
await this.page.close();
|
|
2555
4156
|
}
|
|
2556
4157
|
catch (e) {
|
|
2557
|
-
|
|
4158
|
+
await _commandError(state, e, this);
|
|
2558
4159
|
}
|
|
2559
4160
|
finally {
|
|
2560
|
-
await
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
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;
|
|
2573
4196
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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;
|
|
2581
4269
|
}
|
|
4270
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4271
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2582
4272
|
}
|
|
2583
4273
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2584
4274
|
const startTime = Date.now();
|
|
@@ -2596,15 +4286,16 @@ class StableBrowser {
|
|
|
2596
4286
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2597
4287
|
}
|
|
2598
4288
|
catch (e) {
|
|
2599
|
-
|
|
4289
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2600
4290
|
}
|
|
2601
4291
|
finally {
|
|
2602
4292
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2603
4293
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2604
4294
|
const endTime = Date.now();
|
|
2605
|
-
|
|
4295
|
+
_reportToWorld(world, {
|
|
2606
4296
|
type: Types.SET_VIEWPORT,
|
|
2607
4297
|
text: "set viewport size to " + width + "x" + hight,
|
|
4298
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2608
4299
|
screenshotId,
|
|
2609
4300
|
result: error
|
|
2610
4301
|
? {
|
|
@@ -2632,13 +4323,13 @@ class StableBrowser {
|
|
|
2632
4323
|
await this.page.reload();
|
|
2633
4324
|
}
|
|
2634
4325
|
catch (e) {
|
|
2635
|
-
|
|
4326
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2636
4327
|
}
|
|
2637
4328
|
finally {
|
|
2638
4329
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2639
4330
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2640
4331
|
const endTime = Date.now();
|
|
2641
|
-
|
|
4332
|
+
_reportToWorld(world, {
|
|
2642
4333
|
type: Types.GET_PAGE_STATUS,
|
|
2643
4334
|
text: "page relaod",
|
|
2644
4335
|
screenshotId,
|
|
@@ -2674,11 +4365,254 @@ class StableBrowser {
|
|
|
2674
4365
|
console.log("#-#");
|
|
2675
4366
|
}
|
|
2676
4367
|
}
|
|
2677
|
-
|
|
2678
|
-
if (
|
|
2679
|
-
|
|
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();
|
|
2680
4615
|
}
|
|
2681
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2682
4616
|
}
|
|
2683
4617
|
}
|
|
2684
4618
|
function createTimedPromise(promise, label) {
|
|
@@ -2686,156 +4620,5 @@ function createTimedPromise(promise, label) {
|
|
|
2686
4620
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2687
4621
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2688
4622
|
}
|
|
2689
|
-
const KEYBOARD_EVENTS = [
|
|
2690
|
-
"ALT",
|
|
2691
|
-
"AltGraph",
|
|
2692
|
-
"CapsLock",
|
|
2693
|
-
"Control",
|
|
2694
|
-
"Fn",
|
|
2695
|
-
"FnLock",
|
|
2696
|
-
"Hyper",
|
|
2697
|
-
"Meta",
|
|
2698
|
-
"NumLock",
|
|
2699
|
-
"ScrollLock",
|
|
2700
|
-
"Shift",
|
|
2701
|
-
"Super",
|
|
2702
|
-
"Symbol",
|
|
2703
|
-
"SymbolLock",
|
|
2704
|
-
"Enter",
|
|
2705
|
-
"Tab",
|
|
2706
|
-
"ArrowDown",
|
|
2707
|
-
"ArrowLeft",
|
|
2708
|
-
"ArrowRight",
|
|
2709
|
-
"ArrowUp",
|
|
2710
|
-
"End",
|
|
2711
|
-
"Home",
|
|
2712
|
-
"PageDown",
|
|
2713
|
-
"PageUp",
|
|
2714
|
-
"Backspace",
|
|
2715
|
-
"Clear",
|
|
2716
|
-
"Copy",
|
|
2717
|
-
"CrSel",
|
|
2718
|
-
"Cut",
|
|
2719
|
-
"Delete",
|
|
2720
|
-
"EraseEof",
|
|
2721
|
-
"ExSel",
|
|
2722
|
-
"Insert",
|
|
2723
|
-
"Paste",
|
|
2724
|
-
"Redo",
|
|
2725
|
-
"Undo",
|
|
2726
|
-
"Accept",
|
|
2727
|
-
"Again",
|
|
2728
|
-
"Attn",
|
|
2729
|
-
"Cancel",
|
|
2730
|
-
"ContextMenu",
|
|
2731
|
-
"Escape",
|
|
2732
|
-
"Execute",
|
|
2733
|
-
"Find",
|
|
2734
|
-
"Finish",
|
|
2735
|
-
"Help",
|
|
2736
|
-
"Pause",
|
|
2737
|
-
"Play",
|
|
2738
|
-
"Props",
|
|
2739
|
-
"Select",
|
|
2740
|
-
"ZoomIn",
|
|
2741
|
-
"ZoomOut",
|
|
2742
|
-
"BrightnessDown",
|
|
2743
|
-
"BrightnessUp",
|
|
2744
|
-
"Eject",
|
|
2745
|
-
"LogOff",
|
|
2746
|
-
"Power",
|
|
2747
|
-
"PowerOff",
|
|
2748
|
-
"PrintScreen",
|
|
2749
|
-
"Hibernate",
|
|
2750
|
-
"Standby",
|
|
2751
|
-
"WakeUp",
|
|
2752
|
-
"AllCandidates",
|
|
2753
|
-
"Alphanumeric",
|
|
2754
|
-
"CodeInput",
|
|
2755
|
-
"Compose",
|
|
2756
|
-
"Convert",
|
|
2757
|
-
"Dead",
|
|
2758
|
-
"FinalMode",
|
|
2759
|
-
"GroupFirst",
|
|
2760
|
-
"GroupLast",
|
|
2761
|
-
"GroupNext",
|
|
2762
|
-
"GroupPrevious",
|
|
2763
|
-
"ModeChange",
|
|
2764
|
-
"NextCandidate",
|
|
2765
|
-
"NonConvert",
|
|
2766
|
-
"PreviousCandidate",
|
|
2767
|
-
"Process",
|
|
2768
|
-
"SingleCandidate",
|
|
2769
|
-
"HangulMode",
|
|
2770
|
-
"HanjaMode",
|
|
2771
|
-
"JunjaMode",
|
|
2772
|
-
"Eisu",
|
|
2773
|
-
"Hankaku",
|
|
2774
|
-
"Hiragana",
|
|
2775
|
-
"HiraganaKatakana",
|
|
2776
|
-
"KanaMode",
|
|
2777
|
-
"KanjiMode",
|
|
2778
|
-
"Katakana",
|
|
2779
|
-
"Romaji",
|
|
2780
|
-
"Zenkaku",
|
|
2781
|
-
"ZenkakuHanaku",
|
|
2782
|
-
"F1",
|
|
2783
|
-
"F2",
|
|
2784
|
-
"F3",
|
|
2785
|
-
"F4",
|
|
2786
|
-
"F5",
|
|
2787
|
-
"F6",
|
|
2788
|
-
"F7",
|
|
2789
|
-
"F8",
|
|
2790
|
-
"F9",
|
|
2791
|
-
"F10",
|
|
2792
|
-
"F11",
|
|
2793
|
-
"F12",
|
|
2794
|
-
"Soft1",
|
|
2795
|
-
"Soft2",
|
|
2796
|
-
"Soft3",
|
|
2797
|
-
"Soft4",
|
|
2798
|
-
"ChannelDown",
|
|
2799
|
-
"ChannelUp",
|
|
2800
|
-
"Close",
|
|
2801
|
-
"MailForward",
|
|
2802
|
-
"MailReply",
|
|
2803
|
-
"MailSend",
|
|
2804
|
-
"MediaFastForward",
|
|
2805
|
-
"MediaPause",
|
|
2806
|
-
"MediaPlay",
|
|
2807
|
-
"MediaPlayPause",
|
|
2808
|
-
"MediaRecord",
|
|
2809
|
-
"MediaRewind",
|
|
2810
|
-
"MediaStop",
|
|
2811
|
-
"MediaTrackNext",
|
|
2812
|
-
"MediaTrackPrevious",
|
|
2813
|
-
"AudioBalanceLeft",
|
|
2814
|
-
"AudioBalanceRight",
|
|
2815
|
-
"AudioBassBoostDown",
|
|
2816
|
-
"AudioBassBoostToggle",
|
|
2817
|
-
"AudioBassBoostUp",
|
|
2818
|
-
"AudioFaderFront",
|
|
2819
|
-
"AudioFaderRear",
|
|
2820
|
-
"AudioSurroundModeNext",
|
|
2821
|
-
"AudioTrebleDown",
|
|
2822
|
-
"AudioTrebleUp",
|
|
2823
|
-
"AudioVolumeDown",
|
|
2824
|
-
"AudioVolumeMute",
|
|
2825
|
-
"AudioVolumeUp",
|
|
2826
|
-
"MicrophoneToggle",
|
|
2827
|
-
"MicrophoneVolumeDown",
|
|
2828
|
-
"MicrophoneVolumeMute",
|
|
2829
|
-
"MicrophoneVolumeUp",
|
|
2830
|
-
"TV",
|
|
2831
|
-
"TV3DMode",
|
|
2832
|
-
"TVAntennaCable",
|
|
2833
|
-
"TVAudioDescription",
|
|
2834
|
-
];
|
|
2835
|
-
function unEscapeString(str) {
|
|
2836
|
-
const placeholder = "__NEWLINE__";
|
|
2837
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2838
|
-
return str;
|
|
2839
|
-
}
|
|
2840
4623
|
export { StableBrowser };
|
|
2841
4624
|
//# sourceMappingURL=stable_browser.js.map
|