automation_model 1.0.482-dev → 1.0.482-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 +39 -17
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +6 -3
- package/lib/browser_manager.js +110 -16
- package/lib/browser_manager.js.map +1 -1
- package/lib/command_common.d.ts +1 -0
- package/lib/command_common.js +59 -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 +31 -4
- 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 +3 -0
- package/lib/network.js +155 -0
- package/lib/network.js.map +1 -0
- package/lib/scripts/find_text.js +126 -0
- package/lib/stable_browser.d.ts +49 -17
- package/lib/stable_browser.js +585 -600
- 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 +14 -1
- package/lib/utils.js +302 -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
|
|
14
|
-
import { decrypt } from "./utils.js";
|
|
13
|
+
import { _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";
|
|
18
17
|
import { getContext } from "./init_browser.js";
|
|
19
18
|
import { locate_element } from "./locate_element.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
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 = {
|
|
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,23 +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);
|
|
103
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
104
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
95
105
|
}
|
|
96
106
|
registerEventListeners(context) {
|
|
97
107
|
this.registerConsoleLogListener(this.page, context);
|
|
98
|
-
this.registerRequestListener(this.page, context, this.webLogFile);
|
|
108
|
+
// this.registerRequestListener(this.page, context, this.webLogFile);
|
|
99
109
|
if (!context.pageLoading) {
|
|
100
110
|
context.pageLoading = { status: false };
|
|
101
111
|
}
|
|
102
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
|
+
}
|
|
103
118
|
context.pageLoading.status = true;
|
|
104
119
|
this.page = page;
|
|
105
120
|
context.page = page;
|
|
106
121
|
context.pages.push(page);
|
|
122
|
+
registerNetworkEvents(this.world, this, context, this.page);
|
|
123
|
+
registerDownloadEvent(this.page, this.world, context);
|
|
107
124
|
page.on("close", async () => {
|
|
108
125
|
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
109
126
|
this.context.pages.pop();
|
|
@@ -133,9 +150,9 @@ class StableBrowser {
|
|
|
133
150
|
if (this.appName === appName) {
|
|
134
151
|
return;
|
|
135
152
|
}
|
|
136
|
-
let
|
|
153
|
+
let navigate = false;
|
|
137
154
|
if (!apps[appName]) {
|
|
138
|
-
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);
|
|
139
156
|
newContextCreated = true;
|
|
140
157
|
apps[appName] = {
|
|
141
158
|
context: newContext,
|
|
@@ -144,32 +161,15 @@ class StableBrowser {
|
|
|
144
161
|
};
|
|
145
162
|
}
|
|
146
163
|
const tempContext = {};
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
_copyContext(this, tempContext);
|
|
165
|
+
_copyContext(apps[appName], this);
|
|
149
166
|
apps[this.appName] = tempContext;
|
|
150
167
|
this.appName = appName;
|
|
151
|
-
if (
|
|
152
|
-
this.registerEventListeners(this.context);
|
|
168
|
+
if (navigate) {
|
|
153
169
|
await this.goto(this.context.environment.baseUrl);
|
|
154
170
|
await this.waitForPageLoad();
|
|
155
171
|
}
|
|
156
172
|
}
|
|
157
|
-
_copyContext(from, to) {
|
|
158
|
-
to.browser = from.browser;
|
|
159
|
-
to.page = from.page;
|
|
160
|
-
to.context = from.context;
|
|
161
|
-
}
|
|
162
|
-
getWebLogFile(logFolder) {
|
|
163
|
-
if (!fs.existsSync(logFolder)) {
|
|
164
|
-
fs.mkdirSync(logFolder, { recursive: true });
|
|
165
|
-
}
|
|
166
|
-
let nextIndex = 1;
|
|
167
|
-
while (fs.existsSync(path.join(logFolder, nextIndex.toString() + ".json"))) {
|
|
168
|
-
nextIndex++;
|
|
169
|
-
}
|
|
170
|
-
const fileName = nextIndex + ".json";
|
|
171
|
-
return path.join(logFolder, fileName);
|
|
172
|
-
}
|
|
173
173
|
registerConsoleLogListener(page, context) {
|
|
174
174
|
if (!this.context.webLogger) {
|
|
175
175
|
this.context.webLogger = [];
|
|
@@ -219,7 +219,7 @@ class StableBrowser {
|
|
|
219
219
|
this.world?.attach(JSON.stringify(obj), { mediaType: "application/json+network" });
|
|
220
220
|
}
|
|
221
221
|
catch (error) {
|
|
222
|
-
console.error("Error in request listener", error);
|
|
222
|
+
// console.error("Error in request listener", error);
|
|
223
223
|
context.networkLogger.push({
|
|
224
224
|
error: "not able to listen",
|
|
225
225
|
message: error.message,
|
|
@@ -241,47 +241,8 @@ class StableBrowser {
|
|
|
241
241
|
timeout: 60000,
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
|
-
_fixUsingParams(text, _params) {
|
|
245
|
-
if (!_params || typeof text !== "string") {
|
|
246
|
-
return text;
|
|
247
|
-
}
|
|
248
|
-
for (let key in _params) {
|
|
249
|
-
let regValue = key;
|
|
250
|
-
if (key.startsWith("_")) {
|
|
251
|
-
// remove the _ prefix
|
|
252
|
-
regValue = key.substring(1);
|
|
253
|
-
}
|
|
254
|
-
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
255
|
-
}
|
|
256
|
-
return text;
|
|
257
|
-
}
|
|
258
|
-
_fixLocatorUsingParams(locator, _params) {
|
|
259
|
-
// check if not null
|
|
260
|
-
if (!locator) {
|
|
261
|
-
return locator;
|
|
262
|
-
}
|
|
263
|
-
// clone the locator
|
|
264
|
-
locator = JSON.parse(JSON.stringify(locator));
|
|
265
|
-
this.scanAndManipulate(locator, _params);
|
|
266
|
-
return locator;
|
|
267
|
-
}
|
|
268
|
-
_isObject(value) {
|
|
269
|
-
return value && typeof value === "object" && value.constructor === Object;
|
|
270
|
-
}
|
|
271
|
-
scanAndManipulate(currentObj, _params) {
|
|
272
|
-
for (const key in currentObj) {
|
|
273
|
-
if (typeof currentObj[key] === "string") {
|
|
274
|
-
// Perform string manipulation
|
|
275
|
-
currentObj[key] = this._fixUsingParams(currentObj[key], _params);
|
|
276
|
-
}
|
|
277
|
-
else if (this._isObject(currentObj[key])) {
|
|
278
|
-
// Recursively scan nested objects
|
|
279
|
-
this.scanAndManipulate(currentObj[key], _params);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
244
|
_getLocator(locator, scope, _params) {
|
|
284
|
-
locator =
|
|
245
|
+
locator = _fixLocatorUsingParams(locator, _params);
|
|
285
246
|
let locatorReturn;
|
|
286
247
|
if (locator.role) {
|
|
287
248
|
if (locator.role[1].nameReg) {
|
|
@@ -289,7 +250,7 @@ class StableBrowser {
|
|
|
289
250
|
delete locator.role[1].nameReg;
|
|
290
251
|
}
|
|
291
252
|
// if (locator.role[1].name) {
|
|
292
|
-
// locator.role[1].name =
|
|
253
|
+
// locator.role[1].name = _fixUsingParams(locator.role[1].name, _params);
|
|
293
254
|
// }
|
|
294
255
|
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
295
256
|
}
|
|
@@ -332,7 +293,7 @@ class StableBrowser {
|
|
|
332
293
|
if (css && css.locator) {
|
|
333
294
|
css = css.locator;
|
|
334
295
|
}
|
|
335
|
-
let result = await this._locateElementByText(scope,
|
|
296
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, _params);
|
|
336
297
|
if (result.elementCount === 0) {
|
|
337
298
|
return;
|
|
338
299
|
}
|
|
@@ -348,116 +309,28 @@ class StableBrowser {
|
|
|
348
309
|
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
|
|
349
310
|
//const stringifyText = JSON.stringify(text);
|
|
350
311
|
return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (currentNode === parent) {
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
currentNode = currentNode.parentNode;
|
|
358
|
-
}
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
document.isParent = isParent;
|
|
362
|
-
function getRegex(str) {
|
|
363
|
-
const match = str.match(/^\/(.*?)\/([gimuy]*)$/);
|
|
364
|
-
if (!match) {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
let [_, pattern, flags] = match;
|
|
368
|
-
return new RegExp(pattern, flags);
|
|
369
|
-
}
|
|
370
|
-
document.getRegex = getRegex;
|
|
371
|
-
function collectAllShadowDomElements(element, result = []) {
|
|
372
|
-
// Check and add the element if it has a shadow root
|
|
373
|
-
if (element.shadowRoot) {
|
|
374
|
-
result.push(element);
|
|
375
|
-
// Also search within the shadow root
|
|
376
|
-
document.collectAllShadowDomElements(element.shadowRoot, result);
|
|
377
|
-
}
|
|
378
|
-
// Iterate over child nodes
|
|
379
|
-
element.childNodes.forEach((child) => {
|
|
380
|
-
// Recursively call the function for each child node
|
|
381
|
-
document.collectAllShadowDomElements(child, result);
|
|
382
|
-
});
|
|
383
|
-
return result;
|
|
384
|
-
}
|
|
385
|
-
document.collectAllShadowDomElements = collectAllShadowDomElements;
|
|
386
|
-
if (!tag) {
|
|
387
|
-
tag = "*";
|
|
388
|
-
}
|
|
389
|
-
let regexpSearch = document.getRegex(text);
|
|
390
|
-
if (regexpSearch) {
|
|
391
|
-
regex = true;
|
|
392
|
-
}
|
|
393
|
-
let elements = Array.from(document.querySelectorAll(tag));
|
|
394
|
-
let shadowHosts = [];
|
|
395
|
-
document.collectAllShadowDomElements(document, shadowHosts);
|
|
396
|
-
for (let i = 0; i < shadowHosts.length; i++) {
|
|
397
|
-
let shadowElement = shadowHosts[i].shadowRoot;
|
|
398
|
-
if (!shadowElement) {
|
|
399
|
-
console.log("shadowElement is null, for host " + shadowHosts[i]);
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
let shadowElements = Array.from(shadowElement.querySelectorAll(tag));
|
|
403
|
-
elements = elements.concat(shadowElements);
|
|
404
|
-
}
|
|
405
|
-
let randomToken = null;
|
|
406
|
-
const foundElements = [];
|
|
312
|
+
const options = {
|
|
313
|
+
innerText: true,
|
|
314
|
+
};
|
|
407
315
|
if (regex) {
|
|
408
|
-
|
|
409
|
-
regexpSearch = new RegExp(text, "im");
|
|
410
|
-
}
|
|
411
|
-
for (let i = 0; i < elements.length; i++) {
|
|
412
|
-
const element = elements[i];
|
|
413
|
-
if ((element.innerText && regexpSearch.test(element.innerText)) ||
|
|
414
|
-
(element.value && regexpSearch.test(element.value))) {
|
|
415
|
-
foundElements.push(element);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
316
|
+
options.singleRegex = true;
|
|
418
317
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
for (let i = 0; i < elements.length; i++) {
|
|
422
|
-
const element = elements[i];
|
|
423
|
-
if (partial) {
|
|
424
|
-
if ((element.innerText && element.innerText.toLowerCase().trim().includes(text.toLowerCase())) ||
|
|
425
|
-
(element.value && element.value.toLowerCase().includes(text.toLowerCase()))) {
|
|
426
|
-
foundElements.push(element);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
if ((element.innerText && element.innerText.trim() === text) ||
|
|
431
|
-
(element.value && element.value === text)) {
|
|
432
|
-
foundElements.push(element);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
318
|
+
if (tag) {
|
|
319
|
+
options.tag = tag;
|
|
436
320
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
let element = foundElements[i];
|
|
440
|
-
let hasChild = false;
|
|
441
|
-
for (let j = 0; j < foundElements.length; j++) {
|
|
442
|
-
if (i === j) {
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
if (isParent(element, foundElements[j])) {
|
|
446
|
-
hasChild = true;
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
if (!hasChild) {
|
|
451
|
-
noChildElements.push(element);
|
|
452
|
-
}
|
|
321
|
+
if (!(partial === true)) {
|
|
322
|
+
options.exactMatch = true;
|
|
453
323
|
}
|
|
324
|
+
const elements = window.findMatchingElements(text, options);
|
|
325
|
+
let randomToken = null;
|
|
326
|
+
const foundElements = [];
|
|
454
327
|
let elementCount = 0;
|
|
455
|
-
if (
|
|
456
|
-
for (let i = 0; i <
|
|
328
|
+
if (elements.length > 0) {
|
|
329
|
+
for (let i = 0; i < elements.length; i++) {
|
|
457
330
|
if (randomToken === null) {
|
|
458
331
|
randomToken = Math.random().toString(36).substring(7);
|
|
459
332
|
}
|
|
460
|
-
let element =
|
|
333
|
+
let element = elements[i];
|
|
461
334
|
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
462
335
|
elementCount++;
|
|
463
336
|
}
|
|
@@ -466,9 +339,21 @@ class StableBrowser {
|
|
|
466
339
|
}, [text1, tag1, regex1, partial1]);
|
|
467
340
|
}
|
|
468
341
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
342
|
+
if (!info) {
|
|
343
|
+
info = {};
|
|
344
|
+
}
|
|
345
|
+
if (!info.failCause) {
|
|
346
|
+
info.failCause = {};
|
|
347
|
+
}
|
|
348
|
+
if (!info.log) {
|
|
349
|
+
info.log = "";
|
|
350
|
+
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
351
|
+
}
|
|
469
352
|
let locatorSearch = selectorHierarchy[index];
|
|
353
|
+
let originalLocatorSearch = "";
|
|
470
354
|
try {
|
|
471
|
-
|
|
355
|
+
originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
|
|
356
|
+
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
472
357
|
}
|
|
473
358
|
catch (e) {
|
|
474
359
|
console.error(e);
|
|
@@ -478,13 +363,18 @@ class StableBrowser {
|
|
|
478
363
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
479
364
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
480
365
|
if (!locatorString) {
|
|
366
|
+
info.failCause.textNotFound = true;
|
|
367
|
+
info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
|
|
481
368
|
return;
|
|
482
369
|
}
|
|
483
370
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
484
371
|
}
|
|
485
372
|
else if (locatorSearch.text) {
|
|
486
|
-
let
|
|
373
|
+
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
374
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, _params);
|
|
487
375
|
if (result.elementCount === 0) {
|
|
376
|
+
info.failCause.textNotFound = true;
|
|
377
|
+
info.failCause.lastError = "failed to locate element by text: " + text;
|
|
488
378
|
return;
|
|
489
379
|
}
|
|
490
380
|
locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
|
|
@@ -501,13 +391,22 @@ class StableBrowser {
|
|
|
501
391
|
// cssHref = true;
|
|
502
392
|
// }
|
|
503
393
|
let count = await locator.count();
|
|
394
|
+
if (count > 0 && !info.failCause.count) {
|
|
395
|
+
info.failCause.count = count;
|
|
396
|
+
}
|
|
504
397
|
//info.log += "total elements found " + count + "\n";
|
|
505
398
|
//let visibleCount = 0;
|
|
506
399
|
let visibleLocator = null;
|
|
507
|
-
if (locatorSearch.index && locatorSearch.index < count) {
|
|
400
|
+
if (typeof locatorSearch.index === "number" && locatorSearch.index < count) {
|
|
508
401
|
foundLocators.push(locator.nth(locatorSearch.index));
|
|
402
|
+
if (info.locatorLog) {
|
|
403
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
404
|
+
}
|
|
509
405
|
return;
|
|
510
406
|
}
|
|
407
|
+
if (info.locatorLog && count === 0) {
|
|
408
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
409
|
+
}
|
|
511
410
|
for (let j = 0; j < count; j++) {
|
|
512
411
|
let visible = await locator.nth(j).isVisible();
|
|
513
412
|
const enabled = await locator.nth(j).isEnabled();
|
|
@@ -516,24 +415,40 @@ class StableBrowser {
|
|
|
516
415
|
}
|
|
517
416
|
if (visible && enabled) {
|
|
518
417
|
foundLocators.push(locator.nth(j));
|
|
418
|
+
if (info.locatorLog) {
|
|
419
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
420
|
+
}
|
|
519
421
|
}
|
|
520
422
|
else {
|
|
423
|
+
info.failCause.visible = visible;
|
|
424
|
+
info.failCause.enabled = enabled;
|
|
521
425
|
if (!info.printMessages) {
|
|
522
426
|
info.printMessages = {};
|
|
523
427
|
}
|
|
428
|
+
if (info.locatorLog && !visible) {
|
|
429
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
430
|
+
}
|
|
431
|
+
if (info.locatorLog && !enabled) {
|
|
432
|
+
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
433
|
+
}
|
|
524
434
|
if (!info.printMessages[j.toString()]) {
|
|
525
|
-
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
435
|
+
//info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
526
436
|
info.printMessages[j.toString()] = true;
|
|
527
437
|
}
|
|
528
438
|
}
|
|
529
439
|
}
|
|
530
440
|
}
|
|
531
441
|
async closeUnexpectedPopups(info, _params) {
|
|
442
|
+
if (!info) {
|
|
443
|
+
info = {};
|
|
444
|
+
info.failCause = {};
|
|
445
|
+
info.log = "";
|
|
446
|
+
}
|
|
532
447
|
if (this.configuration.popupHandlers && this.configuration.popupHandlers.length > 0) {
|
|
533
448
|
if (!info) {
|
|
534
449
|
info = {};
|
|
535
450
|
}
|
|
536
|
-
info.log += "scan for popup handlers" + "\n";
|
|
451
|
+
//info.log += "scan for popup handlers" + "\n";
|
|
537
452
|
const handlerGroup = [];
|
|
538
453
|
for (let i = 0; i < this.configuration.popupHandlers.length; i++) {
|
|
539
454
|
handlerGroup.push(this.configuration.popupHandlers[i].locator);
|
|
@@ -560,9 +475,21 @@ class StableBrowser {
|
|
|
560
475
|
}
|
|
561
476
|
if (result.foundElements.length > 0) {
|
|
562
477
|
let dialogCloseLocator = result.foundElements[0].locator;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
478
|
+
try {
|
|
479
|
+
await scope?.evaluate(() => {
|
|
480
|
+
window.__isClosingPopups = true;
|
|
481
|
+
});
|
|
482
|
+
await dialogCloseLocator.click();
|
|
483
|
+
// wait for the dialog to close
|
|
484
|
+
await dialogCloseLocator.waitFor({ state: "hidden" });
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
await scope?.evaluate(() => {
|
|
490
|
+
window.__isClosingPopups = false;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
566
493
|
return { rerun: true };
|
|
567
494
|
}
|
|
568
495
|
}
|
|
@@ -586,7 +513,13 @@ class StableBrowser {
|
|
|
586
513
|
}
|
|
587
514
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
588
515
|
}
|
|
589
|
-
async _findFrameScope(selectors, timeout = 30000) {
|
|
516
|
+
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
517
|
+
if (!info) {
|
|
518
|
+
info = {};
|
|
519
|
+
info.failCause = {};
|
|
520
|
+
info.log = "";
|
|
521
|
+
}
|
|
522
|
+
let startTime = Date.now();
|
|
590
523
|
let scope = this.page;
|
|
591
524
|
if (selectors.frame) {
|
|
592
525
|
return selectors.frame;
|
|
@@ -617,9 +550,11 @@ class StableBrowser {
|
|
|
617
550
|
}
|
|
618
551
|
return framescope;
|
|
619
552
|
};
|
|
553
|
+
let fLocator = null;
|
|
620
554
|
while (true) {
|
|
621
555
|
let frameFound = false;
|
|
622
556
|
if (selectors.nestFrmLoc) {
|
|
557
|
+
fLocator = selectors.nestFrmLoc;
|
|
623
558
|
scope = await findFrame(selectors.nestFrmLoc, scope);
|
|
624
559
|
frameFound = true;
|
|
625
560
|
break;
|
|
@@ -628,6 +563,7 @@ class StableBrowser {
|
|
|
628
563
|
for (let i = 0; i < selectors.frameLocators.length; i++) {
|
|
629
564
|
let frameLocator = selectors.frameLocators[i];
|
|
630
565
|
if (frameLocator.css) {
|
|
566
|
+
fLocator = frameLocator.css;
|
|
631
567
|
scope = scope.frameLocator(frameLocator.css);
|
|
632
568
|
frameFound = true;
|
|
633
569
|
break;
|
|
@@ -635,16 +571,25 @@ class StableBrowser {
|
|
|
635
571
|
}
|
|
636
572
|
}
|
|
637
573
|
if (!frameFound && selectors.iframe_src) {
|
|
574
|
+
fLocator = selectors.iframe_src;
|
|
638
575
|
scope = this.page.frame({ url: selectors.iframe_src });
|
|
639
576
|
}
|
|
640
577
|
if (!scope) {
|
|
641
|
-
info
|
|
642
|
-
|
|
578
|
+
if (info && info.locatorLog) {
|
|
579
|
+
info.locatorLog.setFrameSearchStatus("frame-" + fLocator, "NOT_FOUND");
|
|
580
|
+
}
|
|
581
|
+
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
582
|
+
if (Date.now() - startTime > timeout) {
|
|
583
|
+
info.failCause.iframeNotFound = true;
|
|
584
|
+
info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
|
|
643
585
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
644
586
|
}
|
|
645
587
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
646
588
|
}
|
|
647
589
|
else {
|
|
590
|
+
if (info && info.locatorLog) {
|
|
591
|
+
info.locatorLog.setFrameSearchStatus("frame-" + fLocator, "FOUND");
|
|
592
|
+
}
|
|
648
593
|
break;
|
|
649
594
|
}
|
|
650
595
|
}
|
|
@@ -654,20 +599,27 @@ class StableBrowser {
|
|
|
654
599
|
}
|
|
655
600
|
return scope;
|
|
656
601
|
}
|
|
657
|
-
async _getDocumentBody(selectors, timeout = 30000) {
|
|
658
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
602
|
+
async _getDocumentBody(selectors, timeout = 30000, info) {
|
|
603
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
659
604
|
return scope.evaluate(() => {
|
|
660
605
|
var bodyContent = document.body.innerHTML;
|
|
661
606
|
return bodyContent;
|
|
662
607
|
});
|
|
663
608
|
}
|
|
664
609
|
async _locate_internal(selectors, info, _params, timeout = 30000) {
|
|
610
|
+
if (!info) {
|
|
611
|
+
info = {};
|
|
612
|
+
info.failCause = {};
|
|
613
|
+
info.log = "";
|
|
614
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
615
|
+
}
|
|
665
616
|
let highPriorityTimeout = 5000;
|
|
666
617
|
let visibleOnlyTimeout = 6000;
|
|
667
|
-
let startTime =
|
|
618
|
+
let startTime = Date.now();
|
|
668
619
|
let locatorsCount = 0;
|
|
620
|
+
let lazy_scroll = false;
|
|
669
621
|
//let arrayMode = Array.isArray(selectors);
|
|
670
|
-
let scope = await this._findFrameScope(selectors, timeout);
|
|
622
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
671
623
|
let selectorsLocators = null;
|
|
672
624
|
selectorsLocators = selectors.locators;
|
|
673
625
|
// group selectors by priority
|
|
@@ -754,21 +706,33 @@ class StableBrowser {
|
|
|
754
706
|
return maxCountElement.locator;
|
|
755
707
|
}
|
|
756
708
|
}
|
|
757
|
-
if (
|
|
709
|
+
if (Date.now() - startTime > timeout) {
|
|
758
710
|
break;
|
|
759
711
|
}
|
|
760
|
-
if (
|
|
712
|
+
if (Date.now() - startTime > highPriorityTimeout) {
|
|
761
713
|
info.log += "high priority timeout, will try all elements" + "\n";
|
|
762
714
|
highPriorityOnly = false;
|
|
715
|
+
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
716
|
+
lazy_scroll = true;
|
|
717
|
+
await scrollPageToLoadLazyElements(this.page);
|
|
718
|
+
}
|
|
763
719
|
}
|
|
764
|
-
if (
|
|
720
|
+
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
765
721
|
info.log += "visible only timeout, will try all elements" + "\n";
|
|
766
722
|
visibleOnly = false;
|
|
767
723
|
}
|
|
768
724
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
769
725
|
}
|
|
770
726
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
771
|
-
info.
|
|
727
|
+
// if (info.locatorLog) {
|
|
728
|
+
// const lines = info.locatorLog.toString().split("\n");
|
|
729
|
+
// for (let line of lines) {
|
|
730
|
+
// this.logger.debug(line);
|
|
731
|
+
// }
|
|
732
|
+
// }
|
|
733
|
+
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
734
|
+
info.failCause.locatorNotFound = true;
|
|
735
|
+
info.failCause.lastError = "failed to locate unique element";
|
|
772
736
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
773
737
|
}
|
|
774
738
|
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
@@ -782,8 +746,9 @@ class StableBrowser {
|
|
|
782
746
|
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
783
747
|
}
|
|
784
748
|
catch (e) {
|
|
785
|
-
this
|
|
786
|
-
this.logger.debug(
|
|
749
|
+
// this call can fail it the browser is navigating
|
|
750
|
+
// this.logger.debug("unable to use locator " + JSON.stringify(locatorsGroup[i]));
|
|
751
|
+
// this.logger.debug(e);
|
|
787
752
|
foundLocators = [];
|
|
788
753
|
try {
|
|
789
754
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
@@ -800,10 +765,29 @@ class StableBrowser {
|
|
|
800
765
|
});
|
|
801
766
|
result.locatorIndex = i;
|
|
802
767
|
}
|
|
768
|
+
if (foundLocators.length > 1) {
|
|
769
|
+
info.failCause.foundMultiple = true;
|
|
770
|
+
if (info.locatorLog) {
|
|
771
|
+
info.locatorLog.setLocatorSearchStatus(locatorsGroup[i], "FOUND_NOT_UNIQUE");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
803
774
|
}
|
|
804
775
|
return result;
|
|
805
776
|
}
|
|
806
777
|
async simpleClick(elementDescription, _params, options = {}, world = null) {
|
|
778
|
+
const state = {
|
|
779
|
+
locate: false,
|
|
780
|
+
scroll: false,
|
|
781
|
+
highlight: false,
|
|
782
|
+
_params,
|
|
783
|
+
options,
|
|
784
|
+
world,
|
|
785
|
+
type: Types.CLICK,
|
|
786
|
+
text: "Click element",
|
|
787
|
+
operation: "simpleClick",
|
|
788
|
+
log: "***** click on " + elementDescription + " *****\n",
|
|
789
|
+
};
|
|
790
|
+
_preCommand(state, this);
|
|
807
791
|
const startTime = Date.now();
|
|
808
792
|
let timeout = 30000;
|
|
809
793
|
if (options && options.timeout) {
|
|
@@ -827,13 +811,32 @@ class StableBrowser {
|
|
|
827
811
|
}
|
|
828
812
|
catch (e) {
|
|
829
813
|
if (performance.now() - startTime > timeout) {
|
|
830
|
-
throw e;
|
|
814
|
+
// throw e;
|
|
815
|
+
try {
|
|
816
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
_commandFinally(state, this);
|
|
820
|
+
}
|
|
831
821
|
}
|
|
832
822
|
}
|
|
833
823
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
834
824
|
}
|
|
835
825
|
}
|
|
836
826
|
async simpleClickType(elementDescription, value, _params, options = {}, world = null) {
|
|
827
|
+
const state = {
|
|
828
|
+
locate: false,
|
|
829
|
+
scroll: false,
|
|
830
|
+
highlight: false,
|
|
831
|
+
_params,
|
|
832
|
+
options,
|
|
833
|
+
world,
|
|
834
|
+
type: Types.FILL,
|
|
835
|
+
text: "Fill element",
|
|
836
|
+
operation: "simpleClickType",
|
|
837
|
+
log: "***** click type on " + elementDescription + " *****\n",
|
|
838
|
+
};
|
|
839
|
+
_preCommand(state, this);
|
|
837
840
|
const startTime = Date.now();
|
|
838
841
|
let timeout = 30000;
|
|
839
842
|
if (options && options.timeout) {
|
|
@@ -857,7 +860,13 @@ class StableBrowser {
|
|
|
857
860
|
}
|
|
858
861
|
catch (e) {
|
|
859
862
|
if (performance.now() - startTime > timeout) {
|
|
860
|
-
throw e;
|
|
863
|
+
// throw e;
|
|
864
|
+
try {
|
|
865
|
+
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
_commandFinally(state, this);
|
|
869
|
+
}
|
|
861
870
|
}
|
|
862
871
|
}
|
|
863
872
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -881,13 +890,13 @@ class StableBrowser {
|
|
|
881
890
|
}
|
|
882
891
|
try {
|
|
883
892
|
await state.element.click();
|
|
884
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
893
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
885
894
|
}
|
|
886
895
|
catch (e) {
|
|
887
896
|
// await this.closeUnexpectedPopups();
|
|
888
897
|
state.element = await this._locate(selectors, state.info, _params);
|
|
889
898
|
await state.element.dispatchEvent("click");
|
|
890
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
899
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
891
900
|
}
|
|
892
901
|
await this.waitForPageLoad();
|
|
893
902
|
return state.info;
|
|
@@ -966,7 +975,6 @@ class StableBrowser {
|
|
|
966
975
|
await state.element.hover({ timeout: 10000 });
|
|
967
976
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
968
977
|
}
|
|
969
|
-
await _screenshot(state, this);
|
|
970
978
|
await this.waitForPageLoad();
|
|
971
979
|
return state.info;
|
|
972
980
|
}
|
|
@@ -1094,33 +1102,30 @@ class StableBrowser {
|
|
|
1094
1102
|
}
|
|
1095
1103
|
}
|
|
1096
1104
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1105
|
+
const state = {
|
|
1106
|
+
selectors,
|
|
1107
|
+
_params,
|
|
1108
|
+
value: await this._replaceWithLocalData(value, this),
|
|
1109
|
+
options,
|
|
1110
|
+
world,
|
|
1111
|
+
type: Types.SET_DATE_TIME,
|
|
1112
|
+
text: `Set date time value: ${value}`,
|
|
1113
|
+
operation: "setDateTime",
|
|
1114
|
+
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1115
|
+
throwError: false,
|
|
1116
|
+
};
|
|
1107
1117
|
try {
|
|
1108
|
-
|
|
1109
|
-
let element = await this._locate(selectors, info, _params);
|
|
1110
|
-
//insert red border around the element
|
|
1111
|
-
await this.scrollIfNeeded(element, info);
|
|
1112
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1113
|
-
await this._highlightElements(element);
|
|
1118
|
+
await _preCommand(state, this);
|
|
1114
1119
|
try {
|
|
1115
|
-
await element.click();
|
|
1120
|
+
await state.element.click();
|
|
1116
1121
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1117
1122
|
if (format) {
|
|
1118
|
-
value = dayjs(value).format(format);
|
|
1119
|
-
await element.fill(value);
|
|
1123
|
+
state.value = dayjs(state.value).format(format);
|
|
1124
|
+
await state.element.fill(state.value);
|
|
1120
1125
|
}
|
|
1121
1126
|
else {
|
|
1122
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1123
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1127
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1128
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1124
1129
|
el.value = ""; // clear input
|
|
1125
1130
|
el.value = dateTimeValue;
|
|
1126
1131
|
}, dateTimeValue);
|
|
@@ -1133,20 +1138,19 @@ class StableBrowser {
|
|
|
1133
1138
|
}
|
|
1134
1139
|
catch (err) {
|
|
1135
1140
|
//await this.closeUnexpectedPopups();
|
|
1136
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1141
|
+
this.logger.error("setting date time input failed " + JSON.stringify(state.info));
|
|
1137
1142
|
this.logger.info("Trying again");
|
|
1138
|
-
|
|
1139
|
-
info.
|
|
1140
|
-
Object.assign(err, { info: info });
|
|
1143
|
+
await _screenshot(state, this);
|
|
1144
|
+
Object.assign(err, { info: state.info });
|
|
1141
1145
|
await element.click();
|
|
1142
1146
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1143
1147
|
if (format) {
|
|
1144
|
-
value = dayjs(value).format(format);
|
|
1145
|
-
await element.fill(value);
|
|
1148
|
+
state.value = dayjs(state.value).format(format);
|
|
1149
|
+
await state.element.fill(state.value);
|
|
1146
1150
|
}
|
|
1147
1151
|
else {
|
|
1148
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1149
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1152
|
+
const dateTimeValue = await getDateTimeValue({ value: state.value, element: state.element });
|
|
1153
|
+
await state.element.evaluateHandle((el, dateTimeValue) => {
|
|
1150
1154
|
el.value = ""; // clear input
|
|
1151
1155
|
el.value = dateTimeValue;
|
|
1152
1156
|
}, dateTimeValue);
|
|
@@ -1159,50 +1163,30 @@ class StableBrowser {
|
|
|
1159
1163
|
}
|
|
1160
1164
|
}
|
|
1161
1165
|
catch (e) {
|
|
1162
|
-
|
|
1163
|
-
throw e;
|
|
1166
|
+
await _commandError(state, e, this);
|
|
1164
1167
|
}
|
|
1165
1168
|
finally {
|
|
1166
|
-
|
|
1167
|
-
this._reportToWorld(world, {
|
|
1168
|
-
element_name: selectors.element_name,
|
|
1169
|
-
type: Types.SET_DATE_TIME,
|
|
1170
|
-
screenshotId,
|
|
1171
|
-
value: value,
|
|
1172
|
-
text: `setDateTime input with value: ${value}`,
|
|
1173
|
-
result: error
|
|
1174
|
-
? {
|
|
1175
|
-
status: "FAILED",
|
|
1176
|
-
startTime,
|
|
1177
|
-
endTime,
|
|
1178
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
1179
|
-
}
|
|
1180
|
-
: {
|
|
1181
|
-
status: "PASSED",
|
|
1182
|
-
startTime,
|
|
1183
|
-
endTime,
|
|
1184
|
-
},
|
|
1185
|
-
info: info,
|
|
1186
|
-
});
|
|
1169
|
+
_commandFinally(state, this);
|
|
1187
1170
|
}
|
|
1188
1171
|
}
|
|
1189
1172
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
1173
|
+
_value = unEscapeString(_value);
|
|
1174
|
+
const newValue = await this._replaceWithLocalData(_value, world);
|
|
1190
1175
|
const state = {
|
|
1191
1176
|
selectors,
|
|
1192
1177
|
_params,
|
|
1193
|
-
value:
|
|
1178
|
+
value: newValue,
|
|
1179
|
+
originalValue: _value,
|
|
1194
1180
|
options,
|
|
1195
1181
|
world,
|
|
1196
1182
|
type: Types.FILL,
|
|
1197
1183
|
text: `Click type input with value: ${_value}`,
|
|
1198
1184
|
operation: "clickType",
|
|
1199
|
-
log: "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n",
|
|
1185
|
+
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1200
1186
|
};
|
|
1201
|
-
const newValue = await this._replaceWithLocalData(state.value, world);
|
|
1202
1187
|
if (newValue !== _value) {
|
|
1203
1188
|
//this.logger.info(_value + "=" + newValue);
|
|
1204
1189
|
_value = newValue;
|
|
1205
|
-
state.value = newValue;
|
|
1206
1190
|
}
|
|
1207
1191
|
try {
|
|
1208
1192
|
await _preCommand(state, this);
|
|
@@ -1319,6 +1303,7 @@ class StableBrowser {
|
|
|
1319
1303
|
let screenshotPath = null;
|
|
1320
1304
|
if (!info.log) {
|
|
1321
1305
|
info.log = "";
|
|
1306
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
1322
1307
|
}
|
|
1323
1308
|
info.operation = "getText";
|
|
1324
1309
|
info.selectors = selectors;
|
|
@@ -1657,11 +1642,9 @@ class StableBrowser {
|
|
|
1657
1642
|
if (!fs.existsSync(world.screenshotPath)) {
|
|
1658
1643
|
fs.mkdirSync(world.screenshotPath, { recursive: true });
|
|
1659
1644
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
const screenshotPath = path.join(world.screenshotPath, nextIndex + ".png");
|
|
1645
|
+
// to make sure the path doesn't start with -
|
|
1646
|
+
const uuidStr = "id_" + randomUUID();
|
|
1647
|
+
const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
|
|
1665
1648
|
try {
|
|
1666
1649
|
await this.takeScreenshot(screenshotPath);
|
|
1667
1650
|
// let buffer = await this.page.screenshot({ timeout: 4000 });
|
|
@@ -1675,7 +1658,7 @@ class StableBrowser {
|
|
|
1675
1658
|
catch (e) {
|
|
1676
1659
|
this.logger.info("unable to take screenshot, ignored");
|
|
1677
1660
|
}
|
|
1678
|
-
result.screenshotId =
|
|
1661
|
+
result.screenshotId = uuidStr;
|
|
1679
1662
|
result.screenshotPath = screenshotPath;
|
|
1680
1663
|
if (info && info.box) {
|
|
1681
1664
|
await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
|
|
@@ -1773,74 +1756,101 @@ class StableBrowser {
|
|
|
1773
1756
|
}
|
|
1774
1757
|
}
|
|
1775
1758
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1759
|
+
const state = {
|
|
1760
|
+
selectors,
|
|
1761
|
+
_params,
|
|
1762
|
+
attribute,
|
|
1763
|
+
variable,
|
|
1764
|
+
options,
|
|
1765
|
+
world,
|
|
1766
|
+
type: Types.EXTRACT,
|
|
1767
|
+
text: `Extract attribute from element`,
|
|
1768
|
+
operation: "extractAttribute",
|
|
1769
|
+
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1770
|
+
};
|
|
1781
1771
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1782
|
-
const info = {};
|
|
1783
|
-
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1784
|
-
info.operation = "extract";
|
|
1785
|
-
info.selectors = selectors;
|
|
1786
1772
|
try {
|
|
1787
|
-
|
|
1788
|
-
await this._highlightElements(element);
|
|
1789
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1773
|
+
await _preCommand(state, this);
|
|
1790
1774
|
switch (attribute) {
|
|
1791
1775
|
case "inner_text":
|
|
1792
|
-
|
|
1776
|
+
state.value = await state.element.innerText();
|
|
1793
1777
|
break;
|
|
1794
1778
|
case "href":
|
|
1795
|
-
|
|
1779
|
+
state.value = await state.element.getAttribute("href");
|
|
1796
1780
|
break;
|
|
1797
1781
|
case "value":
|
|
1798
|
-
|
|
1782
|
+
state.value = await state.element.inputValue();
|
|
1799
1783
|
break;
|
|
1800
1784
|
default:
|
|
1801
|
-
|
|
1785
|
+
state.value = await state.element.getAttribute(attribute);
|
|
1802
1786
|
break;
|
|
1803
1787
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1788
|
+
state.info.value = state.value;
|
|
1789
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
1790
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
1791
|
+
return state.info;
|
|
1792
|
+
}
|
|
1793
|
+
catch (e) {
|
|
1794
|
+
await _commandError(state, e, this);
|
|
1795
|
+
}
|
|
1796
|
+
finally {
|
|
1797
|
+
_commandFinally(state, this);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
1801
|
+
const state = {
|
|
1802
|
+
selectors,
|
|
1803
|
+
_params,
|
|
1804
|
+
attribute,
|
|
1805
|
+
value,
|
|
1806
|
+
options,
|
|
1807
|
+
world,
|
|
1808
|
+
type: Types.VERIFY_ATTRIBUTE,
|
|
1809
|
+
text: `Verify element attribute`,
|
|
1810
|
+
operation: "verifyAttribute",
|
|
1811
|
+
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1812
|
+
};
|
|
1813
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1814
|
+
let val;
|
|
1815
|
+
try {
|
|
1816
|
+
await _preCommand(state, this);
|
|
1817
|
+
switch (attribute) {
|
|
1818
|
+
case "innerText":
|
|
1819
|
+
val = String(await state.element.innerText());
|
|
1820
|
+
break;
|
|
1821
|
+
case "value":
|
|
1822
|
+
val = String(await state.element.inputValue());
|
|
1823
|
+
break;
|
|
1824
|
+
case "checked":
|
|
1825
|
+
val = String(await state.element.isChecked());
|
|
1826
|
+
break;
|
|
1827
|
+
case "disabled":
|
|
1828
|
+
val = String(await state.element.isDisabled());
|
|
1829
|
+
break;
|
|
1830
|
+
default:
|
|
1831
|
+
val = String(await state.element.getAttribute(attribute));
|
|
1832
|
+
break;
|
|
1807
1833
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1834
|
+
let regex;
|
|
1835
|
+
if (value.startsWith("/") && value.endsWith("/")) {
|
|
1836
|
+
const patternBody = value.slice(1, -1);
|
|
1837
|
+
regex = new RegExp(patternBody, "g");
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1841
|
+
regex = new RegExp(escapedPattern, "g");
|
|
1842
|
+
}
|
|
1843
|
+
if (!val.match(regex)) {
|
|
1844
|
+
throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
|
|
1845
|
+
}
|
|
1846
|
+
state.info.value = val;
|
|
1847
|
+
return state.info;
|
|
1811
1848
|
}
|
|
1812
1849
|
catch (e) {
|
|
1813
|
-
|
|
1814
|
-
this.logger.error("extract failed " + info.log);
|
|
1815
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1816
|
-
info.screenshotPath = screenshotPath;
|
|
1817
|
-
Object.assign(e, { info: info });
|
|
1818
|
-
error = e;
|
|
1819
|
-
throw e;
|
|
1850
|
+
await _commandError(state, e, this);
|
|
1820
1851
|
}
|
|
1821
1852
|
finally {
|
|
1822
|
-
|
|
1823
|
-
this._reportToWorld(world, {
|
|
1824
|
-
element_name: selectors.element_name,
|
|
1825
|
-
type: Types.EXTRACT_ATTRIBUTE,
|
|
1826
|
-
variable: variable,
|
|
1827
|
-
value: info.value,
|
|
1828
|
-
text: "Extract attribute from element",
|
|
1829
|
-
screenshotId,
|
|
1830
|
-
result: error
|
|
1831
|
-
? {
|
|
1832
|
-
status: "FAILED",
|
|
1833
|
-
startTime,
|
|
1834
|
-
endTime,
|
|
1835
|
-
message: error?.message,
|
|
1836
|
-
}
|
|
1837
|
-
: {
|
|
1838
|
-
status: "PASSED",
|
|
1839
|
-
startTime,
|
|
1840
|
-
endTime,
|
|
1841
|
-
},
|
|
1842
|
-
info: info,
|
|
1843
|
-
});
|
|
1853
|
+
_commandFinally(state, this);
|
|
1844
1854
|
}
|
|
1845
1855
|
}
|
|
1846
1856
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -1861,7 +1871,7 @@ class StableBrowser {
|
|
|
1861
1871
|
if (options && options.timeout) {
|
|
1862
1872
|
timeout = options.timeout;
|
|
1863
1873
|
}
|
|
1864
|
-
const serviceUrl =
|
|
1874
|
+
const serviceUrl = _getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1865
1875
|
const request = {
|
|
1866
1876
|
method: "POST",
|
|
1867
1877
|
url: serviceUrl,
|
|
@@ -1917,7 +1927,8 @@ class StableBrowser {
|
|
|
1917
1927
|
catch (e) {
|
|
1918
1928
|
errorCount++;
|
|
1919
1929
|
if (errorCount > 3) {
|
|
1920
|
-
throw e;
|
|
1930
|
+
// throw e;
|
|
1931
|
+
await _commandError({ text: "extractEmailData", operation: "extractEmailData", emailAddress, info: {} }, e, this);
|
|
1921
1932
|
}
|
|
1922
1933
|
// ignore
|
|
1923
1934
|
}
|
|
@@ -2028,11 +2039,12 @@ class StableBrowser {
|
|
|
2028
2039
|
info.screenshotPath = screenshotPath;
|
|
2029
2040
|
Object.assign(e, { info: info });
|
|
2030
2041
|
error = e;
|
|
2031
|
-
throw e;
|
|
2042
|
+
// throw e;
|
|
2043
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2032
2044
|
}
|
|
2033
2045
|
finally {
|
|
2034
2046
|
const endTime = Date.now();
|
|
2035
|
-
|
|
2047
|
+
_reportToWorld(world, {
|
|
2036
2048
|
type: Types.VERIFY_PAGE_PATH,
|
|
2037
2049
|
text: "Verify page path",
|
|
2038
2050
|
screenshotId,
|
|
@@ -2052,54 +2064,64 @@ class StableBrowser {
|
|
|
2052
2064
|
});
|
|
2053
2065
|
}
|
|
2054
2066
|
}
|
|
2067
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2068
|
+
const frames = this.page.frames();
|
|
2069
|
+
let results = [];
|
|
2070
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2071
|
+
if (dateAlternatives.date) {
|
|
2072
|
+
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2073
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, {});
|
|
2074
|
+
result.frame = frames[i];
|
|
2075
|
+
results.push(result);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
else if (numberAlternatives.number) {
|
|
2079
|
+
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2080
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, {});
|
|
2081
|
+
result.frame = frames[i];
|
|
2082
|
+
results.push(result);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, {});
|
|
2087
|
+
result.frame = frames[i];
|
|
2088
|
+
results.push(result);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
state.info.results = results;
|
|
2092
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2093
|
+
return resultWithElementsFound;
|
|
2094
|
+
}
|
|
2055
2095
|
async verifyTextExistInPage(text, options = {}, world = null) {
|
|
2056
2096
|
text = unEscapeString(text);
|
|
2057
|
-
const
|
|
2097
|
+
const state = {
|
|
2098
|
+
text_search: text,
|
|
2099
|
+
options,
|
|
2100
|
+
world,
|
|
2101
|
+
locate: false,
|
|
2102
|
+
scroll: false,
|
|
2103
|
+
highlight: false,
|
|
2104
|
+
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2105
|
+
text: `Verify text exists in page`,
|
|
2106
|
+
operation: "verifyTextExistInPage",
|
|
2107
|
+
log: "***** verify text " + text + " exists in page *****\n",
|
|
2108
|
+
};
|
|
2058
2109
|
const timeout = this._getLoadTimeout(options);
|
|
2059
|
-
let error = null;
|
|
2060
|
-
let screenshotId = null;
|
|
2061
|
-
let screenshotPath = null;
|
|
2062
2110
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2063
|
-
const info = {};
|
|
2064
|
-
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
2065
|
-
info.operation = "verifyTextExistInPage";
|
|
2066
2111
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2067
2112
|
if (newValue !== text) {
|
|
2068
2113
|
this.logger.info(text + "=" + newValue);
|
|
2069
2114
|
text = newValue;
|
|
2070
2115
|
}
|
|
2071
|
-
info.text = text;
|
|
2072
2116
|
let dateAlternatives = findDateAlternatives(text);
|
|
2073
2117
|
let numberAlternatives = findNumberAlternatives(text);
|
|
2074
2118
|
try {
|
|
2119
|
+
await _preCommand(state, this);
|
|
2120
|
+
state.info.text = text;
|
|
2075
2121
|
while (true) {
|
|
2076
|
-
const
|
|
2077
|
-
let results = [];
|
|
2078
|
-
for (let i = 0; i < frames.length; i++) {
|
|
2079
|
-
if (dateAlternatives.date) {
|
|
2080
|
-
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2081
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*", true, true, {});
|
|
2082
|
-
result.frame = frames[i];
|
|
2083
|
-
results.push(result);
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
else if (numberAlternatives.number) {
|
|
2087
|
-
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2088
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*", true, true, {});
|
|
2089
|
-
result.frame = frames[i];
|
|
2090
|
-
results.push(result);
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
else {
|
|
2094
|
-
const result = await this._locateElementByText(frames[i], text, "*", true, true, {});
|
|
2095
|
-
result.frame = frames[i];
|
|
2096
|
-
results.push(result);
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
info.results = results;
|
|
2100
|
-
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2122
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2101
2123
|
if (resultWithElementsFound.length === 0) {
|
|
2102
|
-
if (Date.now() - startTime > timeout) {
|
|
2124
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2103
2125
|
throw new Error(`Text ${text} not found in page`);
|
|
2104
2126
|
}
|
|
2105
2127
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -2111,55 +2133,163 @@ class StableBrowser {
|
|
|
2111
2133
|
await this._highlightElements(frame, dataAttribute);
|
|
2112
2134
|
const element = await frame.$(dataAttribute);
|
|
2113
2135
|
if (element) {
|
|
2114
|
-
await this.scrollIfNeeded(element, info);
|
|
2136
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2115
2137
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2116
2138
|
}
|
|
2117
2139
|
}
|
|
2118
|
-
|
|
2119
|
-
return info;
|
|
2140
|
+
await _screenshot(state, this);
|
|
2141
|
+
return state.info;
|
|
2120
2142
|
}
|
|
2121
2143
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2122
2144
|
}
|
|
2123
2145
|
catch (e) {
|
|
2124
|
-
|
|
2125
|
-
this.logger.error("verify text exist in page failed " + info.log);
|
|
2126
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2127
|
-
info.screenshotPath = screenshotPath;
|
|
2128
|
-
Object.assign(e, { info: info });
|
|
2129
|
-
error = e;
|
|
2130
|
-
throw e;
|
|
2146
|
+
await _commandError(state, e, this);
|
|
2131
2147
|
}
|
|
2132
2148
|
finally {
|
|
2133
|
-
|
|
2134
|
-
this._reportToWorld(world, {
|
|
2135
|
-
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
2136
|
-
text: "Verify text exists in page",
|
|
2137
|
-
screenshotId,
|
|
2138
|
-
result: error
|
|
2139
|
-
? {
|
|
2140
|
-
status: "FAILED",
|
|
2141
|
-
startTime,
|
|
2142
|
-
endTime,
|
|
2143
|
-
message: error?.message,
|
|
2144
|
-
}
|
|
2145
|
-
: {
|
|
2146
|
-
status: "PASSED",
|
|
2147
|
-
startTime,
|
|
2148
|
-
endTime,
|
|
2149
|
-
},
|
|
2150
|
-
info: info,
|
|
2151
|
-
});
|
|
2149
|
+
_commandFinally(state, this);
|
|
2152
2150
|
}
|
|
2153
2151
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2152
|
+
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
2153
|
+
text = unEscapeString(text);
|
|
2154
|
+
const state = {
|
|
2155
|
+
text_search: text,
|
|
2156
|
+
options,
|
|
2157
|
+
world,
|
|
2158
|
+
locate: false,
|
|
2159
|
+
scroll: false,
|
|
2160
|
+
highlight: false,
|
|
2161
|
+
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2162
|
+
text: `Verify text does not exist in page`,
|
|
2163
|
+
operation: "verifyTextNotExistInPage",
|
|
2164
|
+
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2165
|
+
};
|
|
2166
|
+
const timeout = this._getLoadTimeout(options);
|
|
2167
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2168
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
2169
|
+
if (newValue !== text) {
|
|
2170
|
+
this.logger.info(text + "=" + newValue);
|
|
2171
|
+
text = newValue;
|
|
2158
2172
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2173
|
+
let dateAlternatives = findDateAlternatives(text);
|
|
2174
|
+
let numberAlternatives = findNumberAlternatives(text);
|
|
2175
|
+
try {
|
|
2176
|
+
await _preCommand(state, this);
|
|
2177
|
+
state.info.text = text;
|
|
2178
|
+
while (true) {
|
|
2179
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2180
|
+
if (resultWithElementsFound.length === 0) {
|
|
2181
|
+
await _screenshot(state, this);
|
|
2182
|
+
return state.info;
|
|
2183
|
+
}
|
|
2184
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2185
|
+
throw new Error(`Text ${text} found in page`);
|
|
2186
|
+
}
|
|
2187
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
catch (e) {
|
|
2191
|
+
await _commandError(state, e, this);
|
|
2192
|
+
}
|
|
2193
|
+
finally {
|
|
2194
|
+
_commandFinally(state, this);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
2198
|
+
textAnchor = unEscapeString(textAnchor);
|
|
2199
|
+
textToVerify = unEscapeString(textToVerify);
|
|
2200
|
+
const state = {
|
|
2201
|
+
text_search: textToVerify,
|
|
2202
|
+
options,
|
|
2203
|
+
world,
|
|
2204
|
+
locate: false,
|
|
2205
|
+
scroll: false,
|
|
2206
|
+
highlight: false,
|
|
2207
|
+
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2208
|
+
text: `Verify text with relation to another text`,
|
|
2209
|
+
operation: "verify_text_with_relation",
|
|
2210
|
+
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2211
|
+
};
|
|
2212
|
+
const timeout = this._getLoadTimeout(options);
|
|
2213
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2214
|
+
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2215
|
+
if (newValue !== textAnchor) {
|
|
2216
|
+
this.logger.info(textAnchor + "=" + newValue);
|
|
2217
|
+
textAnchor = newValue;
|
|
2218
|
+
}
|
|
2219
|
+
newValue = await this._replaceWithLocalData(textToVerify, world);
|
|
2220
|
+
if (newValue !== textToVerify) {
|
|
2221
|
+
this.logger.info(textToVerify + "=" + newValue);
|
|
2222
|
+
textToVerify = newValue;
|
|
2223
|
+
}
|
|
2224
|
+
let dateAlternatives = findDateAlternatives(textToVerify);
|
|
2225
|
+
let numberAlternatives = findNumberAlternatives(textToVerify);
|
|
2226
|
+
let foundAncore = false;
|
|
2227
|
+
try {
|
|
2228
|
+
await _preCommand(state, this);
|
|
2229
|
+
state.info.text = textToVerify;
|
|
2230
|
+
while (true) {
|
|
2231
|
+
const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
|
|
2232
|
+
if (resultWithElementsFound.length === 0) {
|
|
2233
|
+
if (Date.now() - state.startTime > timeout) {
|
|
2234
|
+
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
2235
|
+
}
|
|
2236
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2240
|
+
foundAncore = true;
|
|
2241
|
+
const result = resultWithElementsFound[i];
|
|
2242
|
+
const token = result.randomToken;
|
|
2243
|
+
const frame = result.frame;
|
|
2244
|
+
const css = `[data-blinq-id="blinq-id-${token}"]`;
|
|
2245
|
+
const findResult = await frame.evaluate(([css, climb, textToVerify, token]) => {
|
|
2246
|
+
const elements = Array.from(document.querySelectorAll(css));
|
|
2247
|
+
for (let i = 0; i < elements.length; i++) {
|
|
2248
|
+
const element = elements[i];
|
|
2249
|
+
let climbParent = element;
|
|
2250
|
+
for (let j = 0; j < climb; j++) {
|
|
2251
|
+
climbParent = climbParent.parentElement;
|
|
2252
|
+
if (!climbParent) {
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
if (!climbParent) {
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
|
|
2260
|
+
if (foundElements.length > 0) {
|
|
2261
|
+
// set the container element attribute
|
|
2262
|
+
element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
|
|
2263
|
+
climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
|
|
2264
|
+
foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
|
|
2265
|
+
return { found: true };
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return { found: false };
|
|
2269
|
+
}, [css, climb, textToVerify, result.randomToken]);
|
|
2270
|
+
if (findResult.found === true) {
|
|
2271
|
+
const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
|
|
2272
|
+
const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2273
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2274
|
+
await this._highlightElements(frame, cssAnchor);
|
|
2275
|
+
const element = await frame.$(dataAttribute);
|
|
2276
|
+
if (element) {
|
|
2277
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2278
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2279
|
+
}
|
|
2280
|
+
await _screenshot(state, this);
|
|
2281
|
+
return state.info;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2286
|
+
}
|
|
2287
|
+
catch (e) {
|
|
2288
|
+
await _commandError(state, e, this);
|
|
2289
|
+
}
|
|
2290
|
+
finally {
|
|
2291
|
+
_commandFinally(state, this);
|
|
2161
2292
|
}
|
|
2162
|
-
return serviceUrl;
|
|
2163
2293
|
}
|
|
2164
2294
|
async visualVerification(text, options = {}, world = null) {
|
|
2165
2295
|
const startTime = Date.now();
|
|
@@ -2175,7 +2305,7 @@ class StableBrowser {
|
|
|
2175
2305
|
throw new Error("TOKEN is not set");
|
|
2176
2306
|
}
|
|
2177
2307
|
try {
|
|
2178
|
-
let serviceUrl =
|
|
2308
|
+
let serviceUrl = _getServerUrl();
|
|
2179
2309
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2180
2310
|
info.screenshotPath = screenshotPath;
|
|
2181
2311
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2211,11 +2341,12 @@ class StableBrowser {
|
|
|
2211
2341
|
info.screenshotPath = screenshotPath;
|
|
2212
2342
|
Object.assign(e, { info: info });
|
|
2213
2343
|
error = e;
|
|
2214
|
-
throw e;
|
|
2344
|
+
// throw e;
|
|
2345
|
+
await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
|
|
2215
2346
|
}
|
|
2216
2347
|
finally {
|
|
2217
2348
|
const endTime = Date.now();
|
|
2218
|
-
|
|
2349
|
+
_reportToWorld(world, {
|
|
2219
2350
|
type: Types.VERIFY_VISUAL,
|
|
2220
2351
|
text: "Visual verification",
|
|
2221
2352
|
screenshotId,
|
|
@@ -2263,6 +2394,7 @@ class StableBrowser {
|
|
|
2263
2394
|
let screenshotPath = null;
|
|
2264
2395
|
const info = {};
|
|
2265
2396
|
info.log = "";
|
|
2397
|
+
info.locatorLog = new LocatorLog(selectors);
|
|
2266
2398
|
info.operation = "getTableData";
|
|
2267
2399
|
info.selectors = selectors;
|
|
2268
2400
|
try {
|
|
@@ -2278,11 +2410,12 @@ class StableBrowser {
|
|
|
2278
2410
|
info.screenshotPath = screenshotPath;
|
|
2279
2411
|
Object.assign(e, { info: info });
|
|
2280
2412
|
error = e;
|
|
2281
|
-
throw e;
|
|
2413
|
+
// throw e;
|
|
2414
|
+
await _commandError({ text: "getTableData", operation: "getTableData", selectors, info }, e, this);
|
|
2282
2415
|
}
|
|
2283
2416
|
finally {
|
|
2284
2417
|
const endTime = Date.now();
|
|
2285
|
-
|
|
2418
|
+
_reportToWorld(world, {
|
|
2286
2419
|
element_name: selectors.element_name,
|
|
2287
2420
|
type: Types.GET_TABLE_DATA,
|
|
2288
2421
|
text: "Get table data",
|
|
@@ -2337,7 +2470,7 @@ class StableBrowser {
|
|
|
2337
2470
|
info.operation = "analyzeTable";
|
|
2338
2471
|
info.selectors = selectors;
|
|
2339
2472
|
info.query = query;
|
|
2340
|
-
query =
|
|
2473
|
+
query = _fixUsingParams(query, _params);
|
|
2341
2474
|
info.query_fixed = query;
|
|
2342
2475
|
info.operator = operator;
|
|
2343
2476
|
info.value = value;
|
|
@@ -2443,11 +2576,12 @@ class StableBrowser {
|
|
|
2443
2576
|
info.screenshotPath = screenshotPath;
|
|
2444
2577
|
Object.assign(e, { info: info });
|
|
2445
2578
|
error = e;
|
|
2446
|
-
throw e;
|
|
2579
|
+
// throw e;
|
|
2580
|
+
await _commandError({ text: "analyzeTable", operation: "analyzeTable", selectors, query, operator, value }, e, this);
|
|
2447
2581
|
}
|
|
2448
2582
|
finally {
|
|
2449
2583
|
const endTime = Date.now();
|
|
2450
|
-
|
|
2584
|
+
_reportToWorld(world, {
|
|
2451
2585
|
element_name: selectors.element_name,
|
|
2452
2586
|
type: Types.ANALYZE_TABLE,
|
|
2453
2587
|
text: "Analyze table",
|
|
@@ -2469,27 +2603,7 @@ class StableBrowser {
|
|
|
2469
2603
|
}
|
|
2470
2604
|
}
|
|
2471
2605
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2472
|
-
|
|
2473
|
-
return value;
|
|
2474
|
-
}
|
|
2475
|
-
// find all the accurance of {{(.*?)}} and replace with the value
|
|
2476
|
-
let regex = /{{(.*?)}}/g;
|
|
2477
|
-
let matches = value.match(regex);
|
|
2478
|
-
if (matches) {
|
|
2479
|
-
const testData = this.getTestData(world);
|
|
2480
|
-
for (let i = 0; i < matches.length; i++) {
|
|
2481
|
-
let match = matches[i];
|
|
2482
|
-
let key = match.substring(2, match.length - 2);
|
|
2483
|
-
let newValue = objectPath.get(testData, key, null);
|
|
2484
|
-
if (newValue !== null) {
|
|
2485
|
-
value = value.replace(match, newValue);
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
if ((value.startsWith("secret:") || value.startsWith("totp:")) && _decrypt) {
|
|
2490
|
-
return await decrypt(value, null, totpWait);
|
|
2491
|
-
}
|
|
2492
|
-
return value;
|
|
2606
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
2493
2607
|
}
|
|
2494
2608
|
_getLoadTimeout(options) {
|
|
2495
2609
|
let timeout = 15000;
|
|
@@ -2540,7 +2654,7 @@ class StableBrowser {
|
|
|
2540
2654
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2541
2655
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2542
2656
|
const endTime = Date.now();
|
|
2543
|
-
|
|
2657
|
+
_reportToWorld(world, {
|
|
2544
2658
|
type: Types.GET_PAGE_STATUS,
|
|
2545
2659
|
text: "Wait for page load",
|
|
2546
2660
|
screenshotId,
|
|
@@ -2560,41 +2674,35 @@ class StableBrowser {
|
|
|
2560
2674
|
}
|
|
2561
2675
|
}
|
|
2562
2676
|
async closePage(options = {}, world = null) {
|
|
2563
|
-
const
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2677
|
+
const state = {
|
|
2678
|
+
options,
|
|
2679
|
+
world,
|
|
2680
|
+
locate: false,
|
|
2681
|
+
scroll: false,
|
|
2682
|
+
highlight: false,
|
|
2683
|
+
type: Types.CLOSE_PAGE,
|
|
2684
|
+
text: `Close page`,
|
|
2685
|
+
operation: "closePage",
|
|
2686
|
+
log: "***** close page *****\n",
|
|
2687
|
+
throwError: false,
|
|
2688
|
+
};
|
|
2568
2689
|
try {
|
|
2690
|
+
await _preCommand(state, this);
|
|
2569
2691
|
await this.page.close();
|
|
2570
2692
|
}
|
|
2571
2693
|
catch (e) {
|
|
2572
2694
|
console.log(".");
|
|
2695
|
+
await _commandError(state, e, this);
|
|
2573
2696
|
}
|
|
2574
2697
|
finally {
|
|
2575
|
-
|
|
2576
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2577
|
-
const endTime = Date.now();
|
|
2578
|
-
this._reportToWorld(world, {
|
|
2579
|
-
type: Types.CLOSE_PAGE,
|
|
2580
|
-
text: "close page",
|
|
2581
|
-
screenshotId,
|
|
2582
|
-
result: error
|
|
2583
|
-
? {
|
|
2584
|
-
status: "FAILED",
|
|
2585
|
-
startTime,
|
|
2586
|
-
endTime,
|
|
2587
|
-
message: error?.message,
|
|
2588
|
-
}
|
|
2589
|
-
: {
|
|
2590
|
-
status: "PASSED",
|
|
2591
|
-
startTime,
|
|
2592
|
-
endTime,
|
|
2593
|
-
},
|
|
2594
|
-
info: info,
|
|
2595
|
-
});
|
|
2698
|
+
_commandFinally(state, this);
|
|
2596
2699
|
}
|
|
2597
2700
|
}
|
|
2701
|
+
saveTestDataAsGlobal(options, world) {
|
|
2702
|
+
const dataFile = this._getDataFile(world);
|
|
2703
|
+
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2704
|
+
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2705
|
+
}
|
|
2598
2706
|
async setViewportSize(width, hight, options = {}, world = null) {
|
|
2599
2707
|
const startTime = Date.now();
|
|
2600
2708
|
let error = null;
|
|
@@ -2612,12 +2720,13 @@ class StableBrowser {
|
|
|
2612
2720
|
}
|
|
2613
2721
|
catch (e) {
|
|
2614
2722
|
console.log(".");
|
|
2723
|
+
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
2615
2724
|
}
|
|
2616
2725
|
finally {
|
|
2617
2726
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2618
2727
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world));
|
|
2619
2728
|
const endTime = Date.now();
|
|
2620
|
-
|
|
2729
|
+
_reportToWorld(world, {
|
|
2621
2730
|
type: Types.SET_VIEWPORT,
|
|
2622
2731
|
text: "set viewport size to " + width + "x" + hight,
|
|
2623
2732
|
screenshotId,
|
|
@@ -2648,12 +2757,13 @@ class StableBrowser {
|
|
|
2648
2757
|
}
|
|
2649
2758
|
catch (e) {
|
|
2650
2759
|
console.log(".");
|
|
2760
|
+
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
2651
2761
|
}
|
|
2652
2762
|
finally {
|
|
2653
2763
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2654
2764
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2655
2765
|
const endTime = Date.now();
|
|
2656
|
-
|
|
2766
|
+
_reportToWorld(world, {
|
|
2657
2767
|
type: Types.GET_PAGE_STATUS,
|
|
2658
2768
|
text: "page relaod",
|
|
2659
2769
|
screenshotId,
|
|
@@ -2689,11 +2799,37 @@ class StableBrowser {
|
|
|
2689
2799
|
console.log("#-#");
|
|
2690
2800
|
}
|
|
2691
2801
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2802
|
+
async beforeStep(world, step) {
|
|
2803
|
+
this.stepName = step.pickleStep.text;
|
|
2804
|
+
this.logger.info("step: " + this.stepName);
|
|
2805
|
+
if (this.stepIndex === undefined) {
|
|
2806
|
+
this.stepIndex = 0;
|
|
2807
|
+
}
|
|
2808
|
+
else {
|
|
2809
|
+
this.stepIndex++;
|
|
2810
|
+
}
|
|
2811
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2812
|
+
if (this.context.browserObject.context) {
|
|
2813
|
+
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
2817
|
+
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
2818
|
+
// check if @global_test_data tag is present
|
|
2819
|
+
if (this.tags.includes("@global_test_data")) {
|
|
2820
|
+
this.saveTestDataAsGlobal({}, world);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
async afterStep(world, step) {
|
|
2825
|
+
this.stepName = null;
|
|
2826
|
+
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2827
|
+
if (this.context.browserObject.context) {
|
|
2828
|
+
await this.context.browserObject.context.tracing.stopChunk({
|
|
2829
|
+
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2695
2832
|
}
|
|
2696
|
-
world.attach(JSON.stringify(properties), { mediaType: "application/json" });
|
|
2697
2833
|
}
|
|
2698
2834
|
}
|
|
2699
2835
|
function createTimedPromise(promise, label) {
|
|
@@ -2701,156 +2837,5 @@ function createTimedPromise(promise, label) {
|
|
|
2701
2837
|
.then((result) => ({ status: "fulfilled", label, result }))
|
|
2702
2838
|
.catch((error) => Promise.reject({ status: "rejected", label, error }));
|
|
2703
2839
|
}
|
|
2704
|
-
const KEYBOARD_EVENTS = [
|
|
2705
|
-
"ALT",
|
|
2706
|
-
"AltGraph",
|
|
2707
|
-
"CapsLock",
|
|
2708
|
-
"Control",
|
|
2709
|
-
"Fn",
|
|
2710
|
-
"FnLock",
|
|
2711
|
-
"Hyper",
|
|
2712
|
-
"Meta",
|
|
2713
|
-
"NumLock",
|
|
2714
|
-
"ScrollLock",
|
|
2715
|
-
"Shift",
|
|
2716
|
-
"Super",
|
|
2717
|
-
"Symbol",
|
|
2718
|
-
"SymbolLock",
|
|
2719
|
-
"Enter",
|
|
2720
|
-
"Tab",
|
|
2721
|
-
"ArrowDown",
|
|
2722
|
-
"ArrowLeft",
|
|
2723
|
-
"ArrowRight",
|
|
2724
|
-
"ArrowUp",
|
|
2725
|
-
"End",
|
|
2726
|
-
"Home",
|
|
2727
|
-
"PageDown",
|
|
2728
|
-
"PageUp",
|
|
2729
|
-
"Backspace",
|
|
2730
|
-
"Clear",
|
|
2731
|
-
"Copy",
|
|
2732
|
-
"CrSel",
|
|
2733
|
-
"Cut",
|
|
2734
|
-
"Delete",
|
|
2735
|
-
"EraseEof",
|
|
2736
|
-
"ExSel",
|
|
2737
|
-
"Insert",
|
|
2738
|
-
"Paste",
|
|
2739
|
-
"Redo",
|
|
2740
|
-
"Undo",
|
|
2741
|
-
"Accept",
|
|
2742
|
-
"Again",
|
|
2743
|
-
"Attn",
|
|
2744
|
-
"Cancel",
|
|
2745
|
-
"ContextMenu",
|
|
2746
|
-
"Escape",
|
|
2747
|
-
"Execute",
|
|
2748
|
-
"Find",
|
|
2749
|
-
"Finish",
|
|
2750
|
-
"Help",
|
|
2751
|
-
"Pause",
|
|
2752
|
-
"Play",
|
|
2753
|
-
"Props",
|
|
2754
|
-
"Select",
|
|
2755
|
-
"ZoomIn",
|
|
2756
|
-
"ZoomOut",
|
|
2757
|
-
"BrightnessDown",
|
|
2758
|
-
"BrightnessUp",
|
|
2759
|
-
"Eject",
|
|
2760
|
-
"LogOff",
|
|
2761
|
-
"Power",
|
|
2762
|
-
"PowerOff",
|
|
2763
|
-
"PrintScreen",
|
|
2764
|
-
"Hibernate",
|
|
2765
|
-
"Standby",
|
|
2766
|
-
"WakeUp",
|
|
2767
|
-
"AllCandidates",
|
|
2768
|
-
"Alphanumeric",
|
|
2769
|
-
"CodeInput",
|
|
2770
|
-
"Compose",
|
|
2771
|
-
"Convert",
|
|
2772
|
-
"Dead",
|
|
2773
|
-
"FinalMode",
|
|
2774
|
-
"GroupFirst",
|
|
2775
|
-
"GroupLast",
|
|
2776
|
-
"GroupNext",
|
|
2777
|
-
"GroupPrevious",
|
|
2778
|
-
"ModeChange",
|
|
2779
|
-
"NextCandidate",
|
|
2780
|
-
"NonConvert",
|
|
2781
|
-
"PreviousCandidate",
|
|
2782
|
-
"Process",
|
|
2783
|
-
"SingleCandidate",
|
|
2784
|
-
"HangulMode",
|
|
2785
|
-
"HanjaMode",
|
|
2786
|
-
"JunjaMode",
|
|
2787
|
-
"Eisu",
|
|
2788
|
-
"Hankaku",
|
|
2789
|
-
"Hiragana",
|
|
2790
|
-
"HiraganaKatakana",
|
|
2791
|
-
"KanaMode",
|
|
2792
|
-
"KanjiMode",
|
|
2793
|
-
"Katakana",
|
|
2794
|
-
"Romaji",
|
|
2795
|
-
"Zenkaku",
|
|
2796
|
-
"ZenkakuHanaku",
|
|
2797
|
-
"F1",
|
|
2798
|
-
"F2",
|
|
2799
|
-
"F3",
|
|
2800
|
-
"F4",
|
|
2801
|
-
"F5",
|
|
2802
|
-
"F6",
|
|
2803
|
-
"F7",
|
|
2804
|
-
"F8",
|
|
2805
|
-
"F9",
|
|
2806
|
-
"F10",
|
|
2807
|
-
"F11",
|
|
2808
|
-
"F12",
|
|
2809
|
-
"Soft1",
|
|
2810
|
-
"Soft2",
|
|
2811
|
-
"Soft3",
|
|
2812
|
-
"Soft4",
|
|
2813
|
-
"ChannelDown",
|
|
2814
|
-
"ChannelUp",
|
|
2815
|
-
"Close",
|
|
2816
|
-
"MailForward",
|
|
2817
|
-
"MailReply",
|
|
2818
|
-
"MailSend",
|
|
2819
|
-
"MediaFastForward",
|
|
2820
|
-
"MediaPause",
|
|
2821
|
-
"MediaPlay",
|
|
2822
|
-
"MediaPlayPause",
|
|
2823
|
-
"MediaRecord",
|
|
2824
|
-
"MediaRewind",
|
|
2825
|
-
"MediaStop",
|
|
2826
|
-
"MediaTrackNext",
|
|
2827
|
-
"MediaTrackPrevious",
|
|
2828
|
-
"AudioBalanceLeft",
|
|
2829
|
-
"AudioBalanceRight",
|
|
2830
|
-
"AudioBassBoostDown",
|
|
2831
|
-
"AudioBassBoostToggle",
|
|
2832
|
-
"AudioBassBoostUp",
|
|
2833
|
-
"AudioFaderFront",
|
|
2834
|
-
"AudioFaderRear",
|
|
2835
|
-
"AudioSurroundModeNext",
|
|
2836
|
-
"AudioTrebleDown",
|
|
2837
|
-
"AudioTrebleUp",
|
|
2838
|
-
"AudioVolumeDown",
|
|
2839
|
-
"AudioVolumeMute",
|
|
2840
|
-
"AudioVolumeUp",
|
|
2841
|
-
"MicrophoneToggle",
|
|
2842
|
-
"MicrophoneVolumeDown",
|
|
2843
|
-
"MicrophoneVolumeMute",
|
|
2844
|
-
"MicrophoneVolumeUp",
|
|
2845
|
-
"TV",
|
|
2846
|
-
"TV3DMode",
|
|
2847
|
-
"TVAntennaCable",
|
|
2848
|
-
"TVAudioDescription",
|
|
2849
|
-
];
|
|
2850
|
-
function unEscapeString(str) {
|
|
2851
|
-
const placeholder = "__NEWLINE__";
|
|
2852
|
-
str = str.replace(new RegExp(placeholder, "g"), "\n");
|
|
2853
|
-
return str;
|
|
2854
|
-
}
|
|
2855
2840
|
export { StableBrowser };
|
|
2856
2841
|
//# sourceMappingURL=stable_browser.js.map
|