automation_model 1.0.620-dev → 1.0.620-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 +35 -21
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +103 -26
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +70 -22
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.d.ts +2 -0
- package/lib/bruno.js +381 -0
- package/lib/bruno.js.map +1 -0
- package/lib/command_common.d.ts +4 -4
- package/lib/command_common.js +34 -16
- package/lib/command_common.js.map +1 -1
- package/lib/environment.d.ts +1 -0
- package/lib/environment.js +1 -0
- package/lib/environment.js.map +1 -1
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +61 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +2 -2
- package/lib/init_browser.js +33 -27
- 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/locator_log.js +3 -0
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +1 -1
- package/lib/network.js +5 -5
- package/lib/network.js.map +1 -1
- package/lib/snapshot_validation.d.ts +37 -0
- package/lib/snapshot_validation.js +246 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +23 -1
- package/lib/stable_browser.js +675 -127
- package/lib/stable_browser.js.map +1 -1
- package/lib/table_helper.d.ts +19 -0
- package/lib/table_helper.js +116 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +2 -0
- package/lib/test_context.js +2 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +8 -4
- package/lib/utils.js +220 -19
- package/lib/utils.js.map +1 -1
- package/package.json +9 -8
package/lib/stable_browser.js
CHANGED
|
@@ -10,19 +10,24 @@ 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 { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, } 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
17
|
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
18
|
+
import { getTestData } from "./auto_page.js";
|
|
18
19
|
import { locate_element } from "./locate_element.js";
|
|
19
20
|
import { randomUUID } from "crypto";
|
|
20
21
|
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
21
22
|
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
22
23
|
import { LocatorLog } from "./locator_log.js";
|
|
23
24
|
import axios from "axios";
|
|
25
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
27
|
+
import { snapshotValidation } from "./snapshot_validation.js";
|
|
24
28
|
export const Types = {
|
|
25
29
|
CLICK: "click_element",
|
|
30
|
+
WAIT_ELEMENT: "wait_element",
|
|
26
31
|
NAVIGATE: "navigate",
|
|
27
32
|
FILL: "fill_element",
|
|
28
33
|
EXECUTE: "execute_page_method",
|
|
@@ -44,6 +49,7 @@ export const Types = {
|
|
|
44
49
|
UNCHECK: "uncheck_element",
|
|
45
50
|
EXTRACT: "extract_attribute",
|
|
46
51
|
CLOSE_PAGE: "close_page",
|
|
52
|
+
TABLE_OPERATION: "table_operation",
|
|
47
53
|
SET_DATE_TIME: "set_date_time",
|
|
48
54
|
SET_VIEWPORT: "set_viewport",
|
|
49
55
|
VERIFY_VISUAL: "verify_visual",
|
|
@@ -52,6 +58,10 @@ export const Types = {
|
|
|
52
58
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
53
59
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
54
60
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
61
|
+
BRUNO: "bruno",
|
|
62
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
63
|
+
SET_INPUT_FILES: "set_input_files",
|
|
64
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
55
65
|
};
|
|
56
66
|
export const apps = {};
|
|
57
67
|
const formatElementName = (elementName) => {
|
|
@@ -70,6 +80,7 @@ class StableBrowser {
|
|
|
70
80
|
appName = "main";
|
|
71
81
|
tags = null;
|
|
72
82
|
isRecording = false;
|
|
83
|
+
initSnapshotTaken = false;
|
|
73
84
|
constructor(browser, page, logger = null, context = null, world = null) {
|
|
74
85
|
this.browser = browser;
|
|
75
86
|
this.page = page;
|
|
@@ -176,6 +187,30 @@ class StableBrowser {
|
|
|
176
187
|
await this.waitForPageLoad();
|
|
177
188
|
}
|
|
178
189
|
}
|
|
190
|
+
async switchTab(tabTitleOrIndex) {
|
|
191
|
+
// first check if the tabNameOrIndex is a number
|
|
192
|
+
let index = parseInt(tabTitleOrIndex);
|
|
193
|
+
if (!isNaN(index)) {
|
|
194
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
195
|
+
this.page = this.context.pages[index];
|
|
196
|
+
this.context.page = this.page;
|
|
197
|
+
await this.page.bringToFront();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
202
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
203
|
+
let page = this.context.pages[i];
|
|
204
|
+
let title = await page.title();
|
|
205
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
206
|
+
this.page = page;
|
|
207
|
+
this.context.page = this.page;
|
|
208
|
+
await this.page.bringToFront();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
213
|
+
}
|
|
179
214
|
registerConsoleLogListener(page, context) {
|
|
180
215
|
if (!this.context.webLogger) {
|
|
181
216
|
this.context.webLogger = [];
|
|
@@ -271,7 +306,7 @@ class StableBrowser {
|
|
|
271
306
|
_commandError(state, error, this);
|
|
272
307
|
}
|
|
273
308
|
finally {
|
|
274
|
-
_commandFinally(state, this);
|
|
309
|
+
await _commandFinally(state, this);
|
|
275
310
|
}
|
|
276
311
|
}
|
|
277
312
|
async _getLocator(locator, scope, _params) {
|
|
@@ -352,7 +387,7 @@ class StableBrowser {
|
|
|
352
387
|
return resultCss;
|
|
353
388
|
}
|
|
354
389
|
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
355
|
-
const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase)
|
|
390
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
356
391
|
const locator = scope.locator(query);
|
|
357
392
|
const count = await locator.count();
|
|
358
393
|
if (!tag1) {
|
|
@@ -372,6 +407,12 @@ class StableBrowser {
|
|
|
372
407
|
if (!el.setAttribute) {
|
|
373
408
|
el = el.parentElement;
|
|
374
409
|
}
|
|
410
|
+
// remove any attributes start with data-blinq-id
|
|
411
|
+
// for (let i = 0; i < el.attributes.length; i++) {
|
|
412
|
+
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
413
|
+
// el.removeAttribute(el.attributes[i].name);
|
|
414
|
+
// }
|
|
415
|
+
// }
|
|
375
416
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
376
417
|
return true;
|
|
377
418
|
}, [tag1, randomToken]))) {
|
|
@@ -547,10 +588,14 @@ class StableBrowser {
|
|
|
547
588
|
timeout = 30000;
|
|
548
589
|
}
|
|
549
590
|
for (let i = 0; i < 3; i++) {
|
|
550
|
-
|
|
591
|
+
if (process.env.SUPRESS_ERRORS === "true") {
|
|
592
|
+
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
593
|
+
}
|
|
551
594
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
552
595
|
let selector = selectors.locators[j];
|
|
553
|
-
|
|
596
|
+
if (process.env.SUPRESS_ERRORS === "true") {
|
|
597
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
598
|
+
}
|
|
554
599
|
}
|
|
555
600
|
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
556
601
|
if (!element.rerun) {
|
|
@@ -558,15 +603,32 @@ class StableBrowser {
|
|
|
558
603
|
element.evaluate((el, randomToken) => {
|
|
559
604
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
560
605
|
}, randomToken);
|
|
561
|
-
if (element._frame) {
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
const scope = element.page();
|
|
565
|
-
|
|
566
|
-
|
|
606
|
+
// if (element._frame) {
|
|
607
|
+
// return element;
|
|
608
|
+
// }
|
|
609
|
+
const scope = element._frame ?? element.page();
|
|
610
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
611
|
+
let prefixSelector = "";
|
|
612
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
613
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
614
|
+
if (frameSelectorIndex !== -1) {
|
|
615
|
+
// remove everything after the >> internal:control=enter-frame
|
|
616
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
617
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
618
|
+
}
|
|
619
|
+
// if (element?._frame?._selector) {
|
|
620
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
621
|
+
// }
|
|
622
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
623
|
+
return scope.locator(newSelector);
|
|
567
624
|
}
|
|
568
625
|
}
|
|
569
|
-
|
|
626
|
+
if (process.env.SUPRESS_ERRORS === "true") {
|
|
627
|
+
throw new Error("unable to locate the element");
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
631
|
+
}
|
|
570
632
|
}
|
|
571
633
|
async _findFrameScope(selectors, timeout = 30000, info) {
|
|
572
634
|
if (!info) {
|
|
@@ -715,14 +777,9 @@ class StableBrowser {
|
|
|
715
777
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
716
778
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
717
779
|
}
|
|
718
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
780
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
719
781
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
720
782
|
}
|
|
721
|
-
else {
|
|
722
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
723
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
783
|
let foundElements = result.foundElements;
|
|
727
784
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
728
785
|
info.box = foundElements[0].box;
|
|
@@ -777,6 +834,11 @@ class StableBrowser {
|
|
|
777
834
|
visibleOnly = false;
|
|
778
835
|
}
|
|
779
836
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
837
|
+
// sheck of more of half of the timeout has passed
|
|
838
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
839
|
+
highPriorityOnly = false;
|
|
840
|
+
visibleOnly = false;
|
|
841
|
+
}
|
|
780
842
|
}
|
|
781
843
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
782
844
|
// if (info.locatorLog) {
|
|
@@ -790,7 +852,12 @@ class StableBrowser {
|
|
|
790
852
|
if (!info?.failCause?.lastError) {
|
|
791
853
|
info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
|
|
792
854
|
}
|
|
793
|
-
|
|
855
|
+
if (process.env.SUPRESS_ERRORS === "true") {
|
|
856
|
+
throw new Error("failed to locate first element no elements found");
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
860
|
+
}
|
|
794
861
|
}
|
|
795
862
|
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
796
863
|
let foundElements = [];
|
|
@@ -823,9 +890,40 @@ class StableBrowser {
|
|
|
823
890
|
result.locatorIndex = i;
|
|
824
891
|
}
|
|
825
892
|
if (foundLocators.length > 1) {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
893
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
894
|
+
const boxes = [];
|
|
895
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
896
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
897
|
+
}
|
|
898
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
899
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
900
|
+
if (j === k) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
904
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
905
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
906
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
907
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
908
|
+
// as the element is not unique, will remove it
|
|
909
|
+
boxes.splice(k, 1);
|
|
910
|
+
k--;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (boxes.length === 1) {
|
|
915
|
+
result.foundElements.push({
|
|
916
|
+
locator: boxes[0].locator.first(),
|
|
917
|
+
box: boxes[0].box,
|
|
918
|
+
unique: true,
|
|
919
|
+
});
|
|
920
|
+
result.locatorIndex = i;
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
info.failCause.foundMultiple = true;
|
|
924
|
+
if (info.locatorLog) {
|
|
925
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
926
|
+
}
|
|
829
927
|
}
|
|
830
928
|
}
|
|
831
929
|
}
|
|
@@ -873,7 +971,7 @@ class StableBrowser {
|
|
|
873
971
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
874
972
|
}
|
|
875
973
|
finally {
|
|
876
|
-
_commandFinally(state, this);
|
|
974
|
+
await _commandFinally(state, this);
|
|
877
975
|
}
|
|
878
976
|
}
|
|
879
977
|
}
|
|
@@ -922,7 +1020,7 @@ class StableBrowser {
|
|
|
922
1020
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
923
1021
|
}
|
|
924
1022
|
finally {
|
|
925
|
-
_commandFinally(state, this);
|
|
1023
|
+
await _commandFinally(state, this);
|
|
926
1024
|
}
|
|
927
1025
|
}
|
|
928
1026
|
}
|
|
@@ -936,25 +1034,14 @@ class StableBrowser {
|
|
|
936
1034
|
options,
|
|
937
1035
|
world,
|
|
938
1036
|
text: "Click element",
|
|
1037
|
+
_text: "Click on " + selectors.element_name,
|
|
939
1038
|
type: Types.CLICK,
|
|
940
1039
|
operation: "click",
|
|
941
1040
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
942
1041
|
};
|
|
943
1042
|
try {
|
|
944
1043
|
await _preCommand(state, this);
|
|
945
|
-
|
|
946
|
-
// state.selectors.locators[0].text = state.options.context;
|
|
947
|
-
// }
|
|
948
|
-
try {
|
|
949
|
-
await state.element.click();
|
|
950
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
951
|
-
}
|
|
952
|
-
catch (e) {
|
|
953
|
-
// await this.closeUnexpectedPopups();
|
|
954
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
955
|
-
await state.element.dispatchEvent("click");
|
|
956
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
957
|
-
}
|
|
1044
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
958
1045
|
await this.waitForPageLoad();
|
|
959
1046
|
return state.info;
|
|
960
1047
|
}
|
|
@@ -962,9 +1049,41 @@ class StableBrowser {
|
|
|
962
1049
|
await _commandError(state, e, this);
|
|
963
1050
|
}
|
|
964
1051
|
finally {
|
|
965
|
-
_commandFinally(state, this);
|
|
1052
|
+
await _commandFinally(state, this);
|
|
966
1053
|
}
|
|
967
1054
|
}
|
|
1055
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1056
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1057
|
+
const state = {
|
|
1058
|
+
selectors,
|
|
1059
|
+
_params,
|
|
1060
|
+
options,
|
|
1061
|
+
world,
|
|
1062
|
+
text: "Wait for element",
|
|
1063
|
+
_text: "Wait for " + selectors.element_name,
|
|
1064
|
+
type: Types.WAIT_ELEMENT,
|
|
1065
|
+
operation: "waitForElement",
|
|
1066
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1067
|
+
};
|
|
1068
|
+
let found = false;
|
|
1069
|
+
try {
|
|
1070
|
+
await _preCommand(state, this);
|
|
1071
|
+
// if (state.options && state.options.context) {
|
|
1072
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1073
|
+
// }
|
|
1074
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1075
|
+
found = true;
|
|
1076
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1077
|
+
}
|
|
1078
|
+
catch (e) {
|
|
1079
|
+
console.error("Error on waitForElement", e);
|
|
1080
|
+
// await _commandError(state, e, this);
|
|
1081
|
+
}
|
|
1082
|
+
finally {
|
|
1083
|
+
await _commandFinally(state, this);
|
|
1084
|
+
}
|
|
1085
|
+
return found;
|
|
1086
|
+
}
|
|
968
1087
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
969
1088
|
const state = {
|
|
970
1089
|
selectors,
|
|
@@ -973,6 +1092,7 @@ class StableBrowser {
|
|
|
973
1092
|
world,
|
|
974
1093
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
975
1094
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1095
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
976
1096
|
operation: "setCheck",
|
|
977
1097
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
978
1098
|
};
|
|
@@ -984,7 +1104,7 @@ class StableBrowser {
|
|
|
984
1104
|
try {
|
|
985
1105
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
986
1106
|
// console.log(`Highlighting while running from recorder`);
|
|
987
|
-
await this._highlightElements(element);
|
|
1107
|
+
await this._highlightElements(state.element);
|
|
988
1108
|
await state.element.setChecked(checked);
|
|
989
1109
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
990
1110
|
// await this._unHighlightElements(element);
|
|
@@ -1011,7 +1131,7 @@ class StableBrowser {
|
|
|
1011
1131
|
await _commandError(state, e, this);
|
|
1012
1132
|
}
|
|
1013
1133
|
finally {
|
|
1014
|
-
_commandFinally(state, this);
|
|
1134
|
+
await _commandFinally(state, this);
|
|
1015
1135
|
}
|
|
1016
1136
|
}
|
|
1017
1137
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1022,24 +1142,13 @@ class StableBrowser {
|
|
|
1022
1142
|
world,
|
|
1023
1143
|
type: Types.HOVER,
|
|
1024
1144
|
text: `Hover element`,
|
|
1145
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1025
1146
|
operation: "hover",
|
|
1026
1147
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1027
1148
|
};
|
|
1028
1149
|
try {
|
|
1029
1150
|
await _preCommand(state, this);
|
|
1030
|
-
|
|
1031
|
-
await state.element.hover();
|
|
1032
|
-
// await _screenshot(state, this);
|
|
1033
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1034
|
-
}
|
|
1035
|
-
catch (e) {
|
|
1036
|
-
//await this.closeUnexpectedPopups();
|
|
1037
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1038
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1039
|
-
await state.element.hover({ timeout: 10000 });
|
|
1040
|
-
// await _screenshot(state, this);
|
|
1041
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1042
|
-
}
|
|
1151
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1043
1152
|
await _screenshot(state, this);
|
|
1044
1153
|
await this.waitForPageLoad();
|
|
1045
1154
|
return state.info;
|
|
@@ -1048,7 +1157,7 @@ class StableBrowser {
|
|
|
1048
1157
|
await _commandError(state, e, this);
|
|
1049
1158
|
}
|
|
1050
1159
|
finally {
|
|
1051
|
-
_commandFinally(state, this);
|
|
1160
|
+
await _commandFinally(state, this);
|
|
1052
1161
|
}
|
|
1053
1162
|
}
|
|
1054
1163
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1063,6 +1172,7 @@ class StableBrowser {
|
|
|
1063
1172
|
value: values.toString(),
|
|
1064
1173
|
type: Types.SELECT,
|
|
1065
1174
|
text: `Select option: ${values}`,
|
|
1175
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1066
1176
|
operation: "selectOption",
|
|
1067
1177
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1068
1178
|
};
|
|
@@ -1083,7 +1193,7 @@ class StableBrowser {
|
|
|
1083
1193
|
await _commandError(state, e, this);
|
|
1084
1194
|
}
|
|
1085
1195
|
finally {
|
|
1086
|
-
_commandFinally(state, this);
|
|
1196
|
+
await _commandFinally(state, this);
|
|
1087
1197
|
}
|
|
1088
1198
|
}
|
|
1089
1199
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1097,6 +1207,7 @@ class StableBrowser {
|
|
|
1097
1207
|
highlight: false,
|
|
1098
1208
|
type: Types.TYPE_PRESS,
|
|
1099
1209
|
text: `Type value: ${_value}`,
|
|
1210
|
+
_text: `Type value: ${_value}`,
|
|
1100
1211
|
operation: "type",
|
|
1101
1212
|
log: "",
|
|
1102
1213
|
};
|
|
@@ -1128,7 +1239,7 @@ class StableBrowser {
|
|
|
1128
1239
|
await _commandError(state, e, this);
|
|
1129
1240
|
}
|
|
1130
1241
|
finally {
|
|
1131
|
-
_commandFinally(state, this);
|
|
1242
|
+
await _commandFinally(state, this);
|
|
1132
1243
|
}
|
|
1133
1244
|
}
|
|
1134
1245
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1164,7 +1275,7 @@ class StableBrowser {
|
|
|
1164
1275
|
await _commandError(state, e, this);
|
|
1165
1276
|
}
|
|
1166
1277
|
finally {
|
|
1167
|
-
_commandFinally(state, this);
|
|
1278
|
+
await _commandFinally(state, this);
|
|
1168
1279
|
}
|
|
1169
1280
|
}
|
|
1170
1281
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1176,6 +1287,7 @@ class StableBrowser {
|
|
|
1176
1287
|
world,
|
|
1177
1288
|
type: Types.SET_DATE_TIME,
|
|
1178
1289
|
text: `Set date time value: ${value}`,
|
|
1290
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1179
1291
|
operation: "setDateTime",
|
|
1180
1292
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1181
1293
|
throwError: false,
|
|
@@ -1183,7 +1295,7 @@ class StableBrowser {
|
|
|
1183
1295
|
try {
|
|
1184
1296
|
await _preCommand(state, this);
|
|
1185
1297
|
try {
|
|
1186
|
-
await state.element
|
|
1298
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1187
1299
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1188
1300
|
if (format) {
|
|
1189
1301
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1232,7 +1344,7 @@ class StableBrowser {
|
|
|
1232
1344
|
await _commandError(state, e, this);
|
|
1233
1345
|
}
|
|
1234
1346
|
finally {
|
|
1235
|
-
_commandFinally(state, this);
|
|
1347
|
+
await _commandFinally(state, this);
|
|
1236
1348
|
}
|
|
1237
1349
|
}
|
|
1238
1350
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1247,9 +1359,13 @@ class StableBrowser {
|
|
|
1247
1359
|
world,
|
|
1248
1360
|
type: Types.FILL,
|
|
1249
1361
|
text: `Click type input with value: ${_value}`,
|
|
1362
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1250
1363
|
operation: "clickType",
|
|
1251
1364
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1252
1365
|
};
|
|
1366
|
+
if (!options) {
|
|
1367
|
+
options = {};
|
|
1368
|
+
}
|
|
1253
1369
|
if (newValue !== _value) {
|
|
1254
1370
|
//this.logger.info(_value + "=" + newValue);
|
|
1255
1371
|
_value = newValue;
|
|
@@ -1257,7 +1373,7 @@ class StableBrowser {
|
|
|
1257
1373
|
try {
|
|
1258
1374
|
await _preCommand(state, this);
|
|
1259
1375
|
state.info.value = _value;
|
|
1260
|
-
if (
|
|
1376
|
+
if (!options.press) {
|
|
1261
1377
|
try {
|
|
1262
1378
|
let currentValue = await state.element.inputValue();
|
|
1263
1379
|
if (currentValue) {
|
|
@@ -1268,13 +1384,9 @@ class StableBrowser {
|
|
|
1268
1384
|
this.logger.info("unable to clear input value");
|
|
1269
1385
|
}
|
|
1270
1386
|
}
|
|
1271
|
-
if (options
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
catch (e) {
|
|
1276
|
-
await state.element.dispatchEvent("click");
|
|
1277
|
-
}
|
|
1387
|
+
if (options.press) {
|
|
1388
|
+
options.timeout = 5000;
|
|
1389
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1278
1390
|
}
|
|
1279
1391
|
else {
|
|
1280
1392
|
try {
|
|
@@ -1332,7 +1444,7 @@ class StableBrowser {
|
|
|
1332
1444
|
await _commandError(state, e, this);
|
|
1333
1445
|
}
|
|
1334
1446
|
finally {
|
|
1335
|
-
_commandFinally(state, this);
|
|
1447
|
+
await _commandFinally(state, this);
|
|
1336
1448
|
}
|
|
1337
1449
|
}
|
|
1338
1450
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1362,13 +1474,49 @@ class StableBrowser {
|
|
|
1362
1474
|
await _commandError(state, e, this);
|
|
1363
1475
|
}
|
|
1364
1476
|
finally {
|
|
1365
|
-
_commandFinally(state, this);
|
|
1477
|
+
await _commandFinally(state, this);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1481
|
+
const state = {
|
|
1482
|
+
selectors,
|
|
1483
|
+
_params,
|
|
1484
|
+
files,
|
|
1485
|
+
value: '"' + files.join('", "') + '"',
|
|
1486
|
+
options,
|
|
1487
|
+
world,
|
|
1488
|
+
type: Types.SET_INPUT_FILES,
|
|
1489
|
+
text: `Set input files`,
|
|
1490
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1491
|
+
operation: "setInputFiles",
|
|
1492
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1493
|
+
};
|
|
1494
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1495
|
+
try {
|
|
1496
|
+
await _preCommand(state, this);
|
|
1497
|
+
for (let i = 0; i < files.length; i++) {
|
|
1498
|
+
const file = files[i];
|
|
1499
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1500
|
+
if (!fs.existsSync(filePath)) {
|
|
1501
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1502
|
+
}
|
|
1503
|
+
state.files[i] = filePath;
|
|
1504
|
+
}
|
|
1505
|
+
await state.element.setInputFiles(files);
|
|
1506
|
+
return state.info;
|
|
1507
|
+
}
|
|
1508
|
+
catch (e) {
|
|
1509
|
+
await _commandError(state, e, this);
|
|
1510
|
+
}
|
|
1511
|
+
finally {
|
|
1512
|
+
await _commandFinally(state, this);
|
|
1366
1513
|
}
|
|
1367
1514
|
}
|
|
1368
1515
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1369
1516
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1370
1517
|
}
|
|
1371
1518
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1519
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1372
1520
|
_validateSelectors(selectors);
|
|
1373
1521
|
let screenshotId = null;
|
|
1374
1522
|
let screenshotPath = null;
|
|
@@ -1378,7 +1526,7 @@ class StableBrowser {
|
|
|
1378
1526
|
}
|
|
1379
1527
|
info.operation = "getText";
|
|
1380
1528
|
info.selectors = selectors;
|
|
1381
|
-
let element = await this._locate(selectors, info, _params);
|
|
1529
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1382
1530
|
if (climb > 0) {
|
|
1383
1531
|
const climbArray = [];
|
|
1384
1532
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1445,6 +1593,7 @@ class StableBrowser {
|
|
|
1445
1593
|
highlight: false,
|
|
1446
1594
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1447
1595
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1596
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1448
1597
|
operation: "containsPattern",
|
|
1449
1598
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1450
1599
|
};
|
|
@@ -1476,10 +1625,12 @@ class StableBrowser {
|
|
|
1476
1625
|
await _commandError(state, e, this);
|
|
1477
1626
|
}
|
|
1478
1627
|
finally {
|
|
1479
|
-
_commandFinally(state, this);
|
|
1628
|
+
await _commandFinally(state, this);
|
|
1480
1629
|
}
|
|
1481
1630
|
}
|
|
1482
1631
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1632
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1633
|
+
const startTime = Date.now();
|
|
1483
1634
|
const state = {
|
|
1484
1635
|
selectors,
|
|
1485
1636
|
_params,
|
|
@@ -1506,44 +1657,124 @@ class StableBrowser {
|
|
|
1506
1657
|
}
|
|
1507
1658
|
let foundObj = null;
|
|
1508
1659
|
try {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
if (
|
|
1520
|
-
|
|
1660
|
+
while (Date.now() - startTime < timeout) {
|
|
1661
|
+
try {
|
|
1662
|
+
await _preCommand(state, this);
|
|
1663
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1664
|
+
if (foundObj && foundObj.element) {
|
|
1665
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1666
|
+
}
|
|
1667
|
+
await _screenshot(state, this);
|
|
1668
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1669
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1670
|
+
if (dateAlternatives.date) {
|
|
1671
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1672
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1673
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1674
|
+
return state.info;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
else if (numberAlternatives.number) {
|
|
1679
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1680
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1681
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1682
|
+
return state.info;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1521
1687
|
return state.info;
|
|
1522
1688
|
}
|
|
1523
1689
|
}
|
|
1524
|
-
|
|
1690
|
+
catch (e) {
|
|
1691
|
+
// Log error but continue retrying until timeout is reached
|
|
1692
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1693
|
+
}
|
|
1694
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1525
1695
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1696
|
+
state.info.foundText = foundObj?.text;
|
|
1697
|
+
state.info.value = foundObj?.value;
|
|
1698
|
+
throw new Error("element doesn't contain text " + text);
|
|
1699
|
+
}
|
|
1700
|
+
catch (e) {
|
|
1701
|
+
await _commandError(state, e, this);
|
|
1702
|
+
throw e;
|
|
1703
|
+
}
|
|
1704
|
+
finally {
|
|
1705
|
+
await _commandFinally(state, this);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1709
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1710
|
+
const startTime = Date.now();
|
|
1711
|
+
const state = {
|
|
1712
|
+
_params,
|
|
1713
|
+
value: referanceSnapshot,
|
|
1714
|
+
options,
|
|
1715
|
+
world,
|
|
1716
|
+
locate: false,
|
|
1717
|
+
scroll: false,
|
|
1718
|
+
screenshot: true,
|
|
1719
|
+
highlight: false,
|
|
1720
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1721
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1722
|
+
operation: "snapshotValidation",
|
|
1723
|
+
log: "***** verify snapshot *****\n",
|
|
1724
|
+
};
|
|
1725
|
+
if (!referanceSnapshot) {
|
|
1726
|
+
throw new Error("referanceSnapshot is null");
|
|
1727
|
+
}
|
|
1728
|
+
let text = null;
|
|
1729
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1730
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1731
|
+
}
|
|
1732
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1733
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1734
|
+
}
|
|
1735
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1736
|
+
text = referanceSnapshot.substring(5);
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1740
|
+
}
|
|
1741
|
+
state.text = text;
|
|
1742
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1743
|
+
await _preCommand(state, this);
|
|
1744
|
+
let foundObj = null;
|
|
1745
|
+
try {
|
|
1746
|
+
let matchResult = null;
|
|
1747
|
+
while (Date.now() - startTime < timeout) {
|
|
1748
|
+
try {
|
|
1749
|
+
let scope = null;
|
|
1750
|
+
if (!frameSelectors) {
|
|
1751
|
+
scope = this.page;
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1755
|
+
}
|
|
1756
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1757
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1758
|
+
if (matchResult.errorLine !== -1) {
|
|
1759
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1531
1760
|
}
|
|
1761
|
+
// highlight and screenshot
|
|
1762
|
+
return state.info;
|
|
1532
1763
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
throw new Error("element doesn't contain text " + text);
|
|
1764
|
+
catch (e) {
|
|
1765
|
+
// Log error but continue retrying until timeout is reached
|
|
1766
|
+
this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1767
|
+
}
|
|
1768
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1539
1769
|
}
|
|
1540
|
-
|
|
1770
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1541
1771
|
}
|
|
1542
1772
|
catch (e) {
|
|
1543
1773
|
await _commandError(state, e, this);
|
|
1774
|
+
throw e;
|
|
1544
1775
|
}
|
|
1545
1776
|
finally {
|
|
1546
|
-
_commandFinally(state, this);
|
|
1777
|
+
await _commandFinally(state, this);
|
|
1547
1778
|
}
|
|
1548
1779
|
}
|
|
1549
1780
|
async waitForUserInput(message, world = null) {
|
|
@@ -1581,6 +1812,15 @@ class StableBrowser {
|
|
|
1581
1812
|
// save the data to the file
|
|
1582
1813
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1583
1814
|
}
|
|
1815
|
+
overwriteTestData(testData, world = null) {
|
|
1816
|
+
if (!testData) {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
// if data file exists, load it
|
|
1820
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1821
|
+
// save the data to the file
|
|
1822
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1823
|
+
}
|
|
1584
1824
|
_getDataFilePath(fileName) {
|
|
1585
1825
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1586
1826
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1833,7 +2073,7 @@ class StableBrowser {
|
|
|
1833
2073
|
await _commandError(state, e, this);
|
|
1834
2074
|
}
|
|
1835
2075
|
finally {
|
|
1836
|
-
_commandFinally(state, this);
|
|
2076
|
+
await _commandFinally(state, this);
|
|
1837
2077
|
}
|
|
1838
2078
|
}
|
|
1839
2079
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1846,6 +2086,7 @@ class StableBrowser {
|
|
|
1846
2086
|
world,
|
|
1847
2087
|
type: Types.EXTRACT,
|
|
1848
2088
|
text: `Extract attribute from element`,
|
|
2089
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1849
2090
|
operation: "extractAttribute",
|
|
1850
2091
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1851
2092
|
allowDisabled: true,
|
|
@@ -1863,10 +2104,31 @@ class StableBrowser {
|
|
|
1863
2104
|
case "value":
|
|
1864
2105
|
state.value = await state.element.inputValue();
|
|
1865
2106
|
break;
|
|
2107
|
+
case "text":
|
|
2108
|
+
state.value = await state.element.textContent();
|
|
2109
|
+
break;
|
|
1866
2110
|
default:
|
|
1867
2111
|
state.value = await state.element.getAttribute(attribute);
|
|
1868
2112
|
break;
|
|
1869
2113
|
}
|
|
2114
|
+
if (options !== null) {
|
|
2115
|
+
if (options.regex && options.regex !== "") {
|
|
2116
|
+
// Construct a regex pattern from the provided string
|
|
2117
|
+
const regex = options.regex.slice(1, -1);
|
|
2118
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2119
|
+
const matches = state.value.match(regexPattern);
|
|
2120
|
+
if (matches) {
|
|
2121
|
+
let newValue = "";
|
|
2122
|
+
for (const match of matches) {
|
|
2123
|
+
newValue += match;
|
|
2124
|
+
}
|
|
2125
|
+
state.value = newValue;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2129
|
+
state.value = state.value.trim();
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
1870
2132
|
state.info.value = state.value;
|
|
1871
2133
|
this.setTestData({ [variable]: state.value }, world);
|
|
1872
2134
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1877,7 +2139,7 @@ class StableBrowser {
|
|
|
1877
2139
|
await _commandError(state, e, this);
|
|
1878
2140
|
}
|
|
1879
2141
|
finally {
|
|
1880
|
-
_commandFinally(state, this);
|
|
2142
|
+
await _commandFinally(state, this);
|
|
1881
2143
|
}
|
|
1882
2144
|
}
|
|
1883
2145
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1892,6 +2154,7 @@ class StableBrowser {
|
|
|
1892
2154
|
highlight: true,
|
|
1893
2155
|
screenshot: true,
|
|
1894
2156
|
text: `Verify element attribute`,
|
|
2157
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
1895
2158
|
operation: "verifyAttribute",
|
|
1896
2159
|
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1897
2160
|
allowDisabled: true,
|
|
@@ -1901,12 +2164,15 @@ class StableBrowser {
|
|
|
1901
2164
|
let expectedValue;
|
|
1902
2165
|
try {
|
|
1903
2166
|
await _preCommand(state, this);
|
|
1904
|
-
expectedValue = state.value;
|
|
2167
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1905
2168
|
state.info.expectedValue = expectedValue;
|
|
1906
2169
|
switch (attribute) {
|
|
1907
2170
|
case "innerText":
|
|
1908
2171
|
val = String(await state.element.innerText());
|
|
1909
2172
|
break;
|
|
2173
|
+
case "text":
|
|
2174
|
+
val = String(await state.element.textContent());
|
|
2175
|
+
break;
|
|
1910
2176
|
case "value":
|
|
1911
2177
|
val = String(await state.element.inputValue());
|
|
1912
2178
|
break;
|
|
@@ -1946,7 +2212,7 @@ class StableBrowser {
|
|
|
1946
2212
|
await _commandError(state, e, this);
|
|
1947
2213
|
}
|
|
1948
2214
|
finally {
|
|
1949
|
-
_commandFinally(state, this);
|
|
2215
|
+
await _commandFinally(state, this);
|
|
1950
2216
|
}
|
|
1951
2217
|
}
|
|
1952
2218
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2198,6 +2464,69 @@ class StableBrowser {
|
|
|
2198
2464
|
_reportToWorld(world, {
|
|
2199
2465
|
type: Types.VERIFY_PAGE_PATH,
|
|
2200
2466
|
text: "Verify page path",
|
|
2467
|
+
_text: "Verify the page path contains " + pathPart,
|
|
2468
|
+
screenshotId,
|
|
2469
|
+
result: error
|
|
2470
|
+
? {
|
|
2471
|
+
status: "FAILED",
|
|
2472
|
+
startTime,
|
|
2473
|
+
endTime,
|
|
2474
|
+
message: error?.message,
|
|
2475
|
+
}
|
|
2476
|
+
: {
|
|
2477
|
+
status: "PASSED",
|
|
2478
|
+
startTime,
|
|
2479
|
+
endTime,
|
|
2480
|
+
},
|
|
2481
|
+
info: info,
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2486
|
+
const startTime = Date.now();
|
|
2487
|
+
let error = null;
|
|
2488
|
+
let screenshotId = null;
|
|
2489
|
+
let screenshotPath = null;
|
|
2490
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2491
|
+
const info = {};
|
|
2492
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2493
|
+
info.operation = "verifyPageTitle";
|
|
2494
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2495
|
+
if (newValue !== title) {
|
|
2496
|
+
this.logger.info(title + "=" + newValue);
|
|
2497
|
+
title = newValue;
|
|
2498
|
+
}
|
|
2499
|
+
info.title = title;
|
|
2500
|
+
try {
|
|
2501
|
+
for (let i = 0; i < 30; i++) {
|
|
2502
|
+
const foundTitle = await this.page.title();
|
|
2503
|
+
if (!foundTitle.includes(title)) {
|
|
2504
|
+
if (i === 29) {
|
|
2505
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2506
|
+
}
|
|
2507
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2511
|
+
return info;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
catch (e) {
|
|
2515
|
+
//await this.closeUnexpectedPopups();
|
|
2516
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2517
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2518
|
+
info.screenshotPath = screenshotPath;
|
|
2519
|
+
Object.assign(e, { info: info });
|
|
2520
|
+
error = e;
|
|
2521
|
+
// throw e;
|
|
2522
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2523
|
+
}
|
|
2524
|
+
finally {
|
|
2525
|
+
const endTime = Date.now();
|
|
2526
|
+
_reportToWorld(world, {
|
|
2527
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2528
|
+
text: "Verify page title",
|
|
2529
|
+
_text: "Verify the page title contains " + title,
|
|
2201
2530
|
screenshotId,
|
|
2202
2531
|
result: error
|
|
2203
2532
|
? {
|
|
@@ -2215,27 +2544,27 @@ class StableBrowser {
|
|
|
2215
2544
|
});
|
|
2216
2545
|
}
|
|
2217
2546
|
}
|
|
2218
|
-
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2547
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2219
2548
|
const frames = this.page.frames();
|
|
2220
2549
|
let results = [];
|
|
2221
|
-
let ignoreCase = false;
|
|
2550
|
+
// let ignoreCase = false;
|
|
2222
2551
|
for (let i = 0; i < frames.length; i++) {
|
|
2223
2552
|
if (dateAlternatives.date) {
|
|
2224
2553
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2225
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2554
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2226
2555
|
result.frame = frames[i];
|
|
2227
2556
|
results.push(result);
|
|
2228
2557
|
}
|
|
2229
2558
|
}
|
|
2230
2559
|
else if (numberAlternatives.number) {
|
|
2231
2560
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2232
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2561
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2233
2562
|
result.frame = frames[i];
|
|
2234
2563
|
results.push(result);
|
|
2235
2564
|
}
|
|
2236
2565
|
}
|
|
2237
2566
|
else {
|
|
2238
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2567
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2239
2568
|
result.frame = frames[i];
|
|
2240
2569
|
results.push(result);
|
|
2241
2570
|
}
|
|
@@ -2254,11 +2583,15 @@ class StableBrowser {
|
|
|
2254
2583
|
scroll: false,
|
|
2255
2584
|
highlight: false,
|
|
2256
2585
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2257
|
-
text: `Verify text exists in page`,
|
|
2586
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2587
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2258
2588
|
operation: "verifyTextExistInPage",
|
|
2259
2589
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2260
2590
|
};
|
|
2261
|
-
|
|
2591
|
+
if (testForRegex(text)) {
|
|
2592
|
+
text = text.replace(/\\"/g, '"');
|
|
2593
|
+
}
|
|
2594
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2262
2595
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2263
2596
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2264
2597
|
if (newValue !== text) {
|
|
@@ -2328,7 +2661,7 @@ class StableBrowser {
|
|
|
2328
2661
|
await _commandError(state, e, this);
|
|
2329
2662
|
}
|
|
2330
2663
|
finally {
|
|
2331
|
-
_commandFinally(state, this);
|
|
2664
|
+
await _commandFinally(state, this);
|
|
2332
2665
|
}
|
|
2333
2666
|
}
|
|
2334
2667
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2341,11 +2674,15 @@ class StableBrowser {
|
|
|
2341
2674
|
scroll: false,
|
|
2342
2675
|
highlight: false,
|
|
2343
2676
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2344
|
-
text: `Verify text does not exist in page`,
|
|
2677
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2678
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2345
2679
|
operation: "verifyTextNotExistInPage",
|
|
2346
2680
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2347
2681
|
};
|
|
2348
|
-
|
|
2682
|
+
if (testForRegex(text)) {
|
|
2683
|
+
text = text.replace(/\\"/g, '"');
|
|
2684
|
+
}
|
|
2685
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2349
2686
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2350
2687
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2351
2688
|
if (newValue !== text) {
|
|
@@ -2381,7 +2718,7 @@ class StableBrowser {
|
|
|
2381
2718
|
await _commandError(state, e, this);
|
|
2382
2719
|
}
|
|
2383
2720
|
finally {
|
|
2384
|
-
_commandFinally(state, this);
|
|
2721
|
+
await _commandFinally(state, this);
|
|
2385
2722
|
}
|
|
2386
2723
|
}
|
|
2387
2724
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2396,10 +2733,11 @@ class StableBrowser {
|
|
|
2396
2733
|
highlight: false,
|
|
2397
2734
|
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2398
2735
|
text: `Verify text with relation to another text`,
|
|
2736
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
2399
2737
|
operation: "verify_text_with_relation",
|
|
2400
2738
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2401
2739
|
};
|
|
2402
|
-
const timeout = this.
|
|
2740
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2403
2741
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2404
2742
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2405
2743
|
if (newValue !== textAnchor) {
|
|
@@ -2422,7 +2760,7 @@ class StableBrowser {
|
|
|
2422
2760
|
};
|
|
2423
2761
|
while (true) {
|
|
2424
2762
|
try {
|
|
2425
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2763
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2426
2764
|
}
|
|
2427
2765
|
catch (error) {
|
|
2428
2766
|
// ignore
|
|
@@ -2450,7 +2788,7 @@ class StableBrowser {
|
|
|
2450
2788
|
const count = await frame.locator(css).count();
|
|
2451
2789
|
for (let j = 0; j < count; j++) {
|
|
2452
2790
|
const continer = await frame.locator(css).nth(j);
|
|
2453
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2791
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2454
2792
|
if (result.elementCount > 0) {
|
|
2455
2793
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2456
2794
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2491,9 +2829,33 @@ class StableBrowser {
|
|
|
2491
2829
|
await _commandError(state, e, this);
|
|
2492
2830
|
}
|
|
2493
2831
|
finally {
|
|
2494
|
-
_commandFinally(state, this);
|
|
2832
|
+
await _commandFinally(state, this);
|
|
2495
2833
|
}
|
|
2496
2834
|
}
|
|
2835
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2836
|
+
const frames = this.page.frames();
|
|
2837
|
+
let results = [];
|
|
2838
|
+
let ignoreCase = false;
|
|
2839
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2840
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2841
|
+
result.frame = frames[i];
|
|
2842
|
+
const climbArray = [];
|
|
2843
|
+
for (let i = 0; i < climb; i++) {
|
|
2844
|
+
climbArray.push("..");
|
|
2845
|
+
}
|
|
2846
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2847
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2848
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2849
|
+
if (count > 0) {
|
|
2850
|
+
result.elementCount = count;
|
|
2851
|
+
result.locator = newLocator;
|
|
2852
|
+
results.push(result);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
// state.info.results = results;
|
|
2856
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2857
|
+
return resultWithElementsFound;
|
|
2858
|
+
}
|
|
2497
2859
|
async visualVerification(text, options = {}, world = null) {
|
|
2498
2860
|
const startTime = Date.now();
|
|
2499
2861
|
let error = null;
|
|
@@ -2555,6 +2917,7 @@ class StableBrowser {
|
|
|
2555
2917
|
_reportToWorld(world, {
|
|
2556
2918
|
type: Types.VERIFY_VISUAL,
|
|
2557
2919
|
text: "Visual verification",
|
|
2920
|
+
_text: "Visual verification of " + text,
|
|
2558
2921
|
screenshotId,
|
|
2559
2922
|
result: error
|
|
2560
2923
|
? {
|
|
@@ -2809,7 +3172,13 @@ class StableBrowser {
|
|
|
2809
3172
|
}
|
|
2810
3173
|
}
|
|
2811
3174
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2812
|
-
|
|
3175
|
+
try {
|
|
3176
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3177
|
+
}
|
|
3178
|
+
catch (error) {
|
|
3179
|
+
this.logger.debug(error);
|
|
3180
|
+
throw error;
|
|
3181
|
+
}
|
|
2813
3182
|
}
|
|
2814
3183
|
_getLoadTimeout(options) {
|
|
2815
3184
|
let timeout = 15000;
|
|
@@ -2821,6 +3190,15 @@ class StableBrowser {
|
|
|
2821
3190
|
}
|
|
2822
3191
|
return timeout;
|
|
2823
3192
|
}
|
|
3193
|
+
_getFindElementTimeout(options) {
|
|
3194
|
+
if (options && options.timeout) {
|
|
3195
|
+
return options.timeout;
|
|
3196
|
+
}
|
|
3197
|
+
if (this.configuration.find_element_timeout) {
|
|
3198
|
+
return this.configuration.find_element_timeout;
|
|
3199
|
+
}
|
|
3200
|
+
return 30000;
|
|
3201
|
+
}
|
|
2824
3202
|
async saveStoreState(path = null, world = null) {
|
|
2825
3203
|
const storageState = await this.page.context().storageState();
|
|
2826
3204
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
@@ -2837,6 +3215,9 @@ class StableBrowser {
|
|
|
2837
3215
|
this.registerEventListeners(this.context);
|
|
2838
3216
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2839
3217
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3218
|
+
if (this.onRestoreSaveState) {
|
|
3219
|
+
this.onRestoreSaveState(path);
|
|
3220
|
+
}
|
|
2840
3221
|
}
|
|
2841
3222
|
async waitForPageLoad(options = {}, world = null) {
|
|
2842
3223
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2905,6 +3286,7 @@ class StableBrowser {
|
|
|
2905
3286
|
highlight: false,
|
|
2906
3287
|
type: Types.CLOSE_PAGE,
|
|
2907
3288
|
text: `Close page`,
|
|
3289
|
+
_text: `Close the page`,
|
|
2908
3290
|
operation: "closePage",
|
|
2909
3291
|
log: "***** close page *****\n",
|
|
2910
3292
|
throwError: false,
|
|
@@ -2918,11 +3300,98 @@ class StableBrowser {
|
|
|
2918
3300
|
await _commandError(state, e, this);
|
|
2919
3301
|
}
|
|
2920
3302
|
finally {
|
|
2921
|
-
_commandFinally(state, this);
|
|
3303
|
+
await _commandFinally(state, this);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3307
|
+
let operation = null;
|
|
3308
|
+
if (!options || !options.operation) {
|
|
3309
|
+
throw new Error("operation is not defined");
|
|
3310
|
+
}
|
|
3311
|
+
operation = options.operation;
|
|
3312
|
+
// validate operation is one of the supported operations
|
|
3313
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3314
|
+
throw new Error("operation is not supported");
|
|
3315
|
+
}
|
|
3316
|
+
const state = {
|
|
3317
|
+
options,
|
|
3318
|
+
world,
|
|
3319
|
+
locate: false,
|
|
3320
|
+
scroll: false,
|
|
3321
|
+
highlight: false,
|
|
3322
|
+
type: Types.TABLE_OPERATION,
|
|
3323
|
+
text: `Table operation`,
|
|
3324
|
+
_text: `Table ${operation} operation`,
|
|
3325
|
+
operation: operation,
|
|
3326
|
+
log: "***** Table operation *****\n",
|
|
3327
|
+
};
|
|
3328
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3329
|
+
try {
|
|
3330
|
+
await _preCommand(state, this);
|
|
3331
|
+
const start = Date.now();
|
|
3332
|
+
let cellArea = null;
|
|
3333
|
+
while (true) {
|
|
3334
|
+
try {
|
|
3335
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3336
|
+
if (cellArea) {
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
catch (e) {
|
|
3341
|
+
// ignore
|
|
3342
|
+
}
|
|
3343
|
+
if (Date.now() - start > timeout) {
|
|
3344
|
+
throw new Error(`Cell not found in table`);
|
|
3345
|
+
}
|
|
3346
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3347
|
+
}
|
|
3348
|
+
switch (operation) {
|
|
3349
|
+
case "click":
|
|
3350
|
+
if (!options.css) {
|
|
3351
|
+
// will click in the center of the cell
|
|
3352
|
+
let xOffset = 0;
|
|
3353
|
+
let yOffset = 0;
|
|
3354
|
+
if (options.xOffset) {
|
|
3355
|
+
xOffset = options.xOffset;
|
|
3356
|
+
}
|
|
3357
|
+
if (options.yOffset) {
|
|
3358
|
+
yOffset = options.yOffset;
|
|
3359
|
+
}
|
|
3360
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3361
|
+
}
|
|
3362
|
+
else {
|
|
3363
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3364
|
+
if (results.length === 0) {
|
|
3365
|
+
throw new Error(`Element not found in cell area`);
|
|
3366
|
+
}
|
|
3367
|
+
state.element = results[0];
|
|
3368
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3369
|
+
}
|
|
3370
|
+
break;
|
|
3371
|
+
case "hover+click":
|
|
3372
|
+
if (!options.css) {
|
|
3373
|
+
throw new Error("css is not defined");
|
|
3374
|
+
}
|
|
3375
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3376
|
+
if (results.length === 0) {
|
|
3377
|
+
throw new Error(`Element not found in cell area`);
|
|
3378
|
+
}
|
|
3379
|
+
state.element = results[0];
|
|
3380
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3381
|
+
break;
|
|
3382
|
+
default:
|
|
3383
|
+
throw new Error("operation is not supported");
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
catch (e) {
|
|
3387
|
+
await _commandError(state, e, this);
|
|
3388
|
+
}
|
|
3389
|
+
finally {
|
|
3390
|
+
await _commandFinally(state, this);
|
|
2922
3391
|
}
|
|
2923
3392
|
}
|
|
2924
3393
|
saveTestDataAsGlobal(options, world) {
|
|
2925
|
-
const dataFile =
|
|
3394
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2926
3395
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2927
3396
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2928
3397
|
}
|
|
@@ -2952,6 +3421,7 @@ class StableBrowser {
|
|
|
2952
3421
|
_reportToWorld(world, {
|
|
2953
3422
|
type: Types.SET_VIEWPORT,
|
|
2954
3423
|
text: "set viewport size to " + width + "x" + hight,
|
|
3424
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2955
3425
|
screenshotId,
|
|
2956
3426
|
result: error
|
|
2957
3427
|
? {
|
|
@@ -3022,7 +3492,39 @@ class StableBrowser {
|
|
|
3022
3492
|
console.log("#-#");
|
|
3023
3493
|
}
|
|
3024
3494
|
}
|
|
3495
|
+
async beforeScenario(world, scenario) {
|
|
3496
|
+
this.beforeScenarioCalled = true;
|
|
3497
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3498
|
+
this.scenarioName = scenario.pickle.name;
|
|
3499
|
+
}
|
|
3500
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3501
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3502
|
+
}
|
|
3503
|
+
if (this.context) {
|
|
3504
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3505
|
+
}
|
|
3506
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3507
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3508
|
+
// check if @global_test_data tag is present
|
|
3509
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3510
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
// update test data based on feature/scenario
|
|
3514
|
+
let envName = null;
|
|
3515
|
+
if (this.context && this.context.environment) {
|
|
3516
|
+
envName = this.context.environment.name;
|
|
3517
|
+
}
|
|
3518
|
+
if (!process.env.TEMP_RUN) {
|
|
3519
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3520
|
+
}
|
|
3521
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3522
|
+
}
|
|
3523
|
+
async afterScenario(world, scenario) { }
|
|
3025
3524
|
async beforeStep(world, step) {
|
|
3525
|
+
if (!this.beforeScenarioCalled) {
|
|
3526
|
+
this.beforeScenario(world, step);
|
|
3527
|
+
}
|
|
3026
3528
|
if (this.stepIndex === undefined) {
|
|
3027
3529
|
this.stepIndex = 0;
|
|
3028
3530
|
}
|
|
@@ -3039,22 +3541,54 @@ class StableBrowser {
|
|
|
3039
3541
|
else {
|
|
3040
3542
|
this.stepName = "step " + this.stepIndex;
|
|
3041
3543
|
}
|
|
3042
|
-
if (this.context) {
|
|
3043
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3044
|
-
}
|
|
3045
3544
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3046
3545
|
if (this.context.browserObject.context) {
|
|
3047
3546
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3048
3547
|
}
|
|
3049
3548
|
}
|
|
3050
|
-
if (this.
|
|
3051
|
-
this.
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3549
|
+
if (this.initSnapshotTaken === false) {
|
|
3550
|
+
this.initSnapshotTaken = true;
|
|
3551
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3552
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3553
|
+
if (snapshot) {
|
|
3554
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3555
|
+
}
|
|
3055
3556
|
}
|
|
3056
3557
|
}
|
|
3057
3558
|
}
|
|
3559
|
+
async getAriaSnapshot() {
|
|
3560
|
+
try {
|
|
3561
|
+
// find the page url
|
|
3562
|
+
const url = await this.page.url();
|
|
3563
|
+
// extract the path from the url
|
|
3564
|
+
const path = new URL(url).pathname;
|
|
3565
|
+
// get the page title
|
|
3566
|
+
const title = await this.page.title();
|
|
3567
|
+
// go over other frams
|
|
3568
|
+
const frames = this.page.frames();
|
|
3569
|
+
const snapshots = [];
|
|
3570
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3571
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3572
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3573
|
+
const frame = frames[i];
|
|
3574
|
+
try {
|
|
3575
|
+
// Ensure frame is attached and has body
|
|
3576
|
+
const body = frame.locator("body");
|
|
3577
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3578
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3579
|
+
content.push(`- frame: ${i}`);
|
|
3580
|
+
content.push(snapshot);
|
|
3581
|
+
}
|
|
3582
|
+
catch (innerErr) { }
|
|
3583
|
+
}
|
|
3584
|
+
return content.join("\n");
|
|
3585
|
+
}
|
|
3586
|
+
catch (e) {
|
|
3587
|
+
console.log("Error in getAriaSnapshot");
|
|
3588
|
+
//console.debug(e);
|
|
3589
|
+
}
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3058
3592
|
async afterStep(world, step) {
|
|
3059
3593
|
this.stepName = null;
|
|
3060
3594
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3062,11 +3596,25 @@ class StableBrowser {
|
|
|
3062
3596
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3063
3597
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3064
3598
|
});
|
|
3599
|
+
if (world && world.attach) {
|
|
3600
|
+
await world.attach(JSON.stringify({
|
|
3601
|
+
type: "trace",
|
|
3602
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3603
|
+
}), "application/json+trace");
|
|
3604
|
+
}
|
|
3605
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3065
3606
|
}
|
|
3066
3607
|
}
|
|
3067
3608
|
if (this.context) {
|
|
3068
3609
|
this.context.examplesRow = null;
|
|
3069
3610
|
}
|
|
3611
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3612
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3613
|
+
if (snapshot) {
|
|
3614
|
+
const obj = {};
|
|
3615
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3070
3618
|
}
|
|
3071
3619
|
}
|
|
3072
3620
|
function createTimedPromise(promise, label) {
|