automation_model 1.0.618-dev → 1.0.618-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 +105 -30
- 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/network.d.ts +1 -1
- package/lib/network.js +5 -5
- package/lib/network.js.map +1 -1
- package/lib/snapshot_validation.d.ts +35 -0
- package/lib/snapshot_validation.js +239 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +23 -1
- package/lib/stable_browser.js +657 -123
- 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]))) {
|
|
@@ -558,12 +599,24 @@ class StableBrowser {
|
|
|
558
599
|
element.evaluate((el, randomToken) => {
|
|
559
600
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
560
601
|
}, randomToken);
|
|
561
|
-
if (element._frame) {
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
const scope = element.page();
|
|
565
|
-
|
|
566
|
-
|
|
602
|
+
// if (element._frame) {
|
|
603
|
+
// return element;
|
|
604
|
+
// }
|
|
605
|
+
const scope = element._frame ?? element.page();
|
|
606
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
607
|
+
let prefixSelector = "";
|
|
608
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
609
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
610
|
+
if (frameSelectorIndex !== -1) {
|
|
611
|
+
// remove everything after the >> internal:control=enter-frame
|
|
612
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
613
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
614
|
+
}
|
|
615
|
+
// if (element?._frame?._selector) {
|
|
616
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
617
|
+
// }
|
|
618
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
619
|
+
return scope.locator(newSelector);
|
|
567
620
|
}
|
|
568
621
|
}
|
|
569
622
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -715,14 +768,9 @@ class StableBrowser {
|
|
|
715
768
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
716
769
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
717
770
|
}
|
|
718
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
771
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
719
772
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
720
773
|
}
|
|
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
774
|
let foundElements = result.foundElements;
|
|
727
775
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
728
776
|
info.box = foundElements[0].box;
|
|
@@ -777,6 +825,11 @@ class StableBrowser {
|
|
|
777
825
|
visibleOnly = false;
|
|
778
826
|
}
|
|
779
827
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
828
|
+
// sheck of more of half of the timeout has passed
|
|
829
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
830
|
+
highPriorityOnly = false;
|
|
831
|
+
visibleOnly = false;
|
|
832
|
+
}
|
|
780
833
|
}
|
|
781
834
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
782
835
|
// if (info.locatorLog) {
|
|
@@ -823,9 +876,40 @@ class StableBrowser {
|
|
|
823
876
|
result.locatorIndex = i;
|
|
824
877
|
}
|
|
825
878
|
if (foundLocators.length > 1) {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
879
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
880
|
+
const boxes = [];
|
|
881
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
882
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
883
|
+
}
|
|
884
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
885
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
886
|
+
if (j === k) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
890
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
891
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
892
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
893
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
894
|
+
// as the element is not unique, will remove it
|
|
895
|
+
boxes.splice(k, 1);
|
|
896
|
+
k--;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (boxes.length === 1) {
|
|
901
|
+
result.foundElements.push({
|
|
902
|
+
locator: boxes[0].locator.first(),
|
|
903
|
+
box: boxes[0].box,
|
|
904
|
+
unique: true,
|
|
905
|
+
});
|
|
906
|
+
result.locatorIndex = i;
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
info.failCause.foundMultiple = true;
|
|
910
|
+
if (info.locatorLog) {
|
|
911
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
912
|
+
}
|
|
829
913
|
}
|
|
830
914
|
}
|
|
831
915
|
}
|
|
@@ -873,7 +957,7 @@ class StableBrowser {
|
|
|
873
957
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
874
958
|
}
|
|
875
959
|
finally {
|
|
876
|
-
_commandFinally(state, this);
|
|
960
|
+
await _commandFinally(state, this);
|
|
877
961
|
}
|
|
878
962
|
}
|
|
879
963
|
}
|
|
@@ -922,7 +1006,7 @@ class StableBrowser {
|
|
|
922
1006
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
923
1007
|
}
|
|
924
1008
|
finally {
|
|
925
|
-
_commandFinally(state, this);
|
|
1009
|
+
await _commandFinally(state, this);
|
|
926
1010
|
}
|
|
927
1011
|
}
|
|
928
1012
|
}
|
|
@@ -936,25 +1020,14 @@ class StableBrowser {
|
|
|
936
1020
|
options,
|
|
937
1021
|
world,
|
|
938
1022
|
text: "Click element",
|
|
1023
|
+
_text: "Click on " + selectors.element_name,
|
|
939
1024
|
type: Types.CLICK,
|
|
940
1025
|
operation: "click",
|
|
941
1026
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
942
1027
|
};
|
|
943
1028
|
try {
|
|
944
1029
|
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
|
-
}
|
|
1030
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
958
1031
|
await this.waitForPageLoad();
|
|
959
1032
|
return state.info;
|
|
960
1033
|
}
|
|
@@ -962,8 +1035,40 @@ class StableBrowser {
|
|
|
962
1035
|
await _commandError(state, e, this);
|
|
963
1036
|
}
|
|
964
1037
|
finally {
|
|
965
|
-
_commandFinally(state, this);
|
|
1038
|
+
await _commandFinally(state, this);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1042
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1043
|
+
const state = {
|
|
1044
|
+
selectors,
|
|
1045
|
+
_params,
|
|
1046
|
+
options,
|
|
1047
|
+
world,
|
|
1048
|
+
text: "Wait for element",
|
|
1049
|
+
_text: "Wait for " + selectors.element_name,
|
|
1050
|
+
type: Types.WAIT_ELEMENT,
|
|
1051
|
+
operation: "waitForElement",
|
|
1052
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1053
|
+
};
|
|
1054
|
+
let found = false;
|
|
1055
|
+
try {
|
|
1056
|
+
await _preCommand(state, this);
|
|
1057
|
+
// if (state.options && state.options.context) {
|
|
1058
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1059
|
+
// }
|
|
1060
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1061
|
+
found = true;
|
|
1062
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1063
|
+
}
|
|
1064
|
+
catch (e) {
|
|
1065
|
+
console.error("Error on waitForElement", e);
|
|
1066
|
+
// await _commandError(state, e, this);
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
await _commandFinally(state, this);
|
|
966
1070
|
}
|
|
1071
|
+
return found;
|
|
967
1072
|
}
|
|
968
1073
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
969
1074
|
const state = {
|
|
@@ -973,6 +1078,7 @@ class StableBrowser {
|
|
|
973
1078
|
world,
|
|
974
1079
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
975
1080
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1081
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
976
1082
|
operation: "setCheck",
|
|
977
1083
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
978
1084
|
};
|
|
@@ -984,7 +1090,7 @@ class StableBrowser {
|
|
|
984
1090
|
try {
|
|
985
1091
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
986
1092
|
// console.log(`Highlighting while running from recorder`);
|
|
987
|
-
await this._highlightElements(element);
|
|
1093
|
+
await this._highlightElements(state.element);
|
|
988
1094
|
await state.element.setChecked(checked);
|
|
989
1095
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
990
1096
|
// await this._unHighlightElements(element);
|
|
@@ -1011,7 +1117,7 @@ class StableBrowser {
|
|
|
1011
1117
|
await _commandError(state, e, this);
|
|
1012
1118
|
}
|
|
1013
1119
|
finally {
|
|
1014
|
-
_commandFinally(state, this);
|
|
1120
|
+
await _commandFinally(state, this);
|
|
1015
1121
|
}
|
|
1016
1122
|
}
|
|
1017
1123
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1022,24 +1128,13 @@ class StableBrowser {
|
|
|
1022
1128
|
world,
|
|
1023
1129
|
type: Types.HOVER,
|
|
1024
1130
|
text: `Hover element`,
|
|
1131
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1025
1132
|
operation: "hover",
|
|
1026
1133
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1027
1134
|
};
|
|
1028
1135
|
try {
|
|
1029
1136
|
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
|
-
}
|
|
1137
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1043
1138
|
await _screenshot(state, this);
|
|
1044
1139
|
await this.waitForPageLoad();
|
|
1045
1140
|
return state.info;
|
|
@@ -1048,7 +1143,7 @@ class StableBrowser {
|
|
|
1048
1143
|
await _commandError(state, e, this);
|
|
1049
1144
|
}
|
|
1050
1145
|
finally {
|
|
1051
|
-
_commandFinally(state, this);
|
|
1146
|
+
await _commandFinally(state, this);
|
|
1052
1147
|
}
|
|
1053
1148
|
}
|
|
1054
1149
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1063,6 +1158,7 @@ class StableBrowser {
|
|
|
1063
1158
|
value: values.toString(),
|
|
1064
1159
|
type: Types.SELECT,
|
|
1065
1160
|
text: `Select option: ${values}`,
|
|
1161
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1066
1162
|
operation: "selectOption",
|
|
1067
1163
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1068
1164
|
};
|
|
@@ -1083,7 +1179,7 @@ class StableBrowser {
|
|
|
1083
1179
|
await _commandError(state, e, this);
|
|
1084
1180
|
}
|
|
1085
1181
|
finally {
|
|
1086
|
-
_commandFinally(state, this);
|
|
1182
|
+
await _commandFinally(state, this);
|
|
1087
1183
|
}
|
|
1088
1184
|
}
|
|
1089
1185
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1097,6 +1193,7 @@ class StableBrowser {
|
|
|
1097
1193
|
highlight: false,
|
|
1098
1194
|
type: Types.TYPE_PRESS,
|
|
1099
1195
|
text: `Type value: ${_value}`,
|
|
1196
|
+
_text: `Type value: ${_value}`,
|
|
1100
1197
|
operation: "type",
|
|
1101
1198
|
log: "",
|
|
1102
1199
|
};
|
|
@@ -1128,7 +1225,7 @@ class StableBrowser {
|
|
|
1128
1225
|
await _commandError(state, e, this);
|
|
1129
1226
|
}
|
|
1130
1227
|
finally {
|
|
1131
|
-
_commandFinally(state, this);
|
|
1228
|
+
await _commandFinally(state, this);
|
|
1132
1229
|
}
|
|
1133
1230
|
}
|
|
1134
1231
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1164,7 +1261,7 @@ class StableBrowser {
|
|
|
1164
1261
|
await _commandError(state, e, this);
|
|
1165
1262
|
}
|
|
1166
1263
|
finally {
|
|
1167
|
-
_commandFinally(state, this);
|
|
1264
|
+
await _commandFinally(state, this);
|
|
1168
1265
|
}
|
|
1169
1266
|
}
|
|
1170
1267
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1176,6 +1273,7 @@ class StableBrowser {
|
|
|
1176
1273
|
world,
|
|
1177
1274
|
type: Types.SET_DATE_TIME,
|
|
1178
1275
|
text: `Set date time value: ${value}`,
|
|
1276
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1179
1277
|
operation: "setDateTime",
|
|
1180
1278
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1181
1279
|
throwError: false,
|
|
@@ -1183,7 +1281,7 @@ class StableBrowser {
|
|
|
1183
1281
|
try {
|
|
1184
1282
|
await _preCommand(state, this);
|
|
1185
1283
|
try {
|
|
1186
|
-
await state.element
|
|
1284
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1187
1285
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1188
1286
|
if (format) {
|
|
1189
1287
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1232,7 +1330,7 @@ class StableBrowser {
|
|
|
1232
1330
|
await _commandError(state, e, this);
|
|
1233
1331
|
}
|
|
1234
1332
|
finally {
|
|
1235
|
-
_commandFinally(state, this);
|
|
1333
|
+
await _commandFinally(state, this);
|
|
1236
1334
|
}
|
|
1237
1335
|
}
|
|
1238
1336
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1247,9 +1345,13 @@ class StableBrowser {
|
|
|
1247
1345
|
world,
|
|
1248
1346
|
type: Types.FILL,
|
|
1249
1347
|
text: `Click type input with value: ${_value}`,
|
|
1348
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1250
1349
|
operation: "clickType",
|
|
1251
1350
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1252
1351
|
};
|
|
1352
|
+
if (!options) {
|
|
1353
|
+
options = {};
|
|
1354
|
+
}
|
|
1253
1355
|
if (newValue !== _value) {
|
|
1254
1356
|
//this.logger.info(_value + "=" + newValue);
|
|
1255
1357
|
_value = newValue;
|
|
@@ -1257,7 +1359,7 @@ class StableBrowser {
|
|
|
1257
1359
|
try {
|
|
1258
1360
|
await _preCommand(state, this);
|
|
1259
1361
|
state.info.value = _value;
|
|
1260
|
-
if (
|
|
1362
|
+
if (!options.press) {
|
|
1261
1363
|
try {
|
|
1262
1364
|
let currentValue = await state.element.inputValue();
|
|
1263
1365
|
if (currentValue) {
|
|
@@ -1268,13 +1370,9 @@ class StableBrowser {
|
|
|
1268
1370
|
this.logger.info("unable to clear input value");
|
|
1269
1371
|
}
|
|
1270
1372
|
}
|
|
1271
|
-
if (options
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
catch (e) {
|
|
1276
|
-
await state.element.dispatchEvent("click");
|
|
1277
|
-
}
|
|
1373
|
+
if (options.press) {
|
|
1374
|
+
options.timeout = 5000;
|
|
1375
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1278
1376
|
}
|
|
1279
1377
|
else {
|
|
1280
1378
|
try {
|
|
@@ -1332,7 +1430,7 @@ class StableBrowser {
|
|
|
1332
1430
|
await _commandError(state, e, this);
|
|
1333
1431
|
}
|
|
1334
1432
|
finally {
|
|
1335
|
-
_commandFinally(state, this);
|
|
1433
|
+
await _commandFinally(state, this);
|
|
1336
1434
|
}
|
|
1337
1435
|
}
|
|
1338
1436
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1362,13 +1460,49 @@ class StableBrowser {
|
|
|
1362
1460
|
await _commandError(state, e, this);
|
|
1363
1461
|
}
|
|
1364
1462
|
finally {
|
|
1365
|
-
_commandFinally(state, this);
|
|
1463
|
+
await _commandFinally(state, this);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1467
|
+
const state = {
|
|
1468
|
+
selectors,
|
|
1469
|
+
_params,
|
|
1470
|
+
files,
|
|
1471
|
+
value: '"' + files.join('", "') + '"',
|
|
1472
|
+
options,
|
|
1473
|
+
world,
|
|
1474
|
+
type: Types.SET_INPUT_FILES,
|
|
1475
|
+
text: `Set input files`,
|
|
1476
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1477
|
+
operation: "setInputFiles",
|
|
1478
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1479
|
+
};
|
|
1480
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1481
|
+
try {
|
|
1482
|
+
await _preCommand(state, this);
|
|
1483
|
+
for (let i = 0; i < files.length; i++) {
|
|
1484
|
+
const file = files[i];
|
|
1485
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1486
|
+
if (!fs.existsSync(filePath)) {
|
|
1487
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1488
|
+
}
|
|
1489
|
+
state.files[i] = filePath;
|
|
1490
|
+
}
|
|
1491
|
+
await state.element.setInputFiles(files);
|
|
1492
|
+
return state.info;
|
|
1493
|
+
}
|
|
1494
|
+
catch (e) {
|
|
1495
|
+
await _commandError(state, e, this);
|
|
1496
|
+
}
|
|
1497
|
+
finally {
|
|
1498
|
+
await _commandFinally(state, this);
|
|
1366
1499
|
}
|
|
1367
1500
|
}
|
|
1368
1501
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1369
1502
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1370
1503
|
}
|
|
1371
1504
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1505
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1372
1506
|
_validateSelectors(selectors);
|
|
1373
1507
|
let screenshotId = null;
|
|
1374
1508
|
let screenshotPath = null;
|
|
@@ -1378,7 +1512,7 @@ class StableBrowser {
|
|
|
1378
1512
|
}
|
|
1379
1513
|
info.operation = "getText";
|
|
1380
1514
|
info.selectors = selectors;
|
|
1381
|
-
let element = await this._locate(selectors, info, _params);
|
|
1515
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1382
1516
|
if (climb > 0) {
|
|
1383
1517
|
const climbArray = [];
|
|
1384
1518
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1445,6 +1579,7 @@ class StableBrowser {
|
|
|
1445
1579
|
highlight: false,
|
|
1446
1580
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1447
1581
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1582
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1448
1583
|
operation: "containsPattern",
|
|
1449
1584
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1450
1585
|
};
|
|
@@ -1476,10 +1611,12 @@ class StableBrowser {
|
|
|
1476
1611
|
await _commandError(state, e, this);
|
|
1477
1612
|
}
|
|
1478
1613
|
finally {
|
|
1479
|
-
_commandFinally(state, this);
|
|
1614
|
+
await _commandFinally(state, this);
|
|
1480
1615
|
}
|
|
1481
1616
|
}
|
|
1482
1617
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1618
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1619
|
+
const startTime = Date.now();
|
|
1483
1620
|
const state = {
|
|
1484
1621
|
selectors,
|
|
1485
1622
|
_params,
|
|
@@ -1506,44 +1643,124 @@ class StableBrowser {
|
|
|
1506
1643
|
}
|
|
1507
1644
|
let foundObj = null;
|
|
1508
1645
|
try {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
if (
|
|
1520
|
-
|
|
1646
|
+
while (Date.now() - startTime < timeout) {
|
|
1647
|
+
try {
|
|
1648
|
+
await _preCommand(state, this);
|
|
1649
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1650
|
+
if (foundObj && foundObj.element) {
|
|
1651
|
+
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1652
|
+
}
|
|
1653
|
+
await _screenshot(state, this);
|
|
1654
|
+
const dateAlternatives = findDateAlternatives(text);
|
|
1655
|
+
const numberAlternatives = findNumberAlternatives(text);
|
|
1656
|
+
if (dateAlternatives.date) {
|
|
1657
|
+
for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1658
|
+
if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1659
|
+
foundObj?.value?.includes(dateAlternatives.dates[i])) {
|
|
1660
|
+
return state.info;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
else if (numberAlternatives.number) {
|
|
1665
|
+
for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1666
|
+
if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1667
|
+
foundObj?.value?.includes(numberAlternatives.numbers[i])) {
|
|
1668
|
+
return state.info;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
|
|
1521
1673
|
return state.info;
|
|
1522
1674
|
}
|
|
1523
1675
|
}
|
|
1524
|
-
|
|
1676
|
+
catch (e) {
|
|
1677
|
+
// Log error but continue retrying until timeout is reached
|
|
1678
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1679
|
+
}
|
|
1680
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1525
1681
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1682
|
+
state.info.foundText = foundObj?.text;
|
|
1683
|
+
state.info.value = foundObj?.value;
|
|
1684
|
+
throw new Error("element doesn't contain text " + text);
|
|
1685
|
+
}
|
|
1686
|
+
catch (e) {
|
|
1687
|
+
await _commandError(state, e, this);
|
|
1688
|
+
throw e;
|
|
1689
|
+
}
|
|
1690
|
+
finally {
|
|
1691
|
+
await _commandFinally(state, this);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1695
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1696
|
+
const startTime = Date.now();
|
|
1697
|
+
const state = {
|
|
1698
|
+
_params,
|
|
1699
|
+
value: referanceSnapshot,
|
|
1700
|
+
options,
|
|
1701
|
+
world,
|
|
1702
|
+
locate: false,
|
|
1703
|
+
scroll: false,
|
|
1704
|
+
screenshot: true,
|
|
1705
|
+
highlight: false,
|
|
1706
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1707
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1708
|
+
operation: "snapshotValidation",
|
|
1709
|
+
log: "***** verify snapshot *****\n",
|
|
1710
|
+
};
|
|
1711
|
+
if (!referanceSnapshot) {
|
|
1712
|
+
throw new Error("referanceSnapshot is null");
|
|
1713
|
+
}
|
|
1714
|
+
let text = null;
|
|
1715
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1716
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1717
|
+
}
|
|
1718
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1719
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1720
|
+
}
|
|
1721
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1722
|
+
text = referanceSnapshot.substring(5);
|
|
1723
|
+
}
|
|
1724
|
+
else {
|
|
1725
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1726
|
+
}
|
|
1727
|
+
state.text = text;
|
|
1728
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1729
|
+
await _preCommand(state, this);
|
|
1730
|
+
let foundObj = null;
|
|
1731
|
+
try {
|
|
1732
|
+
let matchResult = null;
|
|
1733
|
+
while (Date.now() - startTime < timeout) {
|
|
1734
|
+
try {
|
|
1735
|
+
let scope = null;
|
|
1736
|
+
if (!frameSelectors) {
|
|
1737
|
+
scope = this.page;
|
|
1531
1738
|
}
|
|
1739
|
+
else {
|
|
1740
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1741
|
+
}
|
|
1742
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1743
|
+
matchResult = snapshotValidation(snapshot, newValue);
|
|
1744
|
+
if (matchResult.errorLine !== -1) {
|
|
1745
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1746
|
+
}
|
|
1747
|
+
// highlight and screenshot
|
|
1748
|
+
return state.info;
|
|
1532
1749
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
throw new Error("element doesn't contain text " + text);
|
|
1750
|
+
catch (e) {
|
|
1751
|
+
// Log error but continue retrying until timeout is reached
|
|
1752
|
+
this.logger.warn("Retrying containsText due to: " + e.message);
|
|
1753
|
+
}
|
|
1754
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
|
|
1539
1755
|
}
|
|
1540
|
-
|
|
1756
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1541
1757
|
}
|
|
1542
1758
|
catch (e) {
|
|
1543
1759
|
await _commandError(state, e, this);
|
|
1760
|
+
throw e;
|
|
1544
1761
|
}
|
|
1545
1762
|
finally {
|
|
1546
|
-
_commandFinally(state, this);
|
|
1763
|
+
await _commandFinally(state, this);
|
|
1547
1764
|
}
|
|
1548
1765
|
}
|
|
1549
1766
|
async waitForUserInput(message, world = null) {
|
|
@@ -1581,6 +1798,15 @@ class StableBrowser {
|
|
|
1581
1798
|
// save the data to the file
|
|
1582
1799
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1583
1800
|
}
|
|
1801
|
+
overwriteTestData(testData, world = null) {
|
|
1802
|
+
if (!testData) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
// if data file exists, load it
|
|
1806
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1807
|
+
// save the data to the file
|
|
1808
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1809
|
+
}
|
|
1584
1810
|
_getDataFilePath(fileName) {
|
|
1585
1811
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1586
1812
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1833,7 +2059,7 @@ class StableBrowser {
|
|
|
1833
2059
|
await _commandError(state, e, this);
|
|
1834
2060
|
}
|
|
1835
2061
|
finally {
|
|
1836
|
-
_commandFinally(state, this);
|
|
2062
|
+
await _commandFinally(state, this);
|
|
1837
2063
|
}
|
|
1838
2064
|
}
|
|
1839
2065
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1846,6 +2072,7 @@ class StableBrowser {
|
|
|
1846
2072
|
world,
|
|
1847
2073
|
type: Types.EXTRACT,
|
|
1848
2074
|
text: `Extract attribute from element`,
|
|
2075
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1849
2076
|
operation: "extractAttribute",
|
|
1850
2077
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1851
2078
|
allowDisabled: true,
|
|
@@ -1863,10 +2090,31 @@ class StableBrowser {
|
|
|
1863
2090
|
case "value":
|
|
1864
2091
|
state.value = await state.element.inputValue();
|
|
1865
2092
|
break;
|
|
2093
|
+
case "text":
|
|
2094
|
+
state.value = await state.element.textContent();
|
|
2095
|
+
break;
|
|
1866
2096
|
default:
|
|
1867
2097
|
state.value = await state.element.getAttribute(attribute);
|
|
1868
2098
|
break;
|
|
1869
2099
|
}
|
|
2100
|
+
if (options !== null) {
|
|
2101
|
+
if (options.regex && options.regex !== "") {
|
|
2102
|
+
// Construct a regex pattern from the provided string
|
|
2103
|
+
const regex = options.regex.slice(1, -1);
|
|
2104
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2105
|
+
const matches = state.value.match(regexPattern);
|
|
2106
|
+
if (matches) {
|
|
2107
|
+
let newValue = "";
|
|
2108
|
+
for (const match of matches) {
|
|
2109
|
+
newValue += match;
|
|
2110
|
+
}
|
|
2111
|
+
state.value = newValue;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2115
|
+
state.value = state.value.trim();
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
1870
2118
|
state.info.value = state.value;
|
|
1871
2119
|
this.setTestData({ [variable]: state.value }, world);
|
|
1872
2120
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1877,7 +2125,7 @@ class StableBrowser {
|
|
|
1877
2125
|
await _commandError(state, e, this);
|
|
1878
2126
|
}
|
|
1879
2127
|
finally {
|
|
1880
|
-
_commandFinally(state, this);
|
|
2128
|
+
await _commandFinally(state, this);
|
|
1881
2129
|
}
|
|
1882
2130
|
}
|
|
1883
2131
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1892,6 +2140,7 @@ class StableBrowser {
|
|
|
1892
2140
|
highlight: true,
|
|
1893
2141
|
screenshot: true,
|
|
1894
2142
|
text: `Verify element attribute`,
|
|
2143
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
1895
2144
|
operation: "verifyAttribute",
|
|
1896
2145
|
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1897
2146
|
allowDisabled: true,
|
|
@@ -1901,12 +2150,15 @@ class StableBrowser {
|
|
|
1901
2150
|
let expectedValue;
|
|
1902
2151
|
try {
|
|
1903
2152
|
await _preCommand(state, this);
|
|
1904
|
-
expectedValue = state.value;
|
|
2153
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1905
2154
|
state.info.expectedValue = expectedValue;
|
|
1906
2155
|
switch (attribute) {
|
|
1907
2156
|
case "innerText":
|
|
1908
2157
|
val = String(await state.element.innerText());
|
|
1909
2158
|
break;
|
|
2159
|
+
case "text":
|
|
2160
|
+
val = String(await state.element.textContent());
|
|
2161
|
+
break;
|
|
1910
2162
|
case "value":
|
|
1911
2163
|
val = String(await state.element.inputValue());
|
|
1912
2164
|
break;
|
|
@@ -1946,7 +2198,7 @@ class StableBrowser {
|
|
|
1946
2198
|
await _commandError(state, e, this);
|
|
1947
2199
|
}
|
|
1948
2200
|
finally {
|
|
1949
|
-
_commandFinally(state, this);
|
|
2201
|
+
await _commandFinally(state, this);
|
|
1950
2202
|
}
|
|
1951
2203
|
}
|
|
1952
2204
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2198,6 +2450,69 @@ class StableBrowser {
|
|
|
2198
2450
|
_reportToWorld(world, {
|
|
2199
2451
|
type: Types.VERIFY_PAGE_PATH,
|
|
2200
2452
|
text: "Verify page path",
|
|
2453
|
+
_text: "Verify the page path contains " + pathPart,
|
|
2454
|
+
screenshotId,
|
|
2455
|
+
result: error
|
|
2456
|
+
? {
|
|
2457
|
+
status: "FAILED",
|
|
2458
|
+
startTime,
|
|
2459
|
+
endTime,
|
|
2460
|
+
message: error?.message,
|
|
2461
|
+
}
|
|
2462
|
+
: {
|
|
2463
|
+
status: "PASSED",
|
|
2464
|
+
startTime,
|
|
2465
|
+
endTime,
|
|
2466
|
+
},
|
|
2467
|
+
info: info,
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2472
|
+
const startTime = Date.now();
|
|
2473
|
+
let error = null;
|
|
2474
|
+
let screenshotId = null;
|
|
2475
|
+
let screenshotPath = null;
|
|
2476
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2477
|
+
const info = {};
|
|
2478
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2479
|
+
info.operation = "verifyPageTitle";
|
|
2480
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2481
|
+
if (newValue !== title) {
|
|
2482
|
+
this.logger.info(title + "=" + newValue);
|
|
2483
|
+
title = newValue;
|
|
2484
|
+
}
|
|
2485
|
+
info.title = title;
|
|
2486
|
+
try {
|
|
2487
|
+
for (let i = 0; i < 30; i++) {
|
|
2488
|
+
const foundTitle = await this.page.title();
|
|
2489
|
+
if (!foundTitle.includes(title)) {
|
|
2490
|
+
if (i === 29) {
|
|
2491
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2492
|
+
}
|
|
2493
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2497
|
+
return info;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
catch (e) {
|
|
2501
|
+
//await this.closeUnexpectedPopups();
|
|
2502
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2503
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2504
|
+
info.screenshotPath = screenshotPath;
|
|
2505
|
+
Object.assign(e, { info: info });
|
|
2506
|
+
error = e;
|
|
2507
|
+
// throw e;
|
|
2508
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2509
|
+
}
|
|
2510
|
+
finally {
|
|
2511
|
+
const endTime = Date.now();
|
|
2512
|
+
_reportToWorld(world, {
|
|
2513
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2514
|
+
text: "Verify page title",
|
|
2515
|
+
_text: "Verify the page title contains " + title,
|
|
2201
2516
|
screenshotId,
|
|
2202
2517
|
result: error
|
|
2203
2518
|
? {
|
|
@@ -2215,27 +2530,27 @@ class StableBrowser {
|
|
|
2215
2530
|
});
|
|
2216
2531
|
}
|
|
2217
2532
|
}
|
|
2218
|
-
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2533
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2219
2534
|
const frames = this.page.frames();
|
|
2220
2535
|
let results = [];
|
|
2221
|
-
let ignoreCase = false;
|
|
2536
|
+
// let ignoreCase = false;
|
|
2222
2537
|
for (let i = 0; i < frames.length; i++) {
|
|
2223
2538
|
if (dateAlternatives.date) {
|
|
2224
2539
|
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,
|
|
2540
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2226
2541
|
result.frame = frames[i];
|
|
2227
2542
|
results.push(result);
|
|
2228
2543
|
}
|
|
2229
2544
|
}
|
|
2230
2545
|
else if (numberAlternatives.number) {
|
|
2231
2546
|
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,
|
|
2547
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2233
2548
|
result.frame = frames[i];
|
|
2234
2549
|
results.push(result);
|
|
2235
2550
|
}
|
|
2236
2551
|
}
|
|
2237
2552
|
else {
|
|
2238
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2553
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2239
2554
|
result.frame = frames[i];
|
|
2240
2555
|
results.push(result);
|
|
2241
2556
|
}
|
|
@@ -2254,11 +2569,15 @@ class StableBrowser {
|
|
|
2254
2569
|
scroll: false,
|
|
2255
2570
|
highlight: false,
|
|
2256
2571
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2257
|
-
text: `Verify text exists in page`,
|
|
2572
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2573
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2258
2574
|
operation: "verifyTextExistInPage",
|
|
2259
2575
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2260
2576
|
};
|
|
2261
|
-
|
|
2577
|
+
if (testForRegex(text)) {
|
|
2578
|
+
text = text.replace(/\\"/g, '"');
|
|
2579
|
+
}
|
|
2580
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2262
2581
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2263
2582
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2264
2583
|
if (newValue !== text) {
|
|
@@ -2328,7 +2647,7 @@ class StableBrowser {
|
|
|
2328
2647
|
await _commandError(state, e, this);
|
|
2329
2648
|
}
|
|
2330
2649
|
finally {
|
|
2331
|
-
_commandFinally(state, this);
|
|
2650
|
+
await _commandFinally(state, this);
|
|
2332
2651
|
}
|
|
2333
2652
|
}
|
|
2334
2653
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2341,11 +2660,15 @@ class StableBrowser {
|
|
|
2341
2660
|
scroll: false,
|
|
2342
2661
|
highlight: false,
|
|
2343
2662
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2344
|
-
text: `Verify text does not exist in page`,
|
|
2663
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2664
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2345
2665
|
operation: "verifyTextNotExistInPage",
|
|
2346
2666
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2347
2667
|
};
|
|
2348
|
-
|
|
2668
|
+
if (testForRegex(text)) {
|
|
2669
|
+
text = text.replace(/\\"/g, '"');
|
|
2670
|
+
}
|
|
2671
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2349
2672
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2350
2673
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2351
2674
|
if (newValue !== text) {
|
|
@@ -2381,7 +2704,7 @@ class StableBrowser {
|
|
|
2381
2704
|
await _commandError(state, e, this);
|
|
2382
2705
|
}
|
|
2383
2706
|
finally {
|
|
2384
|
-
_commandFinally(state, this);
|
|
2707
|
+
await _commandFinally(state, this);
|
|
2385
2708
|
}
|
|
2386
2709
|
}
|
|
2387
2710
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2396,10 +2719,11 @@ class StableBrowser {
|
|
|
2396
2719
|
highlight: false,
|
|
2397
2720
|
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2398
2721
|
text: `Verify text with relation to another text`,
|
|
2722
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
2399
2723
|
operation: "verify_text_with_relation",
|
|
2400
2724
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2401
2725
|
};
|
|
2402
|
-
const timeout = this.
|
|
2726
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2403
2727
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2404
2728
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2405
2729
|
if (newValue !== textAnchor) {
|
|
@@ -2422,7 +2746,7 @@ class StableBrowser {
|
|
|
2422
2746
|
};
|
|
2423
2747
|
while (true) {
|
|
2424
2748
|
try {
|
|
2425
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2749
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2426
2750
|
}
|
|
2427
2751
|
catch (error) {
|
|
2428
2752
|
// ignore
|
|
@@ -2450,7 +2774,7 @@ class StableBrowser {
|
|
|
2450
2774
|
const count = await frame.locator(css).count();
|
|
2451
2775
|
for (let j = 0; j < count; j++) {
|
|
2452
2776
|
const continer = await frame.locator(css).nth(j);
|
|
2453
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2777
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2454
2778
|
if (result.elementCount > 0) {
|
|
2455
2779
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2456
2780
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2491,8 +2815,32 @@ class StableBrowser {
|
|
|
2491
2815
|
await _commandError(state, e, this);
|
|
2492
2816
|
}
|
|
2493
2817
|
finally {
|
|
2494
|
-
_commandFinally(state, this);
|
|
2818
|
+
await _commandFinally(state, this);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2822
|
+
const frames = this.page.frames();
|
|
2823
|
+
let results = [];
|
|
2824
|
+
let ignoreCase = false;
|
|
2825
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2826
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2827
|
+
result.frame = frames[i];
|
|
2828
|
+
const climbArray = [];
|
|
2829
|
+
for (let i = 0; i < climb; i++) {
|
|
2830
|
+
climbArray.push("..");
|
|
2831
|
+
}
|
|
2832
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2833
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2834
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2835
|
+
if (count > 0) {
|
|
2836
|
+
result.elementCount = count;
|
|
2837
|
+
result.locator = newLocator;
|
|
2838
|
+
results.push(result);
|
|
2839
|
+
}
|
|
2495
2840
|
}
|
|
2841
|
+
// state.info.results = results;
|
|
2842
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2843
|
+
return resultWithElementsFound;
|
|
2496
2844
|
}
|
|
2497
2845
|
async visualVerification(text, options = {}, world = null) {
|
|
2498
2846
|
const startTime = Date.now();
|
|
@@ -2555,6 +2903,7 @@ class StableBrowser {
|
|
|
2555
2903
|
_reportToWorld(world, {
|
|
2556
2904
|
type: Types.VERIFY_VISUAL,
|
|
2557
2905
|
text: "Visual verification",
|
|
2906
|
+
_text: "Visual verification of " + text,
|
|
2558
2907
|
screenshotId,
|
|
2559
2908
|
result: error
|
|
2560
2909
|
? {
|
|
@@ -2809,7 +3158,13 @@ class StableBrowser {
|
|
|
2809
3158
|
}
|
|
2810
3159
|
}
|
|
2811
3160
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2812
|
-
|
|
3161
|
+
try {
|
|
3162
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3163
|
+
}
|
|
3164
|
+
catch (error) {
|
|
3165
|
+
this.logger.debug(error);
|
|
3166
|
+
throw error;
|
|
3167
|
+
}
|
|
2813
3168
|
}
|
|
2814
3169
|
_getLoadTimeout(options) {
|
|
2815
3170
|
let timeout = 15000;
|
|
@@ -2821,6 +3176,15 @@ class StableBrowser {
|
|
|
2821
3176
|
}
|
|
2822
3177
|
return timeout;
|
|
2823
3178
|
}
|
|
3179
|
+
_getFindElementTimeout(options) {
|
|
3180
|
+
if (options && options.timeout) {
|
|
3181
|
+
return options.timeout;
|
|
3182
|
+
}
|
|
3183
|
+
if (this.configuration.find_element_timeout) {
|
|
3184
|
+
return this.configuration.find_element_timeout;
|
|
3185
|
+
}
|
|
3186
|
+
return 30000;
|
|
3187
|
+
}
|
|
2824
3188
|
async saveStoreState(path = null, world = null) {
|
|
2825
3189
|
const storageState = await this.page.context().storageState();
|
|
2826
3190
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
@@ -2837,6 +3201,9 @@ class StableBrowser {
|
|
|
2837
3201
|
this.registerEventListeners(this.context);
|
|
2838
3202
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2839
3203
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3204
|
+
if (this.onRestoreSaveState) {
|
|
3205
|
+
this.onRestoreSaveState(path);
|
|
3206
|
+
}
|
|
2840
3207
|
}
|
|
2841
3208
|
async waitForPageLoad(options = {}, world = null) {
|
|
2842
3209
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2905,6 +3272,7 @@ class StableBrowser {
|
|
|
2905
3272
|
highlight: false,
|
|
2906
3273
|
type: Types.CLOSE_PAGE,
|
|
2907
3274
|
text: `Close page`,
|
|
3275
|
+
_text: `Close the page`,
|
|
2908
3276
|
operation: "closePage",
|
|
2909
3277
|
log: "***** close page *****\n",
|
|
2910
3278
|
throwError: false,
|
|
@@ -2918,11 +3286,98 @@ class StableBrowser {
|
|
|
2918
3286
|
await _commandError(state, e, this);
|
|
2919
3287
|
}
|
|
2920
3288
|
finally {
|
|
2921
|
-
_commandFinally(state, this);
|
|
3289
|
+
await _commandFinally(state, this);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3293
|
+
let operation = null;
|
|
3294
|
+
if (!options || !options.operation) {
|
|
3295
|
+
throw new Error("operation is not defined");
|
|
3296
|
+
}
|
|
3297
|
+
operation = options.operation;
|
|
3298
|
+
// validate operation is one of the supported operations
|
|
3299
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3300
|
+
throw new Error("operation is not supported");
|
|
3301
|
+
}
|
|
3302
|
+
const state = {
|
|
3303
|
+
options,
|
|
3304
|
+
world,
|
|
3305
|
+
locate: false,
|
|
3306
|
+
scroll: false,
|
|
3307
|
+
highlight: false,
|
|
3308
|
+
type: Types.TABLE_OPERATION,
|
|
3309
|
+
text: `Table operation`,
|
|
3310
|
+
_text: `Table ${operation} operation`,
|
|
3311
|
+
operation: operation,
|
|
3312
|
+
log: "***** Table operation *****\n",
|
|
3313
|
+
};
|
|
3314
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3315
|
+
try {
|
|
3316
|
+
await _preCommand(state, this);
|
|
3317
|
+
const start = Date.now();
|
|
3318
|
+
let cellArea = null;
|
|
3319
|
+
while (true) {
|
|
3320
|
+
try {
|
|
3321
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3322
|
+
if (cellArea) {
|
|
3323
|
+
break;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
catch (e) {
|
|
3327
|
+
// ignore
|
|
3328
|
+
}
|
|
3329
|
+
if (Date.now() - start > timeout) {
|
|
3330
|
+
throw new Error(`Cell not found in table`);
|
|
3331
|
+
}
|
|
3332
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3333
|
+
}
|
|
3334
|
+
switch (operation) {
|
|
3335
|
+
case "click":
|
|
3336
|
+
if (!options.css) {
|
|
3337
|
+
// will click in the center of the cell
|
|
3338
|
+
let xOffset = 0;
|
|
3339
|
+
let yOffset = 0;
|
|
3340
|
+
if (options.xOffset) {
|
|
3341
|
+
xOffset = options.xOffset;
|
|
3342
|
+
}
|
|
3343
|
+
if (options.yOffset) {
|
|
3344
|
+
yOffset = options.yOffset;
|
|
3345
|
+
}
|
|
3346
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3347
|
+
}
|
|
3348
|
+
else {
|
|
3349
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3350
|
+
if (results.length === 0) {
|
|
3351
|
+
throw new Error(`Element not found in cell area`);
|
|
3352
|
+
}
|
|
3353
|
+
state.element = results[0];
|
|
3354
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3355
|
+
}
|
|
3356
|
+
break;
|
|
3357
|
+
case "hover+click":
|
|
3358
|
+
if (!options.css) {
|
|
3359
|
+
throw new Error("css is not defined");
|
|
3360
|
+
}
|
|
3361
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3362
|
+
if (results.length === 0) {
|
|
3363
|
+
throw new Error(`Element not found in cell area`);
|
|
3364
|
+
}
|
|
3365
|
+
state.element = results[0];
|
|
3366
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3367
|
+
break;
|
|
3368
|
+
default:
|
|
3369
|
+
throw new Error("operation is not supported");
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
catch (e) {
|
|
3373
|
+
await _commandError(state, e, this);
|
|
3374
|
+
}
|
|
3375
|
+
finally {
|
|
3376
|
+
await _commandFinally(state, this);
|
|
2922
3377
|
}
|
|
2923
3378
|
}
|
|
2924
3379
|
saveTestDataAsGlobal(options, world) {
|
|
2925
|
-
const dataFile =
|
|
3380
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2926
3381
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2927
3382
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2928
3383
|
}
|
|
@@ -2952,6 +3407,7 @@ class StableBrowser {
|
|
|
2952
3407
|
_reportToWorld(world, {
|
|
2953
3408
|
type: Types.SET_VIEWPORT,
|
|
2954
3409
|
text: "set viewport size to " + width + "x" + hight,
|
|
3410
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2955
3411
|
screenshotId,
|
|
2956
3412
|
result: error
|
|
2957
3413
|
? {
|
|
@@ -3022,7 +3478,39 @@ class StableBrowser {
|
|
|
3022
3478
|
console.log("#-#");
|
|
3023
3479
|
}
|
|
3024
3480
|
}
|
|
3481
|
+
async beforeScenario(world, scenario) {
|
|
3482
|
+
this.beforeScenarioCalled = true;
|
|
3483
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3484
|
+
this.scenarioName = scenario.pickle.name;
|
|
3485
|
+
}
|
|
3486
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3487
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3488
|
+
}
|
|
3489
|
+
if (this.context) {
|
|
3490
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3491
|
+
}
|
|
3492
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3493
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3494
|
+
// check if @global_test_data tag is present
|
|
3495
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3496
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
// update test data based on feature/scenario
|
|
3500
|
+
let envName = null;
|
|
3501
|
+
if (this.context && this.context.environment) {
|
|
3502
|
+
envName = this.context.environment.name;
|
|
3503
|
+
}
|
|
3504
|
+
if (!process.env.TEMP_RUN) {
|
|
3505
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3506
|
+
}
|
|
3507
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3508
|
+
}
|
|
3509
|
+
async afterScenario(world, scenario) { }
|
|
3025
3510
|
async beforeStep(world, step) {
|
|
3511
|
+
if (!this.beforeScenarioCalled) {
|
|
3512
|
+
this.beforeScenario(world, step);
|
|
3513
|
+
}
|
|
3026
3514
|
if (this.stepIndex === undefined) {
|
|
3027
3515
|
this.stepIndex = 0;
|
|
3028
3516
|
}
|
|
@@ -3039,21 +3527,53 @@ class StableBrowser {
|
|
|
3039
3527
|
else {
|
|
3040
3528
|
this.stepName = "step " + this.stepIndex;
|
|
3041
3529
|
}
|
|
3042
|
-
if (this.context) {
|
|
3043
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3044
|
-
}
|
|
3045
3530
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3046
3531
|
if (this.context.browserObject.context) {
|
|
3047
3532
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3048
3533
|
}
|
|
3049
3534
|
}
|
|
3050
|
-
if (this.
|
|
3051
|
-
this.
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3535
|
+
if (this.initSnapshotTaken === false) {
|
|
3536
|
+
this.initSnapshotTaken = true;
|
|
3537
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3538
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3539
|
+
if (snapshot) {
|
|
3540
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
async getAriaSnapshot() {
|
|
3546
|
+
try {
|
|
3547
|
+
// find the page url
|
|
3548
|
+
const url = await this.page.url();
|
|
3549
|
+
// extract the path from the url
|
|
3550
|
+
const path = new URL(url).pathname;
|
|
3551
|
+
// get the page title
|
|
3552
|
+
const title = await this.page.title();
|
|
3553
|
+
// go over other frams
|
|
3554
|
+
const frames = this.page.frames();
|
|
3555
|
+
const snapshots = [];
|
|
3556
|
+
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3557
|
+
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3558
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3559
|
+
const frame = frames[i];
|
|
3560
|
+
try {
|
|
3561
|
+
// Ensure frame is attached and has body
|
|
3562
|
+
const body = frame.locator("body");
|
|
3563
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3564
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3565
|
+
content.push(`- frame: ${i}`);
|
|
3566
|
+
content.push(snapshot);
|
|
3567
|
+
}
|
|
3568
|
+
catch (innerErr) { }
|
|
3055
3569
|
}
|
|
3570
|
+
return content.join("\n");
|
|
3056
3571
|
}
|
|
3572
|
+
catch (e) {
|
|
3573
|
+
console.log("Error in getAriaSnapshot");
|
|
3574
|
+
//console.debug(e);
|
|
3575
|
+
}
|
|
3576
|
+
return null;
|
|
3057
3577
|
}
|
|
3058
3578
|
async afterStep(world, step) {
|
|
3059
3579
|
this.stepName = null;
|
|
@@ -3062,11 +3582,25 @@ class StableBrowser {
|
|
|
3062
3582
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3063
3583
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3064
3584
|
});
|
|
3585
|
+
if (world && world.attach) {
|
|
3586
|
+
await world.attach(JSON.stringify({
|
|
3587
|
+
type: "trace",
|
|
3588
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3589
|
+
}), "application/json+trace");
|
|
3590
|
+
}
|
|
3591
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3065
3592
|
}
|
|
3066
3593
|
}
|
|
3067
3594
|
if (this.context) {
|
|
3068
3595
|
this.context.examplesRow = null;
|
|
3069
3596
|
}
|
|
3597
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3598
|
+
const snapshot = await this.getAriaSnapshot();
|
|
3599
|
+
if (snapshot) {
|
|
3600
|
+
const obj = {};
|
|
3601
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3070
3604
|
}
|
|
3071
3605
|
}
|
|
3072
3606
|
function createTimedPromise(promise, label) {
|