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