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