automation_model 1.0.492-dev → 1.0.492
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 +292 -17
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +6 -3
- package/lib/browser_manager.js +202 -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 -2
- package/lib/network.js +439 -68
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +83 -0
- package/lib/route.js +691 -0
- package/lib/route.js.map +1 -0
- package/lib/scripts/axe.mini.js +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 -823
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +15 -0
- package/lib/table.js +257 -0
- package/lib/table.js.map +1 -0
- package/lib/table_analyze.js.map +1 -1
- package/lib/table_helper.d.ts +19 -0
- package/lib/table_helper.js +130 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +6 -0
- package/lib/test_context.js +5 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +38 -3
- package/lib/utils.js +747 -35
- package/lib/utils.js.map +1 -1
- package/package.json +31 -13
- package/lib/axe/axe.mini.js +0 -12
package/lib/stable_browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
+
import { check_performance } from "./check_performance.js";
|
|
2
3
|
import { expect } from "@playwright/test";
|
|
3
4
|
import dayjs from "dayjs";
|
|
4
5
|
import fs from "fs";
|
|
@@ -10,59 +11,102 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
11
|
import drawRectangle from "./drawRect.js";
|
|
11
12
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
13
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
+
import errorStackParser from "error-stack-parser";
|
|
15
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
14
16
|
import csv from "csv-parser";
|
|
15
17
|
import { Readable } from "node:stream";
|
|
16
18
|
import readline from "readline";
|
|
17
|
-
import { getContext } from "./init_browser.js";
|
|
19
|
+
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
20
|
+
import { getTestData } from "./auto_page.js";
|
|
18
21
|
import { locate_element } from "./locate_element.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
+
import { randomUUID } from "crypto";
|
|
23
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
24
|
+
import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
25
|
+
import { LocatorLog } from "./locator_log.js";
|
|
26
|
+
import axios from "axios";
|
|
27
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
28
|
+
import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
29
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
30
|
+
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
32
|
+
export const Types = {
|
|
22
33
|
CLICK: "click_element",
|
|
34
|
+
WAIT_ELEMENT: "wait_element",
|
|
23
35
|
NAVIGATE: "navigate",
|
|
36
|
+
GO_BACK: "go_back",
|
|
37
|
+
GO_FORWARD: "go_forward",
|
|
24
38
|
FILL: "fill_element",
|
|
25
|
-
EXECUTE: "execute_page_method",
|
|
26
|
-
OPEN: "open_environment",
|
|
39
|
+
EXECUTE: "execute_page_method", //
|
|
40
|
+
OPEN: "open_environment", //
|
|
27
41
|
COMPLETE: "step_complete",
|
|
28
42
|
ASK: "information_needed",
|
|
29
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
30
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
43
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
44
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
31
45
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
46
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
47
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
32
48
|
ANALYZE_TABLE: "analyze_table",
|
|
33
|
-
SELECT: "select_combobox",
|
|
49
|
+
SELECT: "select_combobox", //
|
|
50
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
34
51
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
52
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
35
53
|
TYPE_PRESS: "type_press",
|
|
36
54
|
PRESS: "press_key",
|
|
37
55
|
HOVER: "hover_element",
|
|
38
56
|
CHECK: "check_element",
|
|
39
57
|
UNCHECK: "uncheck_element",
|
|
40
58
|
EXTRACT: "extract_attribute",
|
|
59
|
+
EXTRACT_PROPERTY: "extract_property",
|
|
41
60
|
CLOSE_PAGE: "close_page",
|
|
61
|
+
TABLE_OPERATION: "table_operation",
|
|
42
62
|
SET_DATE_TIME: "set_date_time",
|
|
43
63
|
SET_VIEWPORT: "set_viewport",
|
|
44
64
|
VERIFY_VISUAL: "verify_visual",
|
|
45
65
|
LOAD_DATA: "load_data",
|
|
46
66
|
SET_INPUT: "set_input",
|
|
67
|
+
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
68
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
69
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
70
|
+
BRUNO: "bruno",
|
|
71
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
72
|
+
SET_INPUT_FILES: "set_input_files",
|
|
73
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
74
|
+
REPORT_COMMAND: "report_command",
|
|
75
|
+
STEP_COMPLETE: "step_complete",
|
|
76
|
+
SLEEP: "sleep",
|
|
77
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
47
78
|
};
|
|
48
79
|
export const apps = {};
|
|
80
|
+
const formatElementName = (elementName) => {
|
|
81
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
82
|
+
};
|
|
49
83
|
class StableBrowser {
|
|
50
84
|
browser;
|
|
51
85
|
page;
|
|
52
86
|
logger;
|
|
53
87
|
context;
|
|
54
88
|
world;
|
|
89
|
+
fastMode;
|
|
90
|
+
stepTags;
|
|
55
91
|
project_path = null;
|
|
56
92
|
webLogFile = null;
|
|
57
93
|
networkLogger = null;
|
|
58
94
|
configuration = null;
|
|
59
95
|
appName = "main";
|
|
60
|
-
|
|
96
|
+
tags = null;
|
|
97
|
+
isRecording = false;
|
|
98
|
+
initSnapshotTaken = false;
|
|
99
|
+
onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
|
|
100
|
+
// set to true if the step issue a report
|
|
101
|
+
inStepReport = false;
|
|
102
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
|
|
61
103
|
this.browser = browser;
|
|
62
104
|
this.page = page;
|
|
63
105
|
this.logger = logger;
|
|
64
106
|
this.context = context;
|
|
65
107
|
this.world = world;
|
|
108
|
+
this.fastMode = fastMode;
|
|
109
|
+
this.stepTags = stepTags;
|
|
66
110
|
if (!this.logger) {
|
|
67
111
|
this.logger = console;
|
|
68
112
|
}
|
|
@@ -91,22 +135,54 @@ class StableBrowser {
|
|
|
91
135
|
context.pages = [this.page];
|
|
92
136
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
93
137
|
this.world = world;
|
|
138
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
139
|
+
this.fastMode = true;
|
|
140
|
+
}
|
|
141
|
+
if (process.env.FAST_MODE === "true") {
|
|
142
|
+
// console.log("Fast mode enabled from environment variable");
|
|
143
|
+
this.fastMode = true;
|
|
144
|
+
}
|
|
145
|
+
if (process.env.FAST_MODE === "false") {
|
|
146
|
+
this.fastMode = false;
|
|
147
|
+
}
|
|
148
|
+
if (this.context) {
|
|
149
|
+
this.context.fastMode = this.fastMode;
|
|
150
|
+
}
|
|
94
151
|
this.registerEventListeners(this.context);
|
|
95
152
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
153
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
96
154
|
}
|
|
97
155
|
registerEventListeners(context) {
|
|
98
156
|
this.registerConsoleLogListener(this.page, context);
|
|
99
|
-
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
157
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
100
158
|
if (!context.pageLoading) {
|
|
101
159
|
context.pageLoading = { status: false };
|
|
102
160
|
}
|
|
161
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
162
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
163
|
+
}
|
|
103
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
|
+
}
|
|
104
170
|
context.pageLoading.status = true;
|
|
105
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
|
+
}
|
|
106
180
|
context.page = page;
|
|
107
181
|
context.pages.push(page);
|
|
108
182
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
183
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
109
184
|
page.on("close", async () => {
|
|
185
|
+
// return if browser context is already closed
|
|
110
186
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
111
187
|
this.context.pages.pop();
|
|
112
188
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
@@ -116,7 +192,12 @@ class StableBrowser {
|
|
|
116
192
|
console.log("Switched to page " + title);
|
|
117
193
|
}
|
|
118
194
|
catch (error) {
|
|
119
|
-
|
|
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
|
+
}
|
|
120
201
|
}
|
|
121
202
|
}
|
|
122
203
|
});
|
|
@@ -125,7 +206,12 @@ class StableBrowser {
|
|
|
125
206
|
console.log("Switch page: " + (await page.title()));
|
|
126
207
|
}
|
|
127
208
|
catch (e) {
|
|
128
|
-
|
|
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
|
+
}
|
|
129
215
|
}
|
|
130
216
|
context.pageLoading.status = false;
|
|
131
217
|
}.bind(this));
|
|
@@ -137,7 +223,7 @@ class StableBrowser {
|
|
|
137
223
|
}
|
|
138
224
|
let newContextCreated = false;
|
|
139
225
|
if (!apps[appName]) {
|
|
140
|
-
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);
|
|
141
227
|
newContextCreated = true;
|
|
142
228
|
apps[appName] = {
|
|
143
229
|
context: newContext,
|
|
@@ -146,31 +232,41 @@ class StableBrowser {
|
|
|
146
232
|
};
|
|
147
233
|
}
|
|
148
234
|
const tempContext = {};
|
|
149
|
-
|
|
150
|
-
|
|
235
|
+
_copyContext(this, tempContext);
|
|
236
|
+
_copyContext(apps[appName], this);
|
|
151
237
|
apps[this.appName] = tempContext;
|
|
152
238
|
this.appName = appName;
|
|
153
239
|
if (newContextCreated) {
|
|
154
240
|
this.registerEventListeners(this.context);
|
|
155
241
|
await this.goto(this.context.environment.baseUrl);
|
|
156
|
-
|
|
242
|
+
if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
|
|
243
|
+
await this.waitForPageLoad();
|
|
244
|
+
}
|
|
157
245
|
}
|
|
158
246
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
257
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
268
|
}
|
|
172
|
-
|
|
173
|
-
return path.join(logFolder, fileName);
|
|
269
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
174
270
|
}
|
|
175
271
|
registerConsoleLogListener(page, context) {
|
|
176
272
|
if (!this.context.webLogger) {
|
|
@@ -221,7 +317,7 @@ class StableBrowser {
|
|
|
221
317
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
222
318
|
}
|
|
223
319
|
catch (error) {
|
|
224
|
-
console.error("Error in request listener", error);
|
|
320
|
+
// console.error("Error in request listener", error);
|
|
225
321
|
context.networkLogger.push({
|
|
226
322
|
error: "not able to listen",
|
|
227
323
|
message: error.message,
|
|
@@ -235,55 +331,114 @@ class StableBrowser {
|
|
|
235
331
|
// async closeUnexpectedPopups() {
|
|
236
332
|
// await closeUnexpectedPopups(this.page);
|
|
237
333
|
// }
|
|
238
|
-
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);
|
|
239
339
|
if (!url.startsWith("http")) {
|
|
240
340
|
url = "https://" + url;
|
|
241
341
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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"];
|
|
249
358
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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);
|
|
257
372
|
}
|
|
258
|
-
return text;
|
|
259
373
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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);
|
|
264
401
|
}
|
|
265
|
-
// clone the locator
|
|
266
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
267
|
-
this.scanAndManipulate(locator, _params);
|
|
268
|
-
return locator;
|
|
269
402
|
}
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
431
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
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);
|
|
282
440
|
}
|
|
283
441
|
}
|
|
284
|
-
}
|
|
285
|
-
_getLocator(locator, scope, _params) {
|
|
286
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
287
442
|
let locatorReturn;
|
|
288
443
|
if (locator.role) {
|
|
289
444
|
if (locator.role[1].nameReg) {
|
|
@@ -291,7 +446,7 @@ class StableBrowser {
|
|
|
291
446
|
delete locator.role[1].nameReg;
|
|
292
447
|
}
|
|
293
448
|
// if (locator.role[1].name) {
|
|
294
|
-
// locator.role[1].name =
|
|
449
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
295
450
|
// }
|
|
296
451
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
297
452
|
}
|
|
@@ -334,213 +489,171 @@ class StableBrowser {
|
|
|
334
489
|
if (css && css.locator) {
|
|
335
490
|
css = css.locator;
|
|
336
491
|
}
|
|
337
|
-
let result = await this._locateElementByText(scope,
|
|
492
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
338
493
|
if (result.elementCount === 0) {
|
|
339
494
|
return;
|
|
340
495
|
}
|
|
341
|
-
let textElementCss = "[data-blinq-id
|
|
496
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
342
497
|
// css climb to parent element
|
|
343
498
|
const climbArray = [];
|
|
344
499
|
for (let i = 0; i < climb; i++) {
|
|
345
500
|
climbArray.push("..");
|
|
346
501
|
}
|
|
347
502
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return new RegExp(pattern, flags);
|
|
371
|
-
}
|
|
372
|
-
document.getRegex = getRegex;
|
|
373
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
374
|
-
// Check and add the element if it has a shadow root
|
|
375
|
-
if (element.shadowRoot) {
|
|
376
|
-
result.push(element);
|
|
377
|
-
// Also search within the shadow root
|
|
378
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
379
|
-
}
|
|
380
|
-
// Iterate over child nodes
|
|
381
|
-
element.childNodes.forEach((child) => {
|
|
382
|
-
// Recursively call the function for each child node
|
|
383
|
-
document.collectAllShadowDomElements(child, result);
|
|
384
|
-
});
|
|
385
|
-
return result;
|
|
386
|
-
}
|
|
387
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
388
|
-
if (!tag) {
|
|
389
|
-
tag = "*";
|
|
390
|
-
}
|
|
391
|
-
let regexpSearch = document.getRegex(text);
|
|
392
|
-
if (regexpSearch) {
|
|
393
|
-
regex = true;
|
|
394
|
-
}
|
|
395
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
396
|
-
let shadowHosts = [];
|
|
397
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
398
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
399
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
400
|
-
if (!shadowElement) {
|
|
401
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
405
|
-
elements = elements.concat(shadowElements);
|
|
406
|
-
}
|
|
407
|
-
let randomToken = null;
|
|
408
|
-
const foundElements = [];
|
|
409
|
-
if (regex) {
|
|
410
|
-
if (!regexpSearch) {
|
|
411
|
-
regexpSearch = new RegExp(text, "im");
|
|
412
|
-
}
|
|
413
|
-
for (let i = 0; i < elements.length; i++) {
|
|
414
|
-
const element = elements[i];
|
|
415
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
416
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
417
|
-
foundElements.push(element);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
text = text.trim();
|
|
423
|
-
for (let i = 0; i < elements.length; i++) {
|
|
424
|
-
const element = elements[i];
|
|
425
|
-
if (partial) {
|
|
426
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
427
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
428
|
-
foundElements.push(element);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
433
|
-
(element.value && element.value === text)) {
|
|
434
|
-
foundElements.push(element);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
let noChildElements = [];
|
|
440
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
441
|
-
let element = foundElements[i];
|
|
442
|
-
let hasChild = false;
|
|
443
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
444
|
-
if (i === j) {
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
if (isParent(element, foundElements[j])) {
|
|
448
|
-
hasChild = true;
|
|
449
|
-
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;
|
|
450
525
|
}
|
|
451
526
|
}
|
|
452
|
-
if (!
|
|
453
|
-
|
|
527
|
+
if (!el.setAttribute) {
|
|
528
|
+
el = el.parentElement;
|
|
454
529
|
}
|
|
530
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
531
|
+
return true;
|
|
532
|
+
}, [tag1, randomToken]))) {
|
|
533
|
+
continue;
|
|
455
534
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (randomToken === null) {
|
|
460
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
461
|
-
}
|
|
462
|
-
let element = noChildElements[i];
|
|
463
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
464
|
-
elementCount++;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
return { elementCount: elementCount, randomToken: randomToken };
|
|
468
|
-
}, [text1, tag1, regex1, partial1]);
|
|
535
|
+
tagCount++;
|
|
536
|
+
}
|
|
537
|
+
return { elementCount: tagCount, randomToken };
|
|
469
538
|
}
|
|
470
|
-
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
|
+
}
|
|
471
550
|
let locatorSearch = selectorHierarchy[index];
|
|
472
551
|
try {
|
|
473
|
-
locatorSearch =
|
|
552
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
474
553
|
}
|
|
475
554
|
catch (e) {
|
|
476
555
|
console.error(e);
|
|
477
556
|
}
|
|
557
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
478
558
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
479
559
|
let locator = null;
|
|
480
560
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
481
|
-
|
|
561
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
562
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
482
563
|
if (!locatorString) {
|
|
564
|
+
info.failCause.textNotFound = true;
|
|
565
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
483
566
|
return;
|
|
484
567
|
}
|
|
485
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
568
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
486
569
|
}
|
|
487
570
|
else if (locatorSearch.text) {
|
|
488
|
-
let
|
|
571
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
572
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
489
573
|
if (result.elementCount === 0) {
|
|
574
|
+
info.failCause.textNotFound = true;
|
|
575
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
490
576
|
return;
|
|
491
577
|
}
|
|
492
|
-
locatorSearch.css = "[data-blinq-id
|
|
578
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
493
579
|
if (locatorSearch.childCss) {
|
|
494
580
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
495
581
|
}
|
|
496
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
582
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
497
583
|
}
|
|
498
584
|
else {
|
|
499
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
585
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
500
586
|
}
|
|
501
587
|
// let cssHref = false;
|
|
502
588
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
503
589
|
// cssHref = true;
|
|
504
590
|
// }
|
|
505
591
|
let count = await locator.count();
|
|
592
|
+
if (count > 0 && !info.failCause.count) {
|
|
593
|
+
info.failCause.count = count;
|
|
594
|
+
}
|
|
506
595
|
//info.log += "total elements found " + count + "\n";
|
|
507
596
|
//let visibleCount = 0;
|
|
508
597
|
let visibleLocator = null;
|
|
509
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
598
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
510
599
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
600
|
+
if (info.locatorLog) {
|
|
601
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
602
|
+
}
|
|
511
603
|
return;
|
|
512
604
|
}
|
|
605
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
606
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
607
|
+
}
|
|
513
608
|
for (let j = 0; j < count; j++) {
|
|
514
609
|
let visible = await locator.nth(j).isVisible();
|
|
515
610
|
const enabled = await locator.nth(j).isEnabled();
|
|
516
611
|
if (!visibleOnly) {
|
|
517
612
|
visible = true;
|
|
518
613
|
}
|
|
519
|
-
if (visible && enabled) {
|
|
614
|
+
if (visible && (allowDisabled || enabled)) {
|
|
520
615
|
foundLocators.push(locator.nth(j));
|
|
616
|
+
if (info.locatorLog) {
|
|
617
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
618
|
+
}
|
|
521
619
|
}
|
|
522
|
-
else {
|
|
620
|
+
else if (logErrors) {
|
|
621
|
+
info.failCause.visible = visible;
|
|
622
|
+
info.failCause.enabled = enabled;
|
|
523
623
|
if (!info.printMessages) {
|
|
524
624
|
info.printMessages = {};
|
|
525
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
|
+
}
|
|
526
634
|
if (!info.printMessages[j.toString()]) {
|
|
527
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
635
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
528
636
|
info.printMessages[j.toString()] = true;
|
|
529
637
|
}
|
|
530
638
|
}
|
|
531
639
|
}
|
|
532
640
|
}
|
|
533
641
|
async closeUnexpectedPopups(info, _params) {
|
|
642
|
+
if (!info) {
|
|
643
|
+
info = {};
|
|
644
|
+
info.failCause = {};
|
|
645
|
+
info.log = "";
|
|
646
|
+
}
|
|
534
647
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
535
648
|
if (!info) {
|
|
536
649
|
info = {};
|
|
537
650
|
}
|
|
538
|
-
info.log += "scan for popup handlers" + "\n";
|
|
651
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
539
652
|
const handlerGroup = [];
|
|
540
653
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
541
654
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
542
655
|
}
|
|
543
|
-
const scopes =
|
|
656
|
+
const scopes = this.page.frames().filter((frame) => frame.url() !== "about:blank");
|
|
544
657
|
let result = null;
|
|
545
658
|
let scope = null;
|
|
546
659
|
for (let i = 0; i < scopes.length; i++) {
|
|
@@ -562,33 +675,218 @@ class StableBrowser {
|
|
|
562
675
|
}
|
|
563
676
|
if (result.foundElements.length > 0) {
|
|
564
677
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
+
}
|
|
568
693
|
return { rerun: true };
|
|
569
694
|
}
|
|
570
695
|
}
|
|
571
696
|
}
|
|
572
697
|
return { rerun: false };
|
|
573
698
|
}
|
|
574
|
-
|
|
699
|
+
getFilePath() {
|
|
700
|
+
const stackFrames = errorStackParser.parse(new Error());
|
|
701
|
+
const mjsFrames = stackFrames.filter((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
|
|
702
|
+
const stackFrame = mjsFrames[mjsFrames.length - 2];
|
|
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) {
|
|
575
764
|
if (!timeout) {
|
|
576
765
|
timeout = 30000;
|
|
577
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
|
+
}
|
|
578
774
|
for (let i = 0; i < 3; i++) {
|
|
579
775
|
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
580
776
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
581
777
|
let selector = selectors.locators[j];
|
|
582
778
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
583
779
|
}
|
|
584
|
-
|
|
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
|
+
}
|
|
585
823
|
if (!element.rerun) {
|
|
586
|
-
|
|
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();
|
|
587
879
|
}
|
|
588
880
|
}
|
|
589
881
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
590
882
|
}
|
|
591
|
-
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();
|
|
592
890
|
let scope = this.page;
|
|
593
891
|
if (selectors.frame) {
|
|
594
892
|
return selectors.frame;
|
|
@@ -598,7 +896,7 @@ class StableBrowser {
|
|
|
598
896
|
for (let i = 0; i < frame.selectors.length; i++) {
|
|
599
897
|
let frameLocator = frame.selectors[i];
|
|
600
898
|
if (frameLocator.css) {
|
|
601
|
-
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
899
|
+
let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
|
|
602
900
|
if (frameLocator.index) {
|
|
603
901
|
testframescope = framescope.nth(frameLocator.index);
|
|
604
902
|
}
|
|
@@ -610,7 +908,7 @@ class StableBrowser {
|
|
|
610
908
|
break;
|
|
611
909
|
}
|
|
612
910
|
catch (error) {
|
|
613
|
-
console.error("frame not found " + frameLocator.css);
|
|
911
|
+
// console.error("frame not found " + frameLocator.css);
|
|
614
912
|
}
|
|
615
913
|
}
|
|
616
914
|
}
|
|
@@ -619,9 +917,11 @@ class StableBrowser {
|
|
|
619
917
|
}
|
|
620
918
|
return framescope;
|
|
621
919
|
};
|
|
920
|
+
let fLocator = null;
|
|
622
921
|
while (true) {
|
|
623
922
|
let frameFound = false;
|
|
624
923
|
if (selectors.nestFrmLoc) {
|
|
924
|
+
fLocator = selectors.nestFrmLoc;
|
|
625
925
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
626
926
|
frameFound = true;
|
|
627
927
|
break;
|
|
@@ -630,6 +930,7 @@ class StableBrowser {
|
|
|
630
930
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
631
931
|
let frameLocator = selectors.frameLocators[i];
|
|
632
932
|
if (frameLocator.css) {
|
|
933
|
+
fLocator = frameLocator.css;
|
|
633
934
|
scope = scope.frameLocator(frameLocator.css);
|
|
634
935
|
frameFound = true;
|
|
635
936
|
break;
|
|
@@ -637,16 +938,25 @@ class StableBrowser {
|
|
|
637
938
|
}
|
|
638
939
|
}
|
|
639
940
|
if (!frameFound && selectors.iframe_src) {
|
|
941
|
+
fLocator = selectors.iframe_src;
|
|
640
942
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
641
943
|
}
|
|
642
944
|
if (!scope) {
|
|
643
|
-
info
|
|
644
|
-
|
|
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}"`;
|
|
645
952
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
646
953
|
}
|
|
647
954
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
648
955
|
}
|
|
649
956
|
else {
|
|
957
|
+
if (info && info.locatorLog) {
|
|
958
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
959
|
+
}
|
|
650
960
|
break;
|
|
651
961
|
}
|
|
652
962
|
}
|
|
@@ -656,20 +966,35 @@ class StableBrowser {
|
|
|
656
966
|
}
|
|
657
967
|
return scope;
|
|
658
968
|
}
|
|
659
|
-
async _getDocumentBody(selectors, timeout = 30000) {
|
|
660
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
969
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
970
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
661
971
|
return scope.evaluate(() => {
|
|
662
972
|
var bodyContent = document.body.innerHTML;
|
|
663
973
|
return bodyContent;
|
|
664
974
|
});
|
|
665
975
|
}
|
|
666
|
-
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
|
+
}
|
|
667
992
|
let highPriorityTimeout = 5000;
|
|
668
993
|
let visibleOnlyTimeout = 6000;
|
|
669
|
-
let startTime =
|
|
994
|
+
let startTime = Date.now();
|
|
670
995
|
let locatorsCount = 0;
|
|
996
|
+
let lazy_scroll = false;
|
|
671
997
|
//let arrayMode = Array.isArray(selectors);
|
|
672
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
673
998
|
let selectorsLocators = null;
|
|
674
999
|
selectorsLocators = selectors.locators;
|
|
675
1000
|
// group selectors by priority
|
|
@@ -697,6 +1022,7 @@ class StableBrowser {
|
|
|
697
1022
|
let highPriorityOnly = true;
|
|
698
1023
|
let visibleOnly = true;
|
|
699
1024
|
while (true) {
|
|
1025
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
700
1026
|
locatorsCount = 0;
|
|
701
1027
|
let result = [];
|
|
702
1028
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -705,18 +1031,13 @@ class StableBrowser {
|
|
|
705
1031
|
}
|
|
706
1032
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
707
1033
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
708
|
-
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);
|
|
709
1035
|
if (result.foundElements.length === 0) {
|
|
710
1036
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
711
|
-
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);
|
|
712
1038
|
}
|
|
713
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
714
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
718
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
719
|
-
}
|
|
1039
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
1040
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
720
1041
|
}
|
|
721
1042
|
let foundElements = result.foundElements;
|
|
722
1043
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -756,24 +1077,43 @@ class StableBrowser {
|
|
|
756
1077
|
return maxCountElement.locator;
|
|
757
1078
|
}
|
|
758
1079
|
}
|
|
759
|
-
if (
|
|
1080
|
+
if (Date.now() - startTime > timeout) {
|
|
760
1081
|
break;
|
|
761
1082
|
}
|
|
762
|
-
if (
|
|
763
|
-
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";
|
|
764
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
|
+
}
|
|
765
1090
|
}
|
|
766
|
-
if (
|
|
767
|
-
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";
|
|
768
1093
|
visibleOnly = false;
|
|
769
1094
|
}
|
|
770
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
|
+
}
|
|
771
1101
|
}
|
|
772
1102
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
773
|
-
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
|
+
}
|
|
774
1114
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
775
1115
|
}
|
|
776
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
1116
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
777
1117
|
let foundElements = [];
|
|
778
1118
|
const result = {
|
|
779
1119
|
foundElements: foundElements,
|
|
@@ -781,31 +1121,88 @@ class StableBrowser {
|
|
|
781
1121
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
782
1122
|
let foundLocators = [];
|
|
783
1123
|
try {
|
|
784
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
1124
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
785
1125
|
}
|
|
786
1126
|
catch (e) {
|
|
787
|
-
this
|
|
788
|
-
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);
|
|
789
1130
|
foundLocators = [];
|
|
790
1131
|
try {
|
|
791
|
-
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);
|
|
792
1133
|
}
|
|
793
1134
|
catch (e) {
|
|
794
|
-
|
|
1135
|
+
if (logErrors) {
|
|
1136
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
1137
|
+
}
|
|
795
1138
|
}
|
|
796
1139
|
}
|
|
797
1140
|
if (foundLocators.length === 1) {
|
|
1141
|
+
let box = null;
|
|
1142
|
+
if (!this.onlyFailuresScreenshot) {
|
|
1143
|
+
box = await foundLocators[0].boundingBox();
|
|
1144
|
+
}
|
|
798
1145
|
result.foundElements.push({
|
|
799
1146
|
locator: foundLocators[0],
|
|
800
|
-
box:
|
|
1147
|
+
box: box,
|
|
801
1148
|
unique: true,
|
|
802
1149
|
});
|
|
803
1150
|
result.locatorIndex = i;
|
|
804
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
|
+
}
|
|
805
1189
|
}
|
|
806
1190
|
return result;
|
|
807
1191
|
}
|
|
808
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);
|
|
809
1206
|
const startTime = Date.now();
|
|
810
1207
|
let timeout = 30000;
|
|
811
1208
|
if (options && options.timeout) {
|
|
@@ -829,13 +1226,32 @@ class StableBrowser {
|
|
|
829
1226
|
}
|
|
830
1227
|
catch (e) {
|
|
831
1228
|
if (performance.now() - startTime > timeout) {
|
|
832
|
-
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
|
+
}
|
|
833
1236
|
}
|
|
834
1237
|
}
|
|
835
1238
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
836
1239
|
}
|
|
837
1240
|
}
|
|
838
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);
|
|
839
1255
|
const startTime = Date.now();
|
|
840
1256
|
let timeout = 30000;
|
|
841
1257
|
if (options && options.timeout) {
|
|
@@ -859,7 +1275,13 @@ class StableBrowser {
|
|
|
859
1275
|
}
|
|
860
1276
|
catch (e) {
|
|
861
1277
|
if (performance.now() - startTime > timeout) {
|
|
862
|
-
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
|
+
}
|
|
863
1285
|
}
|
|
864
1286
|
}
|
|
865
1287
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -872,34 +1294,74 @@ class StableBrowser {
|
|
|
872
1294
|
options,
|
|
873
1295
|
world,
|
|
874
1296
|
text: "Click element",
|
|
1297
|
+
_text: "Click on " + selectors.element_name,
|
|
875
1298
|
type: Types.CLICK,
|
|
876
1299
|
operation: "click",
|
|
877
1300
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
878
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
|
+
}
|
|
879
1309
|
try {
|
|
1310
|
+
check_performance("click_preCommand", this.context, true);
|
|
880
1311
|
await _preCommand(state, this);
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
await
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
catch (e) {
|
|
889
|
-
// await this.closeUnexpectedPopups();
|
|
890
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
891
|
-
await state.element.dispatchEvent("click");
|
|
892
|
-
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);
|
|
893
1318
|
}
|
|
894
|
-
await this.waitForPageLoad();
|
|
895
1319
|
return state.info;
|
|
896
1320
|
}
|
|
897
1321
|
catch (e) {
|
|
898
1322
|
await _commandError(state, e, this);
|
|
899
1323
|
}
|
|
900
1324
|
finally {
|
|
901
|
-
|
|
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);
|
|
902
1363
|
}
|
|
1364
|
+
return found;
|
|
903
1365
|
}
|
|
904
1366
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
905
1367
|
const state = {
|
|
@@ -909,6 +1371,7 @@ class StableBrowser {
|
|
|
909
1371
|
world,
|
|
910
1372
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
911
1373
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1374
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
912
1375
|
operation: "setCheck",
|
|
913
1376
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
914
1377
|
};
|
|
@@ -918,30 +1381,53 @@ class StableBrowser {
|
|
|
918
1381
|
// let element = await this._locate(selectors, info, _params);
|
|
919
1382
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
920
1383
|
try {
|
|
921
|
-
//
|
|
922
|
-
|
|
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 });
|
|
923
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);
|
|
924
1393
|
}
|
|
925
1394
|
catch (e) {
|
|
926
1395
|
if (e.message && e.message.includes("did not change its state")) {
|
|
927
1396
|
this.logger.info("element did not change its state, ignoring...");
|
|
928
1397
|
}
|
|
929
1398
|
else {
|
|
1399
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
930
1400
|
//await this.closeUnexpectedPopups();
|
|
931
1401
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
932
|
-
state.
|
|
933
|
-
|
|
934
|
-
|
|
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
|
+
}
|
|
935
1421
|
}
|
|
936
1422
|
}
|
|
937
|
-
await this.waitForPageLoad();
|
|
1423
|
+
//await this.waitForPageLoad();
|
|
938
1424
|
return state.info;
|
|
939
1425
|
}
|
|
940
1426
|
catch (e) {
|
|
941
1427
|
await _commandError(state, e, this);
|
|
942
1428
|
}
|
|
943
1429
|
finally {
|
|
944
|
-
_commandFinally(state, this);
|
|
1430
|
+
await _commandFinally(state, this);
|
|
945
1431
|
}
|
|
946
1432
|
}
|
|
947
1433
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -952,31 +1438,22 @@ class StableBrowser {
|
|
|
952
1438
|
world,
|
|
953
1439
|
type: Types.HOVER,
|
|
954
1440
|
text: `Hover element`,
|
|
1441
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
955
1442
|
operation: "hover",
|
|
956
1443
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
957
1444
|
};
|
|
958
1445
|
try {
|
|
959
1446
|
await _preCommand(state, this);
|
|
960
|
-
|
|
961
|
-
await state.element.hover();
|
|
962
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
963
|
-
}
|
|
964
|
-
catch (e) {
|
|
965
|
-
//await this.closeUnexpectedPopups();
|
|
966
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
967
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
968
|
-
await state.element.hover({ timeout: 10000 });
|
|
969
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
970
|
-
}
|
|
1447
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
971
1448
|
await _screenshot(state, this);
|
|
972
|
-
await this.waitForPageLoad();
|
|
1449
|
+
//await this.waitForPageLoad();
|
|
973
1450
|
return state.info;
|
|
974
1451
|
}
|
|
975
1452
|
catch (e) {
|
|
976
1453
|
await _commandError(state, e, this);
|
|
977
1454
|
}
|
|
978
1455
|
finally {
|
|
979
|
-
_commandFinally(state, this);
|
|
1456
|
+
await _commandFinally(state, this);
|
|
980
1457
|
}
|
|
981
1458
|
}
|
|
982
1459
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -991,6 +1468,7 @@ class StableBrowser {
|
|
|
991
1468
|
value: values.toString(),
|
|
992
1469
|
type: Types.SELECT,
|
|
993
1470
|
text: `Select option: ${values}`,
|
|
1471
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
994
1472
|
operation: "selectOption",
|
|
995
1473
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
996
1474
|
};
|
|
@@ -1004,14 +1482,14 @@ class StableBrowser {
|
|
|
1004
1482
|
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1005
1483
|
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
1006
1484
|
}
|
|
1007
|
-
await this.waitForPageLoad();
|
|
1485
|
+
//await this.waitForPageLoad();
|
|
1008
1486
|
return state.info;
|
|
1009
1487
|
}
|
|
1010
1488
|
catch (e) {
|
|
1011
1489
|
await _commandError(state, e, this);
|
|
1012
1490
|
}
|
|
1013
1491
|
finally {
|
|
1014
|
-
_commandFinally(state, this);
|
|
1492
|
+
await _commandFinally(state, this);
|
|
1015
1493
|
}
|
|
1016
1494
|
}
|
|
1017
1495
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1025,6 +1503,7 @@ class StableBrowser {
|
|
|
1025
1503
|
highlight: false,
|
|
1026
1504
|
type: Types.TYPE_PRESS,
|
|
1027
1505
|
text: `Type value: ${_value}`,
|
|
1506
|
+
_text: `Type value: ${_value}`,
|
|
1028
1507
|
operation: "type",
|
|
1029
1508
|
log: "",
|
|
1030
1509
|
};
|
|
@@ -1056,7 +1535,7 @@ class StableBrowser {
|
|
|
1056
1535
|
await _commandError(state, e, this);
|
|
1057
1536
|
}
|
|
1058
1537
|
finally {
|
|
1059
|
-
_commandFinally(state, this);
|
|
1538
|
+
await _commandFinally(state, this);
|
|
1060
1539
|
}
|
|
1061
1540
|
}
|
|
1062
1541
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1092,37 +1571,35 @@ class StableBrowser {
|
|
|
1092
1571
|
await _commandError(state, e, this);
|
|
1093
1572
|
}
|
|
1094
1573
|
finally {
|
|
1095
|
-
_commandFinally(state, this);
|
|
1574
|
+
await _commandFinally(state, this);
|
|
1096
1575
|
}
|
|
1097
1576
|
}
|
|
1098
1577
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
+
};
|
|
1109
1591
|
try {
|
|
1110
|
-
|
|
1111
|
-
let element = await this._locate(selectors, info, _params);
|
|
1112
|
-
//insert red border around the element
|
|
1113
|
-
await this.scrollIfNeeded(element, info);
|
|
1114
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1115
|
-
await this._highlightElements(element);
|
|
1592
|
+
await _preCommand(state, this);
|
|
1116
1593
|
try {
|
|
1117
|
-
await element
|
|
1594
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1118
1595
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1119
1596
|
if (format) {
|
|
1120
|
-
value = dayjs(value).format(format);
|
|
1121
|
-
await element.fill(value);
|
|
1597
|
+
state.value = dayjs(state.value).format(format);
|
|
1598
|
+
await state.element.fill(state.value);
|
|
1122
1599
|
}
|
|
1123
1600
|
else {
|
|
1124
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1125
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1601
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1602
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1126
1603
|
el.value = ""; // clear input
|
|
1127
1604
|
el.value = dateTimeValue;
|
|
1128
1605
|
}, dateTimeValue);
|
|
@@ -1135,20 +1612,19 @@ class StableBrowser {
|
|
|
1135
1612
|
}
|
|
1136
1613
|
catch (err) {
|
|
1137
1614
|
//await this.closeUnexpectedPopups();
|
|
1138
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1615
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1139
1616
|
this.logger.info("Trying again");
|
|
1140
|
-
|
|
1141
|
-
info.
|
|
1142
|
-
Object.assign(err, { info: info });
|
|
1617
|
+
await _screenshot(state, this);
|
|
1618
|
+
Object.assign(err, { info: state.info });
|
|
1143
1619
|
await element.click();
|
|
1144
1620
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1145
1621
|
if (format) {
|
|
1146
|
-
value = dayjs(value).format(format);
|
|
1147
|
-
await element.fill(value);
|
|
1622
|
+
state.value = dayjs(state.value).format(format);
|
|
1623
|
+
await state.element.fill(state.value);
|
|
1148
1624
|
}
|
|
1149
1625
|
else {
|
|
1150
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1151
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1626
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1627
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1152
1628
|
el.value = ""; // clear input
|
|
1153
1629
|
el.value = dateTimeValue;
|
|
1154
1630
|
}, dateTimeValue);
|
|
@@ -1161,31 +1637,10 @@ class StableBrowser {
|
|
|
1161
1637
|
}
|
|
1162
1638
|
}
|
|
1163
1639
|
catch (e) {
|
|
1164
|
-
|
|
1165
|
-
throw e;
|
|
1640
|
+
await _commandError(state, e, this);
|
|
1166
1641
|
}
|
|
1167
1642
|
finally {
|
|
1168
|
-
|
|
1169
|
-
this._reportToWorld(world, {
|
|
1170
|
-
element_name: selectors.element_name,
|
|
1171
|
-
type: Types.SET_DATE_TIME,
|
|
1172
|
-
screenshotId,
|
|
1173
|
-
value: value,
|
|
1174
|
-
text: `setDateTime input with value: ${value}`,
|
|
1175
|
-
result: error
|
|
1176
|
-
? {
|
|
1177
|
-
status: "FAILED",
|
|
1178
|
-
startTime,
|
|
1179
|
-
endTime,
|
|
1180
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1181
|
-
}
|
|
1182
|
-
: {
|
|
1183
|
-
status: "PASSED",
|
|
1184
|
-
startTime,
|
|
1185
|
-
endTime,
|
|
1186
|
-
},
|
|
1187
|
-
info: info,
|
|
1188
|
-
});
|
|
1643
|
+
await _commandFinally(state, this);
|
|
1189
1644
|
}
|
|
1190
1645
|
}
|
|
1191
1646
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1200,17 +1655,29 @@ class StableBrowser {
|
|
|
1200
1655
|
world,
|
|
1201
1656
|
type: Types.FILL,
|
|
1202
1657
|
text: `Click type input with value: ${_value}`,
|
|
1658
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1203
1659
|
operation: "clickType",
|
|
1204
1660
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1205
1661
|
};
|
|
1662
|
+
if (!options) {
|
|
1663
|
+
options = {};
|
|
1664
|
+
}
|
|
1206
1665
|
if (newValue !== _value) {
|
|
1207
1666
|
//this.logger.info(_value + "=" + newValue);
|
|
1208
1667
|
_value = newValue;
|
|
1209
1668
|
}
|
|
1210
1669
|
try {
|
|
1211
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);
|
|
1212
1679
|
state.info.value = _value;
|
|
1213
|
-
if (
|
|
1680
|
+
if (!options.press) {
|
|
1214
1681
|
try {
|
|
1215
1682
|
let currentValue = await state.element.inputValue();
|
|
1216
1683
|
if (currentValue) {
|
|
@@ -1221,13 +1688,9 @@ class StableBrowser {
|
|
|
1221
1688
|
this.logger.info("unable to clear input value");
|
|
1222
1689
|
}
|
|
1223
1690
|
}
|
|
1224
|
-
if (options
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
catch (e) {
|
|
1229
|
-
await state.element.dispatchEvent("click");
|
|
1230
|
-
}
|
|
1691
|
+
if (options.press) {
|
|
1692
|
+
options.timeout = 5000;
|
|
1693
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1231
1694
|
}
|
|
1232
1695
|
else {
|
|
1233
1696
|
try {
|
|
@@ -1238,6 +1701,25 @@ class StableBrowser {
|
|
|
1238
1701
|
}
|
|
1239
1702
|
}
|
|
1240
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
|
+
}
|
|
1241
1723
|
const valueSegment = state.value.split("&&");
|
|
1242
1724
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1243
1725
|
if (i > 0) {
|
|
@@ -1258,14 +1740,21 @@ class StableBrowser {
|
|
|
1258
1740
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1259
1741
|
}
|
|
1260
1742
|
}
|
|
1743
|
+
//if (!this.fastMode) {
|
|
1261
1744
|
await _screenshot(state, this);
|
|
1745
|
+
//}
|
|
1262
1746
|
if (enter === true) {
|
|
1263
1747
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1264
1748
|
await this.page.keyboard.press("Enter");
|
|
1265
1749
|
await this.waitForPageLoad();
|
|
1266
1750
|
}
|
|
1267
1751
|
else if (enter === false) {
|
|
1268
|
-
|
|
1752
|
+
try {
|
|
1753
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1754
|
+
}
|
|
1755
|
+
catch (e) {
|
|
1756
|
+
// ignore
|
|
1757
|
+
}
|
|
1269
1758
|
//await this.page.keyboard.press("Tab");
|
|
1270
1759
|
}
|
|
1271
1760
|
else {
|
|
@@ -1280,7 +1769,7 @@ class StableBrowser {
|
|
|
1280
1769
|
await _commandError(state, e, this);
|
|
1281
1770
|
}
|
|
1282
1771
|
finally {
|
|
1283
|
-
_commandFinally(state, this);
|
|
1772
|
+
await _commandFinally(state, this);
|
|
1284
1773
|
}
|
|
1285
1774
|
}
|
|
1286
1775
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1302,30 +1791,67 @@ class StableBrowser {
|
|
|
1302
1791
|
if (enter) {
|
|
1303
1792
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1304
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;
|
|
1305
1829
|
}
|
|
1306
|
-
await
|
|
1830
|
+
await state.element.setInputFiles(files);
|
|
1307
1831
|
return state.info;
|
|
1308
1832
|
}
|
|
1309
1833
|
catch (e) {
|
|
1310
1834
|
await _commandError(state, e, this);
|
|
1311
1835
|
}
|
|
1312
1836
|
finally {
|
|
1313
|
-
_commandFinally(state, this);
|
|
1837
|
+
await _commandFinally(state, this);
|
|
1314
1838
|
}
|
|
1315
1839
|
}
|
|
1316
1840
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1317
1841
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1318
1842
|
}
|
|
1319
1843
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1844
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1320
1845
|
_validateSelectors(selectors);
|
|
1321
1846
|
let screenshotId = null;
|
|
1322
1847
|
let screenshotPath = null;
|
|
1323
1848
|
if (!info.log) {
|
|
1324
1849
|
info.log = "";
|
|
1850
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1325
1851
|
}
|
|
1326
1852
|
info.operation = "getText";
|
|
1327
1853
|
info.selectors = selectors;
|
|
1328
|
-
let element = await this._locate(selectors, info, _params);
|
|
1854
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1329
1855
|
if (climb > 0) {
|
|
1330
1856
|
const climbArray = [];
|
|
1331
1857
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1344,6 +1870,18 @@ class StableBrowser {
|
|
|
1344
1870
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1345
1871
|
try {
|
|
1346
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
|
+
// }
|
|
1347
1885
|
const elementText = await element.innerText();
|
|
1348
1886
|
return {
|
|
1349
1887
|
text: elementText,
|
|
@@ -1355,7 +1893,7 @@ class StableBrowser {
|
|
|
1355
1893
|
}
|
|
1356
1894
|
catch (e) {
|
|
1357
1895
|
//await this.closeUnexpectedPopups();
|
|
1358
|
-
this.logger.info("no innerText will use textContent");
|
|
1896
|
+
this.logger.info("no innerText, will use textContent");
|
|
1359
1897
|
const elementText = await element.textContent();
|
|
1360
1898
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1361
1899
|
}
|
|
@@ -1380,6 +1918,7 @@ class StableBrowser {
|
|
|
1380
1918
|
highlight: false,
|
|
1381
1919
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1382
1920
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1921
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1383
1922
|
operation: "containsPattern",
|
|
1384
1923
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1385
1924
|
};
|
|
@@ -1411,10 +1950,12 @@ class StableBrowser {
|
|
|
1411
1950
|
await _commandError(state, e, this);
|
|
1412
1951
|
}
|
|
1413
1952
|
finally {
|
|
1414
|
-
_commandFinally(state, this);
|
|
1953
|
+
await _commandFinally(state, this);
|
|
1415
1954
|
}
|
|
1416
1955
|
}
|
|
1417
1956
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1957
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1958
|
+
const startTime = Date.now();
|
|
1418
1959
|
const state = {
|
|
1419
1960
|
selectors,
|
|
1420
1961
|
_params,
|
|
@@ -1441,61 +1982,137 @@ class StableBrowser {
|
|
|
1441
1982
|
}
|
|
1442
1983
|
let foundObj = null;
|
|
1443
1984
|
try {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1451
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1452
|
-
if (dateAlternatives.date) {
|
|
1453
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1454
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1455
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1456
|
-
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);
|
|
1457
1991
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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)) {
|
|
1465
2012
|
return state.info;
|
|
1466
2013
|
}
|
|
1467
2014
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
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
|
|
1474
2020
|
}
|
|
1475
|
-
|
|
2021
|
+
state.info.foundText = foundObj?.text;
|
|
2022
|
+
state.info.value = foundObj?.value;
|
|
2023
|
+
throw new Error("element doesn't contain text " + text);
|
|
1476
2024
|
}
|
|
1477
2025
|
catch (e) {
|
|
1478
2026
|
await _commandError(state, e, this);
|
|
2027
|
+
throw e;
|
|
1479
2028
|
}
|
|
1480
2029
|
finally {
|
|
1481
|
-
_commandFinally(state, this);
|
|
2030
|
+
await _commandFinally(state, this);
|
|
1482
2031
|
}
|
|
1483
2032
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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");
|
|
1488
2057
|
}
|
|
1489
|
-
else if (
|
|
1490
|
-
|
|
2058
|
+
else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
|
|
2059
|
+
text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
|
|
1491
2060
|
}
|
|
1492
|
-
else if (
|
|
1493
|
-
|
|
2061
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
2062
|
+
text = referanceSnapshot.substring(5);
|
|
1494
2063
|
}
|
|
1495
2064
|
else {
|
|
1496
|
-
|
|
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);
|
|
1497
2115
|
}
|
|
1498
|
-
return dataFile;
|
|
1499
2116
|
}
|
|
1500
2117
|
async waitForUserInput(message, world = null) {
|
|
1501
2118
|
if (!message) {
|
|
@@ -1525,13 +2142,22 @@ class StableBrowser {
|
|
|
1525
2142
|
return;
|
|
1526
2143
|
}
|
|
1527
2144
|
// if data file exists, load it
|
|
1528
|
-
const dataFile =
|
|
2145
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1529
2146
|
let data = this.getTestData(world);
|
|
1530
2147
|
// merge the testData with the existing data
|
|
1531
2148
|
Object.assign(data, testData);
|
|
1532
2149
|
// save the data to the file
|
|
1533
2150
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1534
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
|
+
}
|
|
1535
2161
|
_getDataFilePath(fileName) {
|
|
1536
2162
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1537
2163
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1628,14 +2254,12 @@ class StableBrowser {
|
|
|
1628
2254
|
}
|
|
1629
2255
|
}
|
|
1630
2256
|
getTestData(world = null) {
|
|
1631
|
-
|
|
1632
|
-
let data = {};
|
|
1633
|
-
if (fs.existsSync(dataFile)) {
|
|
1634
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1635
|
-
}
|
|
1636
|
-
return data;
|
|
2257
|
+
return _getTestData(world, this.context, this);
|
|
1637
2258
|
}
|
|
1638
2259
|
async _screenShot(options = {}, world = null, info = null) {
|
|
2260
|
+
if (!options) {
|
|
2261
|
+
options = {};
|
|
2262
|
+
}
|
|
1639
2263
|
// collect url/path/title
|
|
1640
2264
|
if (info) {
|
|
1641
2265
|
if (!info.title) {
|
|
@@ -1660,13 +2284,11 @@ class StableBrowser {
|
|
|
1660
2284
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1661
2285
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1662
2286
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
}
|
|
1667
|
-
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");
|
|
1668
2290
|
try {
|
|
1669
|
-
await this.takeScreenshot(screenshotPath);
|
|
2291
|
+
await this.takeScreenshot(screenshotPath, options.fullPage === true);
|
|
1670
2292
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1671
2293
|
// // save the buffer to the screenshot path asynchrously
|
|
1672
2294
|
// fs.writeFile(screenshotPath, buffer, (err) => {
|
|
@@ -1674,20 +2296,20 @@ class StableBrowser {
|
|
|
1674
2296
|
// this.logger.info("unable to save screenshot " + screenshotPath);
|
|
1675
2297
|
// }
|
|
1676
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
|
+
}
|
|
1677
2304
|
}
|
|
1678
2305
|
catch (e) {
|
|
1679
2306
|
this.logger.info("unable to take screenshot, ignored");
|
|
1680
2307
|
}
|
|
1681
|
-
result.screenshotId = nextIndex;
|
|
1682
|
-
result.screenshotPath = screenshotPath;
|
|
1683
|
-
if (info && info.box) {
|
|
1684
|
-
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
1685
|
-
}
|
|
1686
2308
|
}
|
|
1687
2309
|
else if (options && options.screenshot) {
|
|
1688
2310
|
result.screenshotPath = options.screenshotPath;
|
|
1689
2311
|
try {
|
|
1690
|
-
await this.takeScreenshot(options.screenshotPath);
|
|
2312
|
+
await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
|
|
1691
2313
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
1692
2314
|
// // save the buffer to the screenshot path asynchrously
|
|
1693
2315
|
// fs.writeFile(options.screenshotPath, buffer, (err) => {
|
|
@@ -1705,7 +2327,7 @@ class StableBrowser {
|
|
|
1705
2327
|
}
|
|
1706
2328
|
return result;
|
|
1707
2329
|
}
|
|
1708
|
-
async takeScreenshot(screenshotPath) {
|
|
2330
|
+
async takeScreenshot(screenshotPath, fullPage = false) {
|
|
1709
2331
|
const playContext = this.context.playContext;
|
|
1710
2332
|
// Using CDP to capture the screenshot
|
|
1711
2333
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
@@ -1717,17 +2339,20 @@ class StableBrowser {
|
|
|
1717
2339
|
document.documentElement.clientWidth,
|
|
1718
2340
|
])));
|
|
1719
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
|
+
// }
|
|
1720
2351
|
if (this.context.browserName === "chromium") {
|
|
1721
2352
|
const client = await playContext.newCDPSession(this.page);
|
|
1722
2353
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1723
2354
|
format: "png",
|
|
1724
|
-
|
|
1725
|
-
// x: 0,
|
|
1726
|
-
// y: 0,
|
|
1727
|
-
// width: viewportWidth,
|
|
1728
|
-
// height: viewportHeight,
|
|
1729
|
-
// scale: 1,
|
|
1730
|
-
// },
|
|
2355
|
+
captureBeyondViewport: fullPage,
|
|
1731
2356
|
});
|
|
1732
2357
|
await client.detach();
|
|
1733
2358
|
if (!screenshotPath) {
|
|
@@ -1736,8 +2361,12 @@ class StableBrowser {
|
|
|
1736
2361
|
screenshotBuffer = Buffer.from(data, "base64");
|
|
1737
2362
|
}
|
|
1738
2363
|
else {
|
|
1739
|
-
screenshotBuffer = await this.page.screenshot();
|
|
2364
|
+
screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
|
|
1740
2365
|
}
|
|
2366
|
+
// if (focusedElement) {
|
|
2367
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
2368
|
+
// await this._unhighlightElements(focusedElement);
|
|
2369
|
+
// }
|
|
1741
2370
|
let image = await Jimp.read(screenshotBuffer);
|
|
1742
2371
|
// Get the image dimensions
|
|
1743
2372
|
const { width, height } = image.bitmap;
|
|
@@ -1750,6 +2379,7 @@ class StableBrowser {
|
|
|
1750
2379
|
else {
|
|
1751
2380
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1752
2381
|
}
|
|
2382
|
+
return screenshotBuffer;
|
|
1753
2383
|
}
|
|
1754
2384
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1755
2385
|
const state = {
|
|
@@ -1772,116 +2402,535 @@ class StableBrowser {
|
|
|
1772
2402
|
await _commandError(state, e, this);
|
|
1773
2403
|
}
|
|
1774
2404
|
finally {
|
|
1775
|
-
_commandFinally(state, this);
|
|
2405
|
+
await _commandFinally(state, this);
|
|
1776
2406
|
}
|
|
1777
2407
|
}
|
|
1778
2408
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
+
};
|
|
1784
2423
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1785
|
-
const info = {};
|
|
1786
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1787
|
-
info.operation = "extract";
|
|
1788
|
-
info.selectors = selectors;
|
|
1789
2424
|
try {
|
|
1790
|
-
|
|
1791
|
-
await this._highlightElements(element);
|
|
1792
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2425
|
+
await _preCommand(state, this);
|
|
1793
2426
|
switch (attribute) {
|
|
1794
2427
|
case "inner_text":
|
|
1795
|
-
|
|
2428
|
+
state.value = await state.element.innerText();
|
|
1796
2429
|
break;
|
|
1797
2430
|
case "href":
|
|
1798
|
-
|
|
2431
|
+
state.value = await state.element.getAttribute("href");
|
|
1799
2432
|
break;
|
|
1800
2433
|
case "value":
|
|
1801
|
-
|
|
2434
|
+
state.value = await state.element.inputValue();
|
|
2435
|
+
break;
|
|
2436
|
+
case "text":
|
|
2437
|
+
state.value = await state.element.textContent();
|
|
1802
2438
|
break;
|
|
1803
2439
|
default:
|
|
1804
|
-
|
|
2440
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1805
2441
|
break;
|
|
1806
2442
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
+
}
|
|
1810
2460
|
}
|
|
1811
|
-
|
|
1812
|
-
this.
|
|
1813
|
-
|
|
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;
|
|
1814
2466
|
}
|
|
1815
2467
|
catch (e) {
|
|
1816
|
-
|
|
1817
|
-
this.logger.error("extract failed " + info.log);
|
|
1818
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1819
|
-
info.screenshotPath = screenshotPath;
|
|
1820
|
-
Object.assign(e, { info: info });
|
|
1821
|
-
error = e;
|
|
1822
|
-
throw e;
|
|
2468
|
+
await _commandError(state, e, this);
|
|
1823
2469
|
}
|
|
1824
2470
|
finally {
|
|
1825
|
-
|
|
1826
|
-
this._reportToWorld(world, {
|
|
1827
|
-
element_name: selectors.element_name,
|
|
1828
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1829
|
-
variable: variable,
|
|
1830
|
-
value: info.value,
|
|
1831
|
-
text: "Extract attribute from element",
|
|
1832
|
-
screenshotId,
|
|
1833
|
-
result: error
|
|
1834
|
-
? {
|
|
1835
|
-
status: "FAILED",
|
|
1836
|
-
startTime,
|
|
1837
|
-
endTime,
|
|
1838
|
-
message: error?.message,
|
|
1839
|
-
}
|
|
1840
|
-
: {
|
|
1841
|
-
status: "PASSED",
|
|
1842
|
-
startTime,
|
|
1843
|
-
endTime,
|
|
1844
|
-
},
|
|
1845
|
-
info: info,
|
|
1846
|
-
});
|
|
2471
|
+
await _commandFinally(state, this);
|
|
1847
2472
|
}
|
|
1848
2473
|
}
|
|
1849
|
-
async
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
+
}
|
|
1860
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;
|
|
1861
2537
|
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
if (options && options.timeout) {
|
|
1865
|
-
timeout = options.timeout;
|
|
2538
|
+
catch (e) {
|
|
2539
|
+
await _commandError(state, e, this);
|
|
1866
2540
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
+
// {
|
|
1885
2934
|
// "status": true,
|
|
1886
2935
|
// "content": {
|
|
1887
2936
|
// "url": "",
|
|
@@ -1920,7 +2969,8 @@ class StableBrowser {
|
|
|
1920
2969
|
catch (e) {
|
|
1921
2970
|
errorCount++;
|
|
1922
2971
|
if (errorCount > 3) {
|
|
1923
|
-
throw e;
|
|
2972
|
+
// throw e;
|
|
2973
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1924
2974
|
}
|
|
1925
2975
|
// ignore
|
|
1926
2976
|
}
|
|
@@ -1934,27 +2984,32 @@ class StableBrowser {
|
|
|
1934
2984
|
async _highlightElements(scope, css) {
|
|
1935
2985
|
try {
|
|
1936
2986
|
if (!scope) {
|
|
2987
|
+
// console.log(`Scope is not defined`);
|
|
1937
2988
|
return;
|
|
1938
2989
|
}
|
|
1939
2990
|
if (!css) {
|
|
1940
2991
|
scope
|
|
1941
2992
|
.evaluate((node) => {
|
|
1942
2993
|
if (node && node.style) {
|
|
1943
|
-
let
|
|
1944
|
-
|
|
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}`);
|
|
1945
2999
|
if (window) {
|
|
1946
3000
|
window.addEventListener("beforeunload", function (e) {
|
|
1947
|
-
node.style.
|
|
3001
|
+
node.style.outline = originalOutline;
|
|
1948
3002
|
});
|
|
1949
3003
|
}
|
|
1950
3004
|
setTimeout(function () {
|
|
1951
|
-
node.style.
|
|
3005
|
+
node.style.outline = originalOutline;
|
|
1952
3006
|
}, 2000);
|
|
1953
3007
|
}
|
|
1954
3008
|
})
|
|
1955
3009
|
.then(() => { })
|
|
1956
3010
|
.catch((e) => {
|
|
1957
3011
|
// ignore
|
|
3012
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1958
3013
|
});
|
|
1959
3014
|
}
|
|
1960
3015
|
else {
|
|
@@ -1970,17 +3025,18 @@ class StableBrowser {
|
|
|
1970
3025
|
if (!element.style) {
|
|
1971
3026
|
return;
|
|
1972
3027
|
}
|
|
1973
|
-
|
|
3028
|
+
let originalOutline = element.style.outline;
|
|
3029
|
+
element.__previousOutline = originalOutline;
|
|
1974
3030
|
// Set the new border to be red and 2px solid
|
|
1975
|
-
element.style.
|
|
3031
|
+
element.style.outline = "2px solid red";
|
|
1976
3032
|
if (window) {
|
|
1977
3033
|
window.addEventListener("beforeunload", function (e) {
|
|
1978
|
-
element.style.
|
|
3034
|
+
element.style.outline = originalOutline;
|
|
1979
3035
|
});
|
|
1980
3036
|
}
|
|
1981
3037
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1982
3038
|
setTimeout(function () {
|
|
1983
|
-
element.style.
|
|
3039
|
+
element.style.outline = originalOutline;
|
|
1984
3040
|
}, 2000);
|
|
1985
3041
|
}
|
|
1986
3042
|
return;
|
|
@@ -1988,6 +3044,7 @@ class StableBrowser {
|
|
|
1988
3044
|
.then(() => { })
|
|
1989
3045
|
.catch((e) => {
|
|
1990
3046
|
// ignore
|
|
3047
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
1991
3048
|
});
|
|
1992
3049
|
}
|
|
1993
3050
|
}
|
|
@@ -1995,8 +3052,49 @@ class StableBrowser {
|
|
|
1995
3052
|
console.debug(error);
|
|
1996
3053
|
}
|
|
1997
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
|
+
*/
|
|
1998
3097
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
1999
|
-
const startTime = Date.now();
|
|
2000
3098
|
let error = null;
|
|
2001
3099
|
let screenshotId = null;
|
|
2002
3100
|
let screenshotPath = null;
|
|
@@ -2010,159 +3108,534 @@ class StableBrowser {
|
|
|
2010
3108
|
pathPart = newValue;
|
|
2011
3109
|
}
|
|
2012
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
|
+
};
|
|
2013
3125
|
try {
|
|
3126
|
+
await _preCommand(state, this);
|
|
3127
|
+
state.info.text = queryText;
|
|
2014
3128
|
for (let i = 0; i < 30; i++) {
|
|
2015
3129
|
const url = await this.page.url();
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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`);
|
|
2019
3405
|
}
|
|
2020
3406
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2021
3407
|
continue;
|
|
2022
3408
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
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
|
+
}
|
|
2025
3426
|
}
|
|
2026
3427
|
}
|
|
2027
3428
|
catch (e) {
|
|
2028
|
-
|
|
2029
|
-
this.logger.error("verify page path failed " + info.log);
|
|
2030
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2031
|
-
info.screenshotPath = screenshotPath;
|
|
2032
|
-
Object.assign(e, { info: info });
|
|
2033
|
-
error = e;
|
|
2034
|
-
throw e;
|
|
3429
|
+
await _commandError(state, e, this);
|
|
2035
3430
|
}
|
|
2036
3431
|
finally {
|
|
2037
|
-
|
|
2038
|
-
this._reportToWorld(world, {
|
|
2039
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2040
|
-
text: "Verify page path",
|
|
2041
|
-
screenshotId,
|
|
2042
|
-
result: error
|
|
2043
|
-
? {
|
|
2044
|
-
status: "FAILED",
|
|
2045
|
-
startTime,
|
|
2046
|
-
endTime,
|
|
2047
|
-
message: error?.message,
|
|
2048
|
-
}
|
|
2049
|
-
: {
|
|
2050
|
-
status: "PASSED",
|
|
2051
|
-
startTime,
|
|
2052
|
-
endTime,
|
|
2053
|
-
},
|
|
2054
|
-
info: info,
|
|
2055
|
-
});
|
|
3432
|
+
await _commandFinally(state, this);
|
|
2056
3433
|
}
|
|
2057
3434
|
}
|
|
2058
|
-
async
|
|
3435
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2059
3436
|
text = unEscapeString(text);
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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);
|
|
2065
3454
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2066
|
-
const info = {};
|
|
2067
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2068
|
-
info.operation = "verifyTextExistInPage";
|
|
2069
3455
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2070
3456
|
if (newValue !== text) {
|
|
2071
3457
|
this.logger.info(text + "=" + newValue);
|
|
2072
3458
|
text = newValue;
|
|
2073
3459
|
}
|
|
2074
|
-
|
|
2075
|
-
let
|
|
2076
|
-
|
|
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);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
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);
|
|
3511
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
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;
|
|
2077
3525
|
try {
|
|
3526
|
+
await _preCommand(state, this);
|
|
3527
|
+
state.info.text = textToVerify;
|
|
3528
|
+
let resultWithElementsFound = {
|
|
3529
|
+
length: 0,
|
|
3530
|
+
};
|
|
2078
3531
|
while (true) {
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
|
|
2085
|
-
result.frame = frames[i];
|
|
2086
|
-
results.push(result);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
else if (numberAlternatives.number) {
|
|
2090
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2091
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
|
|
2092
|
-
result.frame = frames[i];
|
|
2093
|
-
results.push(result);
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
else {
|
|
2097
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
|
|
2098
|
-
result.frame = frames[i];
|
|
2099
|
-
results.push(result);
|
|
2100
|
-
}
|
|
3532
|
+
try {
|
|
3533
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
3534
|
+
}
|
|
3535
|
+
catch (error) {
|
|
3536
|
+
// ignore
|
|
2101
3537
|
}
|
|
2102
|
-
info.results = results;
|
|
2103
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2104
3538
|
if (resultWithElementsFound.length === 0) {
|
|
2105
|
-
if (Date.now() - startTime > timeout) {
|
|
2106
|
-
throw new Error(`Text ${
|
|
3539
|
+
if (Date.now() - state.startTime > timeout) {
|
|
3540
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2107
3541
|
}
|
|
2108
3542
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2109
3543
|
continue;
|
|
2110
3544
|
}
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
+
}
|
|
2119
3601
|
}
|
|
2120
3602
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
3603
|
+
catch (error) {
|
|
3604
|
+
console.error(error);
|
|
3605
|
+
}
|
|
2123
3606
|
}
|
|
2124
3607
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2125
3608
|
}
|
|
2126
3609
|
catch (e) {
|
|
2127
|
-
|
|
2128
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2129
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2130
|
-
info.screenshotPath = screenshotPath;
|
|
2131
|
-
Object.assign(e, { info: info });
|
|
2132
|
-
error = e;
|
|
2133
|
-
throw e;
|
|
3610
|
+
await _commandError(state, e, this);
|
|
2134
3611
|
}
|
|
2135
3612
|
finally {
|
|
2136
|
-
|
|
2137
|
-
this._reportToWorld(world, {
|
|
2138
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2139
|
-
text: "Verify text exists in page",
|
|
2140
|
-
screenshotId,
|
|
2141
|
-
result: error
|
|
2142
|
-
? {
|
|
2143
|
-
status: "FAILED",
|
|
2144
|
-
startTime,
|
|
2145
|
-
endTime,
|
|
2146
|
-
message: error?.message,
|
|
2147
|
-
}
|
|
2148
|
-
: {
|
|
2149
|
-
status: "PASSED",
|
|
2150
|
-
startTime,
|
|
2151
|
-
endTime,
|
|
2152
|
-
},
|
|
2153
|
-
info: info,
|
|
2154
|
-
});
|
|
3613
|
+
await _commandFinally(state, this);
|
|
2155
3614
|
}
|
|
2156
3615
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
+
}
|
|
2164
3635
|
}
|
|
2165
|
-
|
|
3636
|
+
// state.info.results = results;
|
|
3637
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
3638
|
+
return resultWithElementsFound;
|
|
2166
3639
|
}
|
|
2167
3640
|
async visualVerification(text, options = {}, world = null) {
|
|
2168
3641
|
const startTime = Date.now();
|
|
@@ -2178,14 +3651,17 @@ class StableBrowser {
|
|
|
2178
3651
|
throw new Error("TOKEN is not set");
|
|
2179
3652
|
}
|
|
2180
3653
|
try {
|
|
2181
|
-
let serviceUrl =
|
|
3654
|
+
let serviceUrl = _getServerUrl();
|
|
2182
3655
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2183
3656
|
info.screenshotPath = screenshotPath;
|
|
2184
3657
|
const screenshot = await this.takeScreenshot();
|
|
2185
|
-
|
|
2186
|
-
method: "
|
|
3658
|
+
let request = {
|
|
3659
|
+
method: "post",
|
|
3660
|
+
maxBodyLength: Infinity,
|
|
2187
3661
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2188
3662
|
headers: {
|
|
3663
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
3664
|
+
"x-source": "aaa",
|
|
2189
3665
|
"Content-Type": "application/json",
|
|
2190
3666
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2191
3667
|
},
|
|
@@ -2194,7 +3670,7 @@ class StableBrowser {
|
|
|
2194
3670
|
screenshot: screenshot,
|
|
2195
3671
|
}),
|
|
2196
3672
|
};
|
|
2197
|
-
|
|
3673
|
+
const result = await axios.request(request);
|
|
2198
3674
|
if (result.data.status !== true) {
|
|
2199
3675
|
throw new Error("Visual validation failed");
|
|
2200
3676
|
}
|
|
@@ -2214,13 +3690,15 @@ class StableBrowser {
|
|
|
2214
3690
|
info.screenshotPath = screenshotPath;
|
|
2215
3691
|
Object.assign(e, { info: info });
|
|
2216
3692
|
error = e;
|
|
2217
|
-
throw e;
|
|
3693
|
+
// throw e;
|
|
3694
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
|
|
2218
3695
|
}
|
|
2219
3696
|
finally {
|
|
2220
3697
|
const endTime = Date.now();
|
|
2221
|
-
|
|
3698
|
+
_reportToWorld(world, {
|
|
2222
3699
|
type: Types.VERIFY_VISUAL,
|
|
2223
3700
|
text: "Visual verification",
|
|
3701
|
+
_text: "Visual verification of " + text,
|
|
2224
3702
|
screenshotId,
|
|
2225
3703
|
result: error
|
|
2226
3704
|
? {
|
|
@@ -2266,6 +3744,7 @@ class StableBrowser {
|
|
|
2266
3744
|
let screenshotPath = null;
|
|
2267
3745
|
const info = {};
|
|
2268
3746
|
info.log = "";
|
|
3747
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2269
3748
|
info.operation = "getTableData";
|
|
2270
3749
|
info.selectors = selectors;
|
|
2271
3750
|
try {
|
|
@@ -2281,11 +3760,12 @@ class StableBrowser {
|
|
|
2281
3760
|
info.screenshotPath = screenshotPath;
|
|
2282
3761
|
Object.assign(e, { info: info });
|
|
2283
3762
|
error = e;
|
|
2284
|
-
throw e;
|
|
3763
|
+
// throw e;
|
|
3764
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2285
3765
|
}
|
|
2286
3766
|
finally {
|
|
2287
3767
|
const endTime = Date.now();
|
|
2288
|
-
|
|
3768
|
+
_reportToWorld(world, {
|
|
2289
3769
|
element_name: selectors.element_name,
|
|
2290
3770
|
type: Types.GET_TABLE_DATA,
|
|
2291
3771
|
text: "Get table data",
|
|
@@ -2340,7 +3820,7 @@ class StableBrowser {
|
|
|
2340
3820
|
info.operation = "analyzeTable";
|
|
2341
3821
|
info.selectors = selectors;
|
|
2342
3822
|
info.query = query;
|
|
2343
|
-
query =
|
|
3823
|
+
query = _fixUsingParams(query, _params);
|
|
2344
3824
|
info.query_fixed = query;
|
|
2345
3825
|
info.operator = operator;
|
|
2346
3826
|
info.value = value;
|
|
@@ -2446,11 +3926,12 @@ class StableBrowser {
|
|
|
2446
3926
|
info.screenshotPath = screenshotPath;
|
|
2447
3927
|
Object.assign(e, { info: info });
|
|
2448
3928
|
error = e;
|
|
2449
|
-
throw e;
|
|
3929
|
+
// throw e;
|
|
3930
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2450
3931
|
}
|
|
2451
3932
|
finally {
|
|
2452
3933
|
const endTime = Date.now();
|
|
2453
|
-
|
|
3934
|
+
_reportToWorld(world, {
|
|
2454
3935
|
element_name: selectors.element_name,
|
|
2455
3936
|
type: Types.ANALYZE_TABLE,
|
|
2456
3937
|
text: "Analyze table",
|
|
@@ -2471,8 +3952,51 @@ class StableBrowser {
|
|
|
2471
3952
|
});
|
|
2472
3953
|
}
|
|
2473
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
|
+
}
|
|
2474
3992
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2475
|
-
|
|
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
|
+
}
|
|
2476
4000
|
}
|
|
2477
4001
|
_getLoadTimeout(options) {
|
|
2478
4002
|
let timeout = 15000;
|
|
@@ -2484,7 +4008,54 @@ class StableBrowser {
|
|
|
2484
4008
|
}
|
|
2485
4009
|
return timeout;
|
|
2486
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
|
+
}
|
|
2487
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");
|
|
2488
4059
|
let timeout = this._getLoadTimeout(options);
|
|
2489
4060
|
const promiseArray = [];
|
|
2490
4061
|
// let waitForNetworkIdle = true;
|
|
@@ -2517,13 +4088,15 @@ class StableBrowser {
|
|
|
2517
4088
|
else if (e.label === "domcontentloaded") {
|
|
2518
4089
|
console.log("waited for the domcontent loaded timeout");
|
|
2519
4090
|
}
|
|
2520
|
-
console.log(".");
|
|
2521
4091
|
}
|
|
2522
4092
|
finally {
|
|
2523
|
-
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
|
+
}
|
|
2524
4097
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2525
4098
|
const endTime = Date.now();
|
|
2526
|
-
|
|
4099
|
+
_reportToWorld(world, {
|
|
2527
4100
|
type: Types.GET_PAGE_STATUS,
|
|
2528
4101
|
text: "Wait for page load",
|
|
2529
4102
|
screenshotId,
|
|
@@ -2543,40 +4116,138 @@ class StableBrowser {
|
|
|
2543
4116
|
}
|
|
2544
4117
|
}
|
|
2545
4118
|
async closePage(options = {}, world = null) {
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
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
|
+
};
|
|
2551
4132
|
try {
|
|
4133
|
+
await _preCommand(state, this);
|
|
2552
4134
|
await this.page.close();
|
|
2553
4135
|
}
|
|
2554
4136
|
catch (e) {
|
|
2555
|
-
|
|
4137
|
+
await _commandError(state, e, this);
|
|
2556
4138
|
}
|
|
2557
4139
|
finally {
|
|
2558
|
-
await
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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;
|
|
2571
4175
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
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);
|
|
4239
|
+
}
|
|
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;
|
|
2579
4248
|
}
|
|
4249
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
4250
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2580
4251
|
}
|
|
2581
4252
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2582
4253
|
const startTime = Date.now();
|
|
@@ -2594,15 +4265,16 @@ class StableBrowser {
|
|
|
2594
4265
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
2595
4266
|
}
|
|
2596
4267
|
catch (e) {
|
|
2597
|
-
|
|
4268
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2598
4269
|
}
|
|
2599
4270
|
finally {
|
|
2600
4271
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2601
4272
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2602
4273
|
const endTime = Date.now();
|
|
2603
|
-
|
|
4274
|
+
_reportToWorld(world, {
|
|
2604
4275
|
type: Types.SET_VIEWPORT,
|
|
2605
4276
|
text: "set viewport size to " + width + "x" + hight,
|
|
4277
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2606
4278
|
screenshotId,
|
|
2607
4279
|
result: error
|
|
2608
4280
|
? {
|
|
@@ -2630,13 +4302,13 @@ class StableBrowser {
|
|
|
2630
4302
|
await this.page.reload();
|
|
2631
4303
|
}
|
|
2632
4304
|
catch (e) {
|
|
2633
|
-
|
|
4305
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2634
4306
|
}
|
|
2635
4307
|
finally {
|
|
2636
4308
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2637
4309
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2638
4310
|
const endTime = Date.now();
|
|
2639
|
-
|
|
4311
|
+
_reportToWorld(world, {
|
|
2640
4312
|
type: Types.GET_PAGE_STATUS,
|
|
2641
4313
|
text: "page relaod",
|
|
2642
4314
|
screenshotId,
|
|
@@ -2672,11 +4344,254 @@ class StableBrowser {
|
|
|
2672
4344
|
console.log("#-#");
|
|
2673
4345
|
}
|
|
2674
4346
|
}
|
|
2675
|
-
|
|
2676
|
-
if (
|
|
2677
|
-
|
|
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
|
+
if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
|
|
4379
|
+
this.trace = true;
|
|
4380
|
+
const traceFolder = path.join(this.context.reportFolder, "trace");
|
|
4381
|
+
if (!fs.existsSync(traceFolder)) {
|
|
4382
|
+
fs.mkdirSync(traceFolder, { recursive: true });
|
|
4383
|
+
}
|
|
4384
|
+
this.traceFolder = traceFolder;
|
|
4385
|
+
await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
async afterScenario(world, scenario) {
|
|
4389
|
+
const id = scenario.testCaseStartedId;
|
|
4390
|
+
if (this.trace) {
|
|
4391
|
+
await this.context.playContext.tracing.stop({
|
|
4392
|
+
path: path.join(this.traceFolder, `trace-${id}.zip`),
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
async beforeStep(world, step) {
|
|
4397
|
+
if (step?.pickleStep && this.trace) {
|
|
4398
|
+
await this.context.playContext.tracing.group(`Step: ${step.pickleStep.text}`);
|
|
4399
|
+
}
|
|
4400
|
+
this.stepTags = [];
|
|
4401
|
+
if (!this.beforeScenarioCalled) {
|
|
4402
|
+
this.beforeScenario(world, step);
|
|
4403
|
+
this.context.loadedRoutes = null;
|
|
4404
|
+
}
|
|
4405
|
+
if (this.stepIndex === undefined) {
|
|
4406
|
+
this.stepIndex = 0;
|
|
4407
|
+
}
|
|
4408
|
+
else {
|
|
4409
|
+
this.stepIndex++;
|
|
4410
|
+
}
|
|
4411
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
4412
|
+
this.stepName = step.pickleStep.text;
|
|
4413
|
+
let printableStepName = this.stepName;
|
|
4414
|
+
// take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
|
|
4415
|
+
printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
|
|
4416
|
+
return `\x1b[33m"${p1}"\x1b[0m`;
|
|
4417
|
+
});
|
|
4418
|
+
this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
|
|
4419
|
+
}
|
|
4420
|
+
else if (step && step.text) {
|
|
4421
|
+
this.stepName = step.text;
|
|
4422
|
+
}
|
|
4423
|
+
else {
|
|
4424
|
+
this.stepName = "step " + this.stepIndex;
|
|
4425
|
+
}
|
|
4426
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
4427
|
+
if (this.context.browserObject.context) {
|
|
4428
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
if (this.initSnapshotTaken === false) {
|
|
4432
|
+
this.initSnapshotTaken = true;
|
|
4433
|
+
if (world &&
|
|
4434
|
+
world.attach &&
|
|
4435
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4436
|
+
(!this.fastMode || this.stepTags.includes("fast-mode"))) {
|
|
4437
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4438
|
+
if (snapshot) {
|
|
4439
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
this.context.routeResults = null;
|
|
4444
|
+
this.context.loadedRoutes = null;
|
|
4445
|
+
await registerBeforeStepRoutes(this.context, this.stepName, world);
|
|
4446
|
+
networkBeforeStep(this.stepName, this.context);
|
|
4447
|
+
this.inStepReport = false;
|
|
4448
|
+
}
|
|
4449
|
+
setStepTags(tags) {
|
|
4450
|
+
this.stepTags = tags;
|
|
4451
|
+
}
|
|
4452
|
+
async getAriaSnapshot() {
|
|
4453
|
+
try {
|
|
4454
|
+
// find the page url
|
|
4455
|
+
const url = await this.page.url();
|
|
4456
|
+
// extract the path from the url
|
|
4457
|
+
const path = new URL(url).pathname;
|
|
4458
|
+
// get the page title
|
|
4459
|
+
const title = await this.page.title();
|
|
4460
|
+
// go over other frams
|
|
4461
|
+
const frames = this.page.frames();
|
|
4462
|
+
const snapshots = [];
|
|
4463
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
4464
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
4465
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4466
|
+
const frame = frames[i];
|
|
4467
|
+
try {
|
|
4468
|
+
// Ensure frame is attached and has body
|
|
4469
|
+
const body = frame.locator("body");
|
|
4470
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
4471
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4472
|
+
if (!snapshot) {
|
|
4473
|
+
continue;
|
|
4474
|
+
}
|
|
4475
|
+
content.push(`- frame: ${i}`);
|
|
4476
|
+
content.push(snapshot);
|
|
4477
|
+
}
|
|
4478
|
+
catch (innerErr) {
|
|
4479
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4480
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
return content.join("\n");
|
|
4484
|
+
}
|
|
4485
|
+
catch (e) {
|
|
4486
|
+
console.log("Error in getAriaSnapshot");
|
|
4487
|
+
//console.debug(e);
|
|
4488
|
+
}
|
|
4489
|
+
return null;
|
|
4490
|
+
}
|
|
4491
|
+
/**
|
|
4492
|
+
* Sends command with custom payload to report.
|
|
4493
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4494
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4495
|
+
* @param content - Content of the command to be shown in the report.
|
|
4496
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4497
|
+
* @param world - Optional world context.
|
|
4498
|
+
* @public
|
|
4499
|
+
*/
|
|
4500
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4501
|
+
const state = {
|
|
4502
|
+
options,
|
|
4503
|
+
world,
|
|
4504
|
+
locate: false,
|
|
4505
|
+
scroll: false,
|
|
4506
|
+
screenshot: options.screenshot ?? false,
|
|
4507
|
+
highlight: options.highlight ?? false,
|
|
4508
|
+
type: Types.REPORT_COMMAND,
|
|
4509
|
+
text: commandText,
|
|
4510
|
+
_text: commandText,
|
|
4511
|
+
operation: "report_command",
|
|
4512
|
+
log: "***** " + commandText + " *****\n",
|
|
4513
|
+
};
|
|
4514
|
+
try {
|
|
4515
|
+
await _preCommand(state, this);
|
|
4516
|
+
const payload = {
|
|
4517
|
+
type: options.type ?? "text",
|
|
4518
|
+
content: content,
|
|
4519
|
+
screenshotId: null,
|
|
4520
|
+
};
|
|
4521
|
+
state.payload = payload;
|
|
4522
|
+
if (commandStatus === "FAILED") {
|
|
4523
|
+
state.throwError = true;
|
|
4524
|
+
throw new Error(commandText);
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
catch (e) {
|
|
4528
|
+
await _commandError(state, e, this);
|
|
4529
|
+
}
|
|
4530
|
+
finally {
|
|
4531
|
+
await _commandFinally(state, this);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
async afterStep(world, step, result) {
|
|
4535
|
+
this.stepName = null;
|
|
4536
|
+
if (this.context) {
|
|
4537
|
+
this.context.examplesRow = null;
|
|
4538
|
+
}
|
|
4539
|
+
if (!this.inStepReport) {
|
|
4540
|
+
// check the step result
|
|
4541
|
+
if (result && result.status === "FAILED" && world && world.attach) {
|
|
4542
|
+
await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
if (world &&
|
|
4546
|
+
world.attach &&
|
|
4547
|
+
!process.env.DISABLE_SNAPSHOT &&
|
|
4548
|
+
!this.fastMode &&
|
|
4549
|
+
!this.stepTags.includes("fast-mode")) {
|
|
4550
|
+
const snapshot = await this.getAriaSnapshot();
|
|
4551
|
+
if (snapshot) {
|
|
4552
|
+
const obj = {};
|
|
4553
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4557
|
+
if (this.context.routeResults) {
|
|
4558
|
+
if (world && world.attach) {
|
|
4559
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
if (!process.env.TEMP_RUN) {
|
|
4563
|
+
const state = {
|
|
4564
|
+
world,
|
|
4565
|
+
locate: false,
|
|
4566
|
+
scroll: false,
|
|
4567
|
+
screenshot: true,
|
|
4568
|
+
highlight: true,
|
|
4569
|
+
type: Types.STEP_COMPLETE,
|
|
4570
|
+
text: "end of scenario",
|
|
4571
|
+
_text: "end of scenario",
|
|
4572
|
+
operation: "step_complete",
|
|
4573
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4574
|
+
};
|
|
4575
|
+
try {
|
|
4576
|
+
await _preCommand(state, this);
|
|
4577
|
+
}
|
|
4578
|
+
catch (e) {
|
|
4579
|
+
await _commandError(state, e, this);
|
|
4580
|
+
}
|
|
4581
|
+
finally {
|
|
4582
|
+
await _commandFinally(state, this);
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
networkAfterStep(this.stepName, this.context);
|
|
4586
|
+
if (process.env.TEMP_RUN === "true") {
|
|
4587
|
+
// Put a sleep for some time to allow the browser to finish processing
|
|
4588
|
+
if (!this.stepTags.includes("fast-mode")) {
|
|
4589
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
if (this.trace) {
|
|
4593
|
+
await this.context.playContext.tracing.groupEnd();
|
|
2678
4594
|
}
|
|
2679
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2680
4595
|
}
|
|
2681
4596
|
}
|
|
2682
4597
|
function createTimedPromise(promise, label) {
|
|
@@ -2684,156 +4599,5 @@ function createTimedPromise(promise, label) {
|
|
|
2684
4599
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2685
4600
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2686
4601
|
}
|
|
2687
|
-
const KEYBOARD_EVENTS = [
|
|
2688
|
-
"ALT",
|
|
2689
|
-
"AltGraph",
|
|
2690
|
-
"CapsLock",
|
|
2691
|
-
"Control",
|
|
2692
|
-
"Fn",
|
|
2693
|
-
"FnLock",
|
|
2694
|
-
"Hyper",
|
|
2695
|
-
"Meta",
|
|
2696
|
-
"NumLock",
|
|
2697
|
-
"ScrollLock",
|
|
2698
|
-
"Shift",
|
|
2699
|
-
"Super",
|
|
2700
|
-
"Symbol",
|
|
2701
|
-
"SymbolLock",
|
|
2702
|
-
"Enter",
|
|
2703
|
-
"Tab",
|
|
2704
|
-
"ArrowDown",
|
|
2705
|
-
"ArrowLeft",
|
|
2706
|
-
"ArrowRight",
|
|
2707
|
-
"ArrowUp",
|
|
2708
|
-
"End",
|
|
2709
|
-
"Home",
|
|
2710
|
-
"PageDown",
|
|
2711
|
-
"PageUp",
|
|
2712
|
-
"Backspace",
|
|
2713
|
-
"Clear",
|
|
2714
|
-
"Copy",
|
|
2715
|
-
"CrSel",
|
|
2716
|
-
"Cut",
|
|
2717
|
-
"Delete",
|
|
2718
|
-
"EraseEof",
|
|
2719
|
-
"ExSel",
|
|
2720
|
-
"Insert",
|
|
2721
|
-
"Paste",
|
|
2722
|
-
"Redo",
|
|
2723
|
-
"Undo",
|
|
2724
|
-
"Accept",
|
|
2725
|
-
"Again",
|
|
2726
|
-
"Attn",
|
|
2727
|
-
"Cancel",
|
|
2728
|
-
"ContextMenu",
|
|
2729
|
-
"Escape",
|
|
2730
|
-
"Execute",
|
|
2731
|
-
"Find",
|
|
2732
|
-
"Finish",
|
|
2733
|
-
"Help",
|
|
2734
|
-
"Pause",
|
|
2735
|
-
"Play",
|
|
2736
|
-
"Props",
|
|
2737
|
-
"Select",
|
|
2738
|
-
"ZoomIn",
|
|
2739
|
-
"ZoomOut",
|
|
2740
|
-
"BrightnessDown",
|
|
2741
|
-
"BrightnessUp",
|
|
2742
|
-
"Eject",
|
|
2743
|
-
"LogOff",
|
|
2744
|
-
"Power",
|
|
2745
|
-
"PowerOff",
|
|
2746
|
-
"PrintScreen",
|
|
2747
|
-
"Hibernate",
|
|
2748
|
-
"Standby",
|
|
2749
|
-
"WakeUp",
|
|
2750
|
-
"AllCandidates",
|
|
2751
|
-
"Alphanumeric",
|
|
2752
|
-
"CodeInput",
|
|
2753
|
-
"Compose",
|
|
2754
|
-
"Convert",
|
|
2755
|
-
"Dead",
|
|
2756
|
-
"FinalMode",
|
|
2757
|
-
"GroupFirst",
|
|
2758
|
-
"GroupLast",
|
|
2759
|
-
"GroupNext",
|
|
2760
|
-
"GroupPrevious",
|
|
2761
|
-
"ModeChange",
|
|
2762
|
-
"NextCandidate",
|
|
2763
|
-
"NonConvert",
|
|
2764
|
-
"PreviousCandidate",
|
|
2765
|
-
"Process",
|
|
2766
|
-
"SingleCandidate",
|
|
2767
|
-
"HangulMode",
|
|
2768
|
-
"HanjaMode",
|
|
2769
|
-
"JunjaMode",
|
|
2770
|
-
"Eisu",
|
|
2771
|
-
"Hankaku",
|
|
2772
|
-
"Hiragana",
|
|
2773
|
-
"HiraganaKatakana",
|
|
2774
|
-
"KanaMode",
|
|
2775
|
-
"KanjiMode",
|
|
2776
|
-
"Katakana",
|
|
2777
|
-
"Romaji",
|
|
2778
|
-
"Zenkaku",
|
|
2779
|
-
"ZenkakuHanaku",
|
|
2780
|
-
"F1",
|
|
2781
|
-
"F2",
|
|
2782
|
-
"F3",
|
|
2783
|
-
"F4",
|
|
2784
|
-
"F5",
|
|
2785
|
-
"F6",
|
|
2786
|
-
"F7",
|
|
2787
|
-
"F8",
|
|
2788
|
-
"F9",
|
|
2789
|
-
"F10",
|
|
2790
|
-
"F11",
|
|
2791
|
-
"F12",
|
|
2792
|
-
"Soft1",
|
|
2793
|
-
"Soft2",
|
|
2794
|
-
"Soft3",
|
|
2795
|
-
"Soft4",
|
|
2796
|
-
"ChannelDown",
|
|
2797
|
-
"ChannelUp",
|
|
2798
|
-
"Close",
|
|
2799
|
-
"MailForward",
|
|
2800
|
-
"MailReply",
|
|
2801
|
-
"MailSend",
|
|
2802
|
-
"MediaFastForward",
|
|
2803
|
-
"MediaPause",
|
|
2804
|
-
"MediaPlay",
|
|
2805
|
-
"MediaPlayPause",
|
|
2806
|
-
"MediaRecord",
|
|
2807
|
-
"MediaRewind",
|
|
2808
|
-
"MediaStop",
|
|
2809
|
-
"MediaTrackNext",
|
|
2810
|
-
"MediaTrackPrevious",
|
|
2811
|
-
"AudioBalanceLeft",
|
|
2812
|
-
"AudioBalanceRight",
|
|
2813
|
-
"AudioBassBoostDown",
|
|
2814
|
-
"AudioBassBoostToggle",
|
|
2815
|
-
"AudioBassBoostUp",
|
|
2816
|
-
"AudioFaderFront",
|
|
2817
|
-
"AudioFaderRear",
|
|
2818
|
-
"AudioSurroundModeNext",
|
|
2819
|
-
"AudioTrebleDown",
|
|
2820
|
-
"AudioTrebleUp",
|
|
2821
|
-
"AudioVolumeDown",
|
|
2822
|
-
"AudioVolumeMute",
|
|
2823
|
-
"AudioVolumeUp",
|
|
2824
|
-
"MicrophoneToggle",
|
|
2825
|
-
"MicrophoneVolumeDown",
|
|
2826
|
-
"MicrophoneVolumeMute",
|
|
2827
|
-
"MicrophoneVolumeUp",
|
|
2828
|
-
"TV",
|
|
2829
|
-
"TV3DMode",
|
|
2830
|
-
"TVAntennaCable",
|
|
2831
|
-
"TVAudioDescription",
|
|
2832
|
-
];
|
|
2833
|
-
function unEscapeString(str) {
|
|
2834
|
-
const placeholder = "__NEWLINE__";
|
|
2835
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2836
|
-
return str;
|
|
2837
|
-
}
|
|
2838
4602
|
export { StableBrowser };
|
|
2839
4603
|
//# sourceMappingURL=stable_browser.js.map
|