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