automation_model 1.0.574-dev → 1.0.574-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 +36 -19
- 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 +48 -24
- 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 +45 -19
- package/lib/network.js.map +1 -1
- package/lib/stable_browser.d.ts +27 -7
- package/lib/stable_browser.js +749 -239
- 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 +10 -3
- package/lib/utils.js +360 -21
- 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;
|
|
@@ -236,6 +244,9 @@ class StableBrowser {
|
|
|
236
244
|
// await closeUnexpectedPopups(this.page);
|
|
237
245
|
// }
|
|
238
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
|
}
|
|
@@ -327,66 +338,60 @@ class StableBrowser {
|
|
|
327
338
|
if (css && css.locator) {
|
|
328
339
|
css = css.locator;
|
|
329
340
|
}
|
|
330
|
-
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);
|
|
331
342
|
if (result.elementCount === 0) {
|
|
332
343
|
return;
|
|
333
344
|
}
|
|
334
|
-
let textElementCss = "[data-blinq-id
|
|
345
|
+
let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
|
|
335
346
|
// css climb to parent element
|
|
336
347
|
const climbArray = [];
|
|
337
348
|
for (let i = 0; i < climb; i++) {
|
|
338
349
|
climbArray.push("..");
|
|
339
350
|
}
|
|
340
351
|
let climbXpath = "xpath=" + climbArray.join("/");
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const pattern = text.slice(1, -1);
|
|
364
|
-
new RegExp(pattern); // Validate pattern
|
|
365
|
-
text = pattern;
|
|
366
|
-
options.singleRegex = true;
|
|
367
|
-
}
|
|
368
|
-
catch (e) {
|
|
369
|
-
throw new Error(`Invalid regex pattern: ${e.message}`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const elements = window.findMatchingElements(text, options);
|
|
373
|
-
let randomToken = null;
|
|
374
|
-
const foundElements = [];
|
|
375
|
-
let elementCount = 0;
|
|
376
|
-
if (elements.length > 0) {
|
|
377
|
-
for (let i = 0; i < elements.length; i++) {
|
|
378
|
-
if (randomToken === null) {
|
|
379
|
-
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;
|
|
380
374
|
}
|
|
381
|
-
let element = elements[i];
|
|
382
|
-
element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
|
|
383
|
-
elementCount++;
|
|
384
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;
|
|
385
389
|
}
|
|
386
|
-
|
|
387
|
-
}
|
|
390
|
+
tagCount++;
|
|
391
|
+
}
|
|
392
|
+
return { elementCount: tagCount, randomToken };
|
|
388
393
|
}
|
|
389
|
-
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) {
|
|
390
395
|
if (!info) {
|
|
391
396
|
info = {};
|
|
392
397
|
}
|
|
@@ -409,23 +414,24 @@ class StableBrowser {
|
|
|
409
414
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
410
415
|
let locator = null;
|
|
411
416
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
412
|
-
|
|
417
|
+
const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
|
|
418
|
+
let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
|
|
413
419
|
if (!locatorString) {
|
|
414
420
|
info.failCause.textNotFound = true;
|
|
415
|
-
info.failCause.lastError =
|
|
421
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
|
|
416
422
|
return;
|
|
417
423
|
}
|
|
418
424
|
locator = await this._getLocator({ css: locatorString }, scope, _params);
|
|
419
425
|
}
|
|
420
426
|
else if (locatorSearch.text) {
|
|
421
427
|
let text = _fixUsingParams(locatorSearch.text, _params);
|
|
422
|
-
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);
|
|
423
429
|
if (result.elementCount === 0) {
|
|
424
430
|
info.failCause.textNotFound = true;
|
|
425
|
-
info.failCause.lastError =
|
|
431
|
+
info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
|
|
426
432
|
return;
|
|
427
433
|
}
|
|
428
|
-
locatorSearch.css = "[data-blinq-id
|
|
434
|
+
locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
|
|
429
435
|
if (locatorSearch.childCss) {
|
|
430
436
|
locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
|
|
431
437
|
}
|
|
@@ -461,7 +467,7 @@ class StableBrowser {
|
|
|
461
467
|
if (!visibleOnly) {
|
|
462
468
|
visible = true;
|
|
463
469
|
}
|
|
464
|
-
if (visible && enabled) {
|
|
470
|
+
if (visible && (allowDisabled || enabled)) {
|
|
465
471
|
foundLocators.push(locator.nth(j));
|
|
466
472
|
if (info.locatorLog) {
|
|
467
473
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
@@ -474,9 +480,11 @@ class StableBrowser {
|
|
|
474
480
|
info.printMessages = {};
|
|
475
481
|
}
|
|
476
482
|
if (info.locatorLog && !visible) {
|
|
483
|
+
info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
|
|
477
484
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
|
|
478
485
|
}
|
|
479
486
|
if (info.locatorLog && !enabled) {
|
|
487
|
+
info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
|
|
480
488
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
|
|
481
489
|
}
|
|
482
490
|
if (!info.printMessages[j.toString()]) {
|
|
@@ -544,7 +552,7 @@ class StableBrowser {
|
|
|
544
552
|
}
|
|
545
553
|
return { rerun: false };
|
|
546
554
|
}
|
|
547
|
-
async _locate(selectors, info, _params, timeout) {
|
|
555
|
+
async _locate(selectors, info, _params, timeout, allowDisabled = false) {
|
|
548
556
|
if (!timeout) {
|
|
549
557
|
timeout = 30000;
|
|
550
558
|
}
|
|
@@ -554,9 +562,30 @@ class StableBrowser {
|
|
|
554
562
|
let selector = selectors.locators[j];
|
|
555
563
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
556
564
|
}
|
|
557
|
-
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
565
|
+
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
558
566
|
if (!element.rerun) {
|
|
559
|
-
|
|
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);
|
|
560
589
|
}
|
|
561
590
|
}
|
|
562
591
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -629,7 +658,7 @@ class StableBrowser {
|
|
|
629
658
|
//info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
|
|
630
659
|
if (Date.now() - startTime > timeout) {
|
|
631
660
|
info.failCause.iframeNotFound = true;
|
|
632
|
-
info.failCause.lastError =
|
|
661
|
+
info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
|
|
633
662
|
throw new Error("unable to locate iframe " + selectors.iframe_src);
|
|
634
663
|
}
|
|
635
664
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -654,7 +683,7 @@ class StableBrowser {
|
|
|
654
683
|
return bodyContent;
|
|
655
684
|
});
|
|
656
685
|
}
|
|
657
|
-
async _locate_internal(selectors, info, _params, timeout = 30000) {
|
|
686
|
+
async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
|
|
658
687
|
if (!info) {
|
|
659
688
|
info = {};
|
|
660
689
|
info.failCause = {};
|
|
@@ -703,18 +732,13 @@ class StableBrowser {
|
|
|
703
732
|
}
|
|
704
733
|
// info.log += "scanning locators in priority 1" + "\n";
|
|
705
734
|
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
706
|
-
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);
|
|
707
736
|
if (result.foundElements.length === 0) {
|
|
708
737
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
709
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
710
|
-
}
|
|
711
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
712
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
738
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
713
739
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
717
|
-
}
|
|
740
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
741
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
718
742
|
}
|
|
719
743
|
let foundElements = result.foundElements;
|
|
720
744
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
@@ -758,7 +782,7 @@ class StableBrowser {
|
|
|
758
782
|
break;
|
|
759
783
|
}
|
|
760
784
|
if (Date.now() - startTime > highPriorityTimeout) {
|
|
761
|
-
info.log += "high priority timeout, will try all elements" + "\n";
|
|
785
|
+
//info.log += "high priority timeout, will try all elements" + "\n";
|
|
762
786
|
highPriorityOnly = false;
|
|
763
787
|
if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
|
|
764
788
|
lazy_scroll = true;
|
|
@@ -766,10 +790,15 @@ class StableBrowser {
|
|
|
766
790
|
}
|
|
767
791
|
}
|
|
768
792
|
if (Date.now() - startTime > visibleOnlyTimeout) {
|
|
769
|
-
info.log += "visible only timeout, will try all elements" + "\n";
|
|
793
|
+
//info.log += "visible only timeout, will try all elements" + "\n";
|
|
770
794
|
visibleOnly = false;
|
|
771
795
|
}
|
|
772
796
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
797
|
+
// sheck of more of half of the timeout has passed
|
|
798
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
799
|
+
highPriorityOnly = false;
|
|
800
|
+
visibleOnly = false;
|
|
801
|
+
}
|
|
773
802
|
}
|
|
774
803
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
775
804
|
// if (info.locatorLog) {
|
|
@@ -780,10 +809,12 @@ class StableBrowser {
|
|
|
780
809
|
// }
|
|
781
810
|
//info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
|
|
782
811
|
info.failCause.locatorNotFound = true;
|
|
783
|
-
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
|
+
}
|
|
784
815
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
785
816
|
}
|
|
786
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
|
|
817
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
787
818
|
let foundElements = [];
|
|
788
819
|
const result = {
|
|
789
820
|
foundElements: foundElements,
|
|
@@ -791,7 +822,7 @@ class StableBrowser {
|
|
|
791
822
|
for (let i = 0; i < locatorsGroup.length; i++) {
|
|
792
823
|
let foundLocators = [];
|
|
793
824
|
try {
|
|
794
|
-
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
|
|
825
|
+
await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
795
826
|
}
|
|
796
827
|
catch (e) {
|
|
797
828
|
// this call can fail it the browser is navigating
|
|
@@ -799,7 +830,7 @@ class StableBrowser {
|
|
|
799
830
|
// this.logger.debug(e);
|
|
800
831
|
foundLocators = [];
|
|
801
832
|
try {
|
|
802
|
-
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);
|
|
803
834
|
}
|
|
804
835
|
catch (e) {
|
|
805
836
|
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
@@ -814,9 +845,40 @@ class StableBrowser {
|
|
|
814
845
|
result.locatorIndex = i;
|
|
815
846
|
}
|
|
816
847
|
if (foundLocators.length > 1) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
+
}
|
|
820
882
|
}
|
|
821
883
|
}
|
|
822
884
|
}
|
|
@@ -927,25 +989,14 @@ class StableBrowser {
|
|
|
927
989
|
options,
|
|
928
990
|
world,
|
|
929
991
|
text: "Click element",
|
|
992
|
+
_text: "Click on " + selectors.element_name,
|
|
930
993
|
type: Types.CLICK,
|
|
931
994
|
operation: "click",
|
|
932
995
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
933
996
|
};
|
|
934
997
|
try {
|
|
935
998
|
await _preCommand(state, this);
|
|
936
|
-
|
|
937
|
-
// state.selectors.locators[0].text = state.options.context;
|
|
938
|
-
// }
|
|
939
|
-
try {
|
|
940
|
-
await state.element.click();
|
|
941
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
942
|
-
}
|
|
943
|
-
catch (e) {
|
|
944
|
-
// await this.closeUnexpectedPopups();
|
|
945
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
946
|
-
await state.element.dispatchEvent("click");
|
|
947
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
948
|
-
}
|
|
999
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
949
1000
|
await this.waitForPageLoad();
|
|
950
1001
|
return state.info;
|
|
951
1002
|
}
|
|
@@ -956,6 +1007,38 @@ class StableBrowser {
|
|
|
956
1007
|
_commandFinally(state, this);
|
|
957
1008
|
}
|
|
958
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
|
+
}
|
|
959
1042
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
960
1043
|
const state = {
|
|
961
1044
|
selectors,
|
|
@@ -964,6 +1047,7 @@ class StableBrowser {
|
|
|
964
1047
|
world,
|
|
965
1048
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
966
1049
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1050
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
967
1051
|
operation: "setCheck",
|
|
968
1052
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
969
1053
|
};
|
|
@@ -973,9 +1057,15 @@ class StableBrowser {
|
|
|
973
1057
|
// let element = await this._locate(selectors, info, _params);
|
|
974
1058
|
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
975
1059
|
try {
|
|
976
|
-
//
|
|
1060
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1061
|
+
// console.log(`Highlighting while running from recorder`);
|
|
1062
|
+
await this._highlightElements(state.element);
|
|
977
1063
|
await state.element.setChecked(checked);
|
|
978
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);
|
|
979
1069
|
}
|
|
980
1070
|
catch (e) {
|
|
981
1071
|
if (e.message && e.message.includes("did not change its state")) {
|
|
@@ -1007,22 +1097,13 @@ class StableBrowser {
|
|
|
1007
1097
|
world,
|
|
1008
1098
|
type: Types.HOVER,
|
|
1009
1099
|
text: `Hover element`,
|
|
1100
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1010
1101
|
operation: "hover",
|
|
1011
1102
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1012
1103
|
};
|
|
1013
1104
|
try {
|
|
1014
1105
|
await _preCommand(state, this);
|
|
1015
|
-
|
|
1016
|
-
await state.element.hover();
|
|
1017
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1018
|
-
}
|
|
1019
|
-
catch (e) {
|
|
1020
|
-
//await this.closeUnexpectedPopups();
|
|
1021
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1022
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1023
|
-
await state.element.hover({ timeout: 10000 });
|
|
1024
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1025
|
-
}
|
|
1106
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1026
1107
|
await _screenshot(state, this);
|
|
1027
1108
|
await this.waitForPageLoad();
|
|
1028
1109
|
return state.info;
|
|
@@ -1046,6 +1127,7 @@ class StableBrowser {
|
|
|
1046
1127
|
value: values.toString(),
|
|
1047
1128
|
type: Types.SELECT,
|
|
1048
1129
|
text: `Select option: ${values}`,
|
|
1130
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1049
1131
|
operation: "selectOption",
|
|
1050
1132
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1051
1133
|
};
|
|
@@ -1080,6 +1162,7 @@ class StableBrowser {
|
|
|
1080
1162
|
highlight: false,
|
|
1081
1163
|
type: Types.TYPE_PRESS,
|
|
1082
1164
|
text: `Type value: ${_value}`,
|
|
1165
|
+
_text: `Type value: ${_value}`,
|
|
1083
1166
|
operation: "type",
|
|
1084
1167
|
log: "",
|
|
1085
1168
|
};
|
|
@@ -1159,6 +1242,7 @@ class StableBrowser {
|
|
|
1159
1242
|
world,
|
|
1160
1243
|
type: Types.SET_DATE_TIME,
|
|
1161
1244
|
text: `Set date time value: ${value}`,
|
|
1245
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1162
1246
|
operation: "setDateTime",
|
|
1163
1247
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1164
1248
|
throwError: false,
|
|
@@ -1166,7 +1250,7 @@ class StableBrowser {
|
|
|
1166
1250
|
try {
|
|
1167
1251
|
await _preCommand(state, this);
|
|
1168
1252
|
try {
|
|
1169
|
-
await state.element
|
|
1253
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1170
1254
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1171
1255
|
if (format) {
|
|
1172
1256
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1230,9 +1314,13 @@ class StableBrowser {
|
|
|
1230
1314
|
world,
|
|
1231
1315
|
type: Types.FILL,
|
|
1232
1316
|
text: `Click type input with value: ${_value}`,
|
|
1317
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1233
1318
|
operation: "clickType",
|
|
1234
1319
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1235
1320
|
};
|
|
1321
|
+
if (!options) {
|
|
1322
|
+
options = {};
|
|
1323
|
+
}
|
|
1236
1324
|
if (newValue !== _value) {
|
|
1237
1325
|
//this.logger.info(_value + "=" + newValue);
|
|
1238
1326
|
_value = newValue;
|
|
@@ -1240,7 +1328,7 @@ class StableBrowser {
|
|
|
1240
1328
|
try {
|
|
1241
1329
|
await _preCommand(state, this);
|
|
1242
1330
|
state.info.value = _value;
|
|
1243
|
-
if (
|
|
1331
|
+
if (!options.press) {
|
|
1244
1332
|
try {
|
|
1245
1333
|
let currentValue = await state.element.inputValue();
|
|
1246
1334
|
if (currentValue) {
|
|
@@ -1251,13 +1339,9 @@ class StableBrowser {
|
|
|
1251
1339
|
this.logger.info("unable to clear input value");
|
|
1252
1340
|
}
|
|
1253
1341
|
}
|
|
1254
|
-
if (options
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
}
|
|
1258
|
-
catch (e) {
|
|
1259
|
-
await state.element.dispatchEvent("click");
|
|
1260
|
-
}
|
|
1342
|
+
if (options.press) {
|
|
1343
|
+
options.timeout = 5000;
|
|
1344
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1261
1345
|
}
|
|
1262
1346
|
else {
|
|
1263
1347
|
try {
|
|
@@ -1295,7 +1379,12 @@ class StableBrowser {
|
|
|
1295
1379
|
await this.waitForPageLoad();
|
|
1296
1380
|
}
|
|
1297
1381
|
else if (enter === false) {
|
|
1298
|
-
|
|
1382
|
+
try {
|
|
1383
|
+
await state.element.dispatchEvent("change", null, { timeout: 5000 });
|
|
1384
|
+
}
|
|
1385
|
+
catch (e) {
|
|
1386
|
+
// ignore
|
|
1387
|
+
}
|
|
1299
1388
|
//await this.page.keyboard.press("Tab");
|
|
1300
1389
|
}
|
|
1301
1390
|
else {
|
|
@@ -1347,6 +1436,7 @@ class StableBrowser {
|
|
|
1347
1436
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1348
1437
|
}
|
|
1349
1438
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1439
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1350
1440
|
_validateSelectors(selectors);
|
|
1351
1441
|
let screenshotId = null;
|
|
1352
1442
|
let screenshotPath = null;
|
|
@@ -1356,7 +1446,7 @@ class StableBrowser {
|
|
|
1356
1446
|
}
|
|
1357
1447
|
info.operation = "getText";
|
|
1358
1448
|
info.selectors = selectors;
|
|
1359
|
-
let element = await this._locate(selectors, info, _params);
|
|
1449
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1360
1450
|
if (climb > 0) {
|
|
1361
1451
|
const climbArray = [];
|
|
1362
1452
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1375,6 +1465,18 @@ class StableBrowser {
|
|
|
1375
1465
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1376
1466
|
try {
|
|
1377
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
|
+
// }
|
|
1378
1480
|
const elementText = await element.innerText();
|
|
1379
1481
|
return {
|
|
1380
1482
|
text: elementText,
|
|
@@ -1386,7 +1488,7 @@ class StableBrowser {
|
|
|
1386
1488
|
}
|
|
1387
1489
|
catch (e) {
|
|
1388
1490
|
//await this.closeUnexpectedPopups();
|
|
1389
|
-
this.logger.info("no innerText will use textContent");
|
|
1491
|
+
this.logger.info("no innerText, will use textContent");
|
|
1390
1492
|
const elementText = await element.textContent();
|
|
1391
1493
|
return { text: elementText, screenshotId, screenshotPath, value: value };
|
|
1392
1494
|
}
|
|
@@ -1411,6 +1513,7 @@ class StableBrowser {
|
|
|
1411
1513
|
highlight: false,
|
|
1412
1514
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1413
1515
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1516
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1414
1517
|
operation: "containsPattern",
|
|
1415
1518
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1416
1519
|
};
|
|
@@ -1446,6 +1549,8 @@ class StableBrowser {
|
|
|
1446
1549
|
}
|
|
1447
1550
|
}
|
|
1448
1551
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1552
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1553
|
+
const startTime = Date.now();
|
|
1449
1554
|
const state = {
|
|
1450
1555
|
selectors,
|
|
1451
1556
|
_params,
|
|
@@ -1472,62 +1577,54 @@ class StableBrowser {
|
|
|
1472
1577
|
}
|
|
1473
1578
|
let foundObj = null;
|
|
1474
1579
|
try {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1482
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1483
|
-
if (dateAlternatives.date) {
|
|
1484
|
-
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1485
|
-
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1486
|
-
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1487
|
-
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: 3000 }, state.info, world);
|
|
1584
|
+
if (foundObj && foundObj.element) {
|
|
1585
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1488
1586
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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)) {
|
|
1496
1607
|
return state.info;
|
|
1497
1608
|
}
|
|
1498
1609
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
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
|
|
1505
1615
|
}
|
|
1506
|
-
|
|
1616
|
+
state.info.foundText = foundObj?.text;
|
|
1617
|
+
state.info.value = foundObj?.value;
|
|
1618
|
+
throw new Error("element doesn't contain text " + text);
|
|
1507
1619
|
}
|
|
1508
1620
|
catch (e) {
|
|
1509
1621
|
await _commandError(state, e, this);
|
|
1622
|
+
throw e;
|
|
1510
1623
|
}
|
|
1511
1624
|
finally {
|
|
1512
1625
|
_commandFinally(state, this);
|
|
1513
1626
|
}
|
|
1514
1627
|
}
|
|
1515
|
-
_getDataFile(world = null) {
|
|
1516
|
-
let dataFile = null;
|
|
1517
|
-
if (world && world.reportFolder) {
|
|
1518
|
-
dataFile = path.join(world.reportFolder, "data.json");
|
|
1519
|
-
}
|
|
1520
|
-
else if (this.reportFolder) {
|
|
1521
|
-
dataFile = path.join(this.reportFolder, "data.json");
|
|
1522
|
-
}
|
|
1523
|
-
else if (this.context && this.context.reportFolder) {
|
|
1524
|
-
dataFile = path.join(this.context.reportFolder, "data.json");
|
|
1525
|
-
}
|
|
1526
|
-
else {
|
|
1527
|
-
dataFile = "data.json";
|
|
1528
|
-
}
|
|
1529
|
-
return dataFile;
|
|
1530
|
-
}
|
|
1531
1628
|
async waitForUserInput(message, world = null) {
|
|
1532
1629
|
if (!message) {
|
|
1533
1630
|
message = "# Wait for user input. Press any key to continue";
|
|
@@ -1556,13 +1653,22 @@ class StableBrowser {
|
|
|
1556
1653
|
return;
|
|
1557
1654
|
}
|
|
1558
1655
|
// if data file exists, load it
|
|
1559
|
-
const dataFile =
|
|
1656
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1560
1657
|
let data = this.getTestData(world);
|
|
1561
1658
|
// merge the testData with the existing data
|
|
1562
1659
|
Object.assign(data, testData);
|
|
1563
1660
|
// save the data to the file
|
|
1564
1661
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1565
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
|
+
}
|
|
1566
1672
|
_getDataFilePath(fileName) {
|
|
1567
1673
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1568
1674
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1659,7 +1765,7 @@ class StableBrowser {
|
|
|
1659
1765
|
}
|
|
1660
1766
|
}
|
|
1661
1767
|
getTestData(world = null) {
|
|
1662
|
-
const dataFile =
|
|
1768
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1663
1769
|
let data = {};
|
|
1664
1770
|
if (fs.existsSync(dataFile)) {
|
|
1665
1771
|
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
@@ -1746,6 +1852,15 @@ class StableBrowser {
|
|
|
1746
1852
|
document.documentElement.clientWidth,
|
|
1747
1853
|
])));
|
|
1748
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
|
+
// }
|
|
1749
1864
|
if (this.context.browserName === "chromium") {
|
|
1750
1865
|
const client = await playContext.newCDPSession(this.page);
|
|
1751
1866
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
@@ -1767,6 +1882,10 @@ class StableBrowser {
|
|
|
1767
1882
|
else {
|
|
1768
1883
|
screenshotBuffer = await this.page.screenshot();
|
|
1769
1884
|
}
|
|
1885
|
+
// if (focusedElement) {
|
|
1886
|
+
// // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
|
|
1887
|
+
// await this._unhighlightElements(focusedElement);
|
|
1888
|
+
// }
|
|
1770
1889
|
let image = await Jimp.read(screenshotBuffer);
|
|
1771
1890
|
// Get the image dimensions
|
|
1772
1891
|
const { width, height } = image.bitmap;
|
|
@@ -1779,6 +1898,7 @@ class StableBrowser {
|
|
|
1779
1898
|
else {
|
|
1780
1899
|
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
1781
1900
|
}
|
|
1901
|
+
return screenshotBuffer;
|
|
1782
1902
|
}
|
|
1783
1903
|
async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
|
|
1784
1904
|
const state = {
|
|
@@ -1814,8 +1934,10 @@ class StableBrowser {
|
|
|
1814
1934
|
world,
|
|
1815
1935
|
type: Types.EXTRACT,
|
|
1816
1936
|
text: `Extract attribute from element`,
|
|
1937
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1817
1938
|
operation: "extractAttribute",
|
|
1818
1939
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1940
|
+
allowDisabled: true,
|
|
1819
1941
|
};
|
|
1820
1942
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1821
1943
|
try {
|
|
@@ -1837,6 +1959,7 @@ class StableBrowser {
|
|
|
1837
1959
|
state.info.value = state.value;
|
|
1838
1960
|
this.setTestData({ [variable]: state.value }, world);
|
|
1839
1961
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
1962
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1840
1963
|
return state.info;
|
|
1841
1964
|
}
|
|
1842
1965
|
catch (e) {
|
|
@@ -1855,14 +1978,21 @@ class StableBrowser {
|
|
|
1855
1978
|
options,
|
|
1856
1979
|
world,
|
|
1857
1980
|
type: Types.VERIFY_ATTRIBUTE,
|
|
1981
|
+
highlight: true,
|
|
1982
|
+
screenshot: true,
|
|
1858
1983
|
text: `Verify element attribute`,
|
|
1984
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
1859
1985
|
operation: "verifyAttribute",
|
|
1860
1986
|
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1987
|
+
allowDisabled: true,
|
|
1861
1988
|
};
|
|
1862
1989
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1863
1990
|
let val;
|
|
1991
|
+
let expectedValue;
|
|
1864
1992
|
try {
|
|
1865
1993
|
await _preCommand(state, this);
|
|
1994
|
+
expectedValue = state.value;
|
|
1995
|
+
state.info.expectedValue = expectedValue;
|
|
1866
1996
|
switch (attribute) {
|
|
1867
1997
|
case "innerText":
|
|
1868
1998
|
val = String(await state.element.innerText());
|
|
@@ -1884,19 +2014,22 @@ class StableBrowser {
|
|
|
1884
2014
|
val = String(await state.element.getAttribute(attribute));
|
|
1885
2015
|
break;
|
|
1886
2016
|
}
|
|
2017
|
+
state.info.value = val;
|
|
1887
2018
|
let regex;
|
|
1888
|
-
if (
|
|
1889
|
-
const patternBody =
|
|
2019
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2020
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
1890
2021
|
regex = new RegExp(patternBody, "g");
|
|
1891
2022
|
}
|
|
1892
2023
|
else {
|
|
1893
|
-
const escapedPattern =
|
|
2024
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1894
2025
|
regex = new RegExp(escapedPattern, "g");
|
|
1895
2026
|
}
|
|
1896
2027
|
if (!val.match(regex)) {
|
|
1897
|
-
|
|
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);
|
|
1898
2032
|
}
|
|
1899
|
-
state.info.expectedValue = val;
|
|
1900
2033
|
return state.info;
|
|
1901
2034
|
}
|
|
1902
2035
|
catch (e) {
|
|
@@ -1995,27 +2128,32 @@ class StableBrowser {
|
|
|
1995
2128
|
async _highlightElements(scope, css) {
|
|
1996
2129
|
try {
|
|
1997
2130
|
if (!scope) {
|
|
2131
|
+
// console.log(`Scope is not defined`);
|
|
1998
2132
|
return;
|
|
1999
2133
|
}
|
|
2000
2134
|
if (!css) {
|
|
2001
2135
|
scope
|
|
2002
2136
|
.evaluate((node) => {
|
|
2003
2137
|
if (node && node.style) {
|
|
2004
|
-
let
|
|
2005
|
-
|
|
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}`);
|
|
2006
2143
|
if (window) {
|
|
2007
2144
|
window.addEventListener("beforeunload", function (e) {
|
|
2008
|
-
node.style.
|
|
2145
|
+
node.style.outline = originalOutline;
|
|
2009
2146
|
});
|
|
2010
2147
|
}
|
|
2011
2148
|
setTimeout(function () {
|
|
2012
|
-
node.style.
|
|
2149
|
+
node.style.outline = originalOutline;
|
|
2013
2150
|
}, 2000);
|
|
2014
2151
|
}
|
|
2015
2152
|
})
|
|
2016
2153
|
.then(() => { })
|
|
2017
2154
|
.catch((e) => {
|
|
2018
2155
|
// ignore
|
|
2156
|
+
// console.error(`Could not highlight node : ${e}`);
|
|
2019
2157
|
});
|
|
2020
2158
|
}
|
|
2021
2159
|
else {
|
|
@@ -2031,17 +2169,18 @@ class StableBrowser {
|
|
|
2031
2169
|
if (!element.style) {
|
|
2032
2170
|
return;
|
|
2033
2171
|
}
|
|
2034
|
-
|
|
2172
|
+
let originalOutline = element.style.outline;
|
|
2173
|
+
element.__previousOutline = originalOutline;
|
|
2035
2174
|
// Set the new border to be red and 2px solid
|
|
2036
|
-
element.style.
|
|
2175
|
+
element.style.outline = "2px solid red";
|
|
2037
2176
|
if (window) {
|
|
2038
2177
|
window.addEventListener("beforeunload", function (e) {
|
|
2039
|
-
element.style.
|
|
2178
|
+
element.style.outline = originalOutline;
|
|
2040
2179
|
});
|
|
2041
2180
|
}
|
|
2042
2181
|
// Set a timeout to revert to the original border after 2 seconds
|
|
2043
2182
|
setTimeout(function () {
|
|
2044
|
-
element.style.
|
|
2183
|
+
element.style.outline = originalOutline;
|
|
2045
2184
|
}, 2000);
|
|
2046
2185
|
}
|
|
2047
2186
|
return;
|
|
@@ -2049,6 +2188,7 @@ class StableBrowser {
|
|
|
2049
2188
|
.then(() => { })
|
|
2050
2189
|
.catch((e) => {
|
|
2051
2190
|
// ignore
|
|
2191
|
+
// console.error(`Could not highlight css: ${e}`);
|
|
2052
2192
|
});
|
|
2053
2193
|
}
|
|
2054
2194
|
}
|
|
@@ -2056,6 +2196,54 @@ class StableBrowser {
|
|
|
2056
2196
|
console.debug(error);
|
|
2057
2197
|
}
|
|
2058
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
|
+
// }
|
|
2059
2247
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2060
2248
|
const startTime = Date.now();
|
|
2061
2249
|
let error = null;
|
|
@@ -2100,6 +2288,7 @@ class StableBrowser {
|
|
|
2100
2288
|
_reportToWorld(world, {
|
|
2101
2289
|
type: Types.VERIFY_PAGE_PATH,
|
|
2102
2290
|
text: "Verify page path",
|
|
2291
|
+
_text: "Verify the page path contains " + pathPart,
|
|
2103
2292
|
screenshotId,
|
|
2104
2293
|
result: error
|
|
2105
2294
|
? {
|
|
@@ -2117,26 +2306,89 @@ class StableBrowser {
|
|
|
2117
2306
|
});
|
|
2118
2307
|
}
|
|
2119
2308
|
}
|
|
2120
|
-
async
|
|
2309
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2310
|
+
const startTime = Date.now();
|
|
2311
|
+
let error = null;
|
|
2312
|
+
let screenshotId = null;
|
|
2313
|
+
let screenshotPath = null;
|
|
2314
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2315
|
+
const info = {};
|
|
2316
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2317
|
+
info.operation = "verifyPageTitle";
|
|
2318
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2319
|
+
if (newValue !== title) {
|
|
2320
|
+
this.logger.info(title + "=" + newValue);
|
|
2321
|
+
title = newValue;
|
|
2322
|
+
}
|
|
2323
|
+
info.title = title;
|
|
2324
|
+
try {
|
|
2325
|
+
for (let i = 0; i < 30; i++) {
|
|
2326
|
+
const foundTitle = await this.page.title();
|
|
2327
|
+
if (!foundTitle.includes(title)) {
|
|
2328
|
+
if (i === 29) {
|
|
2329
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2330
|
+
}
|
|
2331
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2335
|
+
return info;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
catch (e) {
|
|
2339
|
+
//await this.closeUnexpectedPopups();
|
|
2340
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2341
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2342
|
+
info.screenshotPath = screenshotPath;
|
|
2343
|
+
Object.assign(e, { info: info });
|
|
2344
|
+
error = e;
|
|
2345
|
+
// throw e;
|
|
2346
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2347
|
+
}
|
|
2348
|
+
finally {
|
|
2349
|
+
const endTime = Date.now();
|
|
2350
|
+
_reportToWorld(world, {
|
|
2351
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2352
|
+
text: "Verify page title",
|
|
2353
|
+
_text: "Verify the page title contains " + title,
|
|
2354
|
+
screenshotId,
|
|
2355
|
+
result: error
|
|
2356
|
+
? {
|
|
2357
|
+
status: "FAILED",
|
|
2358
|
+
startTime,
|
|
2359
|
+
endTime,
|
|
2360
|
+
message: error?.message,
|
|
2361
|
+
}
|
|
2362
|
+
: {
|
|
2363
|
+
status: "PASSED",
|
|
2364
|
+
startTime,
|
|
2365
|
+
endTime,
|
|
2366
|
+
},
|
|
2367
|
+
info: info,
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2121
2372
|
const frames = this.page.frames();
|
|
2122
2373
|
let results = [];
|
|
2374
|
+
// let ignoreCase = false;
|
|
2123
2375
|
for (let i = 0; i < frames.length; i++) {
|
|
2124
2376
|
if (dateAlternatives.date) {
|
|
2125
2377
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2126
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2378
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2127
2379
|
result.frame = frames[i];
|
|
2128
2380
|
results.push(result);
|
|
2129
2381
|
}
|
|
2130
2382
|
}
|
|
2131
2383
|
else if (numberAlternatives.number) {
|
|
2132
2384
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2133
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2385
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2134
2386
|
result.frame = frames[i];
|
|
2135
2387
|
results.push(result);
|
|
2136
2388
|
}
|
|
2137
2389
|
}
|
|
2138
2390
|
else {
|
|
2139
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2391
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2140
2392
|
result.frame = frames[i];
|
|
2141
2393
|
results.push(result);
|
|
2142
2394
|
}
|
|
@@ -2155,11 +2407,15 @@ class StableBrowser {
|
|
|
2155
2407
|
scroll: false,
|
|
2156
2408
|
highlight: false,
|
|
2157
2409
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2158
|
-
text: `Verify text exists in page`,
|
|
2410
|
+
text: `Verify the text '${text}' exists in page`,
|
|
2411
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2159
2412
|
operation: "verifyTextExistInPage",
|
|
2160
2413
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2161
2414
|
};
|
|
2162
|
-
|
|
2415
|
+
if (testForRegex(text)) {
|
|
2416
|
+
text = text.replace(/\\"/g, '"');
|
|
2417
|
+
}
|
|
2418
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2163
2419
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2164
2420
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2165
2421
|
if (newValue !== text) {
|
|
@@ -2172,7 +2428,15 @@ class StableBrowser {
|
|
|
2172
2428
|
await _preCommand(state, this);
|
|
2173
2429
|
state.info.text = text;
|
|
2174
2430
|
while (true) {
|
|
2175
|
-
|
|
2431
|
+
let resultWithElementsFound = {
|
|
2432
|
+
length: 0,
|
|
2433
|
+
};
|
|
2434
|
+
try {
|
|
2435
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2436
|
+
}
|
|
2437
|
+
catch (error) {
|
|
2438
|
+
// ignore
|
|
2439
|
+
}
|
|
2176
2440
|
if (resultWithElementsFound.length === 0) {
|
|
2177
2441
|
if (Date.now() - state.startTime > timeout) {
|
|
2178
2442
|
throw new Error(`Text ${text} not found in page`);
|
|
@@ -2180,18 +2444,40 @@ class StableBrowser {
|
|
|
2180
2444
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2181
2445
|
continue;
|
|
2182
2446
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2447
|
+
try {
|
|
2448
|
+
if (resultWithElementsFound[0].randomToken) {
|
|
2449
|
+
const frame = resultWithElementsFound[0].frame;
|
|
2450
|
+
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2451
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2452
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2453
|
+
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2454
|
+
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2455
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2456
|
+
// this._unhighlightElements(frame, dataAttribute)
|
|
2457
|
+
// .then(async () => {
|
|
2458
|
+
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2459
|
+
// })
|
|
2460
|
+
// .catch(
|
|
2461
|
+
// (e) => {}
|
|
2462
|
+
// console.error(e)
|
|
2463
|
+
// );
|
|
2464
|
+
// });
|
|
2465
|
+
// }
|
|
2466
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2467
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2468
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2469
|
+
if (element) {
|
|
2470
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2471
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2472
|
+
// await _screenshot(state, this, element);
|
|
2473
|
+
}
|
|
2191
2474
|
}
|
|
2475
|
+
await _screenshot(state, this);
|
|
2476
|
+
return state.info;
|
|
2477
|
+
}
|
|
2478
|
+
catch (error) {
|
|
2479
|
+
console.error(error);
|
|
2192
2480
|
}
|
|
2193
|
-
await _screenshot(state, this);
|
|
2194
|
-
return state.info;
|
|
2195
2481
|
}
|
|
2196
2482
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2197
2483
|
}
|
|
@@ -2212,11 +2498,15 @@ class StableBrowser {
|
|
|
2212
2498
|
scroll: false,
|
|
2213
2499
|
highlight: false,
|
|
2214
2500
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2215
|
-
text: `Verify text does not exist in page`,
|
|
2501
|
+
text: `Verify the text '${text}' does not exist in page`,
|
|
2502
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2216
2503
|
operation: "verifyTextNotExistInPage",
|
|
2217
2504
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2218
2505
|
};
|
|
2219
|
-
|
|
2506
|
+
if (testForRegex(text)) {
|
|
2507
|
+
text = text.replace(/\\"/g, '"');
|
|
2508
|
+
}
|
|
2509
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2220
2510
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2221
2511
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2222
2512
|
if (newValue !== text) {
|
|
@@ -2228,8 +2518,16 @@ class StableBrowser {
|
|
|
2228
2518
|
try {
|
|
2229
2519
|
await _preCommand(state, this);
|
|
2230
2520
|
state.info.text = text;
|
|
2521
|
+
let resultWithElementsFound = {
|
|
2522
|
+
length: null, // initial cannot be 0
|
|
2523
|
+
};
|
|
2231
2524
|
while (true) {
|
|
2232
|
-
|
|
2525
|
+
try {
|
|
2526
|
+
resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
|
|
2527
|
+
}
|
|
2528
|
+
catch (error) {
|
|
2529
|
+
// ignore
|
|
2530
|
+
}
|
|
2233
2531
|
if (resultWithElementsFound.length === 0) {
|
|
2234
2532
|
await _screenshot(state, this);
|
|
2235
2533
|
return state.info;
|
|
@@ -2259,10 +2557,11 @@ class StableBrowser {
|
|
|
2259
2557
|
highlight: false,
|
|
2260
2558
|
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2261
2559
|
text: `Verify text with relation to another text`,
|
|
2560
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
2262
2561
|
operation: "verify_text_with_relation",
|
|
2263
2562
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2264
2563
|
};
|
|
2265
|
-
const timeout = this.
|
|
2564
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2266
2565
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2267
2566
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2268
2567
|
if (newValue !== textAnchor) {
|
|
@@ -2280,8 +2579,16 @@ class StableBrowser {
|
|
|
2280
2579
|
try {
|
|
2281
2580
|
await _preCommand(state, this);
|
|
2282
2581
|
state.info.text = textToVerify;
|
|
2582
|
+
let resultWithElementsFound = {
|
|
2583
|
+
length: 0,
|
|
2584
|
+
};
|
|
2283
2585
|
while (true) {
|
|
2284
|
-
|
|
2586
|
+
try {
|
|
2587
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2588
|
+
}
|
|
2589
|
+
catch (error) {
|
|
2590
|
+
// ignore
|
|
2591
|
+
}
|
|
2285
2592
|
if (resultWithElementsFound.length === 0) {
|
|
2286
2593
|
if (Date.now() - state.startTime > timeout) {
|
|
2287
2594
|
throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
|
|
@@ -2289,51 +2596,56 @@ class StableBrowser {
|
|
|
2289
2596
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2290
2597
|
continue;
|
|
2291
2598
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
const
|
|
2300
|
-
for (let i = 0; i <
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2599
|
+
try {
|
|
2600
|
+
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2601
|
+
foundAncore = true;
|
|
2602
|
+
const result = resultWithElementsFound[i];
|
|
2603
|
+
const token = result.randomToken;
|
|
2604
|
+
const frame = result.frame;
|
|
2605
|
+
let css = `[data-blinq-id-${token}]`;
|
|
2606
|
+
const climbArray1 = [];
|
|
2607
|
+
for (let i = 0; i < climb; i++) {
|
|
2608
|
+
climbArray1.push("..");
|
|
2609
|
+
}
|
|
2610
|
+
let climbXpath = "xpath=" + climbArray1.join("/");
|
|
2611
|
+
css = css + " >> " + climbXpath;
|
|
2612
|
+
const count = await frame.locator(css).count();
|
|
2613
|
+
for (let j = 0; j < count; j++) {
|
|
2614
|
+
const continer = await frame.locator(css).nth(j);
|
|
2615
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2616
|
+
if (result.elementCount > 0) {
|
|
2617
|
+
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2618
|
+
await this._highlightElements(frame, dataAttribute);
|
|
2619
|
+
//const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2620
|
+
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2621
|
+
// console.log(`Highlighting for vtrt while running from recorder`);
|
|
2622
|
+
// this._highlightElements(frame, dataAttribute)
|
|
2623
|
+
// .then(async () => {
|
|
2624
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2625
|
+
// this._unhighlightElements(frame, dataAttribute).then(
|
|
2626
|
+
// () => {}
|
|
2627
|
+
// console.log(`Unhighlighting vrtr in recorder is successful`)
|
|
2628
|
+
// );
|
|
2629
|
+
// })
|
|
2630
|
+
// .catch(e);
|
|
2631
|
+
// }
|
|
2632
|
+
//await this._highlightElements(frame, cssAnchor);
|
|
2633
|
+
const element = await frame.locator(dataAttribute).first();
|
|
2634
|
+
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2635
|
+
// await this._unhighlightElements(frame, dataAttribute);
|
|
2636
|
+
if (element) {
|
|
2637
|
+
await this.scrollIfNeeded(element, state.info);
|
|
2638
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2307
2639
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
continue;
|
|
2311
|
-
}
|
|
2312
|
-
const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
|
|
2313
|
-
if (foundElements.length > 0) {
|
|
2314
|
-
// set the container element attribute
|
|
2315
|
-
element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
|
|
2316
|
-
climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
|
|
2317
|
-
foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
|
|
2318
|
-
return { found: true };
|
|
2640
|
+
await _screenshot(state, this);
|
|
2641
|
+
return state.info;
|
|
2319
2642
|
}
|
|
2320
2643
|
}
|
|
2321
|
-
return { found: false };
|
|
2322
|
-
}, [css, climb, textToVerify, result.randomToken]);
|
|
2323
|
-
if (findResult.found === true) {
|
|
2324
|
-
const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
|
|
2325
|
-
const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
|
|
2326
|
-
await this._highlightElements(frame, dataAttribute);
|
|
2327
|
-
await this._highlightElements(frame, cssAnchor);
|
|
2328
|
-
const element = await frame.$(dataAttribute);
|
|
2329
|
-
if (element) {
|
|
2330
|
-
await this.scrollIfNeeded(element, state.info);
|
|
2331
|
-
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2332
|
-
}
|
|
2333
|
-
await _screenshot(state, this);
|
|
2334
|
-
return state.info;
|
|
2335
2644
|
}
|
|
2336
2645
|
}
|
|
2646
|
+
catch (error) {
|
|
2647
|
+
console.error(error);
|
|
2648
|
+
}
|
|
2337
2649
|
}
|
|
2338
2650
|
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2339
2651
|
}
|
|
@@ -2344,6 +2656,30 @@ class StableBrowser {
|
|
|
2344
2656
|
_commandFinally(state, this);
|
|
2345
2657
|
}
|
|
2346
2658
|
}
|
|
2659
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2660
|
+
const frames = this.page.frames();
|
|
2661
|
+
let results = [];
|
|
2662
|
+
let ignoreCase = false;
|
|
2663
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2664
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2665
|
+
result.frame = frames[i];
|
|
2666
|
+
const climbArray = [];
|
|
2667
|
+
for (let i = 0; i < climb; i++) {
|
|
2668
|
+
climbArray.push("..");
|
|
2669
|
+
}
|
|
2670
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2671
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2672
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2673
|
+
if (count > 0) {
|
|
2674
|
+
result.elementCount = count;
|
|
2675
|
+
result.locator = newLocator;
|
|
2676
|
+
results.push(result);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
// state.info.results = results;
|
|
2680
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2681
|
+
return resultWithElementsFound;
|
|
2682
|
+
}
|
|
2347
2683
|
async visualVerification(text, options = {}, world = null) {
|
|
2348
2684
|
const startTime = Date.now();
|
|
2349
2685
|
let error = null;
|
|
@@ -2362,10 +2698,13 @@ class StableBrowser {
|
|
|
2362
2698
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2363
2699
|
info.screenshotPath = screenshotPath;
|
|
2364
2700
|
const screenshot = await this.takeScreenshot();
|
|
2365
|
-
|
|
2366
|
-
method: "
|
|
2701
|
+
let request = {
|
|
2702
|
+
method: "post",
|
|
2703
|
+
maxBodyLength: Infinity,
|
|
2367
2704
|
url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
|
|
2368
2705
|
headers: {
|
|
2706
|
+
"x-bvt-project-id": path.basename(this.project_path),
|
|
2707
|
+
"x-source": "aaa",
|
|
2369
2708
|
"Content-Type": "application/json",
|
|
2370
2709
|
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
2371
2710
|
},
|
|
@@ -2374,7 +2713,7 @@ class StableBrowser {
|
|
|
2374
2713
|
screenshot: screenshot,
|
|
2375
2714
|
}),
|
|
2376
2715
|
};
|
|
2377
|
-
|
|
2716
|
+
const result = await axios.request(request);
|
|
2378
2717
|
if (result.data.status !== true) {
|
|
2379
2718
|
throw new Error("Visual validation failed");
|
|
2380
2719
|
}
|
|
@@ -2402,6 +2741,7 @@ class StableBrowser {
|
|
|
2402
2741
|
_reportToWorld(world, {
|
|
2403
2742
|
type: Types.VERIFY_VISUAL,
|
|
2404
2743
|
text: "Visual verification",
|
|
2744
|
+
_text: "Visual verification of " + text,
|
|
2405
2745
|
screenshotId,
|
|
2406
2746
|
result: error
|
|
2407
2747
|
? {
|
|
@@ -2668,6 +3008,32 @@ class StableBrowser {
|
|
|
2668
3008
|
}
|
|
2669
3009
|
return timeout;
|
|
2670
3010
|
}
|
|
3011
|
+
_getFindElementTimeout(options) {
|
|
3012
|
+
if (options && options.timeout) {
|
|
3013
|
+
return options.timeout;
|
|
3014
|
+
}
|
|
3015
|
+
if (this.configuration.find_element_timeout) {
|
|
3016
|
+
return this.configuration.find_element_timeout;
|
|
3017
|
+
}
|
|
3018
|
+
return 30000;
|
|
3019
|
+
}
|
|
3020
|
+
async saveStoreState(path = null, world = null) {
|
|
3021
|
+
const storageState = await this.page.context().storageState();
|
|
3022
|
+
//const testDataFile = _getDataFile(world, this.context, this);
|
|
3023
|
+
if (path) {
|
|
3024
|
+
// save { storageState: storageState } into the path
|
|
3025
|
+
fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
|
|
3026
|
+
}
|
|
3027
|
+
else {
|
|
3028
|
+
await this.setTestData({ storageState: storageState }, world);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
async restoreSaveState(path = null, world = null) {
|
|
3032
|
+
await refreshBrowser(this, path, world);
|
|
3033
|
+
this.registerEventListeners(this.context);
|
|
3034
|
+
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
3035
|
+
registerDownloadEvent(this.page, this.world, this.context);
|
|
3036
|
+
}
|
|
2671
3037
|
async waitForPageLoad(options = {}, world = null) {
|
|
2672
3038
|
let timeout = this._getLoadTimeout(options);
|
|
2673
3039
|
const promiseArray = [];
|
|
@@ -2735,6 +3101,7 @@ class StableBrowser {
|
|
|
2735
3101
|
highlight: false,
|
|
2736
3102
|
type: Types.CLOSE_PAGE,
|
|
2737
3103
|
text: `Close page`,
|
|
3104
|
+
_text: `Close the page`,
|
|
2738
3105
|
operation: "closePage",
|
|
2739
3106
|
log: "***** close page *****\n",
|
|
2740
3107
|
throwError: false,
|
|
@@ -2751,8 +3118,95 @@ class StableBrowser {
|
|
|
2751
3118
|
_commandFinally(state, this);
|
|
2752
3119
|
}
|
|
2753
3120
|
}
|
|
3121
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3122
|
+
let operation = null;
|
|
3123
|
+
if (!options || !options.operation) {
|
|
3124
|
+
throw new Error("operation is not defined");
|
|
3125
|
+
}
|
|
3126
|
+
operation = options.operation;
|
|
3127
|
+
// validate operation is one of the supported operations
|
|
3128
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3129
|
+
throw new Error("operation is not supported");
|
|
3130
|
+
}
|
|
3131
|
+
const state = {
|
|
3132
|
+
options,
|
|
3133
|
+
world,
|
|
3134
|
+
locate: false,
|
|
3135
|
+
scroll: false,
|
|
3136
|
+
highlight: false,
|
|
3137
|
+
type: Types.TABLE_OPERATION,
|
|
3138
|
+
text: `Table operation`,
|
|
3139
|
+
_text: `Table ${operation} operation`,
|
|
3140
|
+
operation: operation,
|
|
3141
|
+
log: "***** Table operation *****\n",
|
|
3142
|
+
};
|
|
3143
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3144
|
+
try {
|
|
3145
|
+
await _preCommand(state, this);
|
|
3146
|
+
const start = Date.now();
|
|
3147
|
+
let cellArea = null;
|
|
3148
|
+
while (true) {
|
|
3149
|
+
try {
|
|
3150
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3151
|
+
if (cellArea) {
|
|
3152
|
+
break;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
catch (e) {
|
|
3156
|
+
// ignore
|
|
3157
|
+
}
|
|
3158
|
+
if (Date.now() - start > timeout) {
|
|
3159
|
+
throw new Error(`Cell not found in table`);
|
|
3160
|
+
}
|
|
3161
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3162
|
+
}
|
|
3163
|
+
switch (operation) {
|
|
3164
|
+
case "click":
|
|
3165
|
+
if (!options.css) {
|
|
3166
|
+
// will click in the center of the cell
|
|
3167
|
+
let xOffset = 0;
|
|
3168
|
+
let yOffset = 0;
|
|
3169
|
+
if (options.xOffset) {
|
|
3170
|
+
xOffset = options.xOffset;
|
|
3171
|
+
}
|
|
3172
|
+
if (options.yOffset) {
|
|
3173
|
+
yOffset = options.yOffset;
|
|
3174
|
+
}
|
|
3175
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3176
|
+
}
|
|
3177
|
+
else {
|
|
3178
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3179
|
+
if (results.length === 0) {
|
|
3180
|
+
throw new Error(`Element not found in cell area`);
|
|
3181
|
+
}
|
|
3182
|
+
state.element = results[0];
|
|
3183
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3184
|
+
}
|
|
3185
|
+
break;
|
|
3186
|
+
case "hover+click":
|
|
3187
|
+
if (!options.css) {
|
|
3188
|
+
throw new Error("css is not defined");
|
|
3189
|
+
}
|
|
3190
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3191
|
+
if (results.length === 0) {
|
|
3192
|
+
throw new Error(`Element not found in cell area`);
|
|
3193
|
+
}
|
|
3194
|
+
state.element = results[0];
|
|
3195
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3196
|
+
break;
|
|
3197
|
+
default:
|
|
3198
|
+
throw new Error("operation is not supported");
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
catch (e) {
|
|
3202
|
+
await _commandError(state, e, this);
|
|
3203
|
+
}
|
|
3204
|
+
finally {
|
|
3205
|
+
_commandFinally(state, this);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
2754
3208
|
saveTestDataAsGlobal(options, world) {
|
|
2755
|
-
const dataFile =
|
|
3209
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2756
3210
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2757
3211
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2758
3212
|
}
|
|
@@ -2782,6 +3236,7 @@ class StableBrowser {
|
|
|
2782
3236
|
_reportToWorld(world, {
|
|
2783
3237
|
type: Types.SET_VIEWPORT,
|
|
2784
3238
|
text: "set viewport size to " + width + "x" + hight,
|
|
3239
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2785
3240
|
screenshotId,
|
|
2786
3241
|
result: error
|
|
2787
3242
|
? {
|
|
@@ -2869,6 +3324,9 @@ class StableBrowser {
|
|
|
2869
3324
|
else {
|
|
2870
3325
|
this.stepName = "step " + this.stepIndex;
|
|
2871
3326
|
}
|
|
3327
|
+
if (this.context) {
|
|
3328
|
+
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3329
|
+
}
|
|
2872
3330
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
2873
3331
|
if (this.context.browserObject.context) {
|
|
2874
3332
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
@@ -2881,6 +3339,41 @@ class StableBrowser {
|
|
|
2881
3339
|
this.saveTestDataAsGlobal({}, world);
|
|
2882
3340
|
}
|
|
2883
3341
|
}
|
|
3342
|
+
if (this.initSnapshotTaken === false) {
|
|
3343
|
+
this.initSnapshotTaken = true;
|
|
3344
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3345
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3346
|
+
if (snapshot) {
|
|
3347
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
async getAriaSnapshot() {
|
|
3353
|
+
try {
|
|
3354
|
+
// find the page url
|
|
3355
|
+
const url = await this.page.url();
|
|
3356
|
+
// extract the path from the url
|
|
3357
|
+
const path = new URL(url).pathname;
|
|
3358
|
+
// get the page title
|
|
3359
|
+
const title = await this.page.title();
|
|
3360
|
+
// go over other frams
|
|
3361
|
+
const frames = this.page.frames();
|
|
3362
|
+
const snapshots = [];
|
|
3363
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3364
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3365
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3366
|
+
content.push(`- frame: ${i}`);
|
|
3367
|
+
const frame = frames[i];
|
|
3368
|
+
const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
|
|
3369
|
+
content.push(snapshot);
|
|
3370
|
+
}
|
|
3371
|
+
return content.join("\n");
|
|
3372
|
+
}
|
|
3373
|
+
catch (e) {
|
|
3374
|
+
console.error(e);
|
|
3375
|
+
}
|
|
3376
|
+
return null;
|
|
2884
3377
|
}
|
|
2885
3378
|
async afterStep(world, step) {
|
|
2886
3379
|
this.stepName = null;
|
|
@@ -2889,6 +3382,23 @@ class StableBrowser {
|
|
|
2889
3382
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
2890
3383
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
2891
3384
|
});
|
|
3385
|
+
if (world && world.attach) {
|
|
3386
|
+
await world.attach(JSON.stringify({
|
|
3387
|
+
type: "trace",
|
|
3388
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3389
|
+
}), "application/json+trace");
|
|
3390
|
+
}
|
|
3391
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
if (this.context) {
|
|
3395
|
+
this.context.examplesRow = null;
|
|
3396
|
+
}
|
|
3397
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3398
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3399
|
+
if (snapshot) {
|
|
3400
|
+
const obj = {};
|
|
3401
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
2892
3402
|
}
|
|
2893
3403
|
}
|
|
2894
3404
|
}
|