automation_model 1.0.409-dev → 1.0.409
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/lib/api.d.ts +43 -1
- package/lib/api.js +228 -41
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +2 -1
- package/lib/auto_page.js +43 -17
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +7 -3
- package/lib/browser_manager.js +110 -39
- package/lib/browser_manager.js.map +1 -1
- package/lib/command_common.d.ts +6 -0
- package/lib/command_common.js +164 -0
- package/lib/command_common.js.map +1 -0
- package/lib/environment.d.ts +3 -0
- package/lib/environment.js +5 -2
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.d.ts +6 -0
- package/lib/error-messages.js +188 -0
- package/lib/error-messages.js.map +1 -0
- 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 +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +3 -1
- package/lib/init_browser.js +67 -4
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.d.ts +7 -0
- package/lib/locate_element.js +215 -0
- package/lib/locate_element.js.map +1 -0
- package/lib/locator.d.ts +36 -0
- package/lib/locator.js +165 -0
- package/lib/locator.js.map +1 -1
- package/lib/locator_log.d.ts +26 -0
- package/lib/locator_log.js +69 -0
- package/lib/locator_log.js.map +1 -0
- package/lib/network.d.ts +3 -0
- package/lib/network.js +157 -0
- package/lib/network.js.map +1 -0
- package/lib/scripts/axe.mini.js +12 -0
- package/lib/stable_browser.d.ts +84 -35
- package/lib/stable_browser.js +1211 -1237
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +13 -0
- package/lib/table.js +187 -0
- package/lib/table.js.map +1 -0
- package/lib/test_context.d.ts +4 -0
- package/lib/test_context.js +12 -9
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +15 -1
- package/lib/utils.js +367 -5
- package/lib/utils.js.map +1 -1
- package/package.json +13 -8
package/lib/stable_browser.js
CHANGED
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
import { expect } from "@playwright/test";
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
4
|
import fs from "fs";
|
|
5
|
+
import { Jimp } from "jimp";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import reg_parser from "regex-parser";
|
|
7
|
-
import sharp from "sharp";
|
|
8
8
|
import { findDateAlternatives, findNumberAlternatives } from "./analyze_helper.js";
|
|
9
9
|
import { getDateTimeValue } from "./date_time.js";
|
|
10
10
|
import drawRectangle from "./drawRect.js";
|
|
11
11
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import
|
|
14
|
-
import { decrypt } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, } from "./utils.js";
|
|
15
14
|
import csv from "csv-parser";
|
|
16
15
|
import { Readable } from "node:stream";
|
|
17
|
-
|
|
16
|
+
import readline from "readline";
|
|
17
|
+
import { getContext } from "./init_browser.js";
|
|
18
|
+
import { locate_element } from "./locate_element.js";
|
|
19
|
+
import { randomUUID } from "crypto";
|
|
20
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
21
|
+
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
22
|
+
import { LocatorLog } from "./locator_log.js";
|
|
23
|
+
export const Types = {
|
|
18
24
|
CLICK: "click_element",
|
|
19
25
|
NAVIGATE: "navigate",
|
|
20
26
|
FILL: "fill_element",
|
|
@@ -25,6 +31,8 @@ const Types = {
|
|
|
25
31
|
GET_PAGE_STATUS: "get_page_status",
|
|
26
32
|
CLICK_ROW_ACTION: "click_row_action",
|
|
27
33
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
34
|
+
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
35
|
+
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
28
36
|
ANALYZE_TABLE: "analyze_table",
|
|
29
37
|
SELECT: "select_combobox",
|
|
30
38
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
@@ -40,16 +48,29 @@ const Types = {
|
|
|
40
48
|
VERIFY_VISUAL: "verify_visual",
|
|
41
49
|
LOAD_DATA: "load_data",
|
|
42
50
|
SET_INPUT: "set_input",
|
|
51
|
+
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
52
|
+
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
53
|
+
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
43
54
|
};
|
|
55
|
+
export const apps = {};
|
|
44
56
|
class StableBrowser {
|
|
45
|
-
|
|
57
|
+
browser;
|
|
58
|
+
page;
|
|
59
|
+
logger;
|
|
60
|
+
context;
|
|
61
|
+
world;
|
|
62
|
+
project_path = null;
|
|
63
|
+
webLogFile = null;
|
|
64
|
+
networkLogger = null;
|
|
65
|
+
configuration = null;
|
|
66
|
+
appName = "main";
|
|
67
|
+
tags = null;
|
|
68
|
+
constructor(browser, page, logger = null, context = null, world = null) {
|
|
46
69
|
this.browser = browser;
|
|
47
70
|
this.page = page;
|
|
48
71
|
this.logger = logger;
|
|
49
72
|
this.context = context;
|
|
50
|
-
this.
|
|
51
|
-
this.webLogFile = null;
|
|
52
|
-
this.configuration = null;
|
|
73
|
+
this.world = world;
|
|
53
74
|
if (!this.logger) {
|
|
54
75
|
this.logger = console;
|
|
55
76
|
}
|
|
@@ -75,23 +96,43 @@ class StableBrowser {
|
|
|
75
96
|
this.logger.error("unable to read ai_config.json");
|
|
76
97
|
}
|
|
77
98
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
78
|
-
this.
|
|
79
|
-
this.registerConsoleLogListener(page, context, this.webLogFile);
|
|
80
|
-
this.registerRequestListener();
|
|
99
|
+
this.world = world;
|
|
81
100
|
context.pages = [this.page];
|
|
82
101
|
context.pageLoading = { status: false };
|
|
102
|
+
this.registerEventListeners(this.context);
|
|
103
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
104
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
105
|
+
}
|
|
106
|
+
registerEventListeners(context) {
|
|
107
|
+
this.registerConsoleLogListener(this.page, context);
|
|
108
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
109
|
+
if (!context.pageLoading) {
|
|
110
|
+
context.pageLoading = { status: false };
|
|
111
|
+
}
|
|
83
112
|
context.playContext.on("page", async function (page) {
|
|
113
|
+
if (this.configuration && this.configuration.closePopups === true) {
|
|
114
|
+
console.log("close unexpected popups");
|
|
115
|
+
await page.close();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
84
118
|
context.pageLoading.status = true;
|
|
85
119
|
this.page = page;
|
|
86
120
|
context.page = page;
|
|
87
121
|
context.pages.push(page);
|
|
122
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
123
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
88
124
|
page.on("close", async () => {
|
|
89
|
-
if (this.context && this.context.pages && this.context.pages.length >
|
|
125
|
+
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
90
126
|
this.context.pages.pop();
|
|
91
127
|
this.page = this.context.pages[this.context.pages.length - 1];
|
|
92
128
|
this.context.page = this.page;
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
try {
|
|
130
|
+
let title = await this.page.title();
|
|
131
|
+
console.log("Switched to page " + title);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error("Error on page close", error);
|
|
135
|
+
}
|
|
95
136
|
}
|
|
96
137
|
});
|
|
97
138
|
try {
|
|
@@ -104,132 +145,149 @@ class StableBrowser {
|
|
|
104
145
|
context.pageLoading.status = false;
|
|
105
146
|
}.bind(this));
|
|
106
147
|
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
148
|
+
async switchApp(appName) {
|
|
149
|
+
// check if the current app (this.appName) is the same as the new app
|
|
150
|
+
if (this.appName === appName) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
let navigate = false;
|
|
154
|
+
if (!apps[appName]) {
|
|
155
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
156
|
+
newContextCreated = true;
|
|
157
|
+
apps[appName] = {
|
|
158
|
+
context: newContext,
|
|
159
|
+
browser: newContext.browser,
|
|
160
|
+
page: newContext.page,
|
|
161
|
+
};
|
|
110
162
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
163
|
+
const tempContext = {};
|
|
164
|
+
_copyContext(this, tempContext);
|
|
165
|
+
_copyContext(apps[appName], this);
|
|
166
|
+
apps[this.appName] = tempContext;
|
|
167
|
+
this.appName = appName;
|
|
168
|
+
if (navigate) {
|
|
169
|
+
await this.goto(this.context.environment.baseUrl);
|
|
170
|
+
await this.waitForPageLoad();
|
|
114
171
|
}
|
|
115
|
-
const fileName = nextIndex + ".json";
|
|
116
|
-
return path.join(logFolder, fileName);
|
|
117
172
|
}
|
|
118
|
-
registerConsoleLogListener(page, context
|
|
173
|
+
registerConsoleLogListener(page, context) {
|
|
119
174
|
if (!this.context.webLogger) {
|
|
120
175
|
this.context.webLogger = [];
|
|
121
176
|
}
|
|
122
177
|
page.on("console", async (msg) => {
|
|
123
|
-
|
|
178
|
+
const obj = {
|
|
124
179
|
type: msg.type(),
|
|
125
180
|
text: msg.text(),
|
|
126
181
|
location: msg.location(),
|
|
127
182
|
time: new Date().toISOString(),
|
|
128
|
-
}
|
|
129
|
-
|
|
183
|
+
};
|
|
184
|
+
this.context.webLogger.push(obj);
|
|
185
|
+
if (msg.type() === "error") {
|
|
186
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+log" });
|
|
187
|
+
}
|
|
130
188
|
});
|
|
131
189
|
}
|
|
132
|
-
registerRequestListener() {
|
|
133
|
-
this.
|
|
190
|
+
registerRequestListener(page, context, logFile) {
|
|
191
|
+
if (!this.context.networkLogger) {
|
|
192
|
+
this.context.networkLogger = [];
|
|
193
|
+
}
|
|
194
|
+
page.on("request", async (data) => {
|
|
195
|
+
const startTime = new Date().getTime();
|
|
134
196
|
try {
|
|
135
|
-
const pageUrl = new URL(
|
|
197
|
+
const pageUrl = new URL(page.url());
|
|
136
198
|
const requestUrl = new URL(data.url());
|
|
137
199
|
if (pageUrl.hostname === requestUrl.hostname) {
|
|
138
200
|
const method = data.method();
|
|
139
|
-
if (
|
|
201
|
+
if (["POST", "GET", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
140
202
|
const token = await data.headerValue("Authorization");
|
|
141
203
|
if (token) {
|
|
142
|
-
|
|
204
|
+
context.authtoken = token;
|
|
143
205
|
}
|
|
144
206
|
}
|
|
145
207
|
}
|
|
208
|
+
const response = await data.response();
|
|
209
|
+
const endTime = new Date().getTime();
|
|
210
|
+
const obj = {
|
|
211
|
+
url: data.url(),
|
|
212
|
+
method: data.method(),
|
|
213
|
+
postData: data.postData(),
|
|
214
|
+
error: data.failure() ? data.failure().errorText : null,
|
|
215
|
+
duration: endTime - startTime,
|
|
216
|
+
startTime,
|
|
217
|
+
};
|
|
218
|
+
context.networkLogger.push(obj);
|
|
219
|
+
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
146
220
|
}
|
|
147
221
|
catch (error) {
|
|
148
|
-
console.error("Error in request listener", error);
|
|
222
|
+
// console.error("Error in request listener", error);
|
|
223
|
+
context.networkLogger.push({
|
|
224
|
+
error: "not able to listen",
|
|
225
|
+
message: error.message,
|
|
226
|
+
stack: error.stack,
|
|
227
|
+
time: new Date().toISOString(),
|
|
228
|
+
});
|
|
229
|
+
// await fs.promises.writeFile(logFile, JSON.stringify(context.networkLogger, null, 2));
|
|
149
230
|
}
|
|
150
231
|
});
|
|
151
232
|
}
|
|
152
233
|
// async closeUnexpectedPopups() {
|
|
153
234
|
// await closeUnexpectedPopups(this.page);
|
|
154
235
|
// }
|
|
155
|
-
async goto(url) {
|
|
236
|
+
async goto(url, world = null) {
|
|
156
237
|
if (!url.startsWith("http")) {
|
|
157
238
|
url = "https://" + url;
|
|
158
239
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (!_params || typeof text !== "string") {
|
|
179
|
-
return text;
|
|
240
|
+
const state = {
|
|
241
|
+
value: url,
|
|
242
|
+
world: world,
|
|
243
|
+
type: Types.NAVIGATE,
|
|
244
|
+
text: `Navigate Page to: ${url}`,
|
|
245
|
+
operation: "goto",
|
|
246
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
247
|
+
info: {},
|
|
248
|
+
locate: false,
|
|
249
|
+
scroll: false,
|
|
250
|
+
screenshot: false,
|
|
251
|
+
highlight: false,
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
await _preCommand(state, this);
|
|
255
|
+
await this.page.goto(url, {
|
|
256
|
+
timeout: 60000,
|
|
257
|
+
});
|
|
258
|
+
await _screenshot(state, this);
|
|
180
259
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// remove the _ prefix
|
|
185
|
-
regValue = key.substring(1);
|
|
186
|
-
}
|
|
187
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
260
|
+
catch (error) {
|
|
261
|
+
console.error("Error on goto", error);
|
|
262
|
+
_commandError(state, error, this);
|
|
188
263
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
192
|
-
// check if not null
|
|
193
|
-
if (!locator) {
|
|
194
|
-
return locator;
|
|
264
|
+
finally {
|
|
265
|
+
_commandFinally(state, this);
|
|
195
266
|
}
|
|
196
|
-
// clone the locator
|
|
197
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
198
|
-
this.scanAndManipulate(locator, _params);
|
|
199
|
-
return locator;
|
|
200
|
-
}
|
|
201
|
-
_isObject(value) {
|
|
202
|
-
return value && typeof value === "object" && value.constructor === Object;
|
|
203
267
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
268
|
+
async _getLocator(locator, scope, _params) {
|
|
269
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
270
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
271
|
+
for (let key in locator) {
|
|
272
|
+
if (typeof locator[key] !== "string")
|
|
273
|
+
continue;
|
|
274
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
275
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
209
276
|
}
|
|
210
|
-
else if (this._isObject(currentObj[key])) {
|
|
211
|
-
// Recursively scan nested objects
|
|
212
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
_getLocator(locator, scope, _params) {
|
|
217
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
218
|
-
if (locator.type === "pw_selector") {
|
|
219
|
-
return scope.locator(locator.selector);
|
|
220
277
|
}
|
|
278
|
+
let locatorReturn;
|
|
221
279
|
if (locator.role) {
|
|
222
280
|
if (locator.role[1].nameReg) {
|
|
223
281
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
224
282
|
delete locator.role[1].nameReg;
|
|
225
283
|
}
|
|
226
284
|
// if (locator.role[1].name) {
|
|
227
|
-
// locator.role[1].name =
|
|
285
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
228
286
|
// }
|
|
229
|
-
|
|
287
|
+
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
230
288
|
}
|
|
231
289
|
if (locator.css) {
|
|
232
|
-
|
|
290
|
+
locatorReturn = scope.locator(locator.css);
|
|
233
291
|
}
|
|
234
292
|
// handle role/name locators
|
|
235
293
|
// locator.selector will be something like: textbox[name="Username"i]
|
|
@@ -240,205 +298,191 @@ class StableBrowser {
|
|
|
240
298
|
const role = match[1];
|
|
241
299
|
const name = match[3];
|
|
242
300
|
const flags = match[4];
|
|
243
|
-
|
|
301
|
+
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
244
302
|
}
|
|
245
303
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
304
|
+
if (locator?.engine) {
|
|
305
|
+
if (locator.engine === "css") {
|
|
306
|
+
locatorReturn = scope.locator(locator.selector);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
let selector = locator.selector;
|
|
310
|
+
if (locator.engine === "internal:attr") {
|
|
311
|
+
if (!selector.startsWith("[")) {
|
|
312
|
+
selector = `[${selector}]`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
254
316
|
}
|
|
255
|
-
const newLocator = scope.locator(`${locator.engine}="${selector}"`);
|
|
256
|
-
return newLocator;
|
|
257
317
|
}
|
|
258
|
-
|
|
318
|
+
if (!locatorReturn) {
|
|
319
|
+
console.error(locator);
|
|
320
|
+
throw new Error("Locator undefined");
|
|
321
|
+
}
|
|
322
|
+
return locatorReturn;
|
|
259
323
|
}
|
|
260
324
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
261
|
-
|
|
325
|
+
if (css && css.locator) {
|
|
326
|
+
css = css.locator;
|
|
327
|
+
}
|
|
328
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
262
329
|
if (result.elementCount === 0) {
|
|
263
330
|
return;
|
|
264
331
|
}
|
|
265
|
-
let textElementCss = "[data-blinq-id
|
|
332
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
266
333
|
// css climb to parent element
|
|
267
334
|
const climbArray = [];
|
|
268
335
|
for (let i = 0; i < climb; i++) {
|
|
269
336
|
climbArray.push("..");
|
|
270
337
|
}
|
|
271
338
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
let shadowHosts = [];
|
|
308
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
309
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
310
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
311
|
-
if (!shadowElement) {
|
|
312
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
316
|
-
elements = elements.concat(shadowElements);
|
|
317
|
-
}
|
|
318
|
-
let randomToken = null;
|
|
319
|
-
const foundElements = [];
|
|
320
|
-
if (regex) {
|
|
321
|
-
let regexpSearch = new RegExp(text, "im");
|
|
322
|
-
for (let i = 0; i < elements.length; i++) {
|
|
323
|
-
const element = elements[i];
|
|
324
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
325
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
326
|
-
foundElements.push(element);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
text = text.trim();
|
|
332
|
-
for (let i = 0; i < elements.length; i++) {
|
|
333
|
-
const element = elements[i];
|
|
334
|
-
if (partial) {
|
|
335
|
-
if ((element.innerText && element.innerText.trim().includes(text)) ||
|
|
336
|
-
(element.value && element.value.includes(text))) {
|
|
337
|
-
foundElements.push(element);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
342
|
-
(element.value && element.value === text)) {
|
|
343
|
-
foundElements.push(element);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
let noChildElements = [];
|
|
349
|
-
for (let i = 0; i < foundElements.length; i++) {
|
|
350
|
-
let element = foundElements[i];
|
|
351
|
-
let hasChild = false;
|
|
352
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
353
|
-
if (i === j) {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (isParent(element, foundElements[j])) {
|
|
357
|
-
hasChild = true;
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (!hasChild) {
|
|
362
|
-
noChildElements.push(element);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
let elementCount = 0;
|
|
366
|
-
if (noChildElements.length > 0) {
|
|
367
|
-
for (let i = 0; i < noChildElements.length; i++) {
|
|
368
|
-
if (randomToken === null) {
|
|
369
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
370
|
-
}
|
|
371
|
-
let element = noChildElements[i];
|
|
372
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
373
|
-
elementCount++;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return { elementCount: elementCount, randomToken: randomToken };
|
|
377
|
-
}, [text1, tag1, regex1, partial1]);
|
|
339
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
340
|
+
if (css) {
|
|
341
|
+
resultCss = resultCss + " >> " + css;
|
|
342
|
+
}
|
|
343
|
+
return resultCss;
|
|
344
|
+
}
|
|
345
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
346
|
+
const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
|
|
347
|
+
const locator = scope.locator(query);
|
|
348
|
+
const count = await locator.count();
|
|
349
|
+
if (!tag1) {
|
|
350
|
+
tag1 = "*";
|
|
351
|
+
}
|
|
352
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
353
|
+
let tagCount = 0;
|
|
354
|
+
for (let i = 0; i < count; i++) {
|
|
355
|
+
const element = locator.nth(i);
|
|
356
|
+
// check if the tag matches
|
|
357
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
358
|
+
if (!tag.startsWith("*")) {
|
|
359
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!el.setAttribute) {
|
|
364
|
+
el = el.parentElement;
|
|
365
|
+
}
|
|
366
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
367
|
+
return true;
|
|
368
|
+
}, [tag1, randomToken]))) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
tagCount++;
|
|
372
|
+
}
|
|
373
|
+
return { elementCount: tagCount, randomToken };
|
|
378
374
|
}
|
|
379
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
375
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false) {
|
|
376
|
+
if (!info) {
|
|
377
|
+
info = {};
|
|
378
|
+
}
|
|
379
|
+
if (!info.failCause) {
|
|
380
|
+
info.failCause = {};
|
|
381
|
+
}
|
|
382
|
+
if (!info.log) {
|
|
383
|
+
info.log = "";
|
|
384
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
385
|
+
}
|
|
380
386
|
let locatorSearch = selectorHierarchy[index];
|
|
387
|
+
let originalLocatorSearch = "";
|
|
388
|
+
try {
|
|
389
|
+
originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
|
|
390
|
+
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
console.error(e);
|
|
394
|
+
}
|
|
381
395
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
382
396
|
let locator = null;
|
|
383
397
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
384
398
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
385
399
|
if (!locatorString) {
|
|
400
|
+
info.failCause.textNotFound = true;
|
|
401
|
+
info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
|
|
386
402
|
return;
|
|
387
403
|
}
|
|
388
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
404
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
389
405
|
}
|
|
390
406
|
else if (locatorSearch.text) {
|
|
391
|
-
let
|
|
407
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
408
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
392
409
|
if (result.elementCount === 0) {
|
|
410
|
+
info.failCause.textNotFound = true;
|
|
411
|
+
info.failCause.lastError = "failed to locate element by text: " + text;
|
|
393
412
|
return;
|
|
394
413
|
}
|
|
395
|
-
locatorSearch.css = "[data-blinq-id
|
|
414
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
396
415
|
if (locatorSearch.childCss) {
|
|
397
416
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
398
417
|
}
|
|
399
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
418
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
400
419
|
}
|
|
401
420
|
else {
|
|
402
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
421
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
403
422
|
}
|
|
404
423
|
// let cssHref = false;
|
|
405
424
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
406
425
|
// cssHref = true;
|
|
407
426
|
// }
|
|
408
427
|
let count = await locator.count();
|
|
428
|
+
if (count > 0 && !info.failCause.count) {
|
|
429
|
+
info.failCause.count = count;
|
|
430
|
+
}
|
|
409
431
|
//info.log += "total elements found " + count + "\n";
|
|
410
432
|
//let visibleCount = 0;
|
|
411
433
|
let visibleLocator = null;
|
|
412
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
434
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
413
435
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
436
|
+
if (info.locatorLog) {
|
|
437
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
438
|
+
}
|
|
414
439
|
return;
|
|
415
440
|
}
|
|
441
|
+
if (info.locatorLog && count === 0) {
|
|
442
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
443
|
+
}
|
|
416
444
|
for (let j = 0; j < count; j++) {
|
|
417
445
|
let visible = await locator.nth(j).isVisible();
|
|
418
446
|
const enabled = await locator.nth(j).isEnabled();
|
|
419
447
|
if (!visibleOnly) {
|
|
420
448
|
visible = true;
|
|
421
449
|
}
|
|
422
|
-
if (visible && enabled) {
|
|
450
|
+
if (visible && (allowDisabled || enabled)) {
|
|
423
451
|
foundLocators.push(locator.nth(j));
|
|
452
|
+
if (info.locatorLog) {
|
|
453
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
454
|
+
}
|
|
424
455
|
}
|
|
425
456
|
else {
|
|
457
|
+
info.failCause.visible = visible;
|
|
458
|
+
info.failCause.enabled = enabled;
|
|
426
459
|
if (!info.printMessages) {
|
|
427
460
|
info.printMessages = {};
|
|
428
461
|
}
|
|
462
|
+
if (info.locatorLog && !visible) {
|
|
463
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
464
|
+
}
|
|
465
|
+
if (info.locatorLog && !enabled) {
|
|
466
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
467
|
+
}
|
|
429
468
|
if (!info.printMessages[j.toString()]) {
|
|
430
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
469
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
431
470
|
info.printMessages[j.toString()] = true;
|
|
432
471
|
}
|
|
433
472
|
}
|
|
434
473
|
}
|
|
435
474
|
}
|
|
436
475
|
async closeUnexpectedPopups(info, _params) {
|
|
476
|
+
if (!info) {
|
|
477
|
+
info = {};
|
|
478
|
+
info.failCause = {};
|
|
479
|
+
info.log = "";
|
|
480
|
+
}
|
|
437
481
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
438
482
|
if (!info) {
|
|
439
483
|
info = {};
|
|
440
484
|
}
|
|
441
|
-
info.log += "scan for popup handlers" + "\n";
|
|
485
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
442
486
|
const handlerGroup = [];
|
|
443
487
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
444
488
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
@@ -465,42 +509,95 @@ class StableBrowser {
|
|
|
465
509
|
}
|
|
466
510
|
if (result.foundElements.length > 0) {
|
|
467
511
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
468
|
-
|
|
512
|
+
try {
|
|
513
|
+
await scope?.evaluate(() => {
|
|
514
|
+
window.__isClosingPopups = true;
|
|
515
|
+
});
|
|
516
|
+
await dialogCloseLocator.click();
|
|
517
|
+
// wait for the dialog to close
|
|
518
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
await scope?.evaluate(() => {
|
|
524
|
+
window.__isClosingPopups = false;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
469
527
|
return { rerun: true };
|
|
470
528
|
}
|
|
471
529
|
}
|
|
472
530
|
}
|
|
473
531
|
return { rerun: false };
|
|
474
532
|
}
|
|
475
|
-
async _locate(selectors, info, _params, timeout =
|
|
533
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
534
|
+
if (!timeout) {
|
|
535
|
+
timeout = 30000;
|
|
536
|
+
}
|
|
476
537
|
for (let i = 0; i < 3; i++) {
|
|
477
|
-
info.log += "attempt " + i + ":
|
|
538
|
+
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
478
539
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
479
540
|
let selector = selectors.locators[j];
|
|
480
541
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
481
542
|
}
|
|
482
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
543
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
483
544
|
if (!element.rerun) {
|
|
484
545
|
return element;
|
|
485
546
|
}
|
|
486
547
|
}
|
|
487
548
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
488
549
|
}
|
|
489
|
-
async
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
550
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
551
|
+
if (!info) {
|
|
552
|
+
info = {};
|
|
553
|
+
info.failCause = {};
|
|
554
|
+
info.log = "";
|
|
555
|
+
}
|
|
556
|
+
let startTime = Date.now();
|
|
495
557
|
let scope = this.page;
|
|
558
|
+
if (selectors.frame) {
|
|
559
|
+
return selectors.frame;
|
|
560
|
+
}
|
|
496
561
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
497
|
-
|
|
562
|
+
const findFrame = async (frame, framescope) => {
|
|
563
|
+
for (let i = 0; i < frame.selectors.length; i++) {
|
|
564
|
+
let frameLocator = frame.selectors[i];
|
|
565
|
+
if (frameLocator.css) {
|
|
566
|
+
let testframescope = framescope.frameLocator(frameLocator.css);
|
|
567
|
+
if (frameLocator.index) {
|
|
568
|
+
testframescope = framescope.nth(frameLocator.index);
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
await testframescope.owner().evaluateHandle(() => true, null, {
|
|
572
|
+
timeout: 5000,
|
|
573
|
+
});
|
|
574
|
+
framescope = testframescope;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
console.error("frame not found " + frameLocator.css);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (frame.children) {
|
|
583
|
+
return await findFrame(frame.children, framescope);
|
|
584
|
+
}
|
|
585
|
+
return framescope;
|
|
586
|
+
};
|
|
587
|
+
let fLocator = null;
|
|
498
588
|
while (true) {
|
|
499
589
|
let frameFound = false;
|
|
590
|
+
if (selectors.nestFrmLoc) {
|
|
591
|
+
fLocator = selectors.nestFrmLoc;
|
|
592
|
+
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
593
|
+
frameFound = true;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
500
596
|
if (selectors.frameLocators) {
|
|
501
597
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
502
598
|
let frameLocator = selectors.frameLocators[i];
|
|
503
599
|
if (frameLocator.css) {
|
|
600
|
+
fLocator = frameLocator.css;
|
|
504
601
|
scope = scope.frameLocator(frameLocator.css);
|
|
505
602
|
frameFound = true;
|
|
506
603
|
break;
|
|
@@ -508,20 +605,55 @@ class StableBrowser {
|
|
|
508
605
|
}
|
|
509
606
|
}
|
|
510
607
|
if (!frameFound && selectors.iframe_src) {
|
|
608
|
+
fLocator = selectors.iframe_src;
|
|
511
609
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
512
610
|
}
|
|
513
611
|
if (!scope) {
|
|
514
|
-
info
|
|
515
|
-
|
|
612
|
+
if (info && info.locatorLog) {
|
|
613
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
614
|
+
}
|
|
615
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
616
|
+
if (Date.now() - startTime > timeout) {
|
|
617
|
+
info.failCause.iframeNotFound = true;
|
|
618
|
+
info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
|
|
516
619
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
517
620
|
}
|
|
518
621
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
519
622
|
}
|
|
520
623
|
else {
|
|
624
|
+
if (info && info.locatorLog) {
|
|
625
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
626
|
+
}
|
|
521
627
|
break;
|
|
522
628
|
}
|
|
523
629
|
}
|
|
524
630
|
}
|
|
631
|
+
if (!scope) {
|
|
632
|
+
scope = this.page;
|
|
633
|
+
}
|
|
634
|
+
return scope;
|
|
635
|
+
}
|
|
636
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
637
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
638
|
+
return scope.evaluate(() => {
|
|
639
|
+
var bodyContent = document.body.innerHTML;
|
|
640
|
+
return bodyContent;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
644
|
+
if (!info) {
|
|
645
|
+
info = {};
|
|
646
|
+
info.failCause = {};
|
|
647
|
+
info.log = "";
|
|
648
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
649
|
+
}
|
|
650
|
+
let highPriorityTimeout = 5000;
|
|
651
|
+
let visibleOnlyTimeout = 6000;
|
|
652
|
+
let startTime = Date.now();
|
|
653
|
+
let locatorsCount = 0;
|
|
654
|
+
let lazy_scroll = false;
|
|
655
|
+
//let arrayMode = Array.isArray(selectors);
|
|
656
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
525
657
|
let selectorsLocators = null;
|
|
526
658
|
selectorsLocators = selectors.locators;
|
|
527
659
|
// group selectors by priority
|
|
@@ -557,17 +689,17 @@ class StableBrowser {
|
|
|
557
689
|
}
|
|
558
690
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
559
691
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
560
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
692
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled);
|
|
561
693
|
if (result.foundElements.length === 0) {
|
|
562
694
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
563
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
695
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled);
|
|
564
696
|
}
|
|
565
697
|
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
566
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
698
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
|
|
567
699
|
}
|
|
568
700
|
else {
|
|
569
701
|
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
570
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
702
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
|
|
571
703
|
}
|
|
572
704
|
}
|
|
573
705
|
let foundElements = result.foundElements;
|
|
@@ -608,24 +740,36 @@ class StableBrowser {
|
|
|
608
740
|
return maxCountElement.locator;
|
|
609
741
|
}
|
|
610
742
|
}
|
|
611
|
-
if (
|
|
743
|
+
if (Date.now() - startTime > timeout) {
|
|
612
744
|
break;
|
|
613
745
|
}
|
|
614
|
-
if (
|
|
615
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
746
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
747
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
616
748
|
highPriorityOnly = false;
|
|
749
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
750
|
+
lazy_scroll = true;
|
|
751
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
752
|
+
}
|
|
617
753
|
}
|
|
618
|
-
if (
|
|
619
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
754
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
755
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
620
756
|
visibleOnly = false;
|
|
621
757
|
}
|
|
622
758
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
623
759
|
}
|
|
624
760
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
625
|
-
info.
|
|
761
|
+
// if (info.locatorLog) {
|
|
762
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
763
|
+
// for (let line of lines) {
|
|
764
|
+
// this.logger.debug(line);
|
|
765
|
+
// }
|
|
766
|
+
// }
|
|
767
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
768
|
+
info.failCause.locatorNotFound = true;
|
|
769
|
+
info.failCause.lastError = "failed to locate unique element";
|
|
626
770
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
627
771
|
}
|
|
628
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
772
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false) {
|
|
629
773
|
let foundElements = [];
|
|
630
774
|
const result = {
|
|
631
775
|
foundElements: foundElements,
|
|
@@ -633,14 +777,15 @@ class StableBrowser {
|
|
|
633
777
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
634
778
|
let foundLocators = [];
|
|
635
779
|
try {
|
|
636
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
780
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled);
|
|
637
781
|
}
|
|
638
782
|
catch (e) {
|
|
639
|
-
this
|
|
640
|
-
this.logger.debug(
|
|
783
|
+
// this call can fail it the browser is navigating
|
|
784
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
785
|
+
// this.logger.debug(e);
|
|
641
786
|
foundLocators = [];
|
|
642
787
|
try {
|
|
643
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
788
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled);
|
|
644
789
|
}
|
|
645
790
|
catch (e) {
|
|
646
791
|
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
@@ -654,86 +799,168 @@ class StableBrowser {
|
|
|
654
799
|
});
|
|
655
800
|
result.locatorIndex = i;
|
|
656
801
|
}
|
|
802
|
+
if (foundLocators.length > 1) {
|
|
803
|
+
info.failCause.foundMultiple = true;
|
|
804
|
+
if (info.locatorLog) {
|
|
805
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
657
808
|
}
|
|
658
809
|
return result;
|
|
659
810
|
}
|
|
660
|
-
async
|
|
661
|
-
|
|
811
|
+
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
812
|
+
const state = {
|
|
813
|
+
locate: false,
|
|
814
|
+
scroll: false,
|
|
815
|
+
highlight: false,
|
|
816
|
+
_params,
|
|
817
|
+
options,
|
|
818
|
+
world,
|
|
819
|
+
type: Types.CLICK,
|
|
820
|
+
text: "Click element",
|
|
821
|
+
operation: "simpleClick",
|
|
822
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
823
|
+
};
|
|
824
|
+
_preCommand(state, this);
|
|
662
825
|
const startTime = Date.now();
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
826
|
+
let timeout = 30000;
|
|
827
|
+
if (options && options.timeout) {
|
|
828
|
+
timeout = options.timeout;
|
|
829
|
+
}
|
|
830
|
+
while (true) {
|
|
831
|
+
try {
|
|
832
|
+
const result = await locate_element(this.context, elementDescription, "click");
|
|
833
|
+
if (result?.elementNumber >= 0) {
|
|
834
|
+
const selectors = {
|
|
835
|
+
frame: result?.frame,
|
|
836
|
+
locators: [
|
|
837
|
+
{
|
|
838
|
+
css: result?.css,
|
|
839
|
+
},
|
|
840
|
+
],
|
|
841
|
+
};
|
|
842
|
+
await this.click(selectors, _params, options, world);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch (e) {
|
|
847
|
+
if (performance.now() - startTime > timeout) {
|
|
848
|
+
// throw e;
|
|
849
|
+
try {
|
|
850
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
851
|
+
}
|
|
852
|
+
finally {
|
|
853
|
+
_commandFinally(state, this);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
861
|
+
const state = {
|
|
862
|
+
locate: false,
|
|
863
|
+
scroll: false,
|
|
864
|
+
highlight: false,
|
|
865
|
+
_params,
|
|
866
|
+
options,
|
|
867
|
+
world,
|
|
868
|
+
type: Types.FILL,
|
|
869
|
+
text: "Fill element",
|
|
870
|
+
operation: "simpleClickType",
|
|
871
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
872
|
+
};
|
|
873
|
+
_preCommand(state, this);
|
|
874
|
+
const startTime = Date.now();
|
|
875
|
+
let timeout = 30000;
|
|
876
|
+
if (options && options.timeout) {
|
|
877
|
+
timeout = options.timeout;
|
|
878
|
+
}
|
|
879
|
+
while (true) {
|
|
880
|
+
try {
|
|
881
|
+
const result = await locate_element(this.context, elementDescription, "fill", value);
|
|
882
|
+
if (result?.elementNumber >= 0) {
|
|
883
|
+
const selectors = {
|
|
884
|
+
frame: result?.frame,
|
|
885
|
+
locators: [
|
|
886
|
+
{
|
|
887
|
+
css: result?.css,
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
};
|
|
891
|
+
await this.clickType(selectors, value, false, _params, options, world);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch (e) {
|
|
896
|
+
if (performance.now() - startTime > timeout) {
|
|
897
|
+
// throw e;
|
|
898
|
+
try {
|
|
899
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
900
|
+
}
|
|
901
|
+
finally {
|
|
902
|
+
_commandFinally(state, this);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async click(selectors, _params, options = {}, world = null) {
|
|
910
|
+
const state = {
|
|
911
|
+
selectors,
|
|
912
|
+
_params,
|
|
913
|
+
options,
|
|
914
|
+
world,
|
|
915
|
+
text: "Click element",
|
|
916
|
+
type: Types.CLICK,
|
|
917
|
+
operation: "click",
|
|
918
|
+
log: "***** click on " + selectors.element_name + " *****\n",
|
|
919
|
+
};
|
|
670
920
|
try {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
921
|
+
await _preCommand(state, this);
|
|
922
|
+
// if (state.options && state.options.context) {
|
|
923
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
924
|
+
// }
|
|
674
925
|
try {
|
|
675
|
-
await
|
|
676
|
-
await
|
|
677
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
926
|
+
await state.element.click();
|
|
927
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
678
928
|
}
|
|
679
929
|
catch (e) {
|
|
680
930
|
// await this.closeUnexpectedPopups();
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
await
|
|
684
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
931
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
932
|
+
await state.element.dispatchEvent("click");
|
|
933
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
685
934
|
}
|
|
686
935
|
await this.waitForPageLoad();
|
|
687
|
-
return info;
|
|
936
|
+
return state.info;
|
|
688
937
|
}
|
|
689
938
|
catch (e) {
|
|
690
|
-
|
|
691
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
692
|
-
info.screenshotPath = screenshotPath;
|
|
693
|
-
Object.assign(e, { info: info });
|
|
694
|
-
error = e;
|
|
695
|
-
throw e;
|
|
939
|
+
await _commandError(state, e, this);
|
|
696
940
|
}
|
|
697
941
|
finally {
|
|
698
|
-
|
|
699
|
-
this._reportToWorld(world, {
|
|
700
|
-
element_name: selectors.element_name,
|
|
701
|
-
type: Types.CLICK,
|
|
702
|
-
text: `Click element`,
|
|
703
|
-
screenshotId,
|
|
704
|
-
result: error
|
|
705
|
-
? {
|
|
706
|
-
status: "FAILED",
|
|
707
|
-
startTime,
|
|
708
|
-
endTime,
|
|
709
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
710
|
-
}
|
|
711
|
-
: {
|
|
712
|
-
status: "PASSED",
|
|
713
|
-
startTime,
|
|
714
|
-
endTime,
|
|
715
|
-
},
|
|
716
|
-
info: info,
|
|
717
|
-
});
|
|
942
|
+
_commandFinally(state, this);
|
|
718
943
|
}
|
|
719
944
|
}
|
|
720
945
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
946
|
+
const state = {
|
|
947
|
+
selectors,
|
|
948
|
+
_params,
|
|
949
|
+
options,
|
|
950
|
+
world,
|
|
951
|
+
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
952
|
+
text: checked ? `Check element` : `Uncheck element`,
|
|
953
|
+
operation: "setCheck",
|
|
954
|
+
log: "***** check " + selectors.element_name + " *****\n",
|
|
955
|
+
};
|
|
731
956
|
try {
|
|
732
|
-
|
|
733
|
-
|
|
957
|
+
await _preCommand(state, this);
|
|
958
|
+
state.info.checked = checked;
|
|
959
|
+
// let element = await this._locate(selectors, info, _params);
|
|
960
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
734
961
|
try {
|
|
735
|
-
await this._highlightElements(element);
|
|
736
|
-
await element.setChecked(checked
|
|
962
|
+
// await this._highlightElements(element);
|
|
963
|
+
await state.element.setChecked(checked);
|
|
737
964
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
738
965
|
}
|
|
739
966
|
catch (e) {
|
|
@@ -742,179 +969,108 @@ class StableBrowser {
|
|
|
742
969
|
}
|
|
743
970
|
else {
|
|
744
971
|
//await this.closeUnexpectedPopups();
|
|
745
|
-
info.log += "setCheck failed, will try again" + "\n";
|
|
746
|
-
element = await this._locate(selectors, info, _params);
|
|
747
|
-
await element.setChecked(checked, { timeout: 5000, force: true });
|
|
972
|
+
state.info.log += "setCheck failed, will try again" + "\n";
|
|
973
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
974
|
+
await state.element.setChecked(checked, { timeout: 5000, force: true });
|
|
748
975
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
749
976
|
}
|
|
750
977
|
}
|
|
751
978
|
await this.waitForPageLoad();
|
|
752
|
-
return info;
|
|
979
|
+
return state.info;
|
|
753
980
|
}
|
|
754
981
|
catch (e) {
|
|
755
|
-
|
|
756
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
757
|
-
info.screenshotPath = screenshotPath;
|
|
758
|
-
Object.assign(e, { info: info });
|
|
759
|
-
error = e;
|
|
760
|
-
throw e;
|
|
982
|
+
await _commandError(state, e, this);
|
|
761
983
|
}
|
|
762
984
|
finally {
|
|
763
|
-
|
|
764
|
-
this._reportToWorld(world, {
|
|
765
|
-
element_name: selectors.element_name,
|
|
766
|
-
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
767
|
-
text: checked ? `Check element` : `Uncheck element`,
|
|
768
|
-
screenshotId,
|
|
769
|
-
result: error
|
|
770
|
-
? {
|
|
771
|
-
status: "FAILED",
|
|
772
|
-
startTime,
|
|
773
|
-
endTime,
|
|
774
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
775
|
-
}
|
|
776
|
-
: {
|
|
777
|
-
status: "PASSED",
|
|
778
|
-
startTime,
|
|
779
|
-
endTime,
|
|
780
|
-
},
|
|
781
|
-
info: info,
|
|
782
|
-
});
|
|
985
|
+
_commandFinally(state, this);
|
|
783
986
|
}
|
|
784
987
|
}
|
|
785
988
|
async hover(selectors, _params, options = {}, world = null) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
989
|
+
const state = {
|
|
990
|
+
selectors,
|
|
991
|
+
_params,
|
|
992
|
+
options,
|
|
993
|
+
world,
|
|
994
|
+
type: Types.HOVER,
|
|
995
|
+
text: `Hover element`,
|
|
996
|
+
operation: "hover",
|
|
997
|
+
log: "***** hover " + selectors.element_name + " *****\n",
|
|
998
|
+
};
|
|
795
999
|
try {
|
|
796
|
-
|
|
797
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1000
|
+
await _preCommand(state, this);
|
|
798
1001
|
try {
|
|
799
|
-
await
|
|
800
|
-
await element.hover({ timeout: 10000 });
|
|
1002
|
+
await state.element.hover();
|
|
801
1003
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
802
1004
|
}
|
|
803
1005
|
catch (e) {
|
|
804
1006
|
//await this.closeUnexpectedPopups();
|
|
805
|
-
info.log += "hover failed, will try again" + "\n";
|
|
806
|
-
element = await this._locate(selectors, info, _params);
|
|
807
|
-
await element.hover({ timeout: 10000 });
|
|
1007
|
+
state.info.log += "hover failed, will try again" + "\n";
|
|
1008
|
+
state.element = await this._locate(selectors, state.info, _params);
|
|
1009
|
+
await state.element.hover({ timeout: 10000 });
|
|
808
1010
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
809
1011
|
}
|
|
810
1012
|
await this.waitForPageLoad();
|
|
811
|
-
return info;
|
|
1013
|
+
return state.info;
|
|
812
1014
|
}
|
|
813
1015
|
catch (e) {
|
|
814
|
-
|
|
815
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
816
|
-
info.screenshotPath = screenshotPath;
|
|
817
|
-
Object.assign(e, { info: info });
|
|
818
|
-
error = e;
|
|
819
|
-
throw e;
|
|
1016
|
+
await _commandError(state, e, this);
|
|
820
1017
|
}
|
|
821
1018
|
finally {
|
|
822
|
-
|
|
823
|
-
this._reportToWorld(world, {
|
|
824
|
-
element_name: selectors.element_name,
|
|
825
|
-
type: Types.HOVER,
|
|
826
|
-
text: `Hover element`,
|
|
827
|
-
screenshotId,
|
|
828
|
-
result: error
|
|
829
|
-
? {
|
|
830
|
-
status: "FAILED",
|
|
831
|
-
startTime,
|
|
832
|
-
endTime,
|
|
833
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
834
|
-
}
|
|
835
|
-
: {
|
|
836
|
-
status: "PASSED",
|
|
837
|
-
startTime,
|
|
838
|
-
endTime,
|
|
839
|
-
},
|
|
840
|
-
info: info,
|
|
841
|
-
});
|
|
1019
|
+
_commandFinally(state, this);
|
|
842
1020
|
}
|
|
843
1021
|
}
|
|
844
1022
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
845
|
-
this._validateSelectors(selectors);
|
|
846
1023
|
if (!values) {
|
|
847
1024
|
throw new Error("values is null");
|
|
848
1025
|
}
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1026
|
+
const state = {
|
|
1027
|
+
selectors,
|
|
1028
|
+
_params,
|
|
1029
|
+
options,
|
|
1030
|
+
world,
|
|
1031
|
+
value: values.toString(),
|
|
1032
|
+
type: Types.SELECT,
|
|
1033
|
+
text: `Select option: ${values}`,
|
|
1034
|
+
operation: "selectOption",
|
|
1035
|
+
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1036
|
+
};
|
|
857
1037
|
try {
|
|
858
|
-
|
|
859
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1038
|
+
await _preCommand(state, this);
|
|
860
1039
|
try {
|
|
861
|
-
await
|
|
862
|
-
await element.selectOption(values, { timeout: 5000 });
|
|
1040
|
+
await state.element.selectOption(values);
|
|
863
1041
|
}
|
|
864
1042
|
catch (e) {
|
|
865
1043
|
//await this.closeUnexpectedPopups();
|
|
866
|
-
info.log += "selectOption failed, will try force" + "\n";
|
|
867
|
-
await element.selectOption(values, { timeout: 10000, force: true });
|
|
1044
|
+
state.info.log += "selectOption failed, will try force" + "\n";
|
|
1045
|
+
await state.element.selectOption(values, { timeout: 10000, force: true });
|
|
868
1046
|
}
|
|
869
1047
|
await this.waitForPageLoad();
|
|
870
|
-
return info;
|
|
1048
|
+
return state.info;
|
|
871
1049
|
}
|
|
872
1050
|
catch (e) {
|
|
873
|
-
|
|
874
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
875
|
-
info.screenshotPath = screenshotPath;
|
|
876
|
-
Object.assign(e, { info: info });
|
|
877
|
-
this.logger.info("click failed, will try next selector");
|
|
878
|
-
error = e;
|
|
879
|
-
throw e;
|
|
1051
|
+
await _commandError(state, e, this);
|
|
880
1052
|
}
|
|
881
1053
|
finally {
|
|
882
|
-
|
|
883
|
-
this._reportToWorld(world, {
|
|
884
|
-
element_name: selectors.element_name,
|
|
885
|
-
type: Types.SELECT,
|
|
886
|
-
text: `Select option: ${values}`,
|
|
887
|
-
value: values.toString(),
|
|
888
|
-
screenshotId,
|
|
889
|
-
result: error
|
|
890
|
-
? {
|
|
891
|
-
status: "FAILED",
|
|
892
|
-
startTime,
|
|
893
|
-
endTime,
|
|
894
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
895
|
-
}
|
|
896
|
-
: {
|
|
897
|
-
status: "PASSED",
|
|
898
|
-
startTime,
|
|
899
|
-
endTime,
|
|
900
|
-
},
|
|
901
|
-
info: info,
|
|
902
|
-
});
|
|
1054
|
+
_commandFinally(state, this);
|
|
903
1055
|
}
|
|
904
1056
|
}
|
|
905
1057
|
async type(_value, _params = null, options = {}, world = null) {
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1058
|
+
const state = {
|
|
1059
|
+
value: _value,
|
|
1060
|
+
_params,
|
|
1061
|
+
options,
|
|
1062
|
+
world,
|
|
1063
|
+
locate: false,
|
|
1064
|
+
scroll: false,
|
|
1065
|
+
highlight: false,
|
|
1066
|
+
type: Types.TYPE_PRESS,
|
|
1067
|
+
text: `Type value: ${_value}`,
|
|
1068
|
+
operation: "type",
|
|
1069
|
+
log: "",
|
|
1070
|
+
};
|
|
915
1071
|
try {
|
|
916
|
-
|
|
917
|
-
const valueSegment =
|
|
1072
|
+
await _preCommand(state, this);
|
|
1073
|
+
const valueSegment = state.value.split("&&");
|
|
918
1074
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
919
1075
|
if (i > 0) {
|
|
920
1076
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -934,134 +1090,76 @@ class StableBrowser {
|
|
|
934
1090
|
await this.page.keyboard.type(value);
|
|
935
1091
|
}
|
|
936
1092
|
}
|
|
937
|
-
return info;
|
|
1093
|
+
return state.info;
|
|
938
1094
|
}
|
|
939
1095
|
catch (e) {
|
|
940
|
-
|
|
941
|
-
this.logger.error("type failed " + info.log);
|
|
942
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
943
|
-
info.screenshotPath = screenshotPath;
|
|
944
|
-
Object.assign(e, { info: info });
|
|
945
|
-
error = e;
|
|
946
|
-
throw e;
|
|
1096
|
+
await _commandError(state, e, this);
|
|
947
1097
|
}
|
|
948
1098
|
finally {
|
|
949
|
-
|
|
950
|
-
this._reportToWorld(world, {
|
|
951
|
-
type: Types.TYPE_PRESS,
|
|
952
|
-
screenshotId,
|
|
953
|
-
value: _value,
|
|
954
|
-
text: `type value: ${_value}`,
|
|
955
|
-
result: error
|
|
956
|
-
? {
|
|
957
|
-
status: "FAILED",
|
|
958
|
-
startTime,
|
|
959
|
-
endTime,
|
|
960
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
961
|
-
}
|
|
962
|
-
: {
|
|
963
|
-
status: "PASSED",
|
|
964
|
-
startTime,
|
|
965
|
-
endTime,
|
|
966
|
-
},
|
|
967
|
-
info: info,
|
|
968
|
-
});
|
|
1099
|
+
_commandFinally(state, this);
|
|
969
1100
|
}
|
|
970
1101
|
}
|
|
971
1102
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
let screenshotPath = null;
|
|
1103
|
+
const state = {
|
|
1104
|
+
selectors,
|
|
1105
|
+
_params,
|
|
1106
|
+
value,
|
|
1107
|
+
options,
|
|
1108
|
+
world,
|
|
1109
|
+
type: Types.SET_INPUT,
|
|
1110
|
+
text: `Set input value`,
|
|
1111
|
+
operation: "setInputValue",
|
|
1112
|
+
log: "***** set input value " + selectors.element_name + " *****\n",
|
|
1113
|
+
};
|
|
984
1114
|
try {
|
|
985
|
-
|
|
986
|
-
let
|
|
987
|
-
await this.scrollIfNeeded(element, info);
|
|
988
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
989
|
-
await this._highlightElements(element);
|
|
1115
|
+
await _preCommand(state, this);
|
|
1116
|
+
let value = await this._replaceWithLocalData(state.value, this);
|
|
990
1117
|
try {
|
|
991
|
-
await element.evaluateHandle((el, value) => {
|
|
1118
|
+
await state.element.evaluateHandle((el, value) => {
|
|
992
1119
|
el.value = value;
|
|
993
1120
|
}, value);
|
|
994
1121
|
}
|
|
995
1122
|
catch (error) {
|
|
996
1123
|
this.logger.error("setInputValue failed, will try again");
|
|
997
|
-
|
|
998
|
-
info.
|
|
999
|
-
|
|
1000
|
-
await element.evaluateHandle((el, value) => {
|
|
1124
|
+
await _screenshot(state, this);
|
|
1125
|
+
Object.assign(error, { info: state.info });
|
|
1126
|
+
await state.element.evaluateHandle((el, value) => {
|
|
1001
1127
|
el.value = value;
|
|
1002
1128
|
});
|
|
1003
1129
|
}
|
|
1004
1130
|
}
|
|
1005
1131
|
catch (e) {
|
|
1006
|
-
|
|
1007
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1008
|
-
info.screenshotPath = screenshotPath;
|
|
1009
|
-
Object.assign(e, { info: info });
|
|
1010
|
-
error = e;
|
|
1011
|
-
throw e;
|
|
1132
|
+
await _commandError(state, e, this);
|
|
1012
1133
|
}
|
|
1013
1134
|
finally {
|
|
1014
|
-
|
|
1015
|
-
this._reportToWorld(world, {
|
|
1016
|
-
element_name: selectors.element_name,
|
|
1017
|
-
type: Types.SET_INPUT,
|
|
1018
|
-
text: `Set input value`,
|
|
1019
|
-
value: value,
|
|
1020
|
-
screenshotId,
|
|
1021
|
-
result: error
|
|
1022
|
-
? {
|
|
1023
|
-
status: "FAILED",
|
|
1024
|
-
startTime,
|
|
1025
|
-
endTime,
|
|
1026
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1027
|
-
}
|
|
1028
|
-
: {
|
|
1029
|
-
status: "PASSED",
|
|
1030
|
-
startTime,
|
|
1031
|
-
endTime,
|
|
1032
|
-
},
|
|
1033
|
-
info: info,
|
|
1034
|
-
});
|
|
1135
|
+
_commandFinally(state, this);
|
|
1035
1136
|
}
|
|
1036
1137
|
}
|
|
1037
1138
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1139
|
+
const state = {
|
|
1140
|
+
selectors,
|
|
1141
|
+
_params,
|
|
1142
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1143
|
+
options,
|
|
1144
|
+
world,
|
|
1145
|
+
type: Types.SET_DATE_TIME,
|
|
1146
|
+
text: `Set date time value: ${value}`,
|
|
1147
|
+
operation: "setDateTime",
|
|
1148
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1149
|
+
throwError: false,
|
|
1150
|
+
};
|
|
1048
1151
|
try {
|
|
1049
|
-
|
|
1050
|
-
let element = await this._locate(selectors, info, _params);
|
|
1051
|
-
//insert red border around the element
|
|
1052
|
-
await this.scrollIfNeeded(element, info);
|
|
1053
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1054
|
-
await this._highlightElements(element);
|
|
1152
|
+
await _preCommand(state, this);
|
|
1055
1153
|
try {
|
|
1056
|
-
await element.click();
|
|
1154
|
+
await state.element.click();
|
|
1057
1155
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1058
1156
|
if (format) {
|
|
1059
|
-
value = dayjs(value).format(format);
|
|
1060
|
-
await element.fill(value);
|
|
1157
|
+
state.value = dayjs(state.value).format(format);
|
|
1158
|
+
await state.element.fill(state.value);
|
|
1061
1159
|
}
|
|
1062
1160
|
else {
|
|
1063
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1064
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1161
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1162
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1065
1163
|
el.value = ""; // clear input
|
|
1066
1164
|
el.value = dateTimeValue;
|
|
1067
1165
|
}, dateTimeValue);
|
|
@@ -1074,20 +1172,19 @@ class StableBrowser {
|
|
|
1074
1172
|
}
|
|
1075
1173
|
catch (err) {
|
|
1076
1174
|
//await this.closeUnexpectedPopups();
|
|
1077
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1175
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1078
1176
|
this.logger.info("Trying again");
|
|
1079
|
-
|
|
1080
|
-
info.
|
|
1081
|
-
Object.assign(err, { info: info });
|
|
1177
|
+
await _screenshot(state, this);
|
|
1178
|
+
Object.assign(err, { info: state.info });
|
|
1082
1179
|
await element.click();
|
|
1083
1180
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1084
1181
|
if (format) {
|
|
1085
|
-
value = dayjs(value).format(format);
|
|
1086
|
-
await element.fill(value);
|
|
1182
|
+
state.value = dayjs(state.value).format(format);
|
|
1183
|
+
await state.element.fill(state.value);
|
|
1087
1184
|
}
|
|
1088
1185
|
else {
|
|
1089
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1090
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1186
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1187
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1091
1188
|
el.value = ""; // clear input
|
|
1092
1189
|
el.value = dateTimeValue;
|
|
1093
1190
|
}, dateTimeValue);
|
|
@@ -1100,60 +1197,39 @@ class StableBrowser {
|
|
|
1100
1197
|
}
|
|
1101
1198
|
}
|
|
1102
1199
|
catch (e) {
|
|
1103
|
-
|
|
1104
|
-
throw e;
|
|
1200
|
+
await _commandError(state, e, this);
|
|
1105
1201
|
}
|
|
1106
1202
|
finally {
|
|
1107
|
-
|
|
1108
|
-
this._reportToWorld(world, {
|
|
1109
|
-
element_name: selectors.element_name,
|
|
1110
|
-
type: Types.SET_DATE_TIME,
|
|
1111
|
-
screenshotId,
|
|
1112
|
-
value: value,
|
|
1113
|
-
text: `setDateTime input with value: ${value}`,
|
|
1114
|
-
result: error
|
|
1115
|
-
? {
|
|
1116
|
-
status: "FAILED",
|
|
1117
|
-
startTime,
|
|
1118
|
-
endTime,
|
|
1119
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1120
|
-
}
|
|
1121
|
-
: {
|
|
1122
|
-
status: "PASSED",
|
|
1123
|
-
startTime,
|
|
1124
|
-
endTime,
|
|
1125
|
-
},
|
|
1126
|
-
info: info,
|
|
1127
|
-
});
|
|
1203
|
+
_commandFinally(state, this);
|
|
1128
1204
|
}
|
|
1129
1205
|
}
|
|
1130
1206
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1131
|
-
|
|
1132
|
-
const startTime = Date.now();
|
|
1133
|
-
let error = null;
|
|
1134
|
-
let screenshotId = null;
|
|
1135
|
-
let screenshotPath = null;
|
|
1136
|
-
const info = {};
|
|
1137
|
-
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1138
|
-
info.operation = "clickType";
|
|
1139
|
-
info.selectors = selectors;
|
|
1207
|
+
_value = unEscapeString(_value);
|
|
1140
1208
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1209
|
+
const state = {
|
|
1210
|
+
selectors,
|
|
1211
|
+
_params,
|
|
1212
|
+
value: newValue,
|
|
1213
|
+
originalValue: _value,
|
|
1214
|
+
options,
|
|
1215
|
+
world,
|
|
1216
|
+
type: Types.FILL,
|
|
1217
|
+
text: `Click type input with value: ${_value}`,
|
|
1218
|
+
operation: "clickType",
|
|
1219
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1220
|
+
};
|
|
1141
1221
|
if (newValue !== _value) {
|
|
1142
1222
|
//this.logger.info(_value + "=" + newValue);
|
|
1143
1223
|
_value = newValue;
|
|
1144
1224
|
}
|
|
1145
|
-
info.value = _value;
|
|
1146
1225
|
try {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
await this.scrollIfNeeded(element, info);
|
|
1150
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1151
|
-
await this._highlightElements(element);
|
|
1226
|
+
await _preCommand(state, this);
|
|
1227
|
+
state.info.value = _value;
|
|
1152
1228
|
if (options === null || options === undefined || !options.press) {
|
|
1153
1229
|
try {
|
|
1154
|
-
let currentValue = await element.inputValue();
|
|
1230
|
+
let currentValue = await state.element.inputValue();
|
|
1155
1231
|
if (currentValue) {
|
|
1156
|
-
await element.fill("");
|
|
1232
|
+
await state.element.fill("");
|
|
1157
1233
|
}
|
|
1158
1234
|
}
|
|
1159
1235
|
catch (e) {
|
|
@@ -1162,22 +1238,22 @@ class StableBrowser {
|
|
|
1162
1238
|
}
|
|
1163
1239
|
if (options === null || options === undefined || options.press) {
|
|
1164
1240
|
try {
|
|
1165
|
-
await element.click({ timeout: 5000 });
|
|
1241
|
+
await state.element.click({ timeout: 5000 });
|
|
1166
1242
|
}
|
|
1167
1243
|
catch (e) {
|
|
1168
|
-
await element.dispatchEvent("click");
|
|
1244
|
+
await state.element.dispatchEvent("click");
|
|
1169
1245
|
}
|
|
1170
1246
|
}
|
|
1171
1247
|
else {
|
|
1172
1248
|
try {
|
|
1173
|
-
await element.focus();
|
|
1249
|
+
await state.element.focus();
|
|
1174
1250
|
}
|
|
1175
1251
|
catch (e) {
|
|
1176
|
-
await element.dispatchEvent("focus");
|
|
1252
|
+
await state.element.dispatchEvent("focus");
|
|
1177
1253
|
}
|
|
1178
1254
|
}
|
|
1179
1255
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1180
|
-
const valueSegment =
|
|
1256
|
+
const valueSegment = state.value.split("&&");
|
|
1181
1257
|
for (let i = 0; i < valueSegment.length; i++) {
|
|
1182
1258
|
if (i > 0) {
|
|
1183
1259
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1197,13 +1273,19 @@ class StableBrowser {
|
|
|
1197
1273
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1198
1274
|
}
|
|
1199
1275
|
}
|
|
1276
|
+
await _screenshot(state, this);
|
|
1200
1277
|
if (enter === true) {
|
|
1201
1278
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1202
1279
|
await this.page.keyboard.press("Enter");
|
|
1203
1280
|
await this.waitForPageLoad();
|
|
1204
1281
|
}
|
|
1205
1282
|
else if (enter === false) {
|
|
1206
|
-
|
|
1283
|
+
try {
|
|
1284
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1285
|
+
}
|
|
1286
|
+
catch (e) {
|
|
1287
|
+
// ignore
|
|
1288
|
+
}
|
|
1207
1289
|
//await this.page.keyboard.press("Tab");
|
|
1208
1290
|
}
|
|
1209
1291
|
else {
|
|
@@ -1212,107 +1294,55 @@ class StableBrowser {
|
|
|
1212
1294
|
await this.waitForPageLoad();
|
|
1213
1295
|
}
|
|
1214
1296
|
}
|
|
1215
|
-
return info;
|
|
1297
|
+
return state.info;
|
|
1216
1298
|
}
|
|
1217
1299
|
catch (e) {
|
|
1218
|
-
|
|
1219
|
-
this.logger.error("fill failed " + JSON.stringify(info));
|
|
1220
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1221
|
-
info.screenshotPath = screenshotPath;
|
|
1222
|
-
Object.assign(e, { info: info });
|
|
1223
|
-
error = e;
|
|
1224
|
-
throw e;
|
|
1300
|
+
await _commandError(state, e, this);
|
|
1225
1301
|
}
|
|
1226
1302
|
finally {
|
|
1227
|
-
|
|
1228
|
-
this._reportToWorld(world, {
|
|
1229
|
-
element_name: selectors.element_name,
|
|
1230
|
-
type: Types.FILL,
|
|
1231
|
-
screenshotId,
|
|
1232
|
-
value: _value,
|
|
1233
|
-
text: `clickType input with value: ${_value}`,
|
|
1234
|
-
result: error
|
|
1235
|
-
? {
|
|
1236
|
-
status: "FAILED",
|
|
1237
|
-
startTime,
|
|
1238
|
-
endTime,
|
|
1239
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1240
|
-
}
|
|
1241
|
-
: {
|
|
1242
|
-
status: "PASSED",
|
|
1243
|
-
startTime,
|
|
1244
|
-
endTime,
|
|
1245
|
-
},
|
|
1246
|
-
info: info,
|
|
1247
|
-
});
|
|
1303
|
+
_commandFinally(state, this);
|
|
1248
1304
|
}
|
|
1249
1305
|
}
|
|
1250
1306
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1307
|
+
const state = {
|
|
1308
|
+
selectors,
|
|
1309
|
+
_params,
|
|
1310
|
+
value: unEscapeString(value),
|
|
1311
|
+
options,
|
|
1312
|
+
world,
|
|
1313
|
+
type: Types.FILL,
|
|
1314
|
+
text: `Fill input with value: ${value}`,
|
|
1315
|
+
operation: "fill",
|
|
1316
|
+
log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
|
|
1317
|
+
};
|
|
1261
1318
|
try {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
await
|
|
1265
|
-
await element.fill(value, { timeout: 10000 });
|
|
1266
|
-
await element.dispatchEvent("change");
|
|
1319
|
+
await _preCommand(state, this);
|
|
1320
|
+
await state.element.fill(value);
|
|
1321
|
+
await state.element.dispatchEvent("change");
|
|
1267
1322
|
if (enter) {
|
|
1268
1323
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1269
1324
|
await this.page.keyboard.press("Enter");
|
|
1270
1325
|
}
|
|
1271
1326
|
await this.waitForPageLoad();
|
|
1272
|
-
return info;
|
|
1327
|
+
return state.info;
|
|
1273
1328
|
}
|
|
1274
1329
|
catch (e) {
|
|
1275
|
-
|
|
1276
|
-
this.logger.error("fill failed " + info.log);
|
|
1277
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1278
|
-
info.screenshotPath = screenshotPath;
|
|
1279
|
-
Object.assign(e, { info: info });
|
|
1280
|
-
error = e;
|
|
1281
|
-
throw e;
|
|
1330
|
+
await _commandError(state, e, this);
|
|
1282
1331
|
}
|
|
1283
1332
|
finally {
|
|
1284
|
-
|
|
1285
|
-
this._reportToWorld(world, {
|
|
1286
|
-
element_name: selectors.element_name,
|
|
1287
|
-
type: Types.FILL,
|
|
1288
|
-
screenshotId,
|
|
1289
|
-
value,
|
|
1290
|
-
text: `Fill input with value: ${value}`,
|
|
1291
|
-
result: error
|
|
1292
|
-
? {
|
|
1293
|
-
status: "FAILED",
|
|
1294
|
-
startTime,
|
|
1295
|
-
endTime,
|
|
1296
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1297
|
-
}
|
|
1298
|
-
: {
|
|
1299
|
-
status: "PASSED",
|
|
1300
|
-
startTime,
|
|
1301
|
-
endTime,
|
|
1302
|
-
},
|
|
1303
|
-
info: info,
|
|
1304
|
-
});
|
|
1333
|
+
_commandFinally(state, this);
|
|
1305
1334
|
}
|
|
1306
1335
|
}
|
|
1307
1336
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1308
1337
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1309
1338
|
}
|
|
1310
1339
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1311
|
-
|
|
1340
|
+
_validateSelectors(selectors);
|
|
1312
1341
|
let screenshotId = null;
|
|
1313
1342
|
let screenshotPath = null;
|
|
1314
1343
|
if (!info.log) {
|
|
1315
1344
|
info.log = "";
|
|
1345
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1316
1346
|
}
|
|
1317
1347
|
info.operation = "getText";
|
|
1318
1348
|
info.selectors = selectors;
|
|
@@ -1352,165 +1382,124 @@ class StableBrowser {
|
|
|
1352
1382
|
}
|
|
1353
1383
|
}
|
|
1354
1384
|
async containsPattern(selectors, pattern, text, _params = null, options = {}, world = null) {
|
|
1355
|
-
var _a;
|
|
1356
|
-
this._validateSelectors(selectors);
|
|
1357
1385
|
if (!pattern) {
|
|
1358
1386
|
throw new Error("pattern is null");
|
|
1359
1387
|
}
|
|
1360
1388
|
if (!text) {
|
|
1361
1389
|
throw new Error("text is null");
|
|
1362
1390
|
}
|
|
1391
|
+
const state = {
|
|
1392
|
+
selectors,
|
|
1393
|
+
_params,
|
|
1394
|
+
pattern,
|
|
1395
|
+
value: pattern,
|
|
1396
|
+
options,
|
|
1397
|
+
world,
|
|
1398
|
+
locate: false,
|
|
1399
|
+
scroll: false,
|
|
1400
|
+
screenshot: false,
|
|
1401
|
+
highlight: false,
|
|
1402
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1403
|
+
text: `Verify element contains pattern: ${pattern}`,
|
|
1404
|
+
operation: "containsPattern",
|
|
1405
|
+
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1406
|
+
};
|
|
1363
1407
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1364
1408
|
if (newValue !== text) {
|
|
1365
1409
|
this.logger.info(text + "=" + newValue);
|
|
1366
1410
|
text = newValue;
|
|
1367
1411
|
}
|
|
1368
|
-
const startTime = Date.now();
|
|
1369
|
-
let error = null;
|
|
1370
|
-
let screenshotId = null;
|
|
1371
|
-
let screenshotPath = null;
|
|
1372
|
-
const info = {};
|
|
1373
|
-
info.log =
|
|
1374
|
-
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1375
|
-
info.operation = "containsPattern";
|
|
1376
|
-
info.selectors = selectors;
|
|
1377
|
-
info.value = text;
|
|
1378
|
-
info.pattern = pattern;
|
|
1379
1412
|
let foundObj = null;
|
|
1380
1413
|
try {
|
|
1381
|
-
|
|
1414
|
+
await _preCommand(state, this);
|
|
1415
|
+
state.info.pattern = pattern;
|
|
1416
|
+
foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
|
|
1382
1417
|
if (foundObj && foundObj.element) {
|
|
1383
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1418
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1384
1419
|
}
|
|
1385
|
-
|
|
1420
|
+
await _screenshot(state, this);
|
|
1386
1421
|
let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
1387
1422
|
pattern = pattern.replace("{text}", escapedText);
|
|
1388
1423
|
let regex = new RegExp(pattern, "im");
|
|
1389
|
-
if (!regex.test(foundObj
|
|
1390
|
-
info.foundText = foundObj
|
|
1424
|
+
if (!regex.test(foundObj?.text) && !foundObj?.value?.includes(text)) {
|
|
1425
|
+
state.info.foundText = foundObj?.text;
|
|
1391
1426
|
throw new Error("element doesn't contain text " + text);
|
|
1392
1427
|
}
|
|
1393
|
-
return info;
|
|
1428
|
+
return state.info;
|
|
1394
1429
|
}
|
|
1395
1430
|
catch (e) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
this.logger.error("found text " + (foundObj === null || foundObj === void 0 ? void 0 : foundObj.text) + " pattern " + pattern);
|
|
1399
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1400
|
-
info.screenshotPath = screenshotPath;
|
|
1401
|
-
Object.assign(e, { info: info });
|
|
1402
|
-
error = e;
|
|
1403
|
-
throw e;
|
|
1431
|
+
this.logger.error("found text " + foundObj?.text + " pattern " + pattern);
|
|
1432
|
+
await _commandError(state, e, this);
|
|
1404
1433
|
}
|
|
1405
1434
|
finally {
|
|
1406
|
-
|
|
1407
|
-
this._reportToWorld(world, {
|
|
1408
|
-
element_name: selectors.element_name,
|
|
1409
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1410
|
-
value: pattern,
|
|
1411
|
-
text: `Verify element contains pattern: ${pattern}`,
|
|
1412
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1413
|
-
result: error
|
|
1414
|
-
? {
|
|
1415
|
-
status: "FAILED",
|
|
1416
|
-
startTime,
|
|
1417
|
-
endTime,
|
|
1418
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1419
|
-
}
|
|
1420
|
-
: {
|
|
1421
|
-
status: "PASSED",
|
|
1422
|
-
startTime,
|
|
1423
|
-
endTime,
|
|
1424
|
-
},
|
|
1425
|
-
info: info,
|
|
1426
|
-
});
|
|
1435
|
+
_commandFinally(state, this);
|
|
1427
1436
|
}
|
|
1428
1437
|
}
|
|
1429
1438
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1439
|
+
const state = {
|
|
1440
|
+
selectors,
|
|
1441
|
+
_params,
|
|
1442
|
+
value: text,
|
|
1443
|
+
options,
|
|
1444
|
+
world,
|
|
1445
|
+
locate: false,
|
|
1446
|
+
scroll: false,
|
|
1447
|
+
screenshot: false,
|
|
1448
|
+
highlight: false,
|
|
1449
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1450
|
+
text: `Verify element contains text: ${text}`,
|
|
1451
|
+
operation: "containsText",
|
|
1452
|
+
log: "***** verify element " + selectors.element_name + " contains text " + text + " *****\n",
|
|
1453
|
+
};
|
|
1432
1454
|
if (!text) {
|
|
1433
1455
|
throw new Error("text is null");
|
|
1434
1456
|
}
|
|
1435
|
-
|
|
1436
|
-
let error = null;
|
|
1437
|
-
let screenshotId = null;
|
|
1438
|
-
let screenshotPath = null;
|
|
1439
|
-
const info = {};
|
|
1440
|
-
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1441
|
-
info.operation = "containsText";
|
|
1442
|
-
info.selectors = selectors;
|
|
1457
|
+
text = unEscapeString(text);
|
|
1443
1458
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1444
1459
|
if (newValue !== text) {
|
|
1445
1460
|
this.logger.info(text + "=" + newValue);
|
|
1446
1461
|
text = newValue;
|
|
1447
1462
|
}
|
|
1448
|
-
info.value = text;
|
|
1449
1463
|
let foundObj = null;
|
|
1450
1464
|
try {
|
|
1451
|
-
|
|
1465
|
+
await _preCommand(state, this);
|
|
1466
|
+
foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
|
|
1452
1467
|
if (foundObj && foundObj.element) {
|
|
1453
|
-
await this.scrollIfNeeded(foundObj.element, info);
|
|
1468
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1454
1469
|
}
|
|
1455
|
-
|
|
1470
|
+
await _screenshot(state, this);
|
|
1456
1471
|
const dateAlternatives = findDateAlternatives(text);
|
|
1457
1472
|
const numberAlternatives = findNumberAlternatives(text);
|
|
1458
1473
|
if (dateAlternatives.date) {
|
|
1459
1474
|
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1460
|
-
if (
|
|
1461
|
-
|
|
1462
|
-
return info;
|
|
1475
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1476
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1477
|
+
return state.info;
|
|
1463
1478
|
}
|
|
1464
1479
|
}
|
|
1465
1480
|
throw new Error("element doesn't contain text " + text);
|
|
1466
1481
|
}
|
|
1467
1482
|
else if (numberAlternatives.number) {
|
|
1468
1483
|
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1469
|
-
if (
|
|
1470
|
-
|
|
1471
|
-
return info;
|
|
1484
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1485
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1486
|
+
return state.info;
|
|
1472
1487
|
}
|
|
1473
|
-
}
|
|
1474
|
-
throw new Error("element doesn't contain text " + text);
|
|
1475
|
-
}
|
|
1476
|
-
else if (!
|
|
1477
|
-
info.foundText = foundObj
|
|
1478
|
-
info.value = foundObj
|
|
1479
|
-
throw new Error("element doesn't contain text " + text);
|
|
1480
|
-
}
|
|
1481
|
-
return info;
|
|
1482
|
-
}
|
|
1483
|
-
catch (e) {
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
Object.assign(e, { info: info });
|
|
1489
|
-
error = e;
|
|
1490
|
-
throw e;
|
|
1491
|
-
}
|
|
1492
|
-
finally {
|
|
1493
|
-
const endTime = Date.now();
|
|
1494
|
-
this._reportToWorld(world, {
|
|
1495
|
-
element_name: selectors.element_name,
|
|
1496
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1497
|
-
text: `Verify element contains text: ${text}`,
|
|
1498
|
-
value: text,
|
|
1499
|
-
screenshotId: foundObj === null || foundObj === void 0 ? void 0 : foundObj.screenshotId,
|
|
1500
|
-
result: error
|
|
1501
|
-
? {
|
|
1502
|
-
status: "FAILED",
|
|
1503
|
-
startTime,
|
|
1504
|
-
endTime,
|
|
1505
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1506
|
-
}
|
|
1507
|
-
: {
|
|
1508
|
-
status: "PASSED",
|
|
1509
|
-
startTime,
|
|
1510
|
-
endTime,
|
|
1511
|
-
},
|
|
1512
|
-
info: info,
|
|
1513
|
-
});
|
|
1488
|
+
}
|
|
1489
|
+
throw new Error("element doesn't contain text " + text);
|
|
1490
|
+
}
|
|
1491
|
+
else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
|
|
1492
|
+
state.info.foundText = foundObj?.text;
|
|
1493
|
+
state.info.value = foundObj?.value;
|
|
1494
|
+
throw new Error("element doesn't contain text " + text);
|
|
1495
|
+
}
|
|
1496
|
+
return state.info;
|
|
1497
|
+
}
|
|
1498
|
+
catch (e) {
|
|
1499
|
+
await _commandError(state, e, this);
|
|
1500
|
+
}
|
|
1501
|
+
finally {
|
|
1502
|
+
_commandFinally(state, this);
|
|
1514
1503
|
}
|
|
1515
1504
|
}
|
|
1516
1505
|
_getDataFile(world = null) {
|
|
@@ -1529,6 +1518,29 @@ class StableBrowser {
|
|
|
1529
1518
|
}
|
|
1530
1519
|
return dataFile;
|
|
1531
1520
|
}
|
|
1521
|
+
async waitForUserInput(message, world = null) {
|
|
1522
|
+
if (!message) {
|
|
1523
|
+
message = "# Wait for user input. Press any key to continue";
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
message = "# Wait for user input. " + message;
|
|
1527
|
+
}
|
|
1528
|
+
message += "\n";
|
|
1529
|
+
const value = await new Promise((resolve) => {
|
|
1530
|
+
const rl = readline.createInterface({
|
|
1531
|
+
input: process.stdin,
|
|
1532
|
+
output: process.stdout,
|
|
1533
|
+
});
|
|
1534
|
+
rl.question(message, (answer) => {
|
|
1535
|
+
rl.close();
|
|
1536
|
+
resolve(answer);
|
|
1537
|
+
});
|
|
1538
|
+
});
|
|
1539
|
+
if (value) {
|
|
1540
|
+
this.logger.info(`{{userInput}} was set to: ${value}`);
|
|
1541
|
+
}
|
|
1542
|
+
this.setTestData({ userInput: value }, world);
|
|
1543
|
+
}
|
|
1532
1544
|
setTestData(testData, world = null) {
|
|
1533
1545
|
if (!testData) {
|
|
1534
1546
|
return;
|
|
@@ -1669,11 +1681,9 @@ class StableBrowser {
|
|
|
1669
1681
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1670
1682
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1671
1683
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
}
|
|
1676
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
1684
|
+
// to make sure the path doesn't start with -
|
|
1685
|
+
const uuidStr = "id_" + randomUUID();
|
|
1686
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1677
1687
|
try {
|
|
1678
1688
|
await this.takeScreenshot(screenshotPath);
|
|
1679
1689
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1687,7 +1697,7 @@ class StableBrowser {
|
|
|
1687
1697
|
catch (e) {
|
|
1688
1698
|
this.logger.info("unable to take screenshot, ignored");
|
|
1689
1699
|
}
|
|
1690
|
-
result.screenshotId =
|
|
1700
|
+
result.screenshotId = uuidStr;
|
|
1691
1701
|
result.screenshotPath = screenshotPath;
|
|
1692
1702
|
if (info && info.box) {
|
|
1693
1703
|
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
@@ -1716,7 +1726,6 @@ class StableBrowser {
|
|
|
1716
1726
|
}
|
|
1717
1727
|
async takeScreenshot(screenshotPath) {
|
|
1718
1728
|
const playContext = this.context.playContext;
|
|
1719
|
-
const client = await playContext.newCDPSession(this.page);
|
|
1720
1729
|
// Using CDP to capture the screenshot
|
|
1721
1730
|
const viewportWidth = Math.max(...(await this.page.evaluate(() => [
|
|
1722
1731
|
document.body.scrollWidth,
|
|
@@ -1726,164 +1735,168 @@ class StableBrowser {
|
|
|
1726
1735
|
document.body.clientWidth,
|
|
1727
1736
|
document.documentElement.clientWidth,
|
|
1728
1737
|
])));
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
await client.detach();
|
|
1738
|
+
let screenshotBuffer = null;
|
|
1739
|
+
if (this.context.browserName === "chromium") {
|
|
1740
|
+
const client = await playContext.newCDPSession(this.page);
|
|
1741
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
1742
|
+
format: "png",
|
|
1743
|
+
// clip: {
|
|
1744
|
+
// x: 0,
|
|
1745
|
+
// y: 0,
|
|
1746
|
+
// width: viewportWidth,
|
|
1747
|
+
// height: viewportHeight,
|
|
1748
|
+
// scale: 1,
|
|
1749
|
+
// },
|
|
1750
|
+
});
|
|
1751
|
+
await client.detach();
|
|
1752
|
+
if (!screenshotPath) {
|
|
1753
|
+
return data;
|
|
1754
|
+
}
|
|
1755
|
+
screenshotBuffer = Buffer.from(data, "base64");
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
screenshotBuffer = await this.page.screenshot();
|
|
1759
|
+
}
|
|
1760
|
+
let image = await Jimp.read(screenshotBuffer);
|
|
1761
|
+
// Get the image dimensions
|
|
1762
|
+
const { width, height } = image.bitmap;
|
|
1763
|
+
const resizeRatio = viewportWidth / width;
|
|
1764
|
+
// Resize the image to fit within the viewport dimensions without enlarging
|
|
1765
|
+
if (width > viewportWidth) {
|
|
1766
|
+
image = image.resize({ w: viewportWidth, h: height * resizeRatio }); // Resize the image while maintaining aspect ratio
|
|
1767
|
+
await image.write(screenshotPath);
|
|
1768
|
+
}
|
|
1769
|
+
else {
|
|
1770
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1771
|
+
}
|
|
1764
1772
|
}
|
|
1765
1773
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1774
|
+
const state = {
|
|
1775
|
+
selectors,
|
|
1776
|
+
_params,
|
|
1777
|
+
options,
|
|
1778
|
+
world,
|
|
1779
|
+
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1780
|
+
text: `Verify element exists in page`,
|
|
1781
|
+
operation: "verifyElementExistInPage",
|
|
1782
|
+
log: "***** verify element " + selectors.element_name + " exists in page *****\n",
|
|
1783
|
+
};
|
|
1771
1784
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1772
|
-
const info = {};
|
|
1773
|
-
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1774
|
-
info.operation = "verify";
|
|
1775
|
-
info.selectors = selectors;
|
|
1776
1785
|
try {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
}
|
|
1781
|
-
await this._highlightElements(element);
|
|
1782
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1783
|
-
await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
1784
|
-
return info;
|
|
1786
|
+
await _preCommand(state, this);
|
|
1787
|
+
await expect(state.element).toHaveCount(1, { timeout: 10000 });
|
|
1788
|
+
return state.info;
|
|
1785
1789
|
}
|
|
1786
1790
|
catch (e) {
|
|
1787
|
-
|
|
1788
|
-
this.logger.error("verify failed " + info.log);
|
|
1789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1790
|
-
info.screenshotPath = screenshotPath;
|
|
1791
|
-
Object.assign(e, { info: info });
|
|
1792
|
-
error = e;
|
|
1793
|
-
throw e;
|
|
1791
|
+
await _commandError(state, e, this);
|
|
1794
1792
|
}
|
|
1795
1793
|
finally {
|
|
1796
|
-
|
|
1797
|
-
this._reportToWorld(world, {
|
|
1798
|
-
element_name: selectors.element_name,
|
|
1799
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1800
|
-
text: "Verify element exists in page",
|
|
1801
|
-
screenshotId,
|
|
1802
|
-
result: error
|
|
1803
|
-
? {
|
|
1804
|
-
status: "FAILED",
|
|
1805
|
-
startTime,
|
|
1806
|
-
endTime,
|
|
1807
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1808
|
-
}
|
|
1809
|
-
: {
|
|
1810
|
-
status: "PASSED",
|
|
1811
|
-
startTime,
|
|
1812
|
-
endTime,
|
|
1813
|
-
},
|
|
1814
|
-
info: info,
|
|
1815
|
-
});
|
|
1794
|
+
_commandFinally(state, this);
|
|
1816
1795
|
}
|
|
1817
1796
|
}
|
|
1818
1797
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1798
|
+
const state = {
|
|
1799
|
+
selectors,
|
|
1800
|
+
_params,
|
|
1801
|
+
attribute,
|
|
1802
|
+
variable,
|
|
1803
|
+
options,
|
|
1804
|
+
world,
|
|
1805
|
+
type: Types.EXTRACT,
|
|
1806
|
+
text: `Extract attribute from element`,
|
|
1807
|
+
operation: "extractAttribute",
|
|
1808
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1809
|
+
};
|
|
1824
1810
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1825
|
-
const info = {};
|
|
1826
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1827
|
-
info.operation = "extract";
|
|
1828
|
-
info.selectors = selectors;
|
|
1829
1811
|
try {
|
|
1830
|
-
|
|
1831
|
-
await this._highlightElements(element);
|
|
1832
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1812
|
+
await _preCommand(state, this);
|
|
1833
1813
|
switch (attribute) {
|
|
1834
1814
|
case "inner_text":
|
|
1835
|
-
|
|
1815
|
+
state.value = await state.element.innerText();
|
|
1836
1816
|
break;
|
|
1837
1817
|
case "href":
|
|
1838
|
-
|
|
1818
|
+
state.value = await state.element.getAttribute("href");
|
|
1819
|
+
break;
|
|
1820
|
+
case "value":
|
|
1821
|
+
state.value = await state.element.inputValue();
|
|
1822
|
+
break;
|
|
1823
|
+
default:
|
|
1824
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1825
|
+
break;
|
|
1826
|
+
}
|
|
1827
|
+
state.info.value = state.value;
|
|
1828
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
1829
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
1830
|
+
return state.info;
|
|
1831
|
+
}
|
|
1832
|
+
catch (e) {
|
|
1833
|
+
await _commandError(state, e, this);
|
|
1834
|
+
}
|
|
1835
|
+
finally {
|
|
1836
|
+
_commandFinally(state, this);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
1840
|
+
const state = {
|
|
1841
|
+
selectors,
|
|
1842
|
+
_params,
|
|
1843
|
+
attribute,
|
|
1844
|
+
value,
|
|
1845
|
+
options,
|
|
1846
|
+
world,
|
|
1847
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
1848
|
+
highlight: true,
|
|
1849
|
+
screenshot: true,
|
|
1850
|
+
text: `Verify element attribute`,
|
|
1851
|
+
operation: "verifyAttribute",
|
|
1852
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1853
|
+
allowDisabled: true,
|
|
1854
|
+
};
|
|
1855
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1856
|
+
let val;
|
|
1857
|
+
try {
|
|
1858
|
+
await _preCommand(state, this);
|
|
1859
|
+
switch (attribute) {
|
|
1860
|
+
case "innerText":
|
|
1861
|
+
val = String(await state.element.innerText());
|
|
1839
1862
|
break;
|
|
1840
1863
|
case "value":
|
|
1841
|
-
|
|
1864
|
+
val = String(await state.element.inputValue());
|
|
1865
|
+
break;
|
|
1866
|
+
case "checked":
|
|
1867
|
+
val = String(await state.element.isChecked());
|
|
1868
|
+
break;
|
|
1869
|
+
case "disabled":
|
|
1870
|
+
val = String(await state.element.isDisabled());
|
|
1871
|
+
break;
|
|
1872
|
+
case "readOnly":
|
|
1873
|
+
const isEditable = await state.element.isEditable();
|
|
1874
|
+
val = String(!isEditable);
|
|
1842
1875
|
break;
|
|
1843
1876
|
default:
|
|
1844
|
-
|
|
1877
|
+
val = String(await state.element.getAttribute(attribute));
|
|
1845
1878
|
break;
|
|
1846
1879
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1880
|
+
state.info.expectedValue = val;
|
|
1881
|
+
let regex;
|
|
1882
|
+
if (value.startsWith("/") && value.endsWith("/")) {
|
|
1883
|
+
const patternBody = value.slice(1, -1);
|
|
1884
|
+
regex = new RegExp(patternBody, "g");
|
|
1850
1885
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1886
|
+
else {
|
|
1887
|
+
const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1888
|
+
regex = new RegExp(escapedPattern, "g");
|
|
1889
|
+
}
|
|
1890
|
+
if (!val.match(regex)) {
|
|
1891
|
+
throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
|
|
1892
|
+
}
|
|
1893
|
+
return state.info;
|
|
1854
1894
|
}
|
|
1855
1895
|
catch (e) {
|
|
1856
|
-
|
|
1857
|
-
this.logger.error("extract failed " + info.log);
|
|
1858
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1859
|
-
info.screenshotPath = screenshotPath;
|
|
1860
|
-
Object.assign(e, { info: info });
|
|
1861
|
-
error = e;
|
|
1862
|
-
throw e;
|
|
1896
|
+
await _commandError(state, e, this);
|
|
1863
1897
|
}
|
|
1864
1898
|
finally {
|
|
1865
|
-
|
|
1866
|
-
this._reportToWorld(world, {
|
|
1867
|
-
element_name: selectors.element_name,
|
|
1868
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1869
|
-
variable: variable,
|
|
1870
|
-
value: info.value,
|
|
1871
|
-
text: "Extract attribute from element",
|
|
1872
|
-
screenshotId,
|
|
1873
|
-
result: error
|
|
1874
|
-
? {
|
|
1875
|
-
status: "FAILED",
|
|
1876
|
-
startTime,
|
|
1877
|
-
endTime,
|
|
1878
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1879
|
-
}
|
|
1880
|
-
: {
|
|
1881
|
-
status: "PASSED",
|
|
1882
|
-
startTime,
|
|
1883
|
-
endTime,
|
|
1884
|
-
},
|
|
1885
|
-
info: info,
|
|
1886
|
-
});
|
|
1899
|
+
_commandFinally(state, this);
|
|
1887
1900
|
}
|
|
1888
1901
|
}
|
|
1889
1902
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -1904,7 +1917,7 @@ class StableBrowser {
|
|
|
1904
1917
|
if (options && options.timeout) {
|
|
1905
1918
|
timeout = options.timeout;
|
|
1906
1919
|
}
|
|
1907
|
-
const serviceUrl =
|
|
1920
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1908
1921
|
const request = {
|
|
1909
1922
|
method: "POST",
|
|
1910
1923
|
url: serviceUrl,
|
|
@@ -1960,7 +1973,8 @@ class StableBrowser {
|
|
|
1960
1973
|
catch (e) {
|
|
1961
1974
|
errorCount++;
|
|
1962
1975
|
if (errorCount > 3) {
|
|
1963
|
-
throw e;
|
|
1976
|
+
// throw e;
|
|
1977
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1964
1978
|
}
|
|
1965
1979
|
// ignore
|
|
1966
1980
|
}
|
|
@@ -1980,15 +1994,15 @@ class StableBrowser {
|
|
|
1980
1994
|
scope
|
|
1981
1995
|
.evaluate((node) => {
|
|
1982
1996
|
if (node && node.style) {
|
|
1983
|
-
let originalBorder = node.style.
|
|
1984
|
-
node.style.
|
|
1997
|
+
let originalBorder = node.style.outline;
|
|
1998
|
+
node.style.outline = "2px solid red";
|
|
1985
1999
|
if (window) {
|
|
1986
2000
|
window.addEventListener("beforeunload", function (e) {
|
|
1987
|
-
node.style.
|
|
2001
|
+
node.style.outline = originalBorder;
|
|
1988
2002
|
});
|
|
1989
2003
|
}
|
|
1990
2004
|
setTimeout(function () {
|
|
1991
|
-
node.style.
|
|
2005
|
+
node.style.outline = originalBorder;
|
|
1992
2006
|
}, 2000);
|
|
1993
2007
|
}
|
|
1994
2008
|
})
|
|
@@ -2010,17 +2024,17 @@ class StableBrowser {
|
|
|
2010
2024
|
if (!element.style) {
|
|
2011
2025
|
return;
|
|
2012
2026
|
}
|
|
2013
|
-
var originalBorder = element.style.
|
|
2027
|
+
var originalBorder = element.style.outline;
|
|
2014
2028
|
// Set the new border to be red and 2px solid
|
|
2015
|
-
element.style.
|
|
2029
|
+
element.style.outline = "2px solid red";
|
|
2016
2030
|
if (window) {
|
|
2017
2031
|
window.addEventListener("beforeunload", function (e) {
|
|
2018
|
-
element.style.
|
|
2032
|
+
element.style.outline = originalBorder;
|
|
2019
2033
|
});
|
|
2020
2034
|
}
|
|
2021
2035
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2022
2036
|
setTimeout(function () {
|
|
2023
|
-
element.style.
|
|
2037
|
+
element.style.outline = originalBorder;
|
|
2024
2038
|
}, 2000);
|
|
2025
2039
|
}
|
|
2026
2040
|
return;
|
|
@@ -2071,11 +2085,12 @@ class StableBrowser {
|
|
|
2071
2085
|
info.screenshotPath = screenshotPath;
|
|
2072
2086
|
Object.assign(e, { info: info });
|
|
2073
2087
|
error = e;
|
|
2074
|
-
throw e;
|
|
2088
|
+
// throw e;
|
|
2089
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2075
2090
|
}
|
|
2076
2091
|
finally {
|
|
2077
2092
|
const endTime = Date.now();
|
|
2078
|
-
|
|
2093
|
+
_reportToWorld(world, {
|
|
2079
2094
|
type: Types.VERIFY_PAGE_PATH,
|
|
2080
2095
|
text: "Verify page path",
|
|
2081
2096
|
screenshotId,
|
|
@@ -2084,7 +2099,7 @@ class StableBrowser {
|
|
|
2084
2099
|
status: "FAILED",
|
|
2085
2100
|
startTime,
|
|
2086
2101
|
endTime,
|
|
2087
|
-
message: error
|
|
2102
|
+
message: error?.message,
|
|
2088
2103
|
}
|
|
2089
2104
|
: {
|
|
2090
2105
|
status: "PASSED",
|
|
@@ -2095,53 +2110,65 @@ class StableBrowser {
|
|
|
2095
2110
|
});
|
|
2096
2111
|
}
|
|
2097
2112
|
}
|
|
2113
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2114
|
+
const frames = this.page.frames();
|
|
2115
|
+
let results = [];
|
|
2116
|
+
let ignoreCase = false;
|
|
2117
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2118
|
+
if (dateAlternatives.date) {
|
|
2119
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2120
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2121
|
+
result.frame = frames[i];
|
|
2122
|
+
results.push(result);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
else if (numberAlternatives.number) {
|
|
2126
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2127
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2128
|
+
result.frame = frames[i];
|
|
2129
|
+
results.push(result);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
else {
|
|
2133
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2134
|
+
result.frame = frames[i];
|
|
2135
|
+
results.push(result);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
state.info.results = results;
|
|
2139
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2140
|
+
return resultWithElementsFound;
|
|
2141
|
+
}
|
|
2098
2142
|
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2099
|
-
|
|
2143
|
+
text = unEscapeString(text);
|
|
2144
|
+
const state = {
|
|
2145
|
+
text_search: text,
|
|
2146
|
+
options,
|
|
2147
|
+
world,
|
|
2148
|
+
locate: false,
|
|
2149
|
+
scroll: false,
|
|
2150
|
+
highlight: false,
|
|
2151
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2152
|
+
text: `Verify text exists in page`,
|
|
2153
|
+
operation: "verifyTextExistInPage",
|
|
2154
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
2155
|
+
};
|
|
2100
2156
|
const timeout = this._getLoadTimeout(options);
|
|
2101
|
-
let error = null;
|
|
2102
|
-
let screenshotId = null;
|
|
2103
|
-
let screenshotPath = null;
|
|
2104
2157
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2105
|
-
const info = {};
|
|
2106
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2107
|
-
info.operation = "verifyTextExistInPage";
|
|
2108
2158
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2109
2159
|
if (newValue !== text) {
|
|
2110
2160
|
this.logger.info(text + "=" + newValue);
|
|
2111
2161
|
text = newValue;
|
|
2112
2162
|
}
|
|
2113
|
-
info.text = text;
|
|
2114
2163
|
let dateAlternatives = findDateAlternatives(text);
|
|
2115
2164
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2116
2165
|
try {
|
|
2166
|
+
await _preCommand(state, this);
|
|
2167
|
+
state.info.text = text;
|
|
2117
2168
|
while (true) {
|
|
2118
|
-
const
|
|
2119
|
-
let results = [];
|
|
2120
|
-
for (let i = 0; i < frames.length; i++) {
|
|
2121
|
-
if (dateAlternatives.date) {
|
|
2122
|
-
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2123
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, {});
|
|
2124
|
-
result.frame = frames[i];
|
|
2125
|
-
results.push(result);
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
else if (numberAlternatives.number) {
|
|
2129
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2130
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, {});
|
|
2131
|
-
result.frame = frames[i];
|
|
2132
|
-
results.push(result);
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
else {
|
|
2136
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, {});
|
|
2137
|
-
result.frame = frames[i];
|
|
2138
|
-
results.push(result);
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
info.results = results;
|
|
2142
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2169
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2143
2170
|
if (resultWithElementsFound.length === 0) {
|
|
2144
|
-
if (Date.now() - startTime > timeout) {
|
|
2171
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2145
2172
|
throw new Error(`Text ${text} not found in page`);
|
|
2146
2173
|
}
|
|
2147
2174
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -2149,59 +2176,153 @@ class StableBrowser {
|
|
|
2149
2176
|
}
|
|
2150
2177
|
if (resultWithElementsFound[0].randomToken) {
|
|
2151
2178
|
const frame = resultWithElementsFound[0].frame;
|
|
2152
|
-
const dataAttribute = `[data-blinq-id
|
|
2179
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2153
2180
|
await this._highlightElements(frame, dataAttribute);
|
|
2154
|
-
const element = await frame
|
|
2181
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2155
2182
|
if (element) {
|
|
2156
|
-
await this.scrollIfNeeded(element, info);
|
|
2183
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2157
2184
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2158
2185
|
}
|
|
2159
2186
|
}
|
|
2160
|
-
|
|
2161
|
-
return info;
|
|
2187
|
+
await _screenshot(state, this);
|
|
2188
|
+
return state.info;
|
|
2162
2189
|
}
|
|
2163
2190
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2164
2191
|
}
|
|
2165
2192
|
catch (e) {
|
|
2166
|
-
|
|
2167
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2168
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2169
|
-
info.screenshotPath = screenshotPath;
|
|
2170
|
-
Object.assign(e, { info: info });
|
|
2171
|
-
error = e;
|
|
2172
|
-
throw e;
|
|
2193
|
+
await _commandError(state, e, this);
|
|
2173
2194
|
}
|
|
2174
2195
|
finally {
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2196
|
+
_commandFinally(state, this);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2200
|
+
text = unEscapeString(text);
|
|
2201
|
+
const state = {
|
|
2202
|
+
text_search: text,
|
|
2203
|
+
options,
|
|
2204
|
+
world,
|
|
2205
|
+
locate: false,
|
|
2206
|
+
scroll: false,
|
|
2207
|
+
highlight: false,
|
|
2208
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2209
|
+
text: `Verify text does not exist in page`,
|
|
2210
|
+
operation: "verifyTextNotExistInPage",
|
|
2211
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2212
|
+
};
|
|
2213
|
+
const timeout = this._getLoadTimeout(options);
|
|
2214
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2215
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2216
|
+
if (newValue !== text) {
|
|
2217
|
+
this.logger.info(text + "=" + newValue);
|
|
2218
|
+
text = newValue;
|
|
2219
|
+
}
|
|
2220
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2221
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2222
|
+
try {
|
|
2223
|
+
await _preCommand(state, this);
|
|
2224
|
+
state.info.text = text;
|
|
2225
|
+
while (true) {
|
|
2226
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2227
|
+
if (resultWithElementsFound.length === 0) {
|
|
2228
|
+
await _screenshot(state, this);
|
|
2229
|
+
return state.info;
|
|
2230
|
+
}
|
|
2231
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2232
|
+
throw new Error(`Text ${text} found in page`);
|
|
2233
|
+
}
|
|
2234
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
catch (e) {
|
|
2238
|
+
await _commandError(state, e, this);
|
|
2239
|
+
}
|
|
2240
|
+
finally {
|
|
2241
|
+
_commandFinally(state, this);
|
|
2194
2242
|
}
|
|
2195
2243
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2244
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
2245
|
+
textAnchor = unEscapeString(textAnchor);
|
|
2246
|
+
textToVerify = unEscapeString(textToVerify);
|
|
2247
|
+
const state = {
|
|
2248
|
+
text_search: textToVerify,
|
|
2249
|
+
options,
|
|
2250
|
+
world,
|
|
2251
|
+
locate: false,
|
|
2252
|
+
scroll: false,
|
|
2253
|
+
highlight: false,
|
|
2254
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2255
|
+
text: `Verify text with relation to another text`,
|
|
2256
|
+
operation: "verify_text_with_relation",
|
|
2257
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2258
|
+
};
|
|
2259
|
+
const timeout = this._getLoadTimeout(options);
|
|
2260
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2261
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2262
|
+
if (newValue !== textAnchor) {
|
|
2263
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
2264
|
+
textAnchor = newValue;
|
|
2265
|
+
}
|
|
2266
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
2267
|
+
if (newValue !== textToVerify) {
|
|
2268
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
2269
|
+
textToVerify = newValue;
|
|
2270
|
+
}
|
|
2271
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
2272
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
2273
|
+
let foundAncore = false;
|
|
2274
|
+
try {
|
|
2275
|
+
await _preCommand(state, this);
|
|
2276
|
+
state.info.text = textToVerify;
|
|
2277
|
+
while (true) {
|
|
2278
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
|
|
2279
|
+
if (resultWithElementsFound.length === 0) {
|
|
2280
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2281
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2282
|
+
}
|
|
2283
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2287
|
+
foundAncore = true;
|
|
2288
|
+
const result = resultWithElementsFound[i];
|
|
2289
|
+
const token = result.randomToken;
|
|
2290
|
+
const frame = result.frame;
|
|
2291
|
+
let css = `[data-blinq-id-${token}]`;
|
|
2292
|
+
const climbArray1 = [];
|
|
2293
|
+
for (let i = 0; i < climb; i++) {
|
|
2294
|
+
climbArray1.push("..");
|
|
2295
|
+
}
|
|
2296
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
2297
|
+
css = css + " >> " + climbXpath;
|
|
2298
|
+
const count = await frame.locator(css).count();
|
|
2299
|
+
for (let j = 0; j < count; j++) {
|
|
2300
|
+
const continer = await frame.locator(css).nth(j);
|
|
2301
|
+
const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
|
|
2302
|
+
if (result.elementCount > 0) {
|
|
2303
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2304
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2305
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2306
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
2307
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2308
|
+
if (element) {
|
|
2309
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2310
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2311
|
+
}
|
|
2312
|
+
await _screenshot(state, this);
|
|
2313
|
+
return state.info;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2200
2319
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2320
|
+
catch (e) {
|
|
2321
|
+
await _commandError(state, e, this);
|
|
2322
|
+
}
|
|
2323
|
+
finally {
|
|
2324
|
+
_commandFinally(state, this);
|
|
2203
2325
|
}
|
|
2204
|
-
return serviceUrl;
|
|
2205
2326
|
}
|
|
2206
2327
|
async visualVerification(text, options = {}, world = null) {
|
|
2207
2328
|
const startTime = Date.now();
|
|
@@ -2217,7 +2338,7 @@ class StableBrowser {
|
|
|
2217
2338
|
throw new Error("TOKEN is not set");
|
|
2218
2339
|
}
|
|
2219
2340
|
try {
|
|
2220
|
-
let serviceUrl =
|
|
2341
|
+
let serviceUrl = _getServerUrl();
|
|
2221
2342
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2222
2343
|
info.screenshotPath = screenshotPath;
|
|
2223
2344
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2253,11 +2374,12 @@ class StableBrowser {
|
|
|
2253
2374
|
info.screenshotPath = screenshotPath;
|
|
2254
2375
|
Object.assign(e, { info: info });
|
|
2255
2376
|
error = e;
|
|
2256
|
-
throw e;
|
|
2377
|
+
// throw e;
|
|
2378
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2257
2379
|
}
|
|
2258
2380
|
finally {
|
|
2259
2381
|
const endTime = Date.now();
|
|
2260
|
-
|
|
2382
|
+
_reportToWorld(world, {
|
|
2261
2383
|
type: Types.VERIFY_VISUAL,
|
|
2262
2384
|
text: "Visual verification",
|
|
2263
2385
|
screenshotId,
|
|
@@ -2266,7 +2388,7 @@ class StableBrowser {
|
|
|
2266
2388
|
status: "FAILED",
|
|
2267
2389
|
startTime,
|
|
2268
2390
|
endTime,
|
|
2269
|
-
message: error
|
|
2391
|
+
message: error?.message,
|
|
2270
2392
|
}
|
|
2271
2393
|
: {
|
|
2272
2394
|
status: "PASSED",
|
|
@@ -2298,13 +2420,14 @@ class StableBrowser {
|
|
|
2298
2420
|
this.logger.info("Table data verified");
|
|
2299
2421
|
}
|
|
2300
2422
|
async getTableData(selectors, _params = null, options = {}, world = null) {
|
|
2301
|
-
|
|
2423
|
+
_validateSelectors(selectors);
|
|
2302
2424
|
const startTime = Date.now();
|
|
2303
2425
|
let error = null;
|
|
2304
2426
|
let screenshotId = null;
|
|
2305
2427
|
let screenshotPath = null;
|
|
2306
2428
|
const info = {};
|
|
2307
2429
|
info.log = "";
|
|
2430
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2308
2431
|
info.operation = "getTableData";
|
|
2309
2432
|
info.selectors = selectors;
|
|
2310
2433
|
try {
|
|
@@ -2320,11 +2443,12 @@ class StableBrowser {
|
|
|
2320
2443
|
info.screenshotPath = screenshotPath;
|
|
2321
2444
|
Object.assign(e, { info: info });
|
|
2322
2445
|
error = e;
|
|
2323
|
-
throw e;
|
|
2446
|
+
// throw e;
|
|
2447
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2324
2448
|
}
|
|
2325
2449
|
finally {
|
|
2326
2450
|
const endTime = Date.now();
|
|
2327
|
-
|
|
2451
|
+
_reportToWorld(world, {
|
|
2328
2452
|
element_name: selectors.element_name,
|
|
2329
2453
|
type: Types.GET_TABLE_DATA,
|
|
2330
2454
|
text: "Get table data",
|
|
@@ -2334,7 +2458,7 @@ class StableBrowser {
|
|
|
2334
2458
|
status: "FAILED",
|
|
2335
2459
|
startTime,
|
|
2336
2460
|
endTime,
|
|
2337
|
-
message: error
|
|
2461
|
+
message: error?.message,
|
|
2338
2462
|
}
|
|
2339
2463
|
: {
|
|
2340
2464
|
status: "PASSED",
|
|
@@ -2346,7 +2470,7 @@ class StableBrowser {
|
|
|
2346
2470
|
}
|
|
2347
2471
|
}
|
|
2348
2472
|
async analyzeTable(selectors, query, operator, value, _params = null, options = {}, world = null) {
|
|
2349
|
-
|
|
2473
|
+
_validateSelectors(selectors);
|
|
2350
2474
|
if (!query) {
|
|
2351
2475
|
throw new Error("query is null");
|
|
2352
2476
|
}
|
|
@@ -2379,7 +2503,7 @@ class StableBrowser {
|
|
|
2379
2503
|
info.operation = "analyzeTable";
|
|
2380
2504
|
info.selectors = selectors;
|
|
2381
2505
|
info.query = query;
|
|
2382
|
-
query =
|
|
2506
|
+
query = _fixUsingParams(query, _params);
|
|
2383
2507
|
info.query_fixed = query;
|
|
2384
2508
|
info.operator = operator;
|
|
2385
2509
|
info.value = value;
|
|
@@ -2485,11 +2609,12 @@ class StableBrowser {
|
|
|
2485
2609
|
info.screenshotPath = screenshotPath;
|
|
2486
2610
|
Object.assign(e, { info: info });
|
|
2487
2611
|
error = e;
|
|
2488
|
-
throw e;
|
|
2612
|
+
// throw e;
|
|
2613
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2489
2614
|
}
|
|
2490
2615
|
finally {
|
|
2491
2616
|
const endTime = Date.now();
|
|
2492
|
-
|
|
2617
|
+
_reportToWorld(world, {
|
|
2493
2618
|
element_name: selectors.element_name,
|
|
2494
2619
|
type: Types.ANALYZE_TABLE,
|
|
2495
2620
|
text: "Analyze table",
|
|
@@ -2499,7 +2624,7 @@ class StableBrowser {
|
|
|
2499
2624
|
status: "FAILED",
|
|
2500
2625
|
startTime,
|
|
2501
2626
|
endTime,
|
|
2502
|
-
message: error
|
|
2627
|
+
message: error?.message,
|
|
2503
2628
|
}
|
|
2504
2629
|
: {
|
|
2505
2630
|
status: "PASSED",
|
|
@@ -2511,27 +2636,7 @@ class StableBrowser {
|
|
|
2511
2636
|
}
|
|
2512
2637
|
}
|
|
2513
2638
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2514
|
-
|
|
2515
|
-
return value;
|
|
2516
|
-
}
|
|
2517
|
-
// find all the accurance of {{(.*?)}} and replace with the value
|
|
2518
|
-
let regex = /{{(.*?)}}/g;
|
|
2519
|
-
let matches = value.match(regex);
|
|
2520
|
-
if (matches) {
|
|
2521
|
-
const testData = this.getTestData(world);
|
|
2522
|
-
for (let i = 0; i < matches.length; i++) {
|
|
2523
|
-
let match = matches[i];
|
|
2524
|
-
let key = match.substring(2, match.length - 2);
|
|
2525
|
-
let newValue = objectPath.get(testData, key, null);
|
|
2526
|
-
if (newValue !== null) {
|
|
2527
|
-
value = value.replace(match, newValue);
|
|
2528
|
-
}
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
|
|
2532
|
-
return await decrypt(value, null, totpWait);
|
|
2533
|
-
}
|
|
2534
|
-
return value;
|
|
2639
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2535
2640
|
}
|
|
2536
2641
|
_getLoadTimeout(options) {
|
|
2537
2642
|
let timeout = 15000;
|
|
@@ -2568,13 +2673,13 @@ class StableBrowser {
|
|
|
2568
2673
|
}
|
|
2569
2674
|
catch (e) {
|
|
2570
2675
|
if (e.label === "networkidle") {
|
|
2571
|
-
console.log("
|
|
2676
|
+
console.log("waited for the network to be idle timeout");
|
|
2572
2677
|
}
|
|
2573
2678
|
else if (e.label === "load") {
|
|
2574
|
-
console.log("
|
|
2679
|
+
console.log("waited for the load timeout");
|
|
2575
2680
|
}
|
|
2576
2681
|
else if (e.label === "domcontentloaded") {
|
|
2577
|
-
console.log("
|
|
2682
|
+
console.log("waited for the domcontent loaded timeout");
|
|
2578
2683
|
}
|
|
2579
2684
|
console.log(".");
|
|
2580
2685
|
}
|
|
@@ -2582,7 +2687,7 @@ class StableBrowser {
|
|
|
2582
2687
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2583
2688
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2584
2689
|
const endTime = Date.now();
|
|
2585
|
-
|
|
2690
|
+
_reportToWorld(world, {
|
|
2586
2691
|
type: Types.GET_PAGE_STATUS,
|
|
2587
2692
|
text: "Wait for page load",
|
|
2588
2693
|
screenshotId,
|
|
@@ -2591,7 +2696,7 @@ class StableBrowser {
|
|
|
2591
2696
|
status: "FAILED",
|
|
2592
2697
|
startTime,
|
|
2593
2698
|
endTime,
|
|
2594
|
-
message: error
|
|
2699
|
+
message: error?.message,
|
|
2595
2700
|
}
|
|
2596
2701
|
: {
|
|
2597
2702
|
status: "PASSED",
|
|
@@ -2602,41 +2707,35 @@ class StableBrowser {
|
|
|
2602
2707
|
}
|
|
2603
2708
|
}
|
|
2604
2709
|
async closePage(options = {}, world = null) {
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2710
|
+
const state = {
|
|
2711
|
+
options,
|
|
2712
|
+
world,
|
|
2713
|
+
locate: false,
|
|
2714
|
+
scroll: false,
|
|
2715
|
+
highlight: false,
|
|
2716
|
+
type: Types.CLOSE_PAGE,
|
|
2717
|
+
text: `Close page`,
|
|
2718
|
+
operation: "closePage",
|
|
2719
|
+
log: "***** close page *****\n",
|
|
2720
|
+
throwError: false,
|
|
2721
|
+
};
|
|
2610
2722
|
try {
|
|
2723
|
+
await _preCommand(state, this);
|
|
2611
2724
|
await this.page.close();
|
|
2612
2725
|
}
|
|
2613
2726
|
catch (e) {
|
|
2614
2727
|
console.log(".");
|
|
2728
|
+
await _commandError(state, e, this);
|
|
2615
2729
|
}
|
|
2616
2730
|
finally {
|
|
2617
|
-
|
|
2618
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2619
|
-
const endTime = Date.now();
|
|
2620
|
-
this._reportToWorld(world, {
|
|
2621
|
-
type: Types.CLOSE_PAGE,
|
|
2622
|
-
text: "close page",
|
|
2623
|
-
screenshotId,
|
|
2624
|
-
result: error
|
|
2625
|
-
? {
|
|
2626
|
-
status: "FAILED",
|
|
2627
|
-
startTime,
|
|
2628
|
-
endTime,
|
|
2629
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
2630
|
-
}
|
|
2631
|
-
: {
|
|
2632
|
-
status: "PASSED",
|
|
2633
|
-
startTime,
|
|
2634
|
-
endTime,
|
|
2635
|
-
},
|
|
2636
|
-
info: info,
|
|
2637
|
-
});
|
|
2731
|
+
_commandFinally(state, this);
|
|
2638
2732
|
}
|
|
2639
2733
|
}
|
|
2734
|
+
saveTestDataAsGlobal(options, world) {
|
|
2735
|
+
const dataFile = this._getDataFile(world);
|
|
2736
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2737
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2738
|
+
}
|
|
2640
2739
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2641
2740
|
const startTime = Date.now();
|
|
2642
2741
|
let error = null;
|
|
@@ -2654,12 +2753,13 @@ class StableBrowser {
|
|
|
2654
2753
|
}
|
|
2655
2754
|
catch (e) {
|
|
2656
2755
|
console.log(".");
|
|
2756
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2657
2757
|
}
|
|
2658
2758
|
finally {
|
|
2659
2759
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2660
2760
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2661
2761
|
const endTime = Date.now();
|
|
2662
|
-
|
|
2762
|
+
_reportToWorld(world, {
|
|
2663
2763
|
type: Types.SET_VIEWPORT,
|
|
2664
2764
|
text: "set viewport size to " + width + "x" + hight,
|
|
2665
2765
|
screenshotId,
|
|
@@ -2668,7 +2768,7 @@ class StableBrowser {
|
|
|
2668
2768
|
status: "FAILED",
|
|
2669
2769
|
startTime,
|
|
2670
2770
|
endTime,
|
|
2671
|
-
message: error
|
|
2771
|
+
message: error?.message,
|
|
2672
2772
|
}
|
|
2673
2773
|
: {
|
|
2674
2774
|
status: "PASSED",
|
|
@@ -2690,12 +2790,13 @@ class StableBrowser {
|
|
|
2690
2790
|
}
|
|
2691
2791
|
catch (e) {
|
|
2692
2792
|
console.log(".");
|
|
2793
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2693
2794
|
}
|
|
2694
2795
|
finally {
|
|
2695
2796
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2696
2797
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2697
2798
|
const endTime = Date.now();
|
|
2698
|
-
|
|
2799
|
+
_reportToWorld(world, {
|
|
2699
2800
|
type: Types.GET_PAGE_STATUS,
|
|
2700
2801
|
text: "page relaod",
|
|
2701
2802
|
screenshotId,
|
|
@@ -2704,7 +2805,7 @@ class StableBrowser {
|
|
|
2704
2805
|
status: "FAILED",
|
|
2705
2806
|
startTime,
|
|
2706
2807
|
endTime,
|
|
2707
|
-
message: error
|
|
2808
|
+
message: error?.message,
|
|
2708
2809
|
}
|
|
2709
2810
|
: {
|
|
2710
2811
|
status: "PASSED",
|
|
@@ -2717,40 +2818,59 @@ class StableBrowser {
|
|
|
2717
2818
|
}
|
|
2718
2819
|
async scrollIfNeeded(element, info) {
|
|
2719
2820
|
try {
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
if (rect &&
|
|
2723
|
-
rect.top >= 0 &&
|
|
2724
|
-
rect.left >= 0 &&
|
|
2725
|
-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
2726
|
-
rect.right <= (window.innerWidth || document.documentElement.clientWidth)) {
|
|
2727
|
-
return false;
|
|
2728
|
-
}
|
|
2729
|
-
else {
|
|
2730
|
-
node.scrollIntoView({
|
|
2731
|
-
behavior: "smooth",
|
|
2732
|
-
block: "center",
|
|
2733
|
-
inline: "center",
|
|
2734
|
-
});
|
|
2735
|
-
return true;
|
|
2736
|
-
}
|
|
2821
|
+
await element.scrollIntoViewIfNeeded({
|
|
2822
|
+
timeout: 2000,
|
|
2737
2823
|
});
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
}
|
|
2824
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2825
|
+
if (info) {
|
|
2826
|
+
info.box = await element.boundingBox({
|
|
2827
|
+
timeout: 1000,
|
|
2828
|
+
});
|
|
2743
2829
|
}
|
|
2744
2830
|
}
|
|
2745
2831
|
catch (e) {
|
|
2746
|
-
console.log("
|
|
2832
|
+
console.log("#-#");
|
|
2747
2833
|
}
|
|
2748
2834
|
}
|
|
2749
|
-
|
|
2750
|
-
if (
|
|
2751
|
-
|
|
2835
|
+
async beforeStep(world, step) {
|
|
2836
|
+
if (this.stepIndex === undefined) {
|
|
2837
|
+
this.stepIndex = 0;
|
|
2838
|
+
}
|
|
2839
|
+
else {
|
|
2840
|
+
this.stepIndex++;
|
|
2841
|
+
}
|
|
2842
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
2843
|
+
this.stepName = step.pickleStep.text;
|
|
2844
|
+
this.logger.info("step: " + this.stepName);
|
|
2845
|
+
}
|
|
2846
|
+
else if (step && step.text) {
|
|
2847
|
+
this.stepName = step.text;
|
|
2848
|
+
}
|
|
2849
|
+
else {
|
|
2850
|
+
this.stepName = "step " + this.stepIndex;
|
|
2851
|
+
}
|
|
2852
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2853
|
+
if (this.context.browserObject.context) {
|
|
2854
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
2858
|
+
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
2859
|
+
// check if @global_test_data tag is present
|
|
2860
|
+
if (this.tags.includes("@global_test_data")) {
|
|
2861
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
async afterStep(world, step) {
|
|
2866
|
+
this.stepName = null;
|
|
2867
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2868
|
+
if (this.context.browserObject.context) {
|
|
2869
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
2870
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2752
2873
|
}
|
|
2753
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2754
2874
|
}
|
|
2755
2875
|
}
|
|
2756
2876
|
function createTimedPromise(promise, label) {
|
|
@@ -2758,151 +2878,5 @@ function createTimedPromise(promise, label) {
|
|
|
2758
2878
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2759
2879
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2760
2880
|
}
|
|
2761
|
-
const KEYBOARD_EVENTS = [
|
|
2762
|
-
"ALT",
|
|
2763
|
-
"AltGraph",
|
|
2764
|
-
"CapsLock",
|
|
2765
|
-
"Control",
|
|
2766
|
-
"Fn",
|
|
2767
|
-
"FnLock",
|
|
2768
|
-
"Hyper",
|
|
2769
|
-
"Meta",
|
|
2770
|
-
"NumLock",
|
|
2771
|
-
"ScrollLock",
|
|
2772
|
-
"Shift",
|
|
2773
|
-
"Super",
|
|
2774
|
-
"Symbol",
|
|
2775
|
-
"SymbolLock",
|
|
2776
|
-
"Enter",
|
|
2777
|
-
"Tab",
|
|
2778
|
-
"ArrowDown",
|
|
2779
|
-
"ArrowLeft",
|
|
2780
|
-
"ArrowRight",
|
|
2781
|
-
"ArrowUp",
|
|
2782
|
-
"End",
|
|
2783
|
-
"Home",
|
|
2784
|
-
"PageDown",
|
|
2785
|
-
"PageUp",
|
|
2786
|
-
"Backspace",
|
|
2787
|
-
"Clear",
|
|
2788
|
-
"Copy",
|
|
2789
|
-
"CrSel",
|
|
2790
|
-
"Cut",
|
|
2791
|
-
"Delete",
|
|
2792
|
-
"EraseEof",
|
|
2793
|
-
"ExSel",
|
|
2794
|
-
"Insert",
|
|
2795
|
-
"Paste",
|
|
2796
|
-
"Redo",
|
|
2797
|
-
"Undo",
|
|
2798
|
-
"Accept",
|
|
2799
|
-
"Again",
|
|
2800
|
-
"Attn",
|
|
2801
|
-
"Cancel",
|
|
2802
|
-
"ContextMenu",
|
|
2803
|
-
"Escape",
|
|
2804
|
-
"Execute",
|
|
2805
|
-
"Find",
|
|
2806
|
-
"Finish",
|
|
2807
|
-
"Help",
|
|
2808
|
-
"Pause",
|
|
2809
|
-
"Play",
|
|
2810
|
-
"Props",
|
|
2811
|
-
"Select",
|
|
2812
|
-
"ZoomIn",
|
|
2813
|
-
"ZoomOut",
|
|
2814
|
-
"BrightnessDown",
|
|
2815
|
-
"BrightnessUp",
|
|
2816
|
-
"Eject",
|
|
2817
|
-
"LogOff",
|
|
2818
|
-
"Power",
|
|
2819
|
-
"PowerOff",
|
|
2820
|
-
"PrintScreen",
|
|
2821
|
-
"Hibernate",
|
|
2822
|
-
"Standby",
|
|
2823
|
-
"WakeUp",
|
|
2824
|
-
"AllCandidates",
|
|
2825
|
-
"Alphanumeric",
|
|
2826
|
-
"CodeInput",
|
|
2827
|
-
"Compose",
|
|
2828
|
-
"Convert",
|
|
2829
|
-
"Dead",
|
|
2830
|
-
"FinalMode",
|
|
2831
|
-
"GroupFirst",
|
|
2832
|
-
"GroupLast",
|
|
2833
|
-
"GroupNext",
|
|
2834
|
-
"GroupPrevious",
|
|
2835
|
-
"ModeChange",
|
|
2836
|
-
"NextCandidate",
|
|
2837
|
-
"NonConvert",
|
|
2838
|
-
"PreviousCandidate",
|
|
2839
|
-
"Process",
|
|
2840
|
-
"SingleCandidate",
|
|
2841
|
-
"HangulMode",
|
|
2842
|
-
"HanjaMode",
|
|
2843
|
-
"JunjaMode",
|
|
2844
|
-
"Eisu",
|
|
2845
|
-
"Hankaku",
|
|
2846
|
-
"Hiragana",
|
|
2847
|
-
"HiraganaKatakana",
|
|
2848
|
-
"KanaMode",
|
|
2849
|
-
"KanjiMode",
|
|
2850
|
-
"Katakana",
|
|
2851
|
-
"Romaji",
|
|
2852
|
-
"Zenkaku",
|
|
2853
|
-
"ZenkakuHanaku",
|
|
2854
|
-
"F1",
|
|
2855
|
-
"F2",
|
|
2856
|
-
"F3",
|
|
2857
|
-
"F4",
|
|
2858
|
-
"F5",
|
|
2859
|
-
"F6",
|
|
2860
|
-
"F7",
|
|
2861
|
-
"F8",
|
|
2862
|
-
"F9",
|
|
2863
|
-
"F10",
|
|
2864
|
-
"F11",
|
|
2865
|
-
"F12",
|
|
2866
|
-
"Soft1",
|
|
2867
|
-
"Soft2",
|
|
2868
|
-
"Soft3",
|
|
2869
|
-
"Soft4",
|
|
2870
|
-
"ChannelDown",
|
|
2871
|
-
"ChannelUp",
|
|
2872
|
-
"Close",
|
|
2873
|
-
"MailForward",
|
|
2874
|
-
"MailReply",
|
|
2875
|
-
"MailSend",
|
|
2876
|
-
"MediaFastForward",
|
|
2877
|
-
"MediaPause",
|
|
2878
|
-
"MediaPlay",
|
|
2879
|
-
"MediaPlayPause",
|
|
2880
|
-
"MediaRecord",
|
|
2881
|
-
"MediaRewind",
|
|
2882
|
-
"MediaStop",
|
|
2883
|
-
"MediaTrackNext",
|
|
2884
|
-
"MediaTrackPrevious",
|
|
2885
|
-
"AudioBalanceLeft",
|
|
2886
|
-
"AudioBalanceRight",
|
|
2887
|
-
"AudioBassBoostDown",
|
|
2888
|
-
"AudioBassBoostToggle",
|
|
2889
|
-
"AudioBassBoostUp",
|
|
2890
|
-
"AudioFaderFront",
|
|
2891
|
-
"AudioFaderRear",
|
|
2892
|
-
"AudioSurroundModeNext",
|
|
2893
|
-
"AudioTrebleDown",
|
|
2894
|
-
"AudioTrebleUp",
|
|
2895
|
-
"AudioVolumeDown",
|
|
2896
|
-
"AudioVolumeMute",
|
|
2897
|
-
"AudioVolumeUp",
|
|
2898
|
-
"MicrophoneToggle",
|
|
2899
|
-
"MicrophoneVolumeDown",
|
|
2900
|
-
"MicrophoneVolumeMute",
|
|
2901
|
-
"MicrophoneVolumeUp",
|
|
2902
|
-
"TV",
|
|
2903
|
-
"TV3DMode",
|
|
2904
|
-
"TVAntennaCable",
|
|
2905
|
-
"TVAudioDescription",
|
|
2906
|
-
];
|
|
2907
2881
|
export { StableBrowser };
|
|
2908
2882
|
//# sourceMappingURL=stable_browser.js.map
|