automation_model 1.0.495-dev → 1.0.495-stage
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 +2 -1
- package/lib/api.js +95 -98
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +2 -1
- package/lib/auto_page.js +19 -2
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +6 -3
- package/lib/browser_manager.js +108 -21
- package/lib/browser_manager.js.map +1 -1
- package/lib/command_common.d.ts +1 -0
- package/lib/command_common.js +54 -6
- package/lib/command_common.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 +2 -1
- package/lib/init_browser.js +26 -6
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js +15 -13
- package/lib/locate_element.js.map +1 -1
- 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 +1 -1
- package/lib/network.js +33 -4
- package/lib/network.js.map +1 -1
- package/lib/scripts/find_text.js +126 -0
- package/lib/stable_browser.d.ts +50 -18
- package/lib/stable_browser.js +630 -580
- 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 +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +12 -1
- package/lib/utils.js +235 -3
- package/lib/utils.js.map +1 -1
- package/package.json +7 -5
- /package/lib/{axe → scripts}/axe.mini.js +0 -0
package/lib/stable_browser.js
CHANGED
|
@@ -10,15 +10,17 @@ 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 { maskValue, replaceWithLocalTestData } from "./utils.js";
|
|
13
|
+
import { _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, } from "./utils.js";
|
|
14
14
|
import csv from "csv-parser";
|
|
15
15
|
import { Readable } from "node:stream";
|
|
16
16
|
import readline from "readline";
|
|
17
17
|
import { getContext } from "./init_browser.js";
|
|
18
18
|
import { locate_element } from "./locate_element.js";
|
|
19
|
-
import {
|
|
19
|
+
import { randomUUID } from "crypto";
|
|
20
|
+
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
20
21
|
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
21
|
-
|
|
22
|
+
import { LocatorLog } from "./locator_log.js";
|
|
23
|
+
export const Types = {
|
|
22
24
|
CLICK: "click_element",
|
|
23
25
|
NAVIGATE: "navigate",
|
|
24
26
|
FILL: "fill_element",
|
|
@@ -29,6 +31,8 @@ const Types = {
|
|
|
29
31
|
GET_PAGE_STATUS: "get_page_status",
|
|
30
32
|
CLICK_ROW_ACTION: "click_row_action",
|
|
31
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",
|
|
32
36
|
ANALYZE_TABLE: "analyze_table",
|
|
33
37
|
SELECT: "select_combobox",
|
|
34
38
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
@@ -44,6 +48,9 @@ const Types = {
|
|
|
44
48
|
VERIFY_VISUAL: "verify_visual",
|
|
45
49
|
LOAD_DATA: "load_data",
|
|
46
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",
|
|
47
54
|
};
|
|
48
55
|
export const apps = {};
|
|
49
56
|
class StableBrowser {
|
|
@@ -57,6 +64,7 @@ class StableBrowser {
|
|
|
57
64
|
networkLogger = null;
|
|
58
65
|
configuration = null;
|
|
59
66
|
appName = "main";
|
|
67
|
+
tags = null;
|
|
60
68
|
constructor(browser, page, logger = null, context = null, world = null) {
|
|
61
69
|
this.browser = browser;
|
|
62
70
|
this.page = page;
|
|
@@ -87,27 +95,32 @@ class StableBrowser {
|
|
|
87
95
|
catch (e) {
|
|
88
96
|
this.logger.error("unable to read ai_config.json");
|
|
89
97
|
}
|
|
90
|
-
context.pageLoading = { status: false };
|
|
91
|
-
context.pages = [this.page];
|
|
92
98
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
93
99
|
this.world = world;
|
|
100
|
+
context.pages = [this.page];
|
|
101
|
+
context.pageLoading = { status: false };
|
|
94
102
|
this.registerEventListeners(this.context);
|
|
95
103
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
96
|
-
registerDownloadEvent(this.page);
|
|
104
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
97
105
|
}
|
|
98
106
|
registerEventListeners(context) {
|
|
99
107
|
this.registerConsoleLogListener(this.page, context);
|
|
100
|
-
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
108
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
101
109
|
if (!context.pageLoading) {
|
|
102
110
|
context.pageLoading = { status: false };
|
|
103
111
|
}
|
|
104
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
|
+
}
|
|
105
118
|
context.pageLoading.status = true;
|
|
106
119
|
this.page = page;
|
|
107
120
|
context.page = page;
|
|
108
121
|
context.pages.push(page);
|
|
109
122
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
110
|
-
registerDownloadEvent(this.page);
|
|
123
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
111
124
|
page.on("close", async () => {
|
|
112
125
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
113
126
|
this.context.pages.pop();
|
|
@@ -137,9 +150,9 @@ class StableBrowser {
|
|
|
137
150
|
if (this.appName === appName) {
|
|
138
151
|
return;
|
|
139
152
|
}
|
|
140
|
-
let
|
|
153
|
+
let navigate = false;
|
|
141
154
|
if (!apps[appName]) {
|
|
142
|
-
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this);
|
|
155
|
+
let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
|
|
143
156
|
newContextCreated = true;
|
|
144
157
|
apps[appName] = {
|
|
145
158
|
context: newContext,
|
|
@@ -148,32 +161,15 @@ class StableBrowser {
|
|
|
148
161
|
};
|
|
149
162
|
}
|
|
150
163
|
const tempContext = {};
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
_copyContext(this, tempContext);
|
|
165
|
+
_copyContext(apps[appName], this);
|
|
153
166
|
apps[this.appName] = tempContext;
|
|
154
167
|
this.appName = appName;
|
|
155
|
-
if (
|
|
156
|
-
this.registerEventListeners(this.context);
|
|
168
|
+
if (navigate) {
|
|
157
169
|
await this.goto(this.context.environment.baseUrl);
|
|
158
170
|
await this.waitForPageLoad();
|
|
159
171
|
}
|
|
160
172
|
}
|
|
161
|
-
_copyContext(from, to) {
|
|
162
|
-
to.browser = from.browser;
|
|
163
|
-
to.page = from.page;
|
|
164
|
-
to.context = from.context;
|
|
165
|
-
}
|
|
166
|
-
getWebLogFile(logFolder) {
|
|
167
|
-
if (!fs.existsSync(logFolder)) {
|
|
168
|
-
fs.mkdirSync(logFolder, { recursive: true });
|
|
169
|
-
}
|
|
170
|
-
let nextIndex = 1;
|
|
171
|
-
while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
|
|
172
|
-
nextIndex++;
|
|
173
|
-
}
|
|
174
|
-
const fileName = nextIndex + ".json";
|
|
175
|
-
return path.join(logFolder, fileName);
|
|
176
|
-
}
|
|
177
173
|
registerConsoleLogListener(page, context) {
|
|
178
174
|
if (!this.context.webLogger) {
|
|
179
175
|
this.context.webLogger = [];
|
|
@@ -223,7 +219,7 @@ class StableBrowser {
|
|
|
223
219
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
224
220
|
}
|
|
225
221
|
catch (error) {
|
|
226
|
-
console.error("Error in request listener", error);
|
|
222
|
+
// console.error("Error in request listener", error);
|
|
227
223
|
context.networkLogger.push({
|
|
228
224
|
error: "not able to listen",
|
|
229
225
|
message: error.message,
|
|
@@ -237,55 +233,48 @@ class StableBrowser {
|
|
|
237
233
|
// async closeUnexpectedPopups() {
|
|
238
234
|
// await closeUnexpectedPopups(this.page);
|
|
239
235
|
// }
|
|
240
|
-
async goto(url) {
|
|
236
|
+
async goto(url, world = null) {
|
|
241
237
|
if (!url.startsWith("http")) {
|
|
242
238
|
url = "https://" + url;
|
|
243
239
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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);
|
|
251
259
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// remove the _ prefix
|
|
256
|
-
regValue = key.substring(1);
|
|
257
|
-
}
|
|
258
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
260
|
+
catch (error) {
|
|
261
|
+
console.error("Error on goto", error);
|
|
262
|
+
_commandError(state, error, this);
|
|
259
263
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
263
|
-
// check if not null
|
|
264
|
-
if (!locator) {
|
|
265
|
-
return locator;
|
|
264
|
+
finally {
|
|
265
|
+
_commandFinally(state, this);
|
|
266
266
|
}
|
|
267
|
-
// clone the locator
|
|
268
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
269
|
-
this.scanAndManipulate(locator, _params);
|
|
270
|
-
return locator;
|
|
271
|
-
}
|
|
272
|
-
_isObject(value) {
|
|
273
|
-
return value && typeof value === "object" && value.constructor === Object;
|
|
274
267
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
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);
|
|
284
276
|
}
|
|
285
277
|
}
|
|
286
|
-
}
|
|
287
|
-
_getLocator(locator, scope, _params) {
|
|
288
|
-
locator = this._fixLocatorUsingParams(locator, _params);
|
|
289
278
|
let locatorReturn;
|
|
290
279
|
if (locator.role) {
|
|
291
280
|
if (locator.role[1].nameReg) {
|
|
@@ -293,7 +282,7 @@ class StableBrowser {
|
|
|
293
282
|
delete locator.role[1].nameReg;
|
|
294
283
|
}
|
|
295
284
|
// if (locator.role[1].name) {
|
|
296
|
-
// locator.role[1].name =
|
|
285
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
297
286
|
// }
|
|
298
287
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
299
288
|
}
|
|
@@ -336,7 +325,7 @@ class StableBrowser {
|
|
|
336
325
|
if (css && css.locator) {
|
|
337
326
|
css = css.locator;
|
|
338
327
|
}
|
|
339
|
-
let result = await this._locateElementByText(scope,
|
|
328
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, _params);
|
|
340
329
|
if (result.elementCount === 0) {
|
|
341
330
|
return;
|
|
342
331
|
}
|
|
@@ -352,116 +341,42 @@ class StableBrowser {
|
|
|
352
341
|
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
|
|
353
342
|
//const stringifyText = JSON.stringify(text);
|
|
354
343
|
return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (currentNode === parent) {
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
currentNode = currentNode.parentNode;
|
|
362
|
-
}
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
document.isParent = isParent;
|
|
366
|
-
function getRegex(str) {
|
|
367
|
-
const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
|
|
368
|
-
if (!match) {
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
let [_, pattern, flags] = match;
|
|
372
|
-
return new RegExp(pattern, flags);
|
|
373
|
-
}
|
|
374
|
-
document.getRegex = getRegex;
|
|
375
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
376
|
-
// Check and add the element if it has a shadow root
|
|
377
|
-
if (element.shadowRoot) {
|
|
378
|
-
result.push(element);
|
|
379
|
-
// Also search within the shadow root
|
|
380
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
381
|
-
}
|
|
382
|
-
// Iterate over child nodes
|
|
383
|
-
element.childNodes.forEach((child) => {
|
|
384
|
-
// Recursively call the function for each child node
|
|
385
|
-
document.collectAllShadowDomElements(child, result);
|
|
386
|
-
});
|
|
387
|
-
return result;
|
|
388
|
-
}
|
|
389
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
390
|
-
if (!tag) {
|
|
391
|
-
tag = "*";
|
|
392
|
-
}
|
|
393
|
-
let regexpSearch = document.getRegex(text);
|
|
394
|
-
if (regexpSearch) {
|
|
395
|
-
regex = true;
|
|
396
|
-
}
|
|
397
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
398
|
-
let shadowHosts = [];
|
|
399
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
400
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
401
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
402
|
-
if (!shadowElement) {
|
|
403
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
407
|
-
elements = elements.concat(shadowElements);
|
|
408
|
-
}
|
|
409
|
-
let randomToken = null;
|
|
410
|
-
const foundElements = [];
|
|
344
|
+
const options = {
|
|
345
|
+
innerText: true,
|
|
346
|
+
};
|
|
411
347
|
if (regex) {
|
|
412
|
-
|
|
413
|
-
regexpSearch = new RegExp(text, "im");
|
|
414
|
-
}
|
|
415
|
-
for (let i = 0; i < elements.length; i++) {
|
|
416
|
-
const element = elements[i];
|
|
417
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
418
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
419
|
-
foundElements.push(element);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
348
|
+
options.singleRegex = true;
|
|
422
349
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
for (let i = 0; i < elements.length; i++) {
|
|
426
|
-
const element = elements[i];
|
|
427
|
-
if (partial) {
|
|
428
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
429
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
430
|
-
foundElements.push(element);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
435
|
-
(element.value && element.value === text)) {
|
|
436
|
-
foundElements.push(element);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
350
|
+
if (tag) {
|
|
351
|
+
options.tag = tag;
|
|
440
352
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
if (isParent(element, foundElements[j])) {
|
|
450
|
-
hasChild = true;
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
353
|
+
if (!(partial === true)) {
|
|
354
|
+
options.exactMatch = true;
|
|
355
|
+
}
|
|
356
|
+
if (text.startsWith("/") && text.endsWith("/")) {
|
|
357
|
+
if (text.length < 3) {
|
|
358
|
+
throw new Error("Invalid regex pattern: empty pattern");
|
|
453
359
|
}
|
|
454
|
-
|
|
455
|
-
|
|
360
|
+
try {
|
|
361
|
+
const pattern = text.slice(1, -1);
|
|
362
|
+
new RegExp(pattern); // Validate pattern
|
|
363
|
+
text = pattern;
|
|
364
|
+
options.singleRegex = true;
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
throw new Error(`Invalid regex pattern: ${e.message}`);
|
|
456
368
|
}
|
|
457
369
|
}
|
|
370
|
+
const elements = window.findMatchingElements(text, options);
|
|
371
|
+
let randomToken = null;
|
|
372
|
+
const foundElements = [];
|
|
458
373
|
let elementCount = 0;
|
|
459
|
-
if (
|
|
460
|
-
for (let i = 0; i <
|
|
374
|
+
if (elements.length > 0) {
|
|
375
|
+
for (let i = 0; i < elements.length; i++) {
|
|
461
376
|
if (randomToken === null) {
|
|
462
377
|
randomToken = Math.random().toString(36).substring(7);
|
|
463
378
|
}
|
|
464
|
-
let element =
|
|
379
|
+
let element = elements[i];
|
|
465
380
|
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
466
381
|
elementCount++;
|
|
467
382
|
}
|
|
@@ -470,9 +385,21 @@ class StableBrowser {
|
|
|
470
385
|
}, [text1, tag1, regex1, partial1]);
|
|
471
386
|
}
|
|
472
387
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
388
|
+
if (!info) {
|
|
389
|
+
info = {};
|
|
390
|
+
}
|
|
391
|
+
if (!info.failCause) {
|
|
392
|
+
info.failCause = {};
|
|
393
|
+
}
|
|
394
|
+
if (!info.log) {
|
|
395
|
+
info.log = "";
|
|
396
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
397
|
+
}
|
|
473
398
|
let locatorSearch = selectorHierarchy[index];
|
|
399
|
+
let originalLocatorSearch = "";
|
|
474
400
|
try {
|
|
475
|
-
|
|
401
|
+
originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
|
|
402
|
+
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
476
403
|
}
|
|
477
404
|
catch (e) {
|
|
478
405
|
console.error(e);
|
|
@@ -482,36 +409,50 @@ class StableBrowser {
|
|
|
482
409
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
483
410
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
484
411
|
if (!locatorString) {
|
|
412
|
+
info.failCause.textNotFound = true;
|
|
413
|
+
info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
|
|
485
414
|
return;
|
|
486
415
|
}
|
|
487
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
416
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
488
417
|
}
|
|
489
418
|
else if (locatorSearch.text) {
|
|
490
|
-
let
|
|
419
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
420
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, _params);
|
|
491
421
|
if (result.elementCount === 0) {
|
|
422
|
+
info.failCause.textNotFound = true;
|
|
423
|
+
info.failCause.lastError = "failed to locate element by text: " + text;
|
|
492
424
|
return;
|
|
493
425
|
}
|
|
494
426
|
locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
|
|
495
427
|
if (locatorSearch.childCss) {
|
|
496
428
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
497
429
|
}
|
|
498
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
430
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
499
431
|
}
|
|
500
432
|
else {
|
|
501
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
433
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
502
434
|
}
|
|
503
435
|
// let cssHref = false;
|
|
504
436
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
505
437
|
// cssHref = true;
|
|
506
438
|
// }
|
|
507
439
|
let count = await locator.count();
|
|
440
|
+
if (count > 0 && !info.failCause.count) {
|
|
441
|
+
info.failCause.count = count;
|
|
442
|
+
}
|
|
508
443
|
//info.log += "total elements found " + count + "\n";
|
|
509
444
|
//let visibleCount = 0;
|
|
510
445
|
let visibleLocator = null;
|
|
511
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
446
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
512
447
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
448
|
+
if (info.locatorLog) {
|
|
449
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
450
|
+
}
|
|
513
451
|
return;
|
|
514
452
|
}
|
|
453
|
+
if (info.locatorLog && count === 0) {
|
|
454
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
455
|
+
}
|
|
515
456
|
for (let j = 0; j < count; j++) {
|
|
516
457
|
let visible = await locator.nth(j).isVisible();
|
|
517
458
|
const enabled = await locator.nth(j).isEnabled();
|
|
@@ -520,24 +461,40 @@ class StableBrowser {
|
|
|
520
461
|
}
|
|
521
462
|
if (visible && enabled) {
|
|
522
463
|
foundLocators.push(locator.nth(j));
|
|
464
|
+
if (info.locatorLog) {
|
|
465
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
466
|
+
}
|
|
523
467
|
}
|
|
524
468
|
else {
|
|
469
|
+
info.failCause.visible = visible;
|
|
470
|
+
info.failCause.enabled = enabled;
|
|
525
471
|
if (!info.printMessages) {
|
|
526
472
|
info.printMessages = {};
|
|
527
473
|
}
|
|
474
|
+
if (info.locatorLog && !visible) {
|
|
475
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
476
|
+
}
|
|
477
|
+
if (info.locatorLog && !enabled) {
|
|
478
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
479
|
+
}
|
|
528
480
|
if (!info.printMessages[j.toString()]) {
|
|
529
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
481
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
530
482
|
info.printMessages[j.toString()] = true;
|
|
531
483
|
}
|
|
532
484
|
}
|
|
533
485
|
}
|
|
534
486
|
}
|
|
535
487
|
async closeUnexpectedPopups(info, _params) {
|
|
488
|
+
if (!info) {
|
|
489
|
+
info = {};
|
|
490
|
+
info.failCause = {};
|
|
491
|
+
info.log = "";
|
|
492
|
+
}
|
|
536
493
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
537
494
|
if (!info) {
|
|
538
495
|
info = {};
|
|
539
496
|
}
|
|
540
|
-
info.log += "scan for popup handlers" + "\n";
|
|
497
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
541
498
|
const handlerGroup = [];
|
|
542
499
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
543
500
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
@@ -564,9 +521,21 @@ class StableBrowser {
|
|
|
564
521
|
}
|
|
565
522
|
if (result.foundElements.length > 0) {
|
|
566
523
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
524
|
+
try {
|
|
525
|
+
await scope?.evaluate(() => {
|
|
526
|
+
window.__isClosingPopups = true;
|
|
527
|
+
});
|
|
528
|
+
await dialogCloseLocator.click();
|
|
529
|
+
// wait for the dialog to close
|
|
530
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
await scope?.evaluate(() => {
|
|
536
|
+
window.__isClosingPopups = false;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
570
539
|
return { rerun: true };
|
|
571
540
|
}
|
|
572
541
|
}
|
|
@@ -590,7 +559,13 @@ class StableBrowser {
|
|
|
590
559
|
}
|
|
591
560
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
592
561
|
}
|
|
593
|
-
async _findFrameScope(selectors, timeout = 30000) {
|
|
562
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
563
|
+
if (!info) {
|
|
564
|
+
info = {};
|
|
565
|
+
info.failCause = {};
|
|
566
|
+
info.log = "";
|
|
567
|
+
}
|
|
568
|
+
let startTime = Date.now();
|
|
594
569
|
let scope = this.page;
|
|
595
570
|
if (selectors.frame) {
|
|
596
571
|
return selectors.frame;
|
|
@@ -621,9 +596,11 @@ class StableBrowser {
|
|
|
621
596
|
}
|
|
622
597
|
return framescope;
|
|
623
598
|
};
|
|
599
|
+
let fLocator = null;
|
|
624
600
|
while (true) {
|
|
625
601
|
let frameFound = false;
|
|
626
602
|
if (selectors.nestFrmLoc) {
|
|
603
|
+
fLocator = selectors.nestFrmLoc;
|
|
627
604
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
628
605
|
frameFound = true;
|
|
629
606
|
break;
|
|
@@ -632,6 +609,7 @@ class StableBrowser {
|
|
|
632
609
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
633
610
|
let frameLocator = selectors.frameLocators[i];
|
|
634
611
|
if (frameLocator.css) {
|
|
612
|
+
fLocator = frameLocator.css;
|
|
635
613
|
scope = scope.frameLocator(frameLocator.css);
|
|
636
614
|
frameFound = true;
|
|
637
615
|
break;
|
|
@@ -639,16 +617,25 @@ class StableBrowser {
|
|
|
639
617
|
}
|
|
640
618
|
}
|
|
641
619
|
if (!frameFound && selectors.iframe_src) {
|
|
620
|
+
fLocator = selectors.iframe_src;
|
|
642
621
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
643
622
|
}
|
|
644
623
|
if (!scope) {
|
|
645
|
-
info
|
|
646
|
-
|
|
624
|
+
if (info && info.locatorLog) {
|
|
625
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
626
|
+
}
|
|
627
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
628
|
+
if (Date.now() - startTime > timeout) {
|
|
629
|
+
info.failCause.iframeNotFound = true;
|
|
630
|
+
info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
|
|
647
631
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
648
632
|
}
|
|
649
633
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
650
634
|
}
|
|
651
635
|
else {
|
|
636
|
+
if (info && info.locatorLog) {
|
|
637
|
+
info.locatorLog.setLocatorSearchStatus("frame-" + fLocator, "FOUND");
|
|
638
|
+
}
|
|
652
639
|
break;
|
|
653
640
|
}
|
|
654
641
|
}
|
|
@@ -658,20 +645,27 @@ class StableBrowser {
|
|
|
658
645
|
}
|
|
659
646
|
return scope;
|
|
660
647
|
}
|
|
661
|
-
async _getDocumentBody(selectors, timeout = 30000) {
|
|
662
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
648
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
649
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
663
650
|
return scope.evaluate(() => {
|
|
664
651
|
var bodyContent = document.body.innerHTML;
|
|
665
652
|
return bodyContent;
|
|
666
653
|
});
|
|
667
654
|
}
|
|
668
655
|
async _locate_internal(selectors, info, _params, timeout = 30000) {
|
|
656
|
+
if (!info) {
|
|
657
|
+
info = {};
|
|
658
|
+
info.failCause = {};
|
|
659
|
+
info.log = "";
|
|
660
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
661
|
+
}
|
|
669
662
|
let highPriorityTimeout = 5000;
|
|
670
663
|
let visibleOnlyTimeout = 6000;
|
|
671
|
-
let startTime =
|
|
664
|
+
let startTime = Date.now();
|
|
672
665
|
let locatorsCount = 0;
|
|
666
|
+
let lazy_scroll = false;
|
|
673
667
|
//let arrayMode = Array.isArray(selectors);
|
|
674
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
668
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
675
669
|
let selectorsLocators = null;
|
|
676
670
|
selectorsLocators = selectors.locators;
|
|
677
671
|
// group selectors by priority
|
|
@@ -758,21 +752,33 @@ class StableBrowser {
|
|
|
758
752
|
return maxCountElement.locator;
|
|
759
753
|
}
|
|
760
754
|
}
|
|
761
|
-
if (
|
|
755
|
+
if (Date.now() - startTime > timeout) {
|
|
762
756
|
break;
|
|
763
757
|
}
|
|
764
|
-
if (
|
|
758
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
765
759
|
info.log += "high priority timeout, will try all elements" + "\n";
|
|
766
760
|
highPriorityOnly = false;
|
|
761
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
762
|
+
lazy_scroll = true;
|
|
763
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
764
|
+
}
|
|
767
765
|
}
|
|
768
|
-
if (
|
|
766
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
769
767
|
info.log += "visible only timeout, will try all elements" + "\n";
|
|
770
768
|
visibleOnly = false;
|
|
771
769
|
}
|
|
772
770
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
773
771
|
}
|
|
774
772
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
775
|
-
info.
|
|
773
|
+
// if (info.locatorLog) {
|
|
774
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
775
|
+
// for (let line of lines) {
|
|
776
|
+
// this.logger.debug(line);
|
|
777
|
+
// }
|
|
778
|
+
// }
|
|
779
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
780
|
+
info.failCause.locatorNotFound = true;
|
|
781
|
+
info.failCause.lastError = "failed to locate unique element";
|
|
776
782
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
777
783
|
}
|
|
778
784
|
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
@@ -786,8 +792,9 @@ class StableBrowser {
|
|
|
786
792
|
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
787
793
|
}
|
|
788
794
|
catch (e) {
|
|
789
|
-
this
|
|
790
|
-
this.logger.debug(
|
|
795
|
+
// this call can fail it the browser is navigating
|
|
796
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
797
|
+
// this.logger.debug(e);
|
|
791
798
|
foundLocators = [];
|
|
792
799
|
try {
|
|
793
800
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
@@ -804,10 +811,29 @@ class StableBrowser {
|
|
|
804
811
|
});
|
|
805
812
|
result.locatorIndex = i;
|
|
806
813
|
}
|
|
814
|
+
if (foundLocators.length > 1) {
|
|
815
|
+
info.failCause.foundMultiple = true;
|
|
816
|
+
if (info.locatorLog) {
|
|
817
|
+
info.locatorLog.setLocatorSearchStatus(locatorsGroup[i], "FOUND_NOT_UNIQUE");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
807
820
|
}
|
|
808
821
|
return result;
|
|
809
822
|
}
|
|
810
823
|
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
824
|
+
const state = {
|
|
825
|
+
locate: false,
|
|
826
|
+
scroll: false,
|
|
827
|
+
highlight: false,
|
|
828
|
+
_params,
|
|
829
|
+
options,
|
|
830
|
+
world,
|
|
831
|
+
type: Types.CLICK,
|
|
832
|
+
text: "Click element",
|
|
833
|
+
operation: "simpleClick",
|
|
834
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
835
|
+
};
|
|
836
|
+
_preCommand(state, this);
|
|
811
837
|
const startTime = Date.now();
|
|
812
838
|
let timeout = 30000;
|
|
813
839
|
if (options && options.timeout) {
|
|
@@ -831,13 +857,32 @@ class StableBrowser {
|
|
|
831
857
|
}
|
|
832
858
|
catch (e) {
|
|
833
859
|
if (performance.now() - startTime > timeout) {
|
|
834
|
-
throw e;
|
|
860
|
+
// throw e;
|
|
861
|
+
try {
|
|
862
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
863
|
+
}
|
|
864
|
+
finally {
|
|
865
|
+
_commandFinally(state, this);
|
|
866
|
+
}
|
|
835
867
|
}
|
|
836
868
|
}
|
|
837
869
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
838
870
|
}
|
|
839
871
|
}
|
|
840
872
|
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
873
|
+
const state = {
|
|
874
|
+
locate: false,
|
|
875
|
+
scroll: false,
|
|
876
|
+
highlight: false,
|
|
877
|
+
_params,
|
|
878
|
+
options,
|
|
879
|
+
world,
|
|
880
|
+
type: Types.FILL,
|
|
881
|
+
text: "Fill element",
|
|
882
|
+
operation: "simpleClickType",
|
|
883
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
884
|
+
};
|
|
885
|
+
_preCommand(state, this);
|
|
841
886
|
const startTime = Date.now();
|
|
842
887
|
let timeout = 30000;
|
|
843
888
|
if (options && options.timeout) {
|
|
@@ -861,7 +906,13 @@ class StableBrowser {
|
|
|
861
906
|
}
|
|
862
907
|
catch (e) {
|
|
863
908
|
if (performance.now() - startTime > timeout) {
|
|
864
|
-
throw e;
|
|
909
|
+
// throw e;
|
|
910
|
+
try {
|
|
911
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
912
|
+
}
|
|
913
|
+
finally {
|
|
914
|
+
_commandFinally(state, this);
|
|
915
|
+
}
|
|
865
916
|
}
|
|
866
917
|
}
|
|
867
918
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -880,18 +931,18 @@ class StableBrowser {
|
|
|
880
931
|
};
|
|
881
932
|
try {
|
|
882
933
|
await _preCommand(state, this);
|
|
883
|
-
if (state.options && state.options.context) {
|
|
884
|
-
|
|
885
|
-
}
|
|
934
|
+
// if (state.options && state.options.context) {
|
|
935
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
936
|
+
// }
|
|
886
937
|
try {
|
|
887
938
|
await state.element.click();
|
|
888
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
939
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
889
940
|
}
|
|
890
941
|
catch (e) {
|
|
891
942
|
// await this.closeUnexpectedPopups();
|
|
892
943
|
state.element = await this._locate(selectors, state.info, _params);
|
|
893
944
|
await state.element.dispatchEvent("click");
|
|
894
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
945
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
895
946
|
}
|
|
896
947
|
await this.waitForPageLoad();
|
|
897
948
|
return state.info;
|
|
@@ -970,7 +1021,6 @@ class StableBrowser {
|
|
|
970
1021
|
await state.element.hover({ timeout: 10000 });
|
|
971
1022
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
972
1023
|
}
|
|
973
|
-
await _screenshot(state, this);
|
|
974
1024
|
await this.waitForPageLoad();
|
|
975
1025
|
return state.info;
|
|
976
1026
|
}
|
|
@@ -1098,33 +1148,30 @@ class StableBrowser {
|
|
|
1098
1148
|
}
|
|
1099
1149
|
}
|
|
1100
1150
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1151
|
+
const state = {
|
|
1152
|
+
selectors,
|
|
1153
|
+
_params,
|
|
1154
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1155
|
+
options,
|
|
1156
|
+
world,
|
|
1157
|
+
type: Types.SET_DATE_TIME,
|
|
1158
|
+
text: `Set date time value: ${value}`,
|
|
1159
|
+
operation: "setDateTime",
|
|
1160
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1161
|
+
throwError: false,
|
|
1162
|
+
};
|
|
1111
1163
|
try {
|
|
1112
|
-
|
|
1113
|
-
let element = await this._locate(selectors, info, _params);
|
|
1114
|
-
//insert red border around the element
|
|
1115
|
-
await this.scrollIfNeeded(element, info);
|
|
1116
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1117
|
-
await this._highlightElements(element);
|
|
1164
|
+
await _preCommand(state, this);
|
|
1118
1165
|
try {
|
|
1119
|
-
await element.click();
|
|
1166
|
+
await state.element.click();
|
|
1120
1167
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1121
1168
|
if (format) {
|
|
1122
|
-
value = dayjs(value).format(format);
|
|
1123
|
-
await element.fill(value);
|
|
1169
|
+
state.value = dayjs(state.value).format(format);
|
|
1170
|
+
await state.element.fill(state.value);
|
|
1124
1171
|
}
|
|
1125
1172
|
else {
|
|
1126
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1127
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1173
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1174
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1128
1175
|
el.value = ""; // clear input
|
|
1129
1176
|
el.value = dateTimeValue;
|
|
1130
1177
|
}, dateTimeValue);
|
|
@@ -1137,20 +1184,19 @@ class StableBrowser {
|
|
|
1137
1184
|
}
|
|
1138
1185
|
catch (err) {
|
|
1139
1186
|
//await this.closeUnexpectedPopups();
|
|
1140
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1187
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1141
1188
|
this.logger.info("Trying again");
|
|
1142
|
-
|
|
1143
|
-
info.
|
|
1144
|
-
Object.assign(err, { info: info });
|
|
1189
|
+
await _screenshot(state, this);
|
|
1190
|
+
Object.assign(err, { info: state.info });
|
|
1145
1191
|
await element.click();
|
|
1146
1192
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1147
1193
|
if (format) {
|
|
1148
|
-
value = dayjs(value).format(format);
|
|
1149
|
-
await element.fill(value);
|
|
1194
|
+
state.value = dayjs(state.value).format(format);
|
|
1195
|
+
await state.element.fill(state.value);
|
|
1150
1196
|
}
|
|
1151
1197
|
else {
|
|
1152
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1153
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1198
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1199
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1154
1200
|
el.value = ""; // clear input
|
|
1155
1201
|
el.value = dateTimeValue;
|
|
1156
1202
|
}, dateTimeValue);
|
|
@@ -1163,31 +1209,10 @@ class StableBrowser {
|
|
|
1163
1209
|
}
|
|
1164
1210
|
}
|
|
1165
1211
|
catch (e) {
|
|
1166
|
-
|
|
1167
|
-
throw e;
|
|
1212
|
+
await _commandError(state, e, this);
|
|
1168
1213
|
}
|
|
1169
1214
|
finally {
|
|
1170
|
-
|
|
1171
|
-
this._reportToWorld(world, {
|
|
1172
|
-
element_name: selectors.element_name,
|
|
1173
|
-
type: Types.SET_DATE_TIME,
|
|
1174
|
-
screenshotId,
|
|
1175
|
-
value: value,
|
|
1176
|
-
text: `setDateTime input with value: ${value}`,
|
|
1177
|
-
result: error
|
|
1178
|
-
? {
|
|
1179
|
-
status: "FAILED",
|
|
1180
|
-
startTime,
|
|
1181
|
-
endTime,
|
|
1182
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1183
|
-
}
|
|
1184
|
-
: {
|
|
1185
|
-
status: "PASSED",
|
|
1186
|
-
startTime,
|
|
1187
|
-
endTime,
|
|
1188
|
-
},
|
|
1189
|
-
info: info,
|
|
1190
|
-
});
|
|
1215
|
+
_commandFinally(state, this);
|
|
1191
1216
|
}
|
|
1192
1217
|
}
|
|
1193
1218
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1324,6 +1349,7 @@ class StableBrowser {
|
|
|
1324
1349
|
let screenshotPath = null;
|
|
1325
1350
|
if (!info.log) {
|
|
1326
1351
|
info.log = "";
|
|
1352
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1327
1353
|
}
|
|
1328
1354
|
info.operation = "getText";
|
|
1329
1355
|
info.selectors = selectors;
|
|
@@ -1662,11 +1688,9 @@ class StableBrowser {
|
|
|
1662
1688
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1663
1689
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1664
1690
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
}
|
|
1669
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
1691
|
+
// to make sure the path doesn't start with -
|
|
1692
|
+
const uuidStr = "id_" + randomUUID();
|
|
1693
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1670
1694
|
try {
|
|
1671
1695
|
await this.takeScreenshot(screenshotPath);
|
|
1672
1696
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1680,7 +1704,7 @@ class StableBrowser {
|
|
|
1680
1704
|
catch (e) {
|
|
1681
1705
|
this.logger.info("unable to take screenshot, ignored");
|
|
1682
1706
|
}
|
|
1683
|
-
result.screenshotId =
|
|
1707
|
+
result.screenshotId = uuidStr;
|
|
1684
1708
|
result.screenshotPath = screenshotPath;
|
|
1685
1709
|
if (info && info.box) {
|
|
1686
1710
|
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
@@ -1778,74 +1802,105 @@ class StableBrowser {
|
|
|
1778
1802
|
}
|
|
1779
1803
|
}
|
|
1780
1804
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1805
|
+
const state = {
|
|
1806
|
+
selectors,
|
|
1807
|
+
_params,
|
|
1808
|
+
attribute,
|
|
1809
|
+
variable,
|
|
1810
|
+
options,
|
|
1811
|
+
world,
|
|
1812
|
+
type: Types.EXTRACT,
|
|
1813
|
+
text: `Extract attribute from element`,
|
|
1814
|
+
operation: "extractAttribute",
|
|
1815
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1816
|
+
};
|
|
1786
1817
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1787
|
-
const info = {};
|
|
1788
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1789
|
-
info.operation = "extract";
|
|
1790
|
-
info.selectors = selectors;
|
|
1791
1818
|
try {
|
|
1792
|
-
|
|
1793
|
-
await this._highlightElements(element);
|
|
1794
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1819
|
+
await _preCommand(state, this);
|
|
1795
1820
|
switch (attribute) {
|
|
1796
1821
|
case "inner_text":
|
|
1797
|
-
|
|
1822
|
+
state.value = await state.element.innerText();
|
|
1798
1823
|
break;
|
|
1799
1824
|
case "href":
|
|
1800
|
-
|
|
1825
|
+
state.value = await state.element.getAttribute("href");
|
|
1801
1826
|
break;
|
|
1802
1827
|
case "value":
|
|
1803
|
-
|
|
1828
|
+
state.value = await state.element.inputValue();
|
|
1804
1829
|
break;
|
|
1805
1830
|
default:
|
|
1806
|
-
|
|
1831
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1807
1832
|
break;
|
|
1808
1833
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1834
|
+
state.info.value = state.value;
|
|
1835
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
1836
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
1837
|
+
return state.info;
|
|
1838
|
+
}
|
|
1839
|
+
catch (e) {
|
|
1840
|
+
await _commandError(state, e, this);
|
|
1841
|
+
}
|
|
1842
|
+
finally {
|
|
1843
|
+
_commandFinally(state, this);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
1847
|
+
const state = {
|
|
1848
|
+
selectors,
|
|
1849
|
+
_params,
|
|
1850
|
+
attribute,
|
|
1851
|
+
value,
|
|
1852
|
+
options,
|
|
1853
|
+
world,
|
|
1854
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
1855
|
+
text: `Verify element attribute`,
|
|
1856
|
+
operation: "verifyAttribute",
|
|
1857
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1858
|
+
};
|
|
1859
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1860
|
+
let val;
|
|
1861
|
+
try {
|
|
1862
|
+
await _preCommand(state, this);
|
|
1863
|
+
switch (attribute) {
|
|
1864
|
+
case "innerText":
|
|
1865
|
+
val = String(await state.element.innerText());
|
|
1866
|
+
break;
|
|
1867
|
+
case "value":
|
|
1868
|
+
val = String(await state.element.inputValue());
|
|
1869
|
+
break;
|
|
1870
|
+
case "checked":
|
|
1871
|
+
val = String(await state.element.isChecked());
|
|
1872
|
+
break;
|
|
1873
|
+
case "disabled":
|
|
1874
|
+
val = String(await state.element.isDisabled());
|
|
1875
|
+
break;
|
|
1876
|
+
case "readOnly":
|
|
1877
|
+
const isEditable = await state.element.isEditable();
|
|
1878
|
+
val = String(!isEditable);
|
|
1879
|
+
break;
|
|
1880
|
+
default:
|
|
1881
|
+
val = String(await state.element.getAttribute(attribute));
|
|
1882
|
+
break;
|
|
1812
1883
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1884
|
+
state.info.expectedValue = val;
|
|
1885
|
+
let regex;
|
|
1886
|
+
if (value.startsWith("/") && value.endsWith("/")) {
|
|
1887
|
+
const patternBody = value.slice(1, -1);
|
|
1888
|
+
regex = new RegExp(patternBody, "g");
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1892
|
+
regex = new RegExp(escapedPattern, "g");
|
|
1893
|
+
}
|
|
1894
|
+
if (!val.match(regex)) {
|
|
1895
|
+
throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
|
|
1896
|
+
}
|
|
1897
|
+
return state.info;
|
|
1816
1898
|
}
|
|
1817
1899
|
catch (e) {
|
|
1818
|
-
|
|
1819
|
-
this.logger.error("extract failed " + info.log);
|
|
1820
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1821
|
-
info.screenshotPath = screenshotPath;
|
|
1822
|
-
Object.assign(e, { info: info });
|
|
1823
|
-
error = e;
|
|
1824
|
-
throw e;
|
|
1900
|
+
await _commandError(state, e, this);
|
|
1825
1901
|
}
|
|
1826
1902
|
finally {
|
|
1827
|
-
|
|
1828
|
-
this._reportToWorld(world, {
|
|
1829
|
-
element_name: selectors.element_name,
|
|
1830
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1831
|
-
variable: variable,
|
|
1832
|
-
value: info.value,
|
|
1833
|
-
text: "Extract attribute from element",
|
|
1834
|
-
screenshotId,
|
|
1835
|
-
result: error
|
|
1836
|
-
? {
|
|
1837
|
-
status: "FAILED",
|
|
1838
|
-
startTime,
|
|
1839
|
-
endTime,
|
|
1840
|
-
message: error?.message,
|
|
1841
|
-
}
|
|
1842
|
-
: {
|
|
1843
|
-
status: "PASSED",
|
|
1844
|
-
startTime,
|
|
1845
|
-
endTime,
|
|
1846
|
-
},
|
|
1847
|
-
info: info,
|
|
1848
|
-
});
|
|
1903
|
+
_commandFinally(state, this);
|
|
1849
1904
|
}
|
|
1850
1905
|
}
|
|
1851
1906
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -1866,7 +1921,7 @@ class StableBrowser {
|
|
|
1866
1921
|
if (options && options.timeout) {
|
|
1867
1922
|
timeout = options.timeout;
|
|
1868
1923
|
}
|
|
1869
|
-
const serviceUrl =
|
|
1924
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1870
1925
|
const request = {
|
|
1871
1926
|
method: "POST",
|
|
1872
1927
|
url: serviceUrl,
|
|
@@ -1922,7 +1977,8 @@ class StableBrowser {
|
|
|
1922
1977
|
catch (e) {
|
|
1923
1978
|
errorCount++;
|
|
1924
1979
|
if (errorCount > 3) {
|
|
1925
|
-
throw e;
|
|
1980
|
+
// throw e;
|
|
1981
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1926
1982
|
}
|
|
1927
1983
|
// ignore
|
|
1928
1984
|
}
|
|
@@ -2033,11 +2089,12 @@ class StableBrowser {
|
|
|
2033
2089
|
info.screenshotPath = screenshotPath;
|
|
2034
2090
|
Object.assign(e, { info: info });
|
|
2035
2091
|
error = e;
|
|
2036
|
-
throw e;
|
|
2092
|
+
// throw e;
|
|
2093
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2037
2094
|
}
|
|
2038
2095
|
finally {
|
|
2039
2096
|
const endTime = Date.now();
|
|
2040
|
-
|
|
2097
|
+
_reportToWorld(world, {
|
|
2041
2098
|
type: Types.VERIFY_PAGE_PATH,
|
|
2042
2099
|
text: "Verify page path",
|
|
2043
2100
|
screenshotId,
|
|
@@ -2057,54 +2114,64 @@ class StableBrowser {
|
|
|
2057
2114
|
});
|
|
2058
2115
|
}
|
|
2059
2116
|
}
|
|
2117
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2118
|
+
const frames = this.page.frames();
|
|
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], "*:not(script, style, head)", false, 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], "*:not(script, style, head)", false, true, {});
|
|
2131
|
+
result.frame = frames[i];
|
|
2132
|
+
results.push(result);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
else {
|
|
2136
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, {});
|
|
2137
|
+
result.frame = frames[i];
|
|
2138
|
+
results.push(result);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
state.info.results = results;
|
|
2142
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2143
|
+
return resultWithElementsFound;
|
|
2144
|
+
}
|
|
2060
2145
|
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2061
2146
|
text = unEscapeString(text);
|
|
2062
|
-
const
|
|
2147
|
+
const state = {
|
|
2148
|
+
text_search: text,
|
|
2149
|
+
options,
|
|
2150
|
+
world,
|
|
2151
|
+
locate: false,
|
|
2152
|
+
scroll: false,
|
|
2153
|
+
highlight: false,
|
|
2154
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2155
|
+
text: `Verify text exists in page`,
|
|
2156
|
+
operation: "verifyTextExistInPage",
|
|
2157
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
2158
|
+
};
|
|
2063
2159
|
const timeout = this._getLoadTimeout(options);
|
|
2064
|
-
let error = null;
|
|
2065
|
-
let screenshotId = null;
|
|
2066
|
-
let screenshotPath = null;
|
|
2067
2160
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2068
|
-
const info = {};
|
|
2069
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2070
|
-
info.operation = "verifyTextExistInPage";
|
|
2071
2161
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2072
2162
|
if (newValue !== text) {
|
|
2073
2163
|
this.logger.info(text + "=" + newValue);
|
|
2074
2164
|
text = newValue;
|
|
2075
2165
|
}
|
|
2076
|
-
info.text = text;
|
|
2077
2166
|
let dateAlternatives = findDateAlternatives(text);
|
|
2078
2167
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2079
2168
|
try {
|
|
2169
|
+
await _preCommand(state, this);
|
|
2170
|
+
state.info.text = text;
|
|
2080
2171
|
while (true) {
|
|
2081
|
-
const
|
|
2082
|
-
let results = [];
|
|
2083
|
-
for (let i = 0; i < frames.length; i++) {
|
|
2084
|
-
if (dateAlternatives.date) {
|
|
2085
|
-
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2086
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
|
|
2087
|
-
result.frame = frames[i];
|
|
2088
|
-
results.push(result);
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
else if (numberAlternatives.number) {
|
|
2092
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2093
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
|
|
2094
|
-
result.frame = frames[i];
|
|
2095
|
-
results.push(result);
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
else {
|
|
2099
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
|
|
2100
|
-
result.frame = frames[i];
|
|
2101
|
-
results.push(result);
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
info.results = results;
|
|
2105
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2172
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2106
2173
|
if (resultWithElementsFound.length === 0) {
|
|
2107
|
-
if (Date.now() - startTime > timeout) {
|
|
2174
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2108
2175
|
throw new Error(`Text ${text} not found in page`);
|
|
2109
2176
|
}
|
|
2110
2177
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -2116,55 +2183,163 @@ class StableBrowser {
|
|
|
2116
2183
|
await this._highlightElements(frame, dataAttribute);
|
|
2117
2184
|
const element = await frame.$(dataAttribute);
|
|
2118
2185
|
if (element) {
|
|
2119
|
-
await this.scrollIfNeeded(element, info);
|
|
2186
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2120
2187
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2121
2188
|
}
|
|
2122
2189
|
}
|
|
2123
|
-
|
|
2124
|
-
return info;
|
|
2190
|
+
await _screenshot(state, this);
|
|
2191
|
+
return state.info;
|
|
2125
2192
|
}
|
|
2126
2193
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2127
2194
|
}
|
|
2128
2195
|
catch (e) {
|
|
2129
|
-
|
|
2130
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2131
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2132
|
-
info.screenshotPath = screenshotPath;
|
|
2133
|
-
Object.assign(e, { info: info });
|
|
2134
|
-
error = e;
|
|
2135
|
-
throw e;
|
|
2196
|
+
await _commandError(state, e, this);
|
|
2136
2197
|
}
|
|
2137
2198
|
finally {
|
|
2138
|
-
|
|
2139
|
-
this._reportToWorld(world, {
|
|
2140
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2141
|
-
text: "Verify text exists in page",
|
|
2142
|
-
screenshotId,
|
|
2143
|
-
result: error
|
|
2144
|
-
? {
|
|
2145
|
-
status: "FAILED",
|
|
2146
|
-
startTime,
|
|
2147
|
-
endTime,
|
|
2148
|
-
message: error?.message,
|
|
2149
|
-
}
|
|
2150
|
-
: {
|
|
2151
|
-
status: "PASSED",
|
|
2152
|
-
startTime,
|
|
2153
|
-
endTime,
|
|
2154
|
-
},
|
|
2155
|
-
info: info,
|
|
2156
|
-
});
|
|
2199
|
+
_commandFinally(state, this);
|
|
2157
2200
|
}
|
|
2158
2201
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2202
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2203
|
+
text = unEscapeString(text);
|
|
2204
|
+
const state = {
|
|
2205
|
+
text_search: text,
|
|
2206
|
+
options,
|
|
2207
|
+
world,
|
|
2208
|
+
locate: false,
|
|
2209
|
+
scroll: false,
|
|
2210
|
+
highlight: false,
|
|
2211
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2212
|
+
text: `Verify text does not exist in page`,
|
|
2213
|
+
operation: "verifyTextNotExistInPage",
|
|
2214
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2215
|
+
};
|
|
2216
|
+
const timeout = this._getLoadTimeout(options);
|
|
2217
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2218
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2219
|
+
if (newValue !== text) {
|
|
2220
|
+
this.logger.info(text + "=" + newValue);
|
|
2221
|
+
text = newValue;
|
|
2222
|
+
}
|
|
2223
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2224
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2225
|
+
try {
|
|
2226
|
+
await _preCommand(state, this);
|
|
2227
|
+
state.info.text = text;
|
|
2228
|
+
while (true) {
|
|
2229
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2230
|
+
if (resultWithElementsFound.length === 0) {
|
|
2231
|
+
await _screenshot(state, this);
|
|
2232
|
+
return state.info;
|
|
2233
|
+
}
|
|
2234
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2235
|
+
throw new Error(`Text ${text} found in page`);
|
|
2236
|
+
}
|
|
2237
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
catch (e) {
|
|
2241
|
+
await _commandError(state, e, this);
|
|
2242
|
+
}
|
|
2243
|
+
finally {
|
|
2244
|
+
_commandFinally(state, this);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
2248
|
+
textAnchor = unEscapeString(textAnchor);
|
|
2249
|
+
textToVerify = unEscapeString(textToVerify);
|
|
2250
|
+
const state = {
|
|
2251
|
+
text_search: textToVerify,
|
|
2252
|
+
options,
|
|
2253
|
+
world,
|
|
2254
|
+
locate: false,
|
|
2255
|
+
scroll: false,
|
|
2256
|
+
highlight: false,
|
|
2257
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2258
|
+
text: `Verify text with relation to another text`,
|
|
2259
|
+
operation: "verify_text_with_relation",
|
|
2260
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2261
|
+
};
|
|
2262
|
+
const timeout = this._getLoadTimeout(options);
|
|
2263
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2264
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2265
|
+
if (newValue !== textAnchor) {
|
|
2266
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
2267
|
+
textAnchor = newValue;
|
|
2268
|
+
}
|
|
2269
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
2270
|
+
if (newValue !== textToVerify) {
|
|
2271
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
2272
|
+
textToVerify = newValue;
|
|
2273
|
+
}
|
|
2274
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
2275
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
2276
|
+
let foundAncore = false;
|
|
2277
|
+
try {
|
|
2278
|
+
await _preCommand(state, this);
|
|
2279
|
+
state.info.text = textToVerify;
|
|
2280
|
+
while (true) {
|
|
2281
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
|
|
2282
|
+
if (resultWithElementsFound.length === 0) {
|
|
2283
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2284
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2285
|
+
}
|
|
2286
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2290
|
+
foundAncore = true;
|
|
2291
|
+
const result = resultWithElementsFound[i];
|
|
2292
|
+
const token = result.randomToken;
|
|
2293
|
+
const frame = result.frame;
|
|
2294
|
+
const css = `[data-blinq-id="blinq-id-${token}"]`;
|
|
2295
|
+
const findResult = await frame.evaluate(([css, climb, textToVerify, token]) => {
|
|
2296
|
+
const elements = Array.from(document.querySelectorAll(css));
|
|
2297
|
+
for (let i = 0; i < elements.length; i++) {
|
|
2298
|
+
const element = elements[i];
|
|
2299
|
+
let climbParent = element;
|
|
2300
|
+
for (let j = 0; j < climb; j++) {
|
|
2301
|
+
climbParent = climbParent.parentElement;
|
|
2302
|
+
if (!climbParent) {
|
|
2303
|
+
break;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (!climbParent) {
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
|
|
2310
|
+
if (foundElements.length > 0) {
|
|
2311
|
+
// set the container element attribute
|
|
2312
|
+
element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
|
|
2313
|
+
climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
|
|
2314
|
+
foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
|
|
2315
|
+
return { found: true };
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return { found: false };
|
|
2319
|
+
}, [css, climb, textToVerify, result.randomToken]);
|
|
2320
|
+
if (findResult.found === true) {
|
|
2321
|
+
const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
|
|
2322
|
+
const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2323
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2324
|
+
await this._highlightElements(frame, cssAnchor);
|
|
2325
|
+
const element = await frame.$(dataAttribute);
|
|
2326
|
+
if (element) {
|
|
2327
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2328
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2329
|
+
}
|
|
2330
|
+
await _screenshot(state, this);
|
|
2331
|
+
return state.info;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2336
|
+
}
|
|
2337
|
+
catch (e) {
|
|
2338
|
+
await _commandError(state, e, this);
|
|
2163
2339
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2340
|
+
finally {
|
|
2341
|
+
_commandFinally(state, this);
|
|
2166
2342
|
}
|
|
2167
|
-
return serviceUrl;
|
|
2168
2343
|
}
|
|
2169
2344
|
async visualVerification(text, options = {}, world = null) {
|
|
2170
2345
|
const startTime = Date.now();
|
|
@@ -2180,7 +2355,7 @@ class StableBrowser {
|
|
|
2180
2355
|
throw new Error("TOKEN is not set");
|
|
2181
2356
|
}
|
|
2182
2357
|
try {
|
|
2183
|
-
let serviceUrl =
|
|
2358
|
+
let serviceUrl = _getServerUrl();
|
|
2184
2359
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2185
2360
|
info.screenshotPath = screenshotPath;
|
|
2186
2361
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2216,11 +2391,12 @@ class StableBrowser {
|
|
|
2216
2391
|
info.screenshotPath = screenshotPath;
|
|
2217
2392
|
Object.assign(e, { info: info });
|
|
2218
2393
|
error = e;
|
|
2219
|
-
throw e;
|
|
2394
|
+
// throw e;
|
|
2395
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2220
2396
|
}
|
|
2221
2397
|
finally {
|
|
2222
2398
|
const endTime = Date.now();
|
|
2223
|
-
|
|
2399
|
+
_reportToWorld(world, {
|
|
2224
2400
|
type: Types.VERIFY_VISUAL,
|
|
2225
2401
|
text: "Visual verification",
|
|
2226
2402
|
screenshotId,
|
|
@@ -2268,6 +2444,7 @@ class StableBrowser {
|
|
|
2268
2444
|
let screenshotPath = null;
|
|
2269
2445
|
const info = {};
|
|
2270
2446
|
info.log = "";
|
|
2447
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2271
2448
|
info.operation = "getTableData";
|
|
2272
2449
|
info.selectors = selectors;
|
|
2273
2450
|
try {
|
|
@@ -2283,11 +2460,12 @@ class StableBrowser {
|
|
|
2283
2460
|
info.screenshotPath = screenshotPath;
|
|
2284
2461
|
Object.assign(e, { info: info });
|
|
2285
2462
|
error = e;
|
|
2286
|
-
throw e;
|
|
2463
|
+
// throw e;
|
|
2464
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2287
2465
|
}
|
|
2288
2466
|
finally {
|
|
2289
2467
|
const endTime = Date.now();
|
|
2290
|
-
|
|
2468
|
+
_reportToWorld(world, {
|
|
2291
2469
|
element_name: selectors.element_name,
|
|
2292
2470
|
type: Types.GET_TABLE_DATA,
|
|
2293
2471
|
text: "Get table data",
|
|
@@ -2342,7 +2520,7 @@ class StableBrowser {
|
|
|
2342
2520
|
info.operation = "analyzeTable";
|
|
2343
2521
|
info.selectors = selectors;
|
|
2344
2522
|
info.query = query;
|
|
2345
|
-
query =
|
|
2523
|
+
query = _fixUsingParams(query, _params);
|
|
2346
2524
|
info.query_fixed = query;
|
|
2347
2525
|
info.operator = operator;
|
|
2348
2526
|
info.value = value;
|
|
@@ -2448,11 +2626,12 @@ class StableBrowser {
|
|
|
2448
2626
|
info.screenshotPath = screenshotPath;
|
|
2449
2627
|
Object.assign(e, { info: info });
|
|
2450
2628
|
error = e;
|
|
2451
|
-
throw e;
|
|
2629
|
+
// throw e;
|
|
2630
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2452
2631
|
}
|
|
2453
2632
|
finally {
|
|
2454
2633
|
const endTime = Date.now();
|
|
2455
|
-
|
|
2634
|
+
_reportToWorld(world, {
|
|
2456
2635
|
element_name: selectors.element_name,
|
|
2457
2636
|
type: Types.ANALYZE_TABLE,
|
|
2458
2637
|
text: "Analyze table",
|
|
@@ -2525,7 +2704,7 @@ class StableBrowser {
|
|
|
2525
2704
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2526
2705
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2527
2706
|
const endTime = Date.now();
|
|
2528
|
-
|
|
2707
|
+
_reportToWorld(world, {
|
|
2529
2708
|
type: Types.GET_PAGE_STATUS,
|
|
2530
2709
|
text: "Wait for page load",
|
|
2531
2710
|
screenshotId,
|
|
@@ -2545,41 +2724,35 @@ class StableBrowser {
|
|
|
2545
2724
|
}
|
|
2546
2725
|
}
|
|
2547
2726
|
async closePage(options = {}, world = null) {
|
|
2548
|
-
const
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2727
|
+
const state = {
|
|
2728
|
+
options,
|
|
2729
|
+
world,
|
|
2730
|
+
locate: false,
|
|
2731
|
+
scroll: false,
|
|
2732
|
+
highlight: false,
|
|
2733
|
+
type: Types.CLOSE_PAGE,
|
|
2734
|
+
text: `Close page`,
|
|
2735
|
+
operation: "closePage",
|
|
2736
|
+
log: "***** close page *****\n",
|
|
2737
|
+
throwError: false,
|
|
2738
|
+
};
|
|
2553
2739
|
try {
|
|
2740
|
+
await _preCommand(state, this);
|
|
2554
2741
|
await this.page.close();
|
|
2555
2742
|
}
|
|
2556
2743
|
catch (e) {
|
|
2557
2744
|
console.log(".");
|
|
2745
|
+
await _commandError(state, e, this);
|
|
2558
2746
|
}
|
|
2559
2747
|
finally {
|
|
2560
|
-
|
|
2561
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2562
|
-
const endTime = Date.now();
|
|
2563
|
-
this._reportToWorld(world, {
|
|
2564
|
-
type: Types.CLOSE_PAGE,
|
|
2565
|
-
text: "close page",
|
|
2566
|
-
screenshotId,
|
|
2567
|
-
result: error
|
|
2568
|
-
? {
|
|
2569
|
-
status: "FAILED",
|
|
2570
|
-
startTime,
|
|
2571
|
-
endTime,
|
|
2572
|
-
message: error?.message,
|
|
2573
|
-
}
|
|
2574
|
-
: {
|
|
2575
|
-
status: "PASSED",
|
|
2576
|
-
startTime,
|
|
2577
|
-
endTime,
|
|
2578
|
-
},
|
|
2579
|
-
info: info,
|
|
2580
|
-
});
|
|
2748
|
+
_commandFinally(state, this);
|
|
2581
2749
|
}
|
|
2582
2750
|
}
|
|
2751
|
+
saveTestDataAsGlobal(options, world) {
|
|
2752
|
+
const dataFile = this._getDataFile(world);
|
|
2753
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2754
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2755
|
+
}
|
|
2583
2756
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2584
2757
|
const startTime = Date.now();
|
|
2585
2758
|
let error = null;
|
|
@@ -2597,12 +2770,13 @@ class StableBrowser {
|
|
|
2597
2770
|
}
|
|
2598
2771
|
catch (e) {
|
|
2599
2772
|
console.log(".");
|
|
2773
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2600
2774
|
}
|
|
2601
2775
|
finally {
|
|
2602
2776
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2603
2777
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2604
2778
|
const endTime = Date.now();
|
|
2605
|
-
|
|
2779
|
+
_reportToWorld(world, {
|
|
2606
2780
|
type: Types.SET_VIEWPORT,
|
|
2607
2781
|
text: "set viewport size to " + width + "x" + hight,
|
|
2608
2782
|
screenshotId,
|
|
@@ -2633,12 +2807,13 @@ class StableBrowser {
|
|
|
2633
2807
|
}
|
|
2634
2808
|
catch (e) {
|
|
2635
2809
|
console.log(".");
|
|
2810
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2636
2811
|
}
|
|
2637
2812
|
finally {
|
|
2638
2813
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2639
2814
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2640
2815
|
const endTime = Date.now();
|
|
2641
|
-
|
|
2816
|
+
_reportToWorld(world, {
|
|
2642
2817
|
type: Types.GET_PAGE_STATUS,
|
|
2643
2818
|
text: "page relaod",
|
|
2644
2819
|
screenshotId,
|
|
@@ -2674,11 +2849,37 @@ class StableBrowser {
|
|
|
2674
2849
|
console.log("#-#");
|
|
2675
2850
|
}
|
|
2676
2851
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2852
|
+
async beforeStep(world, step) {
|
|
2853
|
+
this.stepName = step.pickleStep.text;
|
|
2854
|
+
this.logger.info("step: " + this.stepName);
|
|
2855
|
+
if (this.stepIndex === undefined) {
|
|
2856
|
+
this.stepIndex = 0;
|
|
2857
|
+
}
|
|
2858
|
+
else {
|
|
2859
|
+
this.stepIndex++;
|
|
2860
|
+
}
|
|
2861
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2862
|
+
if (this.context.browserObject.context) {
|
|
2863
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
2867
|
+
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
2868
|
+
// check if @global_test_data tag is present
|
|
2869
|
+
if (this.tags.includes("@global_test_data")) {
|
|
2870
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
async afterStep(world, step) {
|
|
2875
|
+
this.stepName = null;
|
|
2876
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2877
|
+
if (this.context.browserObject.context) {
|
|
2878
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
2879
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2680
2882
|
}
|
|
2681
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2682
2883
|
}
|
|
2683
2884
|
}
|
|
2684
2885
|
function createTimedPromise(promise, label) {
|
|
@@ -2686,156 +2887,5 @@ function createTimedPromise(promise, label) {
|
|
|
2686
2887
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2687
2888
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2688
2889
|
}
|
|
2689
|
-
const KEYBOARD_EVENTS = [
|
|
2690
|
-
"ALT",
|
|
2691
|
-
"AltGraph",
|
|
2692
|
-
"CapsLock",
|
|
2693
|
-
"Control",
|
|
2694
|
-
"Fn",
|
|
2695
|
-
"FnLock",
|
|
2696
|
-
"Hyper",
|
|
2697
|
-
"Meta",
|
|
2698
|
-
"NumLock",
|
|
2699
|
-
"ScrollLock",
|
|
2700
|
-
"Shift",
|
|
2701
|
-
"Super",
|
|
2702
|
-
"Symbol",
|
|
2703
|
-
"SymbolLock",
|
|
2704
|
-
"Enter",
|
|
2705
|
-
"Tab",
|
|
2706
|
-
"ArrowDown",
|
|
2707
|
-
"ArrowLeft",
|
|
2708
|
-
"ArrowRight",
|
|
2709
|
-
"ArrowUp",
|
|
2710
|
-
"End",
|
|
2711
|
-
"Home",
|
|
2712
|
-
"PageDown",
|
|
2713
|
-
"PageUp",
|
|
2714
|
-
"Backspace",
|
|
2715
|
-
"Clear",
|
|
2716
|
-
"Copy",
|
|
2717
|
-
"CrSel",
|
|
2718
|
-
"Cut",
|
|
2719
|
-
"Delete",
|
|
2720
|
-
"EraseEof",
|
|
2721
|
-
"ExSel",
|
|
2722
|
-
"Insert",
|
|
2723
|
-
"Paste",
|
|
2724
|
-
"Redo",
|
|
2725
|
-
"Undo",
|
|
2726
|
-
"Accept",
|
|
2727
|
-
"Again",
|
|
2728
|
-
"Attn",
|
|
2729
|
-
"Cancel",
|
|
2730
|
-
"ContextMenu",
|
|
2731
|
-
"Escape",
|
|
2732
|
-
"Execute",
|
|
2733
|
-
"Find",
|
|
2734
|
-
"Finish",
|
|
2735
|
-
"Help",
|
|
2736
|
-
"Pause",
|
|
2737
|
-
"Play",
|
|
2738
|
-
"Props",
|
|
2739
|
-
"Select",
|
|
2740
|
-
"ZoomIn",
|
|
2741
|
-
"ZoomOut",
|
|
2742
|
-
"BrightnessDown",
|
|
2743
|
-
"BrightnessUp",
|
|
2744
|
-
"Eject",
|
|
2745
|
-
"LogOff",
|
|
2746
|
-
"Power",
|
|
2747
|
-
"PowerOff",
|
|
2748
|
-
"PrintScreen",
|
|
2749
|
-
"Hibernate",
|
|
2750
|
-
"Standby",
|
|
2751
|
-
"WakeUp",
|
|
2752
|
-
"AllCandidates",
|
|
2753
|
-
"Alphanumeric",
|
|
2754
|
-
"CodeInput",
|
|
2755
|
-
"Compose",
|
|
2756
|
-
"Convert",
|
|
2757
|
-
"Dead",
|
|
2758
|
-
"FinalMode",
|
|
2759
|
-
"GroupFirst",
|
|
2760
|
-
"GroupLast",
|
|
2761
|
-
"GroupNext",
|
|
2762
|
-
"GroupPrevious",
|
|
2763
|
-
"ModeChange",
|
|
2764
|
-
"NextCandidate",
|
|
2765
|
-
"NonConvert",
|
|
2766
|
-
"PreviousCandidate",
|
|
2767
|
-
"Process",
|
|
2768
|
-
"SingleCandidate",
|
|
2769
|
-
"HangulMode",
|
|
2770
|
-
"HanjaMode",
|
|
2771
|
-
"JunjaMode",
|
|
2772
|
-
"Eisu",
|
|
2773
|
-
"Hankaku",
|
|
2774
|
-
"Hiragana",
|
|
2775
|
-
"HiraganaKatakana",
|
|
2776
|
-
"KanaMode",
|
|
2777
|
-
"KanjiMode",
|
|
2778
|
-
"Katakana",
|
|
2779
|
-
"Romaji",
|
|
2780
|
-
"Zenkaku",
|
|
2781
|
-
"ZenkakuHanaku",
|
|
2782
|
-
"F1",
|
|
2783
|
-
"F2",
|
|
2784
|
-
"F3",
|
|
2785
|
-
"F4",
|
|
2786
|
-
"F5",
|
|
2787
|
-
"F6",
|
|
2788
|
-
"F7",
|
|
2789
|
-
"F8",
|
|
2790
|
-
"F9",
|
|
2791
|
-
"F10",
|
|
2792
|
-
"F11",
|
|
2793
|
-
"F12",
|
|
2794
|
-
"Soft1",
|
|
2795
|
-
"Soft2",
|
|
2796
|
-
"Soft3",
|
|
2797
|
-
"Soft4",
|
|
2798
|
-
"ChannelDown",
|
|
2799
|
-
"ChannelUp",
|
|
2800
|
-
"Close",
|
|
2801
|
-
"MailForward",
|
|
2802
|
-
"MailReply",
|
|
2803
|
-
"MailSend",
|
|
2804
|
-
"MediaFastForward",
|
|
2805
|
-
"MediaPause",
|
|
2806
|
-
"MediaPlay",
|
|
2807
|
-
"MediaPlayPause",
|
|
2808
|
-
"MediaRecord",
|
|
2809
|
-
"MediaRewind",
|
|
2810
|
-
"MediaStop",
|
|
2811
|
-
"MediaTrackNext",
|
|
2812
|
-
"MediaTrackPrevious",
|
|
2813
|
-
"AudioBalanceLeft",
|
|
2814
|
-
"AudioBalanceRight",
|
|
2815
|
-
"AudioBassBoostDown",
|
|
2816
|
-
"AudioBassBoostToggle",
|
|
2817
|
-
"AudioBassBoostUp",
|
|
2818
|
-
"AudioFaderFront",
|
|
2819
|
-
"AudioFaderRear",
|
|
2820
|
-
"AudioSurroundModeNext",
|
|
2821
|
-
"AudioTrebleDown",
|
|
2822
|
-
"AudioTrebleUp",
|
|
2823
|
-
"AudioVolumeDown",
|
|
2824
|
-
"AudioVolumeMute",
|
|
2825
|
-
"AudioVolumeUp",
|
|
2826
|
-
"MicrophoneToggle",
|
|
2827
|
-
"MicrophoneVolumeDown",
|
|
2828
|
-
"MicrophoneVolumeMute",
|
|
2829
|
-
"MicrophoneVolumeUp",
|
|
2830
|
-
"TV",
|
|
2831
|
-
"TV3DMode",
|
|
2832
|
-
"TVAntennaCable",
|
|
2833
|
-
"TVAudioDescription",
|
|
2834
|
-
];
|
|
2835
|
-
function unEscapeString(str) {
|
|
2836
|
-
const placeholder = "__NEWLINE__";
|
|
2837
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2838
|
-
return str;
|
|
2839
|
-
}
|
|
2840
2890
|
export { StableBrowser };
|
|
2841
2891
|
//# sourceMappingURL=stable_browser.js.map
|