automation_model 1.0.565-dev → 1.0.565-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/README.md +130 -0
- package/lib/api.js +3 -0
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +4 -2
- package/lib/auto_page.js +105 -8
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +49 -45
- package/lib/browser_manager.js.map +1 -1
- package/lib/command_common.d.ts +4 -4
- package/lib/command_common.js +42 -20
- package/lib/command_common.js.map +1 -1
- package/lib/error-messages.js +18 -0
- package/lib/error-messages.js.map +1 -1
- package/lib/init_browser.d.ts +3 -2
- package/lib/init_browser.js +64 -11
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js +2 -2
- package/lib/locate_element.js.map +1 -1
- package/lib/network.d.ts +1 -1
- package/lib/network.js +46 -18
- package/lib/network.js.map +1 -1
- package/lib/stable_browser.d.ts +28 -9
- package/lib/stable_browser.js +729 -230
- package/lib/stable_browser.js.map +1 -1
- package/lib/table_helper.d.ts +19 -0
- package/lib/table_helper.js +101 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +3 -0
- package/lib/test_context.js +2 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +7 -2
- package/lib/utils.js +319 -12
- package/lib/utils.js.map +1 -1
- package/package.json +7 -8
- package/lib/scripts/find_text.js +0 -126
package/lib/stable_browser.js
CHANGED
|
@@ -10,18 +10,21 @@ 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 { _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
|
|
14
14
|
import csv from "csv-parser";
|
|
15
15
|
import { Readable } from "node:stream";
|
|
16
16
|
import readline from "readline";
|
|
17
|
-
import { getContext } from "./init_browser.js";
|
|
17
|
+
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
18
18
|
import { locate_element } from "./locate_element.js";
|
|
19
19
|
import { randomUUID } from "crypto";
|
|
20
20
|
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
21
21
|
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
22
22
|
import { LocatorLog } from "./locator_log.js";
|
|
23
|
+
import axios from "axios";
|
|
24
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
23
25
|
export const Types = {
|
|
24
26
|
CLICK: "click_element",
|
|
27
|
+
WAIT_ELEMENT: "wait_element",
|
|
25
28
|
NAVIGATE: "navigate",
|
|
26
29
|
FILL: "fill_element",
|
|
27
30
|
EXECUTE: "execute_page_method",
|
|
@@ -43,6 +46,7 @@ export const Types = {
|
|
|
43
46
|
UNCHECK: "uncheck_element",
|
|
44
47
|
EXTRACT: "extract_attribute",
|
|
45
48
|
CLOSE_PAGE: "close_page",
|
|
49
|
+
TABLE_OPERATION: "table_operation",
|
|
46
50
|
SET_DATE_TIME: "set_date_time",
|
|
47
51
|
SET_VIEWPORT: "set_viewport",
|
|
48
52
|
VERIFY_VISUAL: "verify_visual",
|
|
@@ -53,6 +57,9 @@ export const Types = {
|
|
|
53
57
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
54
58
|
};
|
|
55
59
|
export const apps = {};
|
|
60
|
+
const formatElementName = (elementName) => {
|
|
61
|
+
return elementName ? JSON.stringify(elementName) : "element";
|
|
62
|
+
};
|
|
56
63
|
class StableBrowser {
|
|
57
64
|
browser;
|
|
58
65
|
page;
|
|
@@ -66,6 +73,7 @@ class StableBrowser {
|
|
|
66
73
|
appName = "main";
|
|
67
74
|
tags = null;
|
|
68
75
|
isRecording = false;
|
|
76
|
+
initSnapshotTaken = false;
|
|
69
77
|
constructor(browser, page, logger = null, context = null, world = null) {
|
|
70
78
|
this.browser = browser;
|
|
71
79
|
this.page = page;
|
|
@@ -235,16 +243,51 @@ class StableBrowser {
|
|
|
235
243
|
// async closeUnexpectedPopups() {
|
|
236
244
|
// await closeUnexpectedPopups(this.page);
|
|
237
245
|
// }
|
|
238
|
-
async goto(url) {
|
|
246
|
+
async goto(url, world = null) {
|
|
247
|
+
if (!url) {
|
|
248
|
+
throw new Error("url is null, verify that the environment file is correct");
|
|
249
|
+
}
|
|
239
250
|
if (!url.startsWith("http")) {
|
|
240
251
|
url = "https://" + url;
|
|
241
252
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
253
|
+
const state = {
|
|
254
|
+
value: url,
|
|
255
|
+
world: world,
|
|
256
|
+
type: Types.NAVIGATE,
|
|
257
|
+
text: `Navigate Page to: ${url}`,
|
|
258
|
+
operation: "goto",
|
|
259
|
+
log: "***** navigate page to " + url + " *****\n",
|
|
260
|
+
info: {},
|
|
261
|
+
locate: false,
|
|
262
|
+
scroll: false,
|
|
263
|
+
screenshot: false,
|
|
264
|
+
highlight: false,
|
|
265
|
+
};
|
|
266
|
+
try {
|
|
267
|
+
await _preCommand(state, this);
|
|
268
|
+
await this.page.goto(url, {
|
|
269
|
+
timeout: 60000,
|
|
270
|
+
});
|
|
271
|
+
await _screenshot(state, this);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
console.error("Error on goto", error);
|
|
275
|
+
_commandError(state, error, this);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
_commandFinally(state, this);
|
|
279
|
+
}
|
|
245
280
|
}
|
|
246
|
-
_getLocator(locator, scope, _params) {
|
|
281
|
+
async _getLocator(locator, scope, _params) {
|
|
247
282
|
locator = _fixLocatorUsingParams(locator, _params);
|
|
283
|
+
// locator = await this._replaceWithLocalData(locator);
|
|
284
|
+
for (let key in locator) {
|
|
285
|
+
if (typeof locator[key] !== "string")
|
|
286
|
+
continue;
|
|
287
|
+
if (locator[key].includes("{{") && locator[key].includes("}}")) {
|
|
288
|
+
locator[key] = await this._replaceWithLocalData(locator[key], this.world);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
248
291
|
let locatorReturn;
|
|
249
292
|
if (locator.role) {
|
|
250
293
|
if (locator.role[1].nameReg) {
|
|
@@ -295,52 +338,60 @@ class StableBrowser {
|
|
|
295
338
|
if (css && css.locator) {
|
|
296
339
|
css = css.locator;
|
|
297
340
|
}
|
|
298
|
-
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, _params);
|
|
341
|
+
let result = await this._locateElementByText(scope, _fixUsingParams(text, _params), "*:not(script, style, head)", false, false, true, _params);
|
|
299
342
|
if (result.elementCount === 0) {
|
|
300
343
|
return;
|
|
301
344
|
}
|
|
302
|
-
let textElementCss = "[data-blinq-id
|
|
345
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
303
346
|
// css climb to parent element
|
|
304
347
|
const climbArray = [];
|
|
305
348
|
for (let i = 0; i < climb; i++) {
|
|
306
349
|
climbArray.push("..");
|
|
307
350
|
}
|
|
308
351
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
for (let i = 0; i < elements.length; i++) {
|
|
332
|
-
if (randomToken === null) {
|
|
333
|
-
randomToken = Math.random().toString(36).substring(7);
|
|
352
|
+
let resultCss = textElementCss + " >> " + climbXpath;
|
|
353
|
+
if (css) {
|
|
354
|
+
resultCss = resultCss + " >> " + css;
|
|
355
|
+
}
|
|
356
|
+
return resultCss;
|
|
357
|
+
}
|
|
358
|
+
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
359
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
360
|
+
const locator = scope.locator(query);
|
|
361
|
+
const count = await locator.count();
|
|
362
|
+
if (!tag1) {
|
|
363
|
+
tag1 = "*";
|
|
364
|
+
}
|
|
365
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
366
|
+
let tagCount = 0;
|
|
367
|
+
for (let i = 0; i < count; i++) {
|
|
368
|
+
const element = locator.nth(i);
|
|
369
|
+
// check if the tag matches
|
|
370
|
+
if (!(await element.evaluate((el, [tag, randomToken]) => {
|
|
371
|
+
if (!tag.startsWith("*")) {
|
|
372
|
+
if (el.tagName.toLowerCase() !== tag) {
|
|
373
|
+
return false;
|
|
334
374
|
}
|
|
335
|
-
let element = elements[i];
|
|
336
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
337
|
-
elementCount++;
|
|
338
375
|
}
|
|
376
|
+
if (!el.setAttribute) {
|
|
377
|
+
el = el.parentElement;
|
|
378
|
+
}
|
|
379
|
+
// remove any attributes start with data-blinq-id
|
|
380
|
+
// for (let i = 0; i < el.attributes.length; i++) {
|
|
381
|
+
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
382
|
+
// el.removeAttribute(el.attributes[i].name);
|
|
383
|
+
// }
|
|
384
|
+
// }
|
|
385
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
386
|
+
return true;
|
|
387
|
+
}, [tag1, randomToken]))) {
|
|
388
|
+
continue;
|
|
339
389
|
}
|
|
340
|
-
|
|
341
|
-
}
|
|
390
|
+
tagCount++;
|
|
391
|
+
}
|
|
392
|
+
return { elementCount: tagCount, randomToken };
|
|
342
393
|
}
|
|
343
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
394
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
344
395
|
if (!info) {
|
|
345
396
|
info = {};
|
|
346
397
|
}
|
|
@@ -363,30 +414,31 @@ class StableBrowser {
|
|
|
363
414
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
364
415
|
let locator = null;
|
|
365
416
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
366
|
-
|
|
417
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
418
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
367
419
|
if (!locatorString) {
|
|
368
420
|
info.failCause.textNotFound = true;
|
|
369
|
-
info.failCause.lastError =
|
|
421
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
370
422
|
return;
|
|
371
423
|
}
|
|
372
|
-
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
424
|
+
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
373
425
|
}
|
|
374
426
|
else if (locatorSearch.text) {
|
|
375
427
|
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
376
|
-
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, _params);
|
|
428
|
+
let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
|
|
377
429
|
if (result.elementCount === 0) {
|
|
378
430
|
info.failCause.textNotFound = true;
|
|
379
|
-
info.failCause.lastError =
|
|
431
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
380
432
|
return;
|
|
381
433
|
}
|
|
382
|
-
locatorSearch.css = "[data-blinq-id
|
|
434
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
383
435
|
if (locatorSearch.childCss) {
|
|
384
436
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
385
437
|
}
|
|
386
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
438
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
387
439
|
}
|
|
388
440
|
else {
|
|
389
|
-
locator = this._getLocator(locatorSearch, scope, _params);
|
|
441
|
+
locator = await this._getLocator(locatorSearch, scope, _params);
|
|
390
442
|
}
|
|
391
443
|
// let cssHref = false;
|
|
392
444
|
// if (locatorSearch.css && locatorSearch.css.includes("href=")) {
|
|
@@ -415,7 +467,7 @@ class StableBrowser {
|
|
|
415
467
|
if (!visibleOnly) {
|
|
416
468
|
visible = true;
|
|
417
469
|
}
|
|
418
|
-
if (visible && enabled) {
|
|
470
|
+
if (visible && (allowDisabled || enabled)) {
|
|
419
471
|
foundLocators.push(locator.nth(j));
|
|
420
472
|
if (info.locatorLog) {
|
|
421
473
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
@@ -428,9 +480,11 @@ class StableBrowser {
|
|
|
428
480
|
info.printMessages = {};
|
|
429
481
|
}
|
|
430
482
|
if (info.locatorLog && !visible) {
|
|
483
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
431
484
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
432
485
|
}
|
|
433
486
|
if (info.locatorLog && !enabled) {
|
|
487
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
434
488
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
435
489
|
}
|
|
436
490
|
if (!info.printMessages[j.toString()]) {
|
|
@@ -498,7 +552,7 @@ class StableBrowser {
|
|
|
498
552
|
}
|
|
499
553
|
return { rerun: false };
|
|
500
554
|
}
|
|
501
|
-
async _locate(selectors, info, _params, timeout) {
|
|
555
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
502
556
|
if (!timeout) {
|
|
503
557
|
timeout = 30000;
|
|
504
558
|
}
|
|
@@ -508,9 +562,30 @@ class StableBrowser {
|
|
|
508
562
|
let selector = selectors.locators[j];
|
|
509
563
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
510
564
|
}
|
|
511
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
565
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
512
566
|
if (!element.rerun) {
|
|
513
|
-
|
|
567
|
+
const randomToken = Math.random().toString(36).substring(7);
|
|
568
|
+
element.evaluate((el, randomToken) => {
|
|
569
|
+
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
570
|
+
}, randomToken);
|
|
571
|
+
// if (element._frame) {
|
|
572
|
+
// return element;
|
|
573
|
+
// }
|
|
574
|
+
const scope = element._frame ?? element.page();
|
|
575
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
576
|
+
let prefixSelector = "";
|
|
577
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
578
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
579
|
+
if (frameSelectorIndex !== -1) {
|
|
580
|
+
// remove everything after the >> internal:control=enter-frame
|
|
581
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
582
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame";
|
|
583
|
+
}
|
|
584
|
+
// if (element?._frame?._selector) {
|
|
585
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
586
|
+
// }
|
|
587
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
588
|
+
return scope.locator(newSelector);
|
|
514
589
|
}
|
|
515
590
|
}
|
|
516
591
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -583,7 +658,7 @@ class StableBrowser {
|
|
|
583
658
|
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
584
659
|
if (Date.now() - startTime > timeout) {
|
|
585
660
|
info.failCause.iframeNotFound = true;
|
|
586
|
-
info.failCause.lastError =
|
|
661
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
587
662
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
588
663
|
}
|
|
589
664
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -608,7 +683,7 @@ class StableBrowser {
|
|
|
608
683
|
return bodyContent;
|
|
609
684
|
});
|
|
610
685
|
}
|
|
611
|
-
async _locate_internal(selectors, info, _params, timeout = 30000) {
|
|
686
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
612
687
|
if (!info) {
|
|
613
688
|
info = {};
|
|
614
689
|
info.failCause = {};
|
|
@@ -657,17 +732,17 @@ class StableBrowser {
|
|
|
657
732
|
}
|
|
658
733
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
659
734
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
660
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
735
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
661
736
|
if (result.foundElements.length === 0) {
|
|
662
737
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
663
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
738
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
664
739
|
}
|
|
665
740
|
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
666
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
741
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
667
742
|
}
|
|
668
743
|
else {
|
|
669
744
|
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
670
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
745
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
671
746
|
}
|
|
672
747
|
}
|
|
673
748
|
let foundElements = result.foundElements;
|
|
@@ -712,7 +787,7 @@ class StableBrowser {
|
|
|
712
787
|
break;
|
|
713
788
|
}
|
|
714
789
|
if (Date.now() - startTime > highPriorityTimeout) {
|
|
715
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
790
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
716
791
|
highPriorityOnly = false;
|
|
717
792
|
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
718
793
|
lazy_scroll = true;
|
|
@@ -720,7 +795,7 @@ class StableBrowser {
|
|
|
720
795
|
}
|
|
721
796
|
}
|
|
722
797
|
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
723
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
798
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
724
799
|
visibleOnly = false;
|
|
725
800
|
}
|
|
726
801
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -734,10 +809,12 @@ class StableBrowser {
|
|
|
734
809
|
// }
|
|
735
810
|
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
736
811
|
info.failCause.locatorNotFound = true;
|
|
737
|
-
info
|
|
812
|
+
if (!info?.failCause?.lastError) {
|
|
813
|
+
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
814
|
+
}
|
|
738
815
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
739
816
|
}
|
|
740
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
817
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
741
818
|
let foundElements = [];
|
|
742
819
|
const result = {
|
|
743
820
|
foundElements: foundElements,
|
|
@@ -745,7 +822,7 @@ class StableBrowser {
|
|
|
745
822
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
746
823
|
let foundLocators = [];
|
|
747
824
|
try {
|
|
748
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
825
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
749
826
|
}
|
|
750
827
|
catch (e) {
|
|
751
828
|
// this call can fail it the browser is navigating
|
|
@@ -753,7 +830,7 @@ class StableBrowser {
|
|
|
753
830
|
// this.logger.debug(e);
|
|
754
831
|
foundLocators = [];
|
|
755
832
|
try {
|
|
756
|
-
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
|
|
833
|
+
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
757
834
|
}
|
|
758
835
|
catch (e) {
|
|
759
836
|
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
@@ -768,9 +845,40 @@ class StableBrowser {
|
|
|
768
845
|
result.locatorIndex = i;
|
|
769
846
|
}
|
|
770
847
|
if (foundLocators.length > 1) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
848
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
849
|
+
const boxes = [];
|
|
850
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
851
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
852
|
+
}
|
|
853
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
854
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
855
|
+
if (j === k) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
859
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
860
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
861
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
862
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
863
|
+
// as the element is not unique, will remove it
|
|
864
|
+
boxes.splice(k, 1);
|
|
865
|
+
k--;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (boxes.length === 1) {
|
|
870
|
+
result.foundElements.push({
|
|
871
|
+
locator: boxes[0].locator.first(),
|
|
872
|
+
box: boxes[0].box,
|
|
873
|
+
unique: true,
|
|
874
|
+
});
|
|
875
|
+
result.locatorIndex = i;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
info.failCause.foundMultiple = true;
|
|
879
|
+
if (info.locatorLog) {
|
|
880
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
881
|
+
}
|
|
774
882
|
}
|
|
775
883
|
}
|
|
776
884
|
}
|
|
@@ -881,25 +989,14 @@ class StableBrowser {
|
|
|
881
989
|
options,
|
|
882
990
|
world,
|
|
883
991
|
text: "Click element",
|
|
992
|
+
_text: "Click on " + selectors.element_name,
|
|
884
993
|
type: Types.CLICK,
|
|
885
994
|
operation: "click",
|
|
886
995
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
887
996
|
};
|
|
888
997
|
try {
|
|
889
998
|
await _preCommand(state, this);
|
|
890
|
-
|
|
891
|
-
state.selectors.locators[0].text = state.options.context;
|
|
892
|
-
}
|
|
893
|
-
try {
|
|
894
|
-
await state.element.click();
|
|
895
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
896
|
-
}
|
|
897
|
-
catch (e) {
|
|
898
|
-
// await this.closeUnexpectedPopups();
|
|
899
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
900
|
-
await state.element.dispatchEvent("click");
|
|
901
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
902
|
-
}
|
|
999
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
903
1000
|
await this.waitForPageLoad();
|
|
904
1001
|
return state.info;
|
|
905
1002
|
}
|
|
@@ -910,6 +1007,38 @@ class StableBrowser {
|
|
|
910
1007
|
_commandFinally(state, this);
|
|
911
1008
|
}
|
|
912
1009
|
}
|
|
1010
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1011
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1012
|
+
const state = {
|
|
1013
|
+
selectors,
|
|
1014
|
+
_params,
|
|
1015
|
+
options,
|
|
1016
|
+
world,
|
|
1017
|
+
text: "Wait for element",
|
|
1018
|
+
_text: "Wait for " + selectors.element_name,
|
|
1019
|
+
type: Types.WAIT_ELEMENT,
|
|
1020
|
+
operation: "waitForElement",
|
|
1021
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1022
|
+
};
|
|
1023
|
+
let found = false;
|
|
1024
|
+
try {
|
|
1025
|
+
await _preCommand(state, this);
|
|
1026
|
+
// if (state.options && state.options.context) {
|
|
1027
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1028
|
+
// }
|
|
1029
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1030
|
+
found = true;
|
|
1031
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1032
|
+
}
|
|
1033
|
+
catch (e) {
|
|
1034
|
+
console.error("Error on waitForElement", e);
|
|
1035
|
+
// await _commandError(state, e, this);
|
|
1036
|
+
}
|
|
1037
|
+
finally {
|
|
1038
|
+
_commandFinally(state, this);
|
|
1039
|
+
}
|
|
1040
|
+
return found;
|
|
1041
|
+
}
|
|
913
1042
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
914
1043
|
const state = {
|
|
915
1044
|
selectors,
|
|
@@ -918,6 +1047,7 @@ class StableBrowser {
|
|
|
918
1047
|
world,
|
|
919
1048
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
920
1049
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1050
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
921
1051
|
operation: "setCheck",
|
|
922
1052
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
923
1053
|
};
|
|
@@ -927,9 +1057,15 @@ class StableBrowser {
|
|
|
927
1057
|
// let element = await this._locate(selectors, info, _params);
|
|
928
1058
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
929
1059
|
try {
|
|
930
|
-
//
|
|
1060
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1061
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1062
|
+
await this._highlightElements(state.element);
|
|
931
1063
|
await state.element.setChecked(checked);
|
|
932
1064
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1065
|
+
// await this._unHighlightElements(element);
|
|
1066
|
+
// }
|
|
1067
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1068
|
+
// await this._unHighlightElements(element);
|
|
933
1069
|
}
|
|
934
1070
|
catch (e) {
|
|
935
1071
|
if (e.message && e.message.includes("did not change its state")) {
|
|
@@ -961,22 +1097,13 @@ class StableBrowser {
|
|
|
961
1097
|
world,
|
|
962
1098
|
type: Types.HOVER,
|
|
963
1099
|
text: `Hover element`,
|
|
1100
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
964
1101
|
operation: "hover",
|
|
965
1102
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
966
1103
|
};
|
|
967
1104
|
try {
|
|
968
1105
|
await _preCommand(state, this);
|
|
969
|
-
|
|
970
|
-
await state.element.hover();
|
|
971
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
972
|
-
}
|
|
973
|
-
catch (e) {
|
|
974
|
-
//await this.closeUnexpectedPopups();
|
|
975
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
976
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
977
|
-
await state.element.hover({ timeout: 10000 });
|
|
978
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
979
|
-
}
|
|
1106
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
980
1107
|
await _screenshot(state, this);
|
|
981
1108
|
await this.waitForPageLoad();
|
|
982
1109
|
return state.info;
|
|
@@ -1000,6 +1127,7 @@ class StableBrowser {
|
|
|
1000
1127
|
value: values.toString(),
|
|
1001
1128
|
type: Types.SELECT,
|
|
1002
1129
|
text: `Select option: ${values}`,
|
|
1130
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1003
1131
|
operation: "selectOption",
|
|
1004
1132
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1005
1133
|
};
|
|
@@ -1034,6 +1162,7 @@ class StableBrowser {
|
|
|
1034
1162
|
highlight: false,
|
|
1035
1163
|
type: Types.TYPE_PRESS,
|
|
1036
1164
|
text: `Type value: ${_value}`,
|
|
1165
|
+
_text: `Type value: ${_value}`,
|
|
1037
1166
|
operation: "type",
|
|
1038
1167
|
log: "",
|
|
1039
1168
|
};
|
|
@@ -1113,6 +1242,7 @@ class StableBrowser {
|
|
|
1113
1242
|
world,
|
|
1114
1243
|
type: Types.SET_DATE_TIME,
|
|
1115
1244
|
text: `Set date time value: ${value}`,
|
|
1245
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1116
1246
|
operation: "setDateTime",
|
|
1117
1247
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1118
1248
|
throwError: false,
|
|
@@ -1120,7 +1250,7 @@ class StableBrowser {
|
|
|
1120
1250
|
try {
|
|
1121
1251
|
await _preCommand(state, this);
|
|
1122
1252
|
try {
|
|
1123
|
-
await state.element
|
|
1253
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1124
1254
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1125
1255
|
if (format) {
|
|
1126
1256
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1184,9 +1314,13 @@ class StableBrowser {
|
|
|
1184
1314
|
world,
|
|
1185
1315
|
type: Types.FILL,
|
|
1186
1316
|
text: `Click type input with value: ${_value}`,
|
|
1317
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1187
1318
|
operation: "clickType",
|
|
1188
1319
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1189
1320
|
};
|
|
1321
|
+
if (!options) {
|
|
1322
|
+
options = {};
|
|
1323
|
+
}
|
|
1190
1324
|
if (newValue !== _value) {
|
|
1191
1325
|
//this.logger.info(_value + "=" + newValue);
|
|
1192
1326
|
_value = newValue;
|
|
@@ -1194,7 +1328,7 @@ class StableBrowser {
|
|
|
1194
1328
|
try {
|
|
1195
1329
|
await _preCommand(state, this);
|
|
1196
1330
|
state.info.value = _value;
|
|
1197
|
-
if (
|
|
1331
|
+
if (!options.press) {
|
|
1198
1332
|
try {
|
|
1199
1333
|
let currentValue = await state.element.inputValue();
|
|
1200
1334
|
if (currentValue) {
|
|
@@ -1205,13 +1339,9 @@ class StableBrowser {
|
|
|
1205
1339
|
this.logger.info("unable to clear input value");
|
|
1206
1340
|
}
|
|
1207
1341
|
}
|
|
1208
|
-
if (options
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1212
|
-
catch (e) {
|
|
1213
|
-
await state.element.dispatchEvent("click");
|
|
1214
|
-
}
|
|
1342
|
+
if (options.press) {
|
|
1343
|
+
options.timeout = 5000;
|
|
1344
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1215
1345
|
}
|
|
1216
1346
|
else {
|
|
1217
1347
|
try {
|
|
@@ -1249,7 +1379,12 @@ class StableBrowser {
|
|
|
1249
1379
|
await this.waitForPageLoad();
|
|
1250
1380
|
}
|
|
1251
1381
|
else if (enter === false) {
|
|
1252
|
-
|
|
1382
|
+
try {
|
|
1383
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1384
|
+
}
|
|
1385
|
+
catch (e) {
|
|
1386
|
+
// ignore
|
|
1387
|
+
}
|
|
1253
1388
|
//await this.page.keyboard.press("Tab");
|
|
1254
1389
|
}
|
|
1255
1390
|
else {
|
|
@@ -1301,6 +1436,7 @@ class StableBrowser {
|
|
|
1301
1436
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1302
1437
|
}
|
|
1303
1438
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1439
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1304
1440
|
_validateSelectors(selectors);
|
|
1305
1441
|
let screenshotId = null;
|
|
1306
1442
|
let screenshotPath = null;
|
|
@@ -1310,7 +1446,7 @@ class StableBrowser {
|
|
|
1310
1446
|
}
|
|
1311
1447
|
info.operation = "getText";
|
|
1312
1448
|
info.selectors = selectors;
|
|
1313
|
-
let element = await this._locate(selectors, info, _params);
|
|
1449
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1314
1450
|
if (climb > 0) {
|
|
1315
1451
|
const climbArray = [];
|
|
1316
1452
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1329,6 +1465,18 @@ class StableBrowser {
|
|
|
1329
1465
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1330
1466
|
try {
|
|
1331
1467
|
await this._highlightElements(element);
|
|
1468
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1469
|
+
// // console.log(`Highlighting for get text while running from recorder`);
|
|
1470
|
+
// this._highlightElements(element)
|
|
1471
|
+
// .then(async () => {
|
|
1472
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1473
|
+
// this._unhighlightElements(element).then(
|
|
1474
|
+
// () => {}
|
|
1475
|
+
// // console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
1476
|
+
// );
|
|
1477
|
+
// })
|
|
1478
|
+
// .catch(e);
|
|
1479
|
+
// }
|
|
1332
1480
|
const elementText = await element.innerText();
|
|
1333
1481
|
return {
|
|
1334
1482
|
text: elementText,
|
|
@@ -1340,7 +1488,7 @@ class StableBrowser {
|
|
|
1340
1488
|
}
|
|
1341
1489
|
catch (e) {
|
|
1342
1490
|
//await this.closeUnexpectedPopups();
|
|
1343
|
-
this.logger.info("no innerText will use textContent");
|
|
1491
|
+
this.logger.info("no innerText, will use textContent");
|
|
1344
1492
|
const elementText = await element.textContent();
|
|
1345
1493
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1346
1494
|
}
|
|
@@ -1365,6 +1513,7 @@ class StableBrowser {
|
|
|
1365
1513
|
highlight: false,
|
|
1366
1514
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1367
1515
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1516
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1368
1517
|
operation: "containsPattern",
|
|
1369
1518
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1370
1519
|
};
|
|
@@ -1400,6 +1549,8 @@ class StableBrowser {
|
|
|
1400
1549
|
}
|
|
1401
1550
|
}
|
|
1402
1551
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1552
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1553
|
+
const startTime = Date.now();
|
|
1403
1554
|
const state = {
|
|
1404
1555
|
selectors,
|
|
1405
1556
|
_params,
|
|
@@ -1426,62 +1577,54 @@ class StableBrowser {
|
|
|
1426
1577
|
}
|
|
1427
1578
|
let foundObj = null;
|
|
1428
1579
|
try {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1436
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1437
|
-
if (dateAlternatives.date) {
|
|
1438
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1439
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1440
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1441
|
-
return state.info;
|
|
1580
|
+
while (Date.now() - startTime < timeout) {
|
|
1581
|
+
try {
|
|
1582
|
+
await _preCommand(state, this);
|
|
1583
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
|
|
1584
|
+
if (foundObj && foundObj.element) {
|
|
1585
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1442
1586
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1587
|
+
await _screenshot(state, this);
|
|
1588
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1589
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1590
|
+
if (dateAlternatives.date) {
|
|
1591
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1592
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1593
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1594
|
+
return state.info;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
else if (numberAlternatives.number) {
|
|
1599
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1600
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1601
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1602
|
+
return state.info;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1450
1607
|
return state.info;
|
|
1451
1608
|
}
|
|
1452
1609
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
throw new Error("element doesn't contain text " + text);
|
|
1610
|
+
catch (e) {
|
|
1611
|
+
// Log error but continue retrying until timeout is reached
|
|
1612
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1613
|
+
}
|
|
1614
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1459
1615
|
}
|
|
1460
|
-
|
|
1616
|
+
state.info.foundText = foundObj?.text;
|
|
1617
|
+
state.info.value = foundObj?.value;
|
|
1618
|
+
throw new Error("element doesn't contain text " + text);
|
|
1461
1619
|
}
|
|
1462
1620
|
catch (e) {
|
|
1463
1621
|
await _commandError(state, e, this);
|
|
1622
|
+
throw e;
|
|
1464
1623
|
}
|
|
1465
1624
|
finally {
|
|
1466
1625
|
_commandFinally(state, this);
|
|
1467
1626
|
}
|
|
1468
1627
|
}
|
|
1469
|
-
_getDataFile(world = null) {
|
|
1470
|
-
let dataFile = null;
|
|
1471
|
-
if (world && world.reportFolder) {
|
|
1472
|
-
dataFile = path.join(world.reportFolder, "data.json");
|
|
1473
|
-
}
|
|
1474
|
-
else if (this.reportFolder) {
|
|
1475
|
-
dataFile = path.join(this.reportFolder, "data.json");
|
|
1476
|
-
}
|
|
1477
|
-
else if (this.context && this.context.reportFolder) {
|
|
1478
|
-
dataFile = path.join(this.context.reportFolder, "data.json");
|
|
1479
|
-
}
|
|
1480
|
-
else {
|
|
1481
|
-
dataFile = "data.json";
|
|
1482
|
-
}
|
|
1483
|
-
return dataFile;
|
|
1484
|
-
}
|
|
1485
1628
|
async waitForUserInput(message, world = null) {
|
|
1486
1629
|
if (!message) {
|
|
1487
1630
|
message = "# Wait for user input. Press any key to continue";
|
|
@@ -1510,13 +1653,22 @@ class StableBrowser {
|
|
|
1510
1653
|
return;
|
|
1511
1654
|
}
|
|
1512
1655
|
// if data file exists, load it
|
|
1513
|
-
const dataFile =
|
|
1656
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1514
1657
|
let data = this.getTestData(world);
|
|
1515
1658
|
// merge the testData with the existing data
|
|
1516
1659
|
Object.assign(data, testData);
|
|
1517
1660
|
// save the data to the file
|
|
1518
1661
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1519
1662
|
}
|
|
1663
|
+
overwriteTestData(testData, world = null) {
|
|
1664
|
+
if (!testData) {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
// if data file exists, load it
|
|
1668
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1669
|
+
// save the data to the file
|
|
1670
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1671
|
+
}
|
|
1520
1672
|
_getDataFilePath(fileName) {
|
|
1521
1673
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1522
1674
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1613,7 +1765,7 @@ class StableBrowser {
|
|
|
1613
1765
|
}
|
|
1614
1766
|
}
|
|
1615
1767
|
getTestData(world = null) {
|
|
1616
|
-
const dataFile =
|
|
1768
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1617
1769
|
let data = {};
|
|
1618
1770
|
if (fs.existsSync(dataFile)) {
|
|
1619
1771
|
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
@@ -1700,6 +1852,15 @@ class StableBrowser {
|
|
|
1700
1852
|
document.documentElement.clientWidth,
|
|
1701
1853
|
])));
|
|
1702
1854
|
let screenshotBuffer = null;
|
|
1855
|
+
// if (focusedElement) {
|
|
1856
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
1857
|
+
// await this._unhighlightElements(focusedElement);
|
|
1858
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1859
|
+
// console.log(`Unhighlighted previous element`);
|
|
1860
|
+
// }
|
|
1861
|
+
// if (focusedElement) {
|
|
1862
|
+
// await this._highlightElements(focusedElement);
|
|
1863
|
+
// }
|
|
1703
1864
|
if (this.context.browserName === "chromium") {
|
|
1704
1865
|
const client = await playContext.newCDPSession(this.page);
|
|
1705
1866
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
@@ -1721,6 +1882,10 @@ class StableBrowser {
|
|
|
1721
1882
|
else {
|
|
1722
1883
|
screenshotBuffer = await this.page.screenshot();
|
|
1723
1884
|
}
|
|
1885
|
+
// if (focusedElement) {
|
|
1886
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
1887
|
+
// await this._unhighlightElements(focusedElement);
|
|
1888
|
+
// }
|
|
1724
1889
|
let image = await Jimp.read(screenshotBuffer);
|
|
1725
1890
|
// Get the image dimensions
|
|
1726
1891
|
const { width, height } = image.bitmap;
|
|
@@ -1733,6 +1898,7 @@ class StableBrowser {
|
|
|
1733
1898
|
else {
|
|
1734
1899
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1735
1900
|
}
|
|
1901
|
+
return screenshotBuffer;
|
|
1736
1902
|
}
|
|
1737
1903
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1738
1904
|
const state = {
|
|
@@ -1768,8 +1934,10 @@ class StableBrowser {
|
|
|
1768
1934
|
world,
|
|
1769
1935
|
type: Types.EXTRACT,
|
|
1770
1936
|
text: `Extract attribute from element`,
|
|
1937
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1771
1938
|
operation: "extractAttribute",
|
|
1772
1939
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1940
|
+
allowDisabled: true,
|
|
1773
1941
|
};
|
|
1774
1942
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1775
1943
|
try {
|
|
@@ -1791,6 +1959,7 @@ class StableBrowser {
|
|
|
1791
1959
|
state.info.value = state.value;
|
|
1792
1960
|
this.setTestData({ [variable]: state.value }, world);
|
|
1793
1961
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
1962
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1794
1963
|
return state.info;
|
|
1795
1964
|
}
|
|
1796
1965
|
catch (e) {
|
|
@@ -1809,14 +1978,21 @@ class StableBrowser {
|
|
|
1809
1978
|
options,
|
|
1810
1979
|
world,
|
|
1811
1980
|
type: Types.VERIFY_ATTRIBUTE,
|
|
1981
|
+
highlight: true,
|
|
1982
|
+
screenshot: true,
|
|
1812
1983
|
text: `Verify element attribute`,
|
|
1984
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
1813
1985
|
operation: "verifyAttribute",
|
|
1814
1986
|
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1987
|
+
allowDisabled: true,
|
|
1815
1988
|
};
|
|
1816
1989
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1817
1990
|
let val;
|
|
1991
|
+
let expectedValue;
|
|
1818
1992
|
try {
|
|
1819
1993
|
await _preCommand(state, this);
|
|
1994
|
+
expectedValue = state.value;
|
|
1995
|
+
state.info.expectedValue = expectedValue;
|
|
1820
1996
|
switch (attribute) {
|
|
1821
1997
|
case "innerText":
|
|
1822
1998
|
val = String(await state.element.innerText());
|
|
@@ -1830,23 +2006,30 @@ class StableBrowser {
|
|
|
1830
2006
|
case "disabled":
|
|
1831
2007
|
val = String(await state.element.isDisabled());
|
|
1832
2008
|
break;
|
|
2009
|
+
case "readOnly":
|
|
2010
|
+
const isEditable = await state.element.isEditable();
|
|
2011
|
+
val = String(!isEditable);
|
|
2012
|
+
break;
|
|
1833
2013
|
default:
|
|
1834
2014
|
val = String(await state.element.getAttribute(attribute));
|
|
1835
2015
|
break;
|
|
1836
2016
|
}
|
|
2017
|
+
state.info.value = val;
|
|
1837
2018
|
let regex;
|
|
1838
|
-
if (
|
|
1839
|
-
const patternBody =
|
|
2019
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2020
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
1840
2021
|
regex = new RegExp(patternBody, "g");
|
|
1841
2022
|
}
|
|
1842
2023
|
else {
|
|
1843
|
-
const escapedPattern =
|
|
2024
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1844
2025
|
regex = new RegExp(escapedPattern, "g");
|
|
1845
2026
|
}
|
|
1846
2027
|
if (!val.match(regex)) {
|
|
1847
|
-
|
|
2028
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2029
|
+
state.info.failCause.assertionFailed = true;
|
|
2030
|
+
state.info.failCause.lastError = errorMessage;
|
|
2031
|
+
throw new Error(errorMessage);
|
|
1848
2032
|
}
|
|
1849
|
-
state.info.value = val;
|
|
1850
2033
|
return state.info;
|
|
1851
2034
|
}
|
|
1852
2035
|
catch (e) {
|
|
@@ -1945,27 +2128,32 @@ class StableBrowser {
|
|
|
1945
2128
|
async _highlightElements(scope, css) {
|
|
1946
2129
|
try {
|
|
1947
2130
|
if (!scope) {
|
|
2131
|
+
// console.log(`Scope is not defined`);
|
|
1948
2132
|
return;
|
|
1949
2133
|
}
|
|
1950
2134
|
if (!css) {
|
|
1951
2135
|
scope
|
|
1952
2136
|
.evaluate((node) => {
|
|
1953
2137
|
if (node && node.style) {
|
|
1954
|
-
let
|
|
1955
|
-
|
|
2138
|
+
let originalOutline = node.style.outline;
|
|
2139
|
+
// console.log(`Original outline was: ${originalOutline}`);
|
|
2140
|
+
// node.__previousOutline = originalOutline;
|
|
2141
|
+
node.style.outline = "2px solid red";
|
|
2142
|
+
// console.log(`New outline is: ${node.style.outline}`);
|
|
1956
2143
|
if (window) {
|
|
1957
2144
|
window.addEventListener("beforeunload", function (e) {
|
|
1958
|
-
node.style.
|
|
2145
|
+
node.style.outline = originalOutline;
|
|
1959
2146
|
});
|
|
1960
2147
|
}
|
|
1961
2148
|
setTimeout(function () {
|
|
1962
|
-
node.style.
|
|
2149
|
+
node.style.outline = originalOutline;
|
|
1963
2150
|
}, 2000);
|
|
1964
2151
|
}
|
|
1965
2152
|
})
|
|
1966
2153
|
.then(() => { })
|
|
1967
2154
|
.catch((e) => {
|
|
1968
2155
|
// ignore
|
|
2156
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
1969
2157
|
});
|
|
1970
2158
|
}
|
|
1971
2159
|
else {
|
|
@@ -1981,17 +2169,18 @@ class StableBrowser {
|
|
|
1981
2169
|
if (!element.style) {
|
|
1982
2170
|
return;
|
|
1983
2171
|
}
|
|
1984
|
-
|
|
2172
|
+
let originalOutline = element.style.outline;
|
|
2173
|
+
element.__previousOutline = originalOutline;
|
|
1985
2174
|
// Set the new border to be red and 2px solid
|
|
1986
|
-
element.style.
|
|
2175
|
+
element.style.outline = "2px solid red";
|
|
1987
2176
|
if (window) {
|
|
1988
2177
|
window.addEventListener("beforeunload", function (e) {
|
|
1989
|
-
element.style.
|
|
2178
|
+
element.style.outline = originalOutline;
|
|
1990
2179
|
});
|
|
1991
2180
|
}
|
|
1992
2181
|
// Set a timeout to revert to the original border after 2 seconds
|
|
1993
2182
|
setTimeout(function () {
|
|
1994
|
-
element.style.
|
|
2183
|
+
element.style.outline = originalOutline;
|
|
1995
2184
|
}, 2000);
|
|
1996
2185
|
}
|
|
1997
2186
|
return;
|
|
@@ -1999,6 +2188,7 @@ class StableBrowser {
|
|
|
1999
2188
|
.then(() => { })
|
|
2000
2189
|
.catch((e) => {
|
|
2001
2190
|
// ignore
|
|
2191
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2002
2192
|
});
|
|
2003
2193
|
}
|
|
2004
2194
|
}
|
|
@@ -2006,6 +2196,54 @@ class StableBrowser {
|
|
|
2006
2196
|
console.debug(error);
|
|
2007
2197
|
}
|
|
2008
2198
|
}
|
|
2199
|
+
// async _unhighlightElements(scope, css) {
|
|
2200
|
+
// try {
|
|
2201
|
+
// if (!scope) {
|
|
2202
|
+
// return;
|
|
2203
|
+
// }
|
|
2204
|
+
// if (!css) {
|
|
2205
|
+
// scope
|
|
2206
|
+
// .evaluate((node) => {
|
|
2207
|
+
// if (node && node.style) {
|
|
2208
|
+
// if (!node.__previousOutline) {
|
|
2209
|
+
// node.style.outline = "";
|
|
2210
|
+
// } else {
|
|
2211
|
+
// node.style.outline = node.__previousOutline;
|
|
2212
|
+
// }
|
|
2213
|
+
// }
|
|
2214
|
+
// })
|
|
2215
|
+
// .then(() => {})
|
|
2216
|
+
// .catch((e) => {
|
|
2217
|
+
// // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
|
|
2218
|
+
// });
|
|
2219
|
+
// } else {
|
|
2220
|
+
// scope
|
|
2221
|
+
// .evaluate(([css]) => {
|
|
2222
|
+
// if (!css) {
|
|
2223
|
+
// return;
|
|
2224
|
+
// }
|
|
2225
|
+
// let elements = Array.from(document.querySelectorAll(css));
|
|
2226
|
+
// for (i = 0; i < elements.length; i++) {
|
|
2227
|
+
// let element = elements[i];
|
|
2228
|
+
// if (!element.style) {
|
|
2229
|
+
// return;
|
|
2230
|
+
// }
|
|
2231
|
+
// if (!element.__previousOutline) {
|
|
2232
|
+
// element.style.outline = "";
|
|
2233
|
+
// } else {
|
|
2234
|
+
// element.style.outline = element.__previousOutline;
|
|
2235
|
+
// }
|
|
2236
|
+
// }
|
|
2237
|
+
// })
|
|
2238
|
+
// .then(() => {})
|
|
2239
|
+
// .catch((e) => {
|
|
2240
|
+
// // console.error(`Error while unhighlighting element in css: ${e}`);
|
|
2241
|
+
// });
|
|
2242
|
+
// }
|
|
2243
|
+
// } catch (error) {
|
|
2244
|
+
// // console.debug(error);
|
|
2245
|
+
// }
|
|
2246
|
+
// }
|
|
2009
2247
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2010
2248
|
const startTime = Date.now();
|
|
2011
2249
|
let error = null;
|
|
@@ -2050,6 +2288,7 @@ class StableBrowser {
|
|
|
2050
2288
|
_reportToWorld(world, {
|
|
2051
2289
|
type: Types.VERIFY_PAGE_PATH,
|
|
2052
2290
|
text: "Verify page path",
|
|
2291
|
+
_text: "Verify the page path contains " + pathPart,
|
|
2053
2292
|
screenshotId,
|
|
2054
2293
|
result: error
|
|
2055
2294
|
? {
|
|
@@ -2067,26 +2306,27 @@ class StableBrowser {
|
|
|
2067
2306
|
});
|
|
2068
2307
|
}
|
|
2069
2308
|
}
|
|
2070
|
-
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2309
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2071
2310
|
const frames = this.page.frames();
|
|
2072
2311
|
let results = [];
|
|
2312
|
+
// let ignoreCase = false;
|
|
2073
2313
|
for (let i = 0; i < frames.length; i++) {
|
|
2074
2314
|
if (dateAlternatives.date) {
|
|
2075
2315
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2076
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2316
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2077
2317
|
result.frame = frames[i];
|
|
2078
2318
|
results.push(result);
|
|
2079
2319
|
}
|
|
2080
2320
|
}
|
|
2081
2321
|
else if (numberAlternatives.number) {
|
|
2082
2322
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2083
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2323
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2084
2324
|
result.frame = frames[i];
|
|
2085
2325
|
results.push(result);
|
|
2086
2326
|
}
|
|
2087
2327
|
}
|
|
2088
2328
|
else {
|
|
2089
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2329
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2090
2330
|
result.frame = frames[i];
|
|
2091
2331
|
results.push(result);
|
|
2092
2332
|
}
|
|
@@ -2105,11 +2345,15 @@ class StableBrowser {
|
|
|
2105
2345
|
scroll: false,
|
|
2106
2346
|
highlight: false,
|
|
2107
2347
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2108
|
-
text: `Verify text exists in page`,
|
|
2348
|
+
text: `Verify the text '${text}' exists in page`,
|
|
2349
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2109
2350
|
operation: "verifyTextExistInPage",
|
|
2110
2351
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2111
2352
|
};
|
|
2112
|
-
|
|
2353
|
+
if (testForRegex(text)) {
|
|
2354
|
+
text = text.replace(/\\"/g, '"');
|
|
2355
|
+
}
|
|
2356
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2113
2357
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2114
2358
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2115
2359
|
if (newValue !== text) {
|
|
@@ -2122,7 +2366,15 @@ class StableBrowser {
|
|
|
2122
2366
|
await _preCommand(state, this);
|
|
2123
2367
|
state.info.text = text;
|
|
2124
2368
|
while (true) {
|
|
2125
|
-
|
|
2369
|
+
let resultWithElementsFound = {
|
|
2370
|
+
length: 0,
|
|
2371
|
+
};
|
|
2372
|
+
try {
|
|
2373
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2374
|
+
}
|
|
2375
|
+
catch (error) {
|
|
2376
|
+
// ignore
|
|
2377
|
+
}
|
|
2126
2378
|
if (resultWithElementsFound.length === 0) {
|
|
2127
2379
|
if (Date.now() - state.startTime > timeout) {
|
|
2128
2380
|
throw new Error(`Text ${text} not found in page`);
|
|
@@ -2130,18 +2382,40 @@ class StableBrowser {
|
|
|
2130
2382
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2131
2383
|
continue;
|
|
2132
2384
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2385
|
+
try {
|
|
2386
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
2387
|
+
const frame = resultWithElementsFound[0].frame;
|
|
2388
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2389
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2390
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2391
|
+
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2392
|
+
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2393
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2394
|
+
// this._unhighlightElements(frame, dataAttribute)
|
|
2395
|
+
// .then(async () => {
|
|
2396
|
+
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2397
|
+
// })
|
|
2398
|
+
// .catch(
|
|
2399
|
+
// (e) => {}
|
|
2400
|
+
// console.error(e)
|
|
2401
|
+
// );
|
|
2402
|
+
// });
|
|
2403
|
+
// }
|
|
2404
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2405
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2406
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2407
|
+
if (element) {
|
|
2408
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2409
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2410
|
+
// await _screenshot(state, this, element);
|
|
2411
|
+
}
|
|
2141
2412
|
}
|
|
2413
|
+
await _screenshot(state, this);
|
|
2414
|
+
return state.info;
|
|
2415
|
+
}
|
|
2416
|
+
catch (error) {
|
|
2417
|
+
console.error(error);
|
|
2142
2418
|
}
|
|
2143
|
-
await _screenshot(state, this);
|
|
2144
|
-
return state.info;
|
|
2145
2419
|
}
|
|
2146
2420
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2147
2421
|
}
|
|
@@ -2162,11 +2436,15 @@ class StableBrowser {
|
|
|
2162
2436
|
scroll: false,
|
|
2163
2437
|
highlight: false,
|
|
2164
2438
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2165
|
-
text: `Verify text does not exist in page`,
|
|
2439
|
+
text: `Verify the text '${text}' does not exist in page`,
|
|
2440
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2166
2441
|
operation: "verifyTextNotExistInPage",
|
|
2167
2442
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2168
2443
|
};
|
|
2169
|
-
|
|
2444
|
+
if (testForRegex(text)) {
|
|
2445
|
+
text = text.replace(/\\"/g, '"');
|
|
2446
|
+
}
|
|
2447
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2170
2448
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2171
2449
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2172
2450
|
if (newValue !== text) {
|
|
@@ -2178,8 +2456,16 @@ class StableBrowser {
|
|
|
2178
2456
|
try {
|
|
2179
2457
|
await _preCommand(state, this);
|
|
2180
2458
|
state.info.text = text;
|
|
2459
|
+
let resultWithElementsFound = {
|
|
2460
|
+
length: null, // initial cannot be 0
|
|
2461
|
+
};
|
|
2181
2462
|
while (true) {
|
|
2182
|
-
|
|
2463
|
+
try {
|
|
2464
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2465
|
+
}
|
|
2466
|
+
catch (error) {
|
|
2467
|
+
// ignore
|
|
2468
|
+
}
|
|
2183
2469
|
if (resultWithElementsFound.length === 0) {
|
|
2184
2470
|
await _screenshot(state, this);
|
|
2185
2471
|
return state.info;
|
|
@@ -2209,10 +2495,11 @@ class StableBrowser {
|
|
|
2209
2495
|
highlight: false,
|
|
2210
2496
|
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2211
2497
|
text: `Verify text with relation to another text`,
|
|
2498
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
2212
2499
|
operation: "verify_text_with_relation",
|
|
2213
2500
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2214
2501
|
};
|
|
2215
|
-
const timeout = this.
|
|
2502
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2216
2503
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2217
2504
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2218
2505
|
if (newValue !== textAnchor) {
|
|
@@ -2230,8 +2517,16 @@ class StableBrowser {
|
|
|
2230
2517
|
try {
|
|
2231
2518
|
await _preCommand(state, this);
|
|
2232
2519
|
state.info.text = textToVerify;
|
|
2520
|
+
let resultWithElementsFound = {
|
|
2521
|
+
length: 0,
|
|
2522
|
+
};
|
|
2233
2523
|
while (true) {
|
|
2234
|
-
|
|
2524
|
+
try {
|
|
2525
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2526
|
+
}
|
|
2527
|
+
catch (error) {
|
|
2528
|
+
// ignore
|
|
2529
|
+
}
|
|
2235
2530
|
if (resultWithElementsFound.length === 0) {
|
|
2236
2531
|
if (Date.now() - state.startTime > timeout) {
|
|
2237
2532
|
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
@@ -2239,51 +2534,56 @@ class StableBrowser {
|
|
|
2239
2534
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2240
2535
|
continue;
|
|
2241
2536
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
const
|
|
2250
|
-
for (let i = 0; i <
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2537
|
+
try {
|
|
2538
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2539
|
+
foundAncore = true;
|
|
2540
|
+
const result = resultWithElementsFound[i];
|
|
2541
|
+
const token = result.randomToken;
|
|
2542
|
+
const frame = result.frame;
|
|
2543
|
+
let css = `[data-blinq-id-${token}]`;
|
|
2544
|
+
const climbArray1 = [];
|
|
2545
|
+
for (let i = 0; i < climb; i++) {
|
|
2546
|
+
climbArray1.push("..");
|
|
2547
|
+
}
|
|
2548
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
2549
|
+
css = css + " >> " + climbXpath;
|
|
2550
|
+
const count = await frame.locator(css).count();
|
|
2551
|
+
for (let j = 0; j < count; j++) {
|
|
2552
|
+
const continer = await frame.locator(css).nth(j);
|
|
2553
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
|
|
2554
|
+
if (result.elementCount > 0) {
|
|
2555
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2556
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2557
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2558
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2559
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
2560
|
+
// this._highlightElements(frame, dataAttribute)
|
|
2561
|
+
// .then(async () => {
|
|
2562
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2563
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
2564
|
+
// () => {}
|
|
2565
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
2566
|
+
// );
|
|
2567
|
+
// })
|
|
2568
|
+
// .catch(e);
|
|
2569
|
+
// }
|
|
2570
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
2571
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2572
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2573
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2574
|
+
if (element) {
|
|
2575
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2576
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2257
2577
|
}
|
|
2578
|
+
await _screenshot(state, this);
|
|
2579
|
+
return state.info;
|
|
2258
2580
|
}
|
|
2259
|
-
if (!climbParent) {
|
|
2260
|
-
continue;
|
|
2261
|
-
}
|
|
2262
|
-
const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
|
|
2263
|
-
if (foundElements.length > 0) {
|
|
2264
|
-
// set the container element attribute
|
|
2265
|
-
element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
|
|
2266
|
-
climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
|
|
2267
|
-
foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
|
|
2268
|
-
return { found: true };
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
return { found: false };
|
|
2272
|
-
}, [css, climb, textToVerify, result.randomToken]);
|
|
2273
|
-
if (findResult.found === true) {
|
|
2274
|
-
const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
|
|
2275
|
-
const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2276
|
-
await this._highlightElements(frame, dataAttribute);
|
|
2277
|
-
await this._highlightElements(frame, cssAnchor);
|
|
2278
|
-
const element = await frame.$(dataAttribute);
|
|
2279
|
-
if (element) {
|
|
2280
|
-
await this.scrollIfNeeded(element, state.info);
|
|
2281
|
-
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2282
2581
|
}
|
|
2283
|
-
await _screenshot(state, this);
|
|
2284
|
-
return state.info;
|
|
2285
2582
|
}
|
|
2286
2583
|
}
|
|
2584
|
+
catch (error) {
|
|
2585
|
+
console.error(error);
|
|
2586
|
+
}
|
|
2287
2587
|
}
|
|
2288
2588
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2289
2589
|
}
|
|
@@ -2294,6 +2594,30 @@ class StableBrowser {
|
|
|
2294
2594
|
_commandFinally(state, this);
|
|
2295
2595
|
}
|
|
2296
2596
|
}
|
|
2597
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2598
|
+
const frames = this.page.frames();
|
|
2599
|
+
let results = [];
|
|
2600
|
+
let ignoreCase = false;
|
|
2601
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2602
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2603
|
+
result.frame = frames[i];
|
|
2604
|
+
const climbArray = [];
|
|
2605
|
+
for (let i = 0; i < climb; i++) {
|
|
2606
|
+
climbArray.push("..");
|
|
2607
|
+
}
|
|
2608
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2609
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2610
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2611
|
+
if (count > 0) {
|
|
2612
|
+
result.elementCount = count;
|
|
2613
|
+
result.locator = newLocator;
|
|
2614
|
+
results.push(result);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
// state.info.results = results;
|
|
2618
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2619
|
+
return resultWithElementsFound;
|
|
2620
|
+
}
|
|
2297
2621
|
async visualVerification(text, options = {}, world = null) {
|
|
2298
2622
|
const startTime = Date.now();
|
|
2299
2623
|
let error = null;
|
|
@@ -2312,10 +2636,13 @@ class StableBrowser {
|
|
|
2312
2636
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2313
2637
|
info.screenshotPath = screenshotPath;
|
|
2314
2638
|
const screenshot = await this.takeScreenshot();
|
|
2315
|
-
|
|
2316
|
-
method: "
|
|
2639
|
+
let request = {
|
|
2640
|
+
method: "post",
|
|
2641
|
+
maxBodyLength: Infinity,
|
|
2317
2642
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2318
2643
|
headers: {
|
|
2644
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
2645
|
+
"x-source": "aaa",
|
|
2319
2646
|
"Content-Type": "application/json",
|
|
2320
2647
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2321
2648
|
},
|
|
@@ -2324,7 +2651,7 @@ class StableBrowser {
|
|
|
2324
2651
|
screenshot: screenshot,
|
|
2325
2652
|
}),
|
|
2326
2653
|
};
|
|
2327
|
-
|
|
2654
|
+
const result = await axios.request(request);
|
|
2328
2655
|
if (result.data.status !== true) {
|
|
2329
2656
|
throw new Error("Visual validation failed");
|
|
2330
2657
|
}
|
|
@@ -2352,6 +2679,7 @@ class StableBrowser {
|
|
|
2352
2679
|
_reportToWorld(world, {
|
|
2353
2680
|
type: Types.VERIFY_VISUAL,
|
|
2354
2681
|
text: "Visual verification",
|
|
2682
|
+
_text: "Visual verification of " + text,
|
|
2355
2683
|
screenshotId,
|
|
2356
2684
|
result: error
|
|
2357
2685
|
? {
|
|
@@ -2618,6 +2946,32 @@ class StableBrowser {
|
|
|
2618
2946
|
}
|
|
2619
2947
|
return timeout;
|
|
2620
2948
|
}
|
|
2949
|
+
_getFindElementTimeout(options) {
|
|
2950
|
+
if (options && options.timeout) {
|
|
2951
|
+
return options.timeout;
|
|
2952
|
+
}
|
|
2953
|
+
if (this.configuration.find_element_timeout) {
|
|
2954
|
+
return this.configuration.find_element_timeout;
|
|
2955
|
+
}
|
|
2956
|
+
return 30000;
|
|
2957
|
+
}
|
|
2958
|
+
async saveStoreState(path = null, world = null) {
|
|
2959
|
+
const storageState = await this.page.context().storageState();
|
|
2960
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2961
|
+
if (path) {
|
|
2962
|
+
// save { storageState: storageState } into the path
|
|
2963
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
2964
|
+
}
|
|
2965
|
+
else {
|
|
2966
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
async restoreSaveState(path = null, world = null) {
|
|
2970
|
+
await refreshBrowser(this, path, world);
|
|
2971
|
+
this.registerEventListeners(this.context);
|
|
2972
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2973
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
2974
|
+
}
|
|
2621
2975
|
async waitForPageLoad(options = {}, world = null) {
|
|
2622
2976
|
let timeout = this._getLoadTimeout(options);
|
|
2623
2977
|
const promiseArray = [];
|
|
@@ -2685,6 +3039,7 @@ class StableBrowser {
|
|
|
2685
3039
|
highlight: false,
|
|
2686
3040
|
type: Types.CLOSE_PAGE,
|
|
2687
3041
|
text: `Close page`,
|
|
3042
|
+
_text: `Close the page`,
|
|
2688
3043
|
operation: "closePage",
|
|
2689
3044
|
log: "***** close page *****\n",
|
|
2690
3045
|
throwError: false,
|
|
@@ -2701,8 +3056,95 @@ class StableBrowser {
|
|
|
2701
3056
|
_commandFinally(state, this);
|
|
2702
3057
|
}
|
|
2703
3058
|
}
|
|
3059
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3060
|
+
let operation = null;
|
|
3061
|
+
if (!options || !options.operation) {
|
|
3062
|
+
throw new Error("operation is not defined");
|
|
3063
|
+
}
|
|
3064
|
+
operation = options.operation;
|
|
3065
|
+
// validate operation is one of the supported operations
|
|
3066
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3067
|
+
throw new Error("operation is not supported");
|
|
3068
|
+
}
|
|
3069
|
+
const state = {
|
|
3070
|
+
options,
|
|
3071
|
+
world,
|
|
3072
|
+
locate: false,
|
|
3073
|
+
scroll: false,
|
|
3074
|
+
highlight: false,
|
|
3075
|
+
type: Types.TABLE_OPERATION,
|
|
3076
|
+
text: `Table operation`,
|
|
3077
|
+
_text: `Table ${operation} operation`,
|
|
3078
|
+
operation: operation,
|
|
3079
|
+
log: "***** Table operation *****\n",
|
|
3080
|
+
};
|
|
3081
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3082
|
+
try {
|
|
3083
|
+
await _preCommand(state, this);
|
|
3084
|
+
const start = Date.now();
|
|
3085
|
+
let cellArea = null;
|
|
3086
|
+
while (true) {
|
|
3087
|
+
try {
|
|
3088
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3089
|
+
if (cellArea) {
|
|
3090
|
+
break;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
catch (e) {
|
|
3094
|
+
// ignore
|
|
3095
|
+
}
|
|
3096
|
+
if (Date.now() - start > timeout) {
|
|
3097
|
+
throw new Error(`Cell not found in table`);
|
|
3098
|
+
}
|
|
3099
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3100
|
+
}
|
|
3101
|
+
switch (operation) {
|
|
3102
|
+
case "click":
|
|
3103
|
+
if (!options.css) {
|
|
3104
|
+
// will click in the center of the cell
|
|
3105
|
+
let xOffset = 0;
|
|
3106
|
+
let yOffset = 0;
|
|
3107
|
+
if (options.xOffset) {
|
|
3108
|
+
xOffset = options.xOffset;
|
|
3109
|
+
}
|
|
3110
|
+
if (options.yOffset) {
|
|
3111
|
+
yOffset = options.yOffset;
|
|
3112
|
+
}
|
|
3113
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3114
|
+
}
|
|
3115
|
+
else {
|
|
3116
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3117
|
+
if (results.length === 0) {
|
|
3118
|
+
throw new Error(`Element not found in cell area`);
|
|
3119
|
+
}
|
|
3120
|
+
state.element = results[0];
|
|
3121
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3122
|
+
}
|
|
3123
|
+
break;
|
|
3124
|
+
case "hover+click":
|
|
3125
|
+
if (!options.css) {
|
|
3126
|
+
throw new Error("css is not defined");
|
|
3127
|
+
}
|
|
3128
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3129
|
+
if (results.length === 0) {
|
|
3130
|
+
throw new Error(`Element not found in cell area`);
|
|
3131
|
+
}
|
|
3132
|
+
state.element = results[0];
|
|
3133
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3134
|
+
break;
|
|
3135
|
+
default:
|
|
3136
|
+
throw new Error("operation is not supported");
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
catch (e) {
|
|
3140
|
+
await _commandError(state, e, this);
|
|
3141
|
+
}
|
|
3142
|
+
finally {
|
|
3143
|
+
_commandFinally(state, this);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
2704
3146
|
saveTestDataAsGlobal(options, world) {
|
|
2705
|
-
const dataFile =
|
|
3147
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2706
3148
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2707
3149
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2708
3150
|
}
|
|
@@ -2732,6 +3174,7 @@ class StableBrowser {
|
|
|
2732
3174
|
_reportToWorld(world, {
|
|
2733
3175
|
type: Types.SET_VIEWPORT,
|
|
2734
3176
|
text: "set viewport size to " + width + "x" + hight,
|
|
3177
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2735
3178
|
screenshotId,
|
|
2736
3179
|
result: error
|
|
2737
3180
|
? {
|
|
@@ -2803,14 +3246,25 @@ class StableBrowser {
|
|
|
2803
3246
|
}
|
|
2804
3247
|
}
|
|
2805
3248
|
async beforeStep(world, step) {
|
|
2806
|
-
this.stepName = step.pickleStep.text;
|
|
2807
|
-
this.logger.info("step: " + this.stepName);
|
|
2808
3249
|
if (this.stepIndex === undefined) {
|
|
2809
3250
|
this.stepIndex = 0;
|
|
2810
3251
|
}
|
|
2811
3252
|
else {
|
|
2812
3253
|
this.stepIndex++;
|
|
2813
3254
|
}
|
|
3255
|
+
if (step && step.pickleStep && step.pickleStep.text) {
|
|
3256
|
+
this.stepName = step.pickleStep.text;
|
|
3257
|
+
this.logger.info("step: " + this.stepName);
|
|
3258
|
+
}
|
|
3259
|
+
else if (step && step.text) {
|
|
3260
|
+
this.stepName = step.text;
|
|
3261
|
+
}
|
|
3262
|
+
else {
|
|
3263
|
+
this.stepName = "step " + this.stepIndex;
|
|
3264
|
+
}
|
|
3265
|
+
if (this.context) {
|
|
3266
|
+
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3267
|
+
}
|
|
2814
3268
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2815
3269
|
if (this.context.browserObject.context) {
|
|
2816
3270
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
@@ -2823,6 +3277,41 @@ class StableBrowser {
|
|
|
2823
3277
|
this.saveTestDataAsGlobal({}, world);
|
|
2824
3278
|
}
|
|
2825
3279
|
}
|
|
3280
|
+
if (this.initSnapshotTaken === false) {
|
|
3281
|
+
this.initSnapshotTaken = true;
|
|
3282
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3283
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3284
|
+
if (snapshot) {
|
|
3285
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
async getAriaSnapshot() {
|
|
3291
|
+
try {
|
|
3292
|
+
// find the page url
|
|
3293
|
+
const url = await this.page.url();
|
|
3294
|
+
// extract the path from the url
|
|
3295
|
+
const path = new URL(url).pathname;
|
|
3296
|
+
// get the page title
|
|
3297
|
+
const title = await this.page.title();
|
|
3298
|
+
// go over other frams
|
|
3299
|
+
const frames = this.page.frames();
|
|
3300
|
+
const snapshots = [];
|
|
3301
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3302
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3303
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3304
|
+
content.push(`- frame: ${i}`);
|
|
3305
|
+
const frame = frames[i];
|
|
3306
|
+
const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
|
|
3307
|
+
content.push(snapshot);
|
|
3308
|
+
}
|
|
3309
|
+
return content.join("\n");
|
|
3310
|
+
}
|
|
3311
|
+
catch (e) {
|
|
3312
|
+
console.error(e);
|
|
3313
|
+
}
|
|
3314
|
+
return null;
|
|
2826
3315
|
}
|
|
2827
3316
|
async afterStep(world, step) {
|
|
2828
3317
|
this.stepName = null;
|
|
@@ -2833,6 +3322,16 @@ class StableBrowser {
|
|
|
2833
3322
|
});
|
|
2834
3323
|
}
|
|
2835
3324
|
}
|
|
3325
|
+
if (this.context) {
|
|
3326
|
+
this.context.examplesRow = null;
|
|
3327
|
+
}
|
|
3328
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3329
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3330
|
+
if (snapshot) {
|
|
3331
|
+
const obj = {};
|
|
3332
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
2836
3335
|
}
|
|
2837
3336
|
}
|
|
2838
3337
|
function createTimedPromise(promise, label) {
|