automation_model 1.0.624-dev → 1.0.624-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 +99 -28
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +68 -23
- 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 +37 -0
- package/lib/snapshot_validation.js +246 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +21 -1
- package/lib/stable_browser.js +654 -139
- 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 +221 -20
- package/lib/utils.js.map +1 -1
- package/package.json +7 -6
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) => {
|
|
@@ -177,6 +187,30 @@ class StableBrowser {
|
|
|
177
187
|
await this.waitForPageLoad();
|
|
178
188
|
}
|
|
179
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
|
+
}
|
|
180
214
|
registerConsoleLogListener(page, context) {
|
|
181
215
|
if (!this.context.webLogger) {
|
|
182
216
|
this.context.webLogger = [];
|
|
@@ -272,7 +306,7 @@ class StableBrowser {
|
|
|
272
306
|
_commandError(state, error, this);
|
|
273
307
|
}
|
|
274
308
|
finally {
|
|
275
|
-
_commandFinally(state, this);
|
|
309
|
+
await _commandFinally(state, this);
|
|
276
310
|
}
|
|
277
311
|
}
|
|
278
312
|
async _getLocator(locator, scope, _params) {
|
|
@@ -353,7 +387,7 @@ class StableBrowser {
|
|
|
353
387
|
return resultCss;
|
|
354
388
|
}
|
|
355
389
|
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
356
|
-
const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase)
|
|
390
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
357
391
|
const locator = scope.locator(query);
|
|
358
392
|
const count = await locator.count();
|
|
359
393
|
if (!tag1) {
|
|
@@ -373,6 +407,12 @@ class StableBrowser {
|
|
|
373
407
|
if (!el.setAttribute) {
|
|
374
408
|
el = el.parentElement;
|
|
375
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
|
+
// }
|
|
376
416
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
377
417
|
return true;
|
|
378
418
|
}, [tag1, randomToken]))) {
|
|
@@ -559,12 +599,24 @@ class StableBrowser {
|
|
|
559
599
|
element.evaluate((el, randomToken) => {
|
|
560
600
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
561
601
|
}, randomToken);
|
|
562
|
-
if (element._frame) {
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
const scope = element.page();
|
|
566
|
-
|
|
567
|
-
|
|
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);
|
|
568
620
|
}
|
|
569
621
|
}
|
|
570
622
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -716,14 +768,9 @@ class StableBrowser {
|
|
|
716
768
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
717
769
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
718
770
|
}
|
|
719
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
771
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
720
772
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
721
773
|
}
|
|
722
|
-
else {
|
|
723
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
724
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
774
|
let foundElements = result.foundElements;
|
|
728
775
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
729
776
|
info.box = foundElements[0].box;
|
|
@@ -778,6 +825,11 @@ class StableBrowser {
|
|
|
778
825
|
visibleOnly = false;
|
|
779
826
|
}
|
|
780
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
|
+
}
|
|
781
833
|
}
|
|
782
834
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
783
835
|
// if (info.locatorLog) {
|
|
@@ -824,9 +876,40 @@ class StableBrowser {
|
|
|
824
876
|
result.locatorIndex = i;
|
|
825
877
|
}
|
|
826
878
|
if (foundLocators.length > 1) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
}
|
|
830
913
|
}
|
|
831
914
|
}
|
|
832
915
|
}
|
|
@@ -874,7 +957,7 @@ class StableBrowser {
|
|
|
874
957
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
875
958
|
}
|
|
876
959
|
finally {
|
|
877
|
-
_commandFinally(state, this);
|
|
960
|
+
await _commandFinally(state, this);
|
|
878
961
|
}
|
|
879
962
|
}
|
|
880
963
|
}
|
|
@@ -923,7 +1006,7 @@ class StableBrowser {
|
|
|
923
1006
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
924
1007
|
}
|
|
925
1008
|
finally {
|
|
926
|
-
_commandFinally(state, this);
|
|
1009
|
+
await _commandFinally(state, this);
|
|
927
1010
|
}
|
|
928
1011
|
}
|
|
929
1012
|
}
|
|
@@ -937,25 +1020,14 @@ class StableBrowser {
|
|
|
937
1020
|
options,
|
|
938
1021
|
world,
|
|
939
1022
|
text: "Click element",
|
|
1023
|
+
_text: "Click on " + selectors.element_name,
|
|
940
1024
|
type: Types.CLICK,
|
|
941
1025
|
operation: "click",
|
|
942
1026
|
log: "***** click on " + selectors.element_name + " *****\n",
|
|
943
1027
|
};
|
|
944
1028
|
try {
|
|
945
1029
|
await _preCommand(state, this);
|
|
946
|
-
|
|
947
|
-
// state.selectors.locators[0].text = state.options.context;
|
|
948
|
-
// }
|
|
949
|
-
try {
|
|
950
|
-
await state.element.click();
|
|
951
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
952
|
-
}
|
|
953
|
-
catch (e) {
|
|
954
|
-
// await this.closeUnexpectedPopups();
|
|
955
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
956
|
-
await state.element.dispatchEvent("click");
|
|
957
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
958
|
-
}
|
|
1030
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
959
1031
|
await this.waitForPageLoad();
|
|
960
1032
|
return state.info;
|
|
961
1033
|
}
|
|
@@ -963,8 +1035,40 @@ class StableBrowser {
|
|
|
963
1035
|
await _commandError(state, e, this);
|
|
964
1036
|
}
|
|
965
1037
|
finally {
|
|
966
|
-
_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);
|
|
967
1070
|
}
|
|
1071
|
+
return found;
|
|
968
1072
|
}
|
|
969
1073
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
970
1074
|
const state = {
|
|
@@ -974,6 +1078,7 @@ class StableBrowser {
|
|
|
974
1078
|
world,
|
|
975
1079
|
type: checked ? Types.CHECK : Types.UNCHECK,
|
|
976
1080
|
text: checked ? `Check element` : `Uncheck element`,
|
|
1081
|
+
_text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
|
|
977
1082
|
operation: "setCheck",
|
|
978
1083
|
log: "***** check " + selectors.element_name + " *****\n",
|
|
979
1084
|
};
|
|
@@ -985,7 +1090,7 @@ class StableBrowser {
|
|
|
985
1090
|
try {
|
|
986
1091
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
987
1092
|
// console.log(`Highlighting while running from recorder`);
|
|
988
|
-
await this._highlightElements(element);
|
|
1093
|
+
await this._highlightElements(state.element);
|
|
989
1094
|
await state.element.setChecked(checked);
|
|
990
1095
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
991
1096
|
// await this._unHighlightElements(element);
|
|
@@ -1012,7 +1117,7 @@ class StableBrowser {
|
|
|
1012
1117
|
await _commandError(state, e, this);
|
|
1013
1118
|
}
|
|
1014
1119
|
finally {
|
|
1015
|
-
_commandFinally(state, this);
|
|
1120
|
+
await _commandFinally(state, this);
|
|
1016
1121
|
}
|
|
1017
1122
|
}
|
|
1018
1123
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1023,24 +1128,13 @@ class StableBrowser {
|
|
|
1023
1128
|
world,
|
|
1024
1129
|
type: Types.HOVER,
|
|
1025
1130
|
text: `Hover element`,
|
|
1131
|
+
_text: `Hover on ${selectors.element_name}`,
|
|
1026
1132
|
operation: "hover",
|
|
1027
1133
|
log: "***** hover " + selectors.element_name + " *****\n",
|
|
1028
1134
|
};
|
|
1029
1135
|
try {
|
|
1030
1136
|
await _preCommand(state, this);
|
|
1031
|
-
|
|
1032
|
-
await state.element.hover();
|
|
1033
|
-
// await _screenshot(state, this);
|
|
1034
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1035
|
-
}
|
|
1036
|
-
catch (e) {
|
|
1037
|
-
//await this.closeUnexpectedPopups();
|
|
1038
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1039
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1040
|
-
await state.element.hover({ timeout: 10000 });
|
|
1041
|
-
// await _screenshot(state, this);
|
|
1042
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1043
|
-
}
|
|
1137
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1044
1138
|
await _screenshot(state, this);
|
|
1045
1139
|
await this.waitForPageLoad();
|
|
1046
1140
|
return state.info;
|
|
@@ -1049,7 +1143,7 @@ class StableBrowser {
|
|
|
1049
1143
|
await _commandError(state, e, this);
|
|
1050
1144
|
}
|
|
1051
1145
|
finally {
|
|
1052
|
-
_commandFinally(state, this);
|
|
1146
|
+
await _commandFinally(state, this);
|
|
1053
1147
|
}
|
|
1054
1148
|
}
|
|
1055
1149
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1064,6 +1158,7 @@ class StableBrowser {
|
|
|
1064
1158
|
value: values.toString(),
|
|
1065
1159
|
type: Types.SELECT,
|
|
1066
1160
|
text: `Select option: ${values}`,
|
|
1161
|
+
_text: `Select option: ${values} on ${selectors.element_name}`,
|
|
1067
1162
|
operation: "selectOption",
|
|
1068
1163
|
log: "***** select option " + selectors.element_name + " *****\n",
|
|
1069
1164
|
};
|
|
@@ -1084,7 +1179,7 @@ class StableBrowser {
|
|
|
1084
1179
|
await _commandError(state, e, this);
|
|
1085
1180
|
}
|
|
1086
1181
|
finally {
|
|
1087
|
-
_commandFinally(state, this);
|
|
1182
|
+
await _commandFinally(state, this);
|
|
1088
1183
|
}
|
|
1089
1184
|
}
|
|
1090
1185
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1098,6 +1193,7 @@ class StableBrowser {
|
|
|
1098
1193
|
highlight: false,
|
|
1099
1194
|
type: Types.TYPE_PRESS,
|
|
1100
1195
|
text: `Type value: ${_value}`,
|
|
1196
|
+
_text: `Type value: ${_value}`,
|
|
1101
1197
|
operation: "type",
|
|
1102
1198
|
log: "",
|
|
1103
1199
|
};
|
|
@@ -1129,7 +1225,7 @@ class StableBrowser {
|
|
|
1129
1225
|
await _commandError(state, e, this);
|
|
1130
1226
|
}
|
|
1131
1227
|
finally {
|
|
1132
|
-
_commandFinally(state, this);
|
|
1228
|
+
await _commandFinally(state, this);
|
|
1133
1229
|
}
|
|
1134
1230
|
}
|
|
1135
1231
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1165,7 +1261,7 @@ class StableBrowser {
|
|
|
1165
1261
|
await _commandError(state, e, this);
|
|
1166
1262
|
}
|
|
1167
1263
|
finally {
|
|
1168
|
-
_commandFinally(state, this);
|
|
1264
|
+
await _commandFinally(state, this);
|
|
1169
1265
|
}
|
|
1170
1266
|
}
|
|
1171
1267
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1177,6 +1273,7 @@ class StableBrowser {
|
|
|
1177
1273
|
world,
|
|
1178
1274
|
type: Types.SET_DATE_TIME,
|
|
1179
1275
|
text: `Set date time value: ${value}`,
|
|
1276
|
+
_text: `Set date time value: ${value} on ${selectors.element_name}`,
|
|
1180
1277
|
operation: "setDateTime",
|
|
1181
1278
|
log: "***** set date time value " + selectors.element_name + " *****\n",
|
|
1182
1279
|
throwError: false,
|
|
@@ -1184,7 +1281,7 @@ class StableBrowser {
|
|
|
1184
1281
|
try {
|
|
1185
1282
|
await _preCommand(state, this);
|
|
1186
1283
|
try {
|
|
1187
|
-
await state.element
|
|
1284
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1188
1285
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1189
1286
|
if (format) {
|
|
1190
1287
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1233,7 +1330,7 @@ class StableBrowser {
|
|
|
1233
1330
|
await _commandError(state, e, this);
|
|
1234
1331
|
}
|
|
1235
1332
|
finally {
|
|
1236
|
-
_commandFinally(state, this);
|
|
1333
|
+
await _commandFinally(state, this);
|
|
1237
1334
|
}
|
|
1238
1335
|
}
|
|
1239
1336
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1248,9 +1345,13 @@ class StableBrowser {
|
|
|
1248
1345
|
world,
|
|
1249
1346
|
type: Types.FILL,
|
|
1250
1347
|
text: `Click type input with value: ${_value}`,
|
|
1348
|
+
_text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
|
|
1251
1349
|
operation: "clickType",
|
|
1252
1350
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1253
1351
|
};
|
|
1352
|
+
if (!options) {
|
|
1353
|
+
options = {};
|
|
1354
|
+
}
|
|
1254
1355
|
if (newValue !== _value) {
|
|
1255
1356
|
//this.logger.info(_value + "=" + newValue);
|
|
1256
1357
|
_value = newValue;
|
|
@@ -1258,7 +1359,7 @@ class StableBrowser {
|
|
|
1258
1359
|
try {
|
|
1259
1360
|
await _preCommand(state, this);
|
|
1260
1361
|
state.info.value = _value;
|
|
1261
|
-
if (
|
|
1362
|
+
if (!options.press) {
|
|
1262
1363
|
try {
|
|
1263
1364
|
let currentValue = await state.element.inputValue();
|
|
1264
1365
|
if (currentValue) {
|
|
@@ -1269,13 +1370,9 @@ class StableBrowser {
|
|
|
1269
1370
|
this.logger.info("unable to clear input value");
|
|
1270
1371
|
}
|
|
1271
1372
|
}
|
|
1272
|
-
if (options
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
catch (e) {
|
|
1277
|
-
await state.element.dispatchEvent("click");
|
|
1278
|
-
}
|
|
1373
|
+
if (options.press) {
|
|
1374
|
+
options.timeout = 5000;
|
|
1375
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1279
1376
|
}
|
|
1280
1377
|
else {
|
|
1281
1378
|
try {
|
|
@@ -1333,7 +1430,7 @@ class StableBrowser {
|
|
|
1333
1430
|
await _commandError(state, e, this);
|
|
1334
1431
|
}
|
|
1335
1432
|
finally {
|
|
1336
|
-
_commandFinally(state, this);
|
|
1433
|
+
await _commandFinally(state, this);
|
|
1337
1434
|
}
|
|
1338
1435
|
}
|
|
1339
1436
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1363,13 +1460,49 @@ class StableBrowser {
|
|
|
1363
1460
|
await _commandError(state, e, this);
|
|
1364
1461
|
}
|
|
1365
1462
|
finally {
|
|
1366
|
-
_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);
|
|
1367
1499
|
}
|
|
1368
1500
|
}
|
|
1369
1501
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
1370
1502
|
return await this._getText(selectors, 0, _params, options, info, world);
|
|
1371
1503
|
}
|
|
1372
1504
|
async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
|
|
1505
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1373
1506
|
_validateSelectors(selectors);
|
|
1374
1507
|
let screenshotId = null;
|
|
1375
1508
|
let screenshotPath = null;
|
|
@@ -1379,7 +1512,7 @@ class StableBrowser {
|
|
|
1379
1512
|
}
|
|
1380
1513
|
info.operation = "getText";
|
|
1381
1514
|
info.selectors = selectors;
|
|
1382
|
-
let element = await this._locate(selectors, info, _params);
|
|
1515
|
+
let element = await this._locate(selectors, info, _params, timeout);
|
|
1383
1516
|
if (climb > 0) {
|
|
1384
1517
|
const climbArray = [];
|
|
1385
1518
|
for (let i = 0; i < climb; i++) {
|
|
@@ -1446,6 +1579,7 @@ class StableBrowser {
|
|
|
1446
1579
|
highlight: false,
|
|
1447
1580
|
type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
|
|
1448
1581
|
text: `Verify element contains pattern: ${pattern}`,
|
|
1582
|
+
_text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
|
|
1449
1583
|
operation: "containsPattern",
|
|
1450
1584
|
log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
|
|
1451
1585
|
};
|
|
@@ -1477,10 +1611,12 @@ class StableBrowser {
|
|
|
1477
1611
|
await _commandError(state, e, this);
|
|
1478
1612
|
}
|
|
1479
1613
|
finally {
|
|
1480
|
-
_commandFinally(state, this);
|
|
1614
|
+
await _commandFinally(state, this);
|
|
1481
1615
|
}
|
|
1482
1616
|
}
|
|
1483
1617
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1618
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1619
|
+
const startTime = Date.now();
|
|
1484
1620
|
const state = {
|
|
1485
1621
|
selectors,
|
|
1486
1622
|
_params,
|
|
@@ -1507,44 +1643,124 @@ class StableBrowser {
|
|
|
1507
1643
|
}
|
|
1508
1644
|
let foundObj = null;
|
|
1509
1645
|
try {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if (
|
|
1521
|
-
|
|
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)) {
|
|
1522
1673
|
return state.info;
|
|
1523
1674
|
}
|
|
1524
1675
|
}
|
|
1525
|
-
|
|
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
|
|
1526
1681
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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;
|
|
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, referanceSnapshot);
|
|
1744
|
+
if (matchResult.errorLine !== -1) {
|
|
1745
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1532
1746
|
}
|
|
1747
|
+
// highlight and screenshot
|
|
1748
|
+
return state.info;
|
|
1533
1749
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
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 snapshot validation due to: " + e.message);
|
|
1753
|
+
}
|
|
1754
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1540
1755
|
}
|
|
1541
|
-
|
|
1756
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1542
1757
|
}
|
|
1543
1758
|
catch (e) {
|
|
1544
1759
|
await _commandError(state, e, this);
|
|
1760
|
+
throw e;
|
|
1545
1761
|
}
|
|
1546
1762
|
finally {
|
|
1547
|
-
_commandFinally(state, this);
|
|
1763
|
+
await _commandFinally(state, this);
|
|
1548
1764
|
}
|
|
1549
1765
|
}
|
|
1550
1766
|
async waitForUserInput(message, world = null) {
|
|
@@ -1582,6 +1798,15 @@ class StableBrowser {
|
|
|
1582
1798
|
// save the data to the file
|
|
1583
1799
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1584
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
|
+
}
|
|
1585
1810
|
_getDataFilePath(fileName) {
|
|
1586
1811
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1587
1812
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1834,7 +2059,7 @@ class StableBrowser {
|
|
|
1834
2059
|
await _commandError(state, e, this);
|
|
1835
2060
|
}
|
|
1836
2061
|
finally {
|
|
1837
|
-
_commandFinally(state, this);
|
|
2062
|
+
await _commandFinally(state, this);
|
|
1838
2063
|
}
|
|
1839
2064
|
}
|
|
1840
2065
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1847,6 +2072,7 @@ class StableBrowser {
|
|
|
1847
2072
|
world,
|
|
1848
2073
|
type: Types.EXTRACT,
|
|
1849
2074
|
text: `Extract attribute from element`,
|
|
2075
|
+
_text: `Extract attribute ${attribute} from ${selectors.element_name}`,
|
|
1850
2076
|
operation: "extractAttribute",
|
|
1851
2077
|
log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1852
2078
|
allowDisabled: true,
|
|
@@ -1864,10 +2090,31 @@ class StableBrowser {
|
|
|
1864
2090
|
case "value":
|
|
1865
2091
|
state.value = await state.element.inputValue();
|
|
1866
2092
|
break;
|
|
2093
|
+
case "text":
|
|
2094
|
+
state.value = await state.element.textContent();
|
|
2095
|
+
break;
|
|
1867
2096
|
default:
|
|
1868
2097
|
state.value = await state.element.getAttribute(attribute);
|
|
1869
2098
|
break;
|
|
1870
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
|
+
}
|
|
1871
2118
|
state.info.value = state.value;
|
|
1872
2119
|
this.setTestData({ [variable]: state.value }, world);
|
|
1873
2120
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1878,7 +2125,7 @@ class StableBrowser {
|
|
|
1878
2125
|
await _commandError(state, e, this);
|
|
1879
2126
|
}
|
|
1880
2127
|
finally {
|
|
1881
|
-
_commandFinally(state, this);
|
|
2128
|
+
await _commandFinally(state, this);
|
|
1882
2129
|
}
|
|
1883
2130
|
}
|
|
1884
2131
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1893,6 +2140,7 @@ class StableBrowser {
|
|
|
1893
2140
|
highlight: true,
|
|
1894
2141
|
screenshot: true,
|
|
1895
2142
|
text: `Verify element attribute`,
|
|
2143
|
+
_text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
|
|
1896
2144
|
operation: "verifyAttribute",
|
|
1897
2145
|
log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
|
|
1898
2146
|
allowDisabled: true,
|
|
@@ -1902,12 +2150,15 @@ class StableBrowser {
|
|
|
1902
2150
|
let expectedValue;
|
|
1903
2151
|
try {
|
|
1904
2152
|
await _preCommand(state, this);
|
|
1905
|
-
expectedValue = state.value;
|
|
2153
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1906
2154
|
state.info.expectedValue = expectedValue;
|
|
1907
2155
|
switch (attribute) {
|
|
1908
2156
|
case "innerText":
|
|
1909
2157
|
val = String(await state.element.innerText());
|
|
1910
2158
|
break;
|
|
2159
|
+
case "text":
|
|
2160
|
+
val = String(await state.element.textContent());
|
|
2161
|
+
break;
|
|
1911
2162
|
case "value":
|
|
1912
2163
|
val = String(await state.element.inputValue());
|
|
1913
2164
|
break;
|
|
@@ -1929,17 +2180,42 @@ class StableBrowser {
|
|
|
1929
2180
|
let regex;
|
|
1930
2181
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1931
2182
|
const patternBody = expectedValue.slice(1, -1);
|
|
1932
|
-
|
|
2183
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2184
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2185
|
+
state.info.regex = true;
|
|
1933
2186
|
}
|
|
1934
2187
|
else {
|
|
1935
2188
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1936
2189
|
regex = new RegExp(escapedPattern, "g");
|
|
1937
2190
|
}
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2191
|
+
if (attribute === "innerText") {
|
|
2192
|
+
if (state.info.regex) {
|
|
2193
|
+
if (!regex.test(val)) {
|
|
2194
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2195
|
+
state.info.failCause.assertionFailed = true;
|
|
2196
|
+
state.info.failCause.lastError = errorMessage;
|
|
2197
|
+
throw new Error(errorMessage);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
const valLines = val.split("\n");
|
|
2202
|
+
const expectedLines = expectedValue.split("\n");
|
|
2203
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2204
|
+
if (!isPart) {
|
|
2205
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2206
|
+
state.info.failCause.assertionFailed = true;
|
|
2207
|
+
state.info.failCause.lastError = errorMessage;
|
|
2208
|
+
throw new Error(errorMessage);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
else {
|
|
2213
|
+
if (!val.match(regex)) {
|
|
2214
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2215
|
+
state.info.failCause.assertionFailed = true;
|
|
2216
|
+
state.info.failCause.lastError = errorMessage;
|
|
2217
|
+
throw new Error(errorMessage);
|
|
2218
|
+
}
|
|
1943
2219
|
}
|
|
1944
2220
|
return state.info;
|
|
1945
2221
|
}
|
|
@@ -1947,7 +2223,7 @@ class StableBrowser {
|
|
|
1947
2223
|
await _commandError(state, e, this);
|
|
1948
2224
|
}
|
|
1949
2225
|
finally {
|
|
1950
|
-
_commandFinally(state, this);
|
|
2226
|
+
await _commandFinally(state, this);
|
|
1951
2227
|
}
|
|
1952
2228
|
}
|
|
1953
2229
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2192,13 +2468,76 @@ class StableBrowser {
|
|
|
2192
2468
|
Object.assign(e, { info: info });
|
|
2193
2469
|
error = e;
|
|
2194
2470
|
// throw e;
|
|
2195
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2471
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
|
|
2196
2472
|
}
|
|
2197
2473
|
finally {
|
|
2198
2474
|
const endTime = Date.now();
|
|
2199
2475
|
_reportToWorld(world, {
|
|
2200
2476
|
type: Types.VERIFY_PAGE_PATH,
|
|
2201
2477
|
text: "Verify page path",
|
|
2478
|
+
_text: "Verify the page path contains " + pathPart,
|
|
2479
|
+
screenshotId,
|
|
2480
|
+
result: error
|
|
2481
|
+
? {
|
|
2482
|
+
status: "FAILED",
|
|
2483
|
+
startTime,
|
|
2484
|
+
endTime,
|
|
2485
|
+
message: error?.message,
|
|
2486
|
+
}
|
|
2487
|
+
: {
|
|
2488
|
+
status: "PASSED",
|
|
2489
|
+
startTime,
|
|
2490
|
+
endTime,
|
|
2491
|
+
},
|
|
2492
|
+
info: info,
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2497
|
+
const startTime = Date.now();
|
|
2498
|
+
let error = null;
|
|
2499
|
+
let screenshotId = null;
|
|
2500
|
+
let screenshotPath = null;
|
|
2501
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2502
|
+
const info = {};
|
|
2503
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2504
|
+
info.operation = "verifyPageTitle";
|
|
2505
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2506
|
+
if (newValue !== title) {
|
|
2507
|
+
this.logger.info(title + "=" + newValue);
|
|
2508
|
+
title = newValue;
|
|
2509
|
+
}
|
|
2510
|
+
info.title = title;
|
|
2511
|
+
try {
|
|
2512
|
+
for (let i = 0; i < 30; i++) {
|
|
2513
|
+
const foundTitle = await this.page.title();
|
|
2514
|
+
if (!foundTitle.includes(title)) {
|
|
2515
|
+
if (i === 29) {
|
|
2516
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2517
|
+
}
|
|
2518
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2522
|
+
return info;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
catch (e) {
|
|
2526
|
+
//await this.closeUnexpectedPopups();
|
|
2527
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2528
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2529
|
+
info.screenshotPath = screenshotPath;
|
|
2530
|
+
Object.assign(e, { info: info });
|
|
2531
|
+
error = e;
|
|
2532
|
+
// throw e;
|
|
2533
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2534
|
+
}
|
|
2535
|
+
finally {
|
|
2536
|
+
const endTime = Date.now();
|
|
2537
|
+
_reportToWorld(world, {
|
|
2538
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2539
|
+
text: "Verify page title",
|
|
2540
|
+
_text: "Verify the page title contains " + title,
|
|
2202
2541
|
screenshotId,
|
|
2203
2542
|
result: error
|
|
2204
2543
|
? {
|
|
@@ -2216,27 +2555,27 @@ class StableBrowser {
|
|
|
2216
2555
|
});
|
|
2217
2556
|
}
|
|
2218
2557
|
}
|
|
2219
|
-
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2558
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2220
2559
|
const frames = this.page.frames();
|
|
2221
2560
|
let results = [];
|
|
2222
|
-
let ignoreCase = false;
|
|
2561
|
+
// let ignoreCase = false;
|
|
2223
2562
|
for (let i = 0; i < frames.length; i++) {
|
|
2224
2563
|
if (dateAlternatives.date) {
|
|
2225
2564
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2226
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2565
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2227
2566
|
result.frame = frames[i];
|
|
2228
2567
|
results.push(result);
|
|
2229
2568
|
}
|
|
2230
2569
|
}
|
|
2231
2570
|
else if (numberAlternatives.number) {
|
|
2232
2571
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2233
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2572
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2234
2573
|
result.frame = frames[i];
|
|
2235
2574
|
results.push(result);
|
|
2236
2575
|
}
|
|
2237
2576
|
}
|
|
2238
2577
|
else {
|
|
2239
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2578
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2240
2579
|
result.frame = frames[i];
|
|
2241
2580
|
results.push(result);
|
|
2242
2581
|
}
|
|
@@ -2255,11 +2594,15 @@ class StableBrowser {
|
|
|
2255
2594
|
scroll: false,
|
|
2256
2595
|
highlight: false,
|
|
2257
2596
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2258
|
-
text: `Verify text exists in page`,
|
|
2597
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2598
|
+
_text: `Verify the text '${text}' exists in page`,
|
|
2259
2599
|
operation: "verifyTextExistInPage",
|
|
2260
2600
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2261
2601
|
};
|
|
2262
|
-
|
|
2602
|
+
if (testForRegex(text)) {
|
|
2603
|
+
text = text.replace(/\\"/g, '"');
|
|
2604
|
+
}
|
|
2605
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2263
2606
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2264
2607
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2265
2608
|
if (newValue !== text) {
|
|
@@ -2329,7 +2672,7 @@ class StableBrowser {
|
|
|
2329
2672
|
await _commandError(state, e, this);
|
|
2330
2673
|
}
|
|
2331
2674
|
finally {
|
|
2332
|
-
_commandFinally(state, this);
|
|
2675
|
+
await _commandFinally(state, this);
|
|
2333
2676
|
}
|
|
2334
2677
|
}
|
|
2335
2678
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2342,11 +2685,15 @@ class StableBrowser {
|
|
|
2342
2685
|
scroll: false,
|
|
2343
2686
|
highlight: false,
|
|
2344
2687
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2345
|
-
text: `Verify text does not exist in page`,
|
|
2688
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2689
|
+
_text: `Verify the text '${text}' does not exist in page`,
|
|
2346
2690
|
operation: "verifyTextNotExistInPage",
|
|
2347
2691
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2348
2692
|
};
|
|
2349
|
-
|
|
2693
|
+
if (testForRegex(text)) {
|
|
2694
|
+
text = text.replace(/\\"/g, '"');
|
|
2695
|
+
}
|
|
2696
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2350
2697
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2351
2698
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
2352
2699
|
if (newValue !== text) {
|
|
@@ -2382,7 +2729,7 @@ class StableBrowser {
|
|
|
2382
2729
|
await _commandError(state, e, this);
|
|
2383
2730
|
}
|
|
2384
2731
|
finally {
|
|
2385
|
-
_commandFinally(state, this);
|
|
2732
|
+
await _commandFinally(state, this);
|
|
2386
2733
|
}
|
|
2387
2734
|
}
|
|
2388
2735
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2397,10 +2744,11 @@ class StableBrowser {
|
|
|
2397
2744
|
highlight: false,
|
|
2398
2745
|
type: Types.VERIFY_TEXT_WITH_RELATION,
|
|
2399
2746
|
text: `Verify text with relation to another text`,
|
|
2747
|
+
_text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
|
|
2400
2748
|
operation: "verify_text_with_relation",
|
|
2401
2749
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2402
2750
|
};
|
|
2403
|
-
const timeout = this.
|
|
2751
|
+
const timeout = this._getFindElementTimeout(options);
|
|
2404
2752
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2405
2753
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
2406
2754
|
if (newValue !== textAnchor) {
|
|
@@ -2423,7 +2771,7 @@ class StableBrowser {
|
|
|
2423
2771
|
};
|
|
2424
2772
|
while (true) {
|
|
2425
2773
|
try {
|
|
2426
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2774
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2427
2775
|
}
|
|
2428
2776
|
catch (error) {
|
|
2429
2777
|
// ignore
|
|
@@ -2451,7 +2799,7 @@ class StableBrowser {
|
|
|
2451
2799
|
const count = await frame.locator(css).count();
|
|
2452
2800
|
for (let j = 0; j < count; j++) {
|
|
2453
2801
|
const continer = await frame.locator(css).nth(j);
|
|
2454
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2802
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2455
2803
|
if (result.elementCount > 0) {
|
|
2456
2804
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2457
2805
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2492,9 +2840,33 @@ class StableBrowser {
|
|
|
2492
2840
|
await _commandError(state, e, this);
|
|
2493
2841
|
}
|
|
2494
2842
|
finally {
|
|
2495
|
-
_commandFinally(state, this);
|
|
2843
|
+
await _commandFinally(state, this);
|
|
2496
2844
|
}
|
|
2497
2845
|
}
|
|
2846
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2847
|
+
const frames = this.page.frames();
|
|
2848
|
+
let results = [];
|
|
2849
|
+
let ignoreCase = false;
|
|
2850
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2851
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2852
|
+
result.frame = frames[i];
|
|
2853
|
+
const climbArray = [];
|
|
2854
|
+
for (let i = 0; i < climb; i++) {
|
|
2855
|
+
climbArray.push("..");
|
|
2856
|
+
}
|
|
2857
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2858
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2859
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2860
|
+
if (count > 0) {
|
|
2861
|
+
result.elementCount = count;
|
|
2862
|
+
result.locator = newLocator;
|
|
2863
|
+
results.push(result);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
// state.info.results = results;
|
|
2867
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2868
|
+
return resultWithElementsFound;
|
|
2869
|
+
}
|
|
2498
2870
|
async visualVerification(text, options = {}, world = null) {
|
|
2499
2871
|
const startTime = Date.now();
|
|
2500
2872
|
let error = null;
|
|
@@ -2556,6 +2928,7 @@ class StableBrowser {
|
|
|
2556
2928
|
_reportToWorld(world, {
|
|
2557
2929
|
type: Types.VERIFY_VISUAL,
|
|
2558
2930
|
text: "Visual verification",
|
|
2931
|
+
_text: "Visual verification of " + text,
|
|
2559
2932
|
screenshotId,
|
|
2560
2933
|
result: error
|
|
2561
2934
|
? {
|
|
@@ -2810,7 +3183,13 @@ class StableBrowser {
|
|
|
2810
3183
|
}
|
|
2811
3184
|
}
|
|
2812
3185
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2813
|
-
|
|
3186
|
+
try {
|
|
3187
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3188
|
+
}
|
|
3189
|
+
catch (error) {
|
|
3190
|
+
this.logger.debug(error);
|
|
3191
|
+
throw error;
|
|
3192
|
+
}
|
|
2814
3193
|
}
|
|
2815
3194
|
_getLoadTimeout(options) {
|
|
2816
3195
|
let timeout = 15000;
|
|
@@ -2822,6 +3201,15 @@ class StableBrowser {
|
|
|
2822
3201
|
}
|
|
2823
3202
|
return timeout;
|
|
2824
3203
|
}
|
|
3204
|
+
_getFindElementTimeout(options) {
|
|
3205
|
+
if (options && options.timeout) {
|
|
3206
|
+
return options.timeout;
|
|
3207
|
+
}
|
|
3208
|
+
if (this.configuration.find_element_timeout) {
|
|
3209
|
+
return this.configuration.find_element_timeout;
|
|
3210
|
+
}
|
|
3211
|
+
return 30000;
|
|
3212
|
+
}
|
|
2825
3213
|
async saveStoreState(path = null, world = null) {
|
|
2826
3214
|
const storageState = await this.page.context().storageState();
|
|
2827
3215
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
@@ -2838,6 +3226,9 @@ class StableBrowser {
|
|
|
2838
3226
|
this.registerEventListeners(this.context);
|
|
2839
3227
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2840
3228
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3229
|
+
if (this.onRestoreSaveState) {
|
|
3230
|
+
this.onRestoreSaveState(path);
|
|
3231
|
+
}
|
|
2841
3232
|
}
|
|
2842
3233
|
async waitForPageLoad(options = {}, world = null) {
|
|
2843
3234
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2906,6 +3297,7 @@ class StableBrowser {
|
|
|
2906
3297
|
highlight: false,
|
|
2907
3298
|
type: Types.CLOSE_PAGE,
|
|
2908
3299
|
text: `Close page`,
|
|
3300
|
+
_text: `Close the page`,
|
|
2909
3301
|
operation: "closePage",
|
|
2910
3302
|
log: "***** close page *****\n",
|
|
2911
3303
|
throwError: false,
|
|
@@ -2919,11 +3311,98 @@ class StableBrowser {
|
|
|
2919
3311
|
await _commandError(state, e, this);
|
|
2920
3312
|
}
|
|
2921
3313
|
finally {
|
|
2922
|
-
_commandFinally(state, this);
|
|
3314
|
+
await _commandFinally(state, this);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3318
|
+
let operation = null;
|
|
3319
|
+
if (!options || !options.operation) {
|
|
3320
|
+
throw new Error("operation is not defined");
|
|
3321
|
+
}
|
|
3322
|
+
operation = options.operation;
|
|
3323
|
+
// validate operation is one of the supported operations
|
|
3324
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3325
|
+
throw new Error("operation is not supported");
|
|
3326
|
+
}
|
|
3327
|
+
const state = {
|
|
3328
|
+
options,
|
|
3329
|
+
world,
|
|
3330
|
+
locate: false,
|
|
3331
|
+
scroll: false,
|
|
3332
|
+
highlight: false,
|
|
3333
|
+
type: Types.TABLE_OPERATION,
|
|
3334
|
+
text: `Table operation`,
|
|
3335
|
+
_text: `Table ${operation} operation`,
|
|
3336
|
+
operation: operation,
|
|
3337
|
+
log: "***** Table operation *****\n",
|
|
3338
|
+
};
|
|
3339
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3340
|
+
try {
|
|
3341
|
+
await _preCommand(state, this);
|
|
3342
|
+
const start = Date.now();
|
|
3343
|
+
let cellArea = null;
|
|
3344
|
+
while (true) {
|
|
3345
|
+
try {
|
|
3346
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3347
|
+
if (cellArea) {
|
|
3348
|
+
break;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
catch (e) {
|
|
3352
|
+
// ignore
|
|
3353
|
+
}
|
|
3354
|
+
if (Date.now() - start > timeout) {
|
|
3355
|
+
throw new Error(`Cell not found in table`);
|
|
3356
|
+
}
|
|
3357
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3358
|
+
}
|
|
3359
|
+
switch (operation) {
|
|
3360
|
+
case "click":
|
|
3361
|
+
if (!options.css) {
|
|
3362
|
+
// will click in the center of the cell
|
|
3363
|
+
let xOffset = 0;
|
|
3364
|
+
let yOffset = 0;
|
|
3365
|
+
if (options.xOffset) {
|
|
3366
|
+
xOffset = options.xOffset;
|
|
3367
|
+
}
|
|
3368
|
+
if (options.yOffset) {
|
|
3369
|
+
yOffset = options.yOffset;
|
|
3370
|
+
}
|
|
3371
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3372
|
+
}
|
|
3373
|
+
else {
|
|
3374
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3375
|
+
if (results.length === 0) {
|
|
3376
|
+
throw new Error(`Element not found in cell area`);
|
|
3377
|
+
}
|
|
3378
|
+
state.element = results[0];
|
|
3379
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3380
|
+
}
|
|
3381
|
+
break;
|
|
3382
|
+
case "hover+click":
|
|
3383
|
+
if (!options.css) {
|
|
3384
|
+
throw new Error("css is not defined");
|
|
3385
|
+
}
|
|
3386
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3387
|
+
if (results.length === 0) {
|
|
3388
|
+
throw new Error(`Element not found in cell area`);
|
|
3389
|
+
}
|
|
3390
|
+
state.element = results[0];
|
|
3391
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3392
|
+
break;
|
|
3393
|
+
default:
|
|
3394
|
+
throw new Error("operation is not supported");
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
catch (e) {
|
|
3398
|
+
await _commandError(state, e, this);
|
|
3399
|
+
}
|
|
3400
|
+
finally {
|
|
3401
|
+
await _commandFinally(state, this);
|
|
2923
3402
|
}
|
|
2924
3403
|
}
|
|
2925
3404
|
saveTestDataAsGlobal(options, world) {
|
|
2926
|
-
const dataFile =
|
|
3405
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2927
3406
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2928
3407
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2929
3408
|
}
|
|
@@ -2953,6 +3432,7 @@ class StableBrowser {
|
|
|
2953
3432
|
_reportToWorld(world, {
|
|
2954
3433
|
type: Types.SET_VIEWPORT,
|
|
2955
3434
|
text: "set viewport size to " + width + "x" + hight,
|
|
3435
|
+
_text: "Set the viewport size to " + width + "x" + hight,
|
|
2956
3436
|
screenshotId,
|
|
2957
3437
|
result: error
|
|
2958
3438
|
? {
|
|
@@ -3023,7 +3503,39 @@ class StableBrowser {
|
|
|
3023
3503
|
console.log("#-#");
|
|
3024
3504
|
}
|
|
3025
3505
|
}
|
|
3506
|
+
async beforeScenario(world, scenario) {
|
|
3507
|
+
this.beforeScenarioCalled = true;
|
|
3508
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3509
|
+
this.scenarioName = scenario.pickle.name;
|
|
3510
|
+
}
|
|
3511
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3512
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3513
|
+
}
|
|
3514
|
+
if (this.context) {
|
|
3515
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3516
|
+
}
|
|
3517
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3518
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3519
|
+
// check if @global_test_data tag is present
|
|
3520
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3521
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
// update test data based on feature/scenario
|
|
3525
|
+
let envName = null;
|
|
3526
|
+
if (this.context && this.context.environment) {
|
|
3527
|
+
envName = this.context.environment.name;
|
|
3528
|
+
}
|
|
3529
|
+
if (!process.env.TEMP_RUN) {
|
|
3530
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3531
|
+
}
|
|
3532
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3533
|
+
}
|
|
3534
|
+
async afterScenario(world, scenario) { }
|
|
3026
3535
|
async beforeStep(world, step) {
|
|
3536
|
+
if (!this.beforeScenarioCalled) {
|
|
3537
|
+
this.beforeScenario(world, step);
|
|
3538
|
+
}
|
|
3027
3539
|
if (this.stepIndex === undefined) {
|
|
3028
3540
|
this.stepIndex = 0;
|
|
3029
3541
|
}
|
|
@@ -3040,27 +3552,17 @@ class StableBrowser {
|
|
|
3040
3552
|
else {
|
|
3041
3553
|
this.stepName = "step " + this.stepIndex;
|
|
3042
3554
|
}
|
|
3043
|
-
if (this.context) {
|
|
3044
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3045
|
-
}
|
|
3046
3555
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3047
3556
|
if (this.context.browserObject.context) {
|
|
3048
3557
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3049
3558
|
}
|
|
3050
3559
|
}
|
|
3051
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3052
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3053
|
-
// check if @global_test_data tag is present
|
|
3054
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3055
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3058
3560
|
if (this.initSnapshotTaken === false) {
|
|
3059
3561
|
this.initSnapshotTaken = true;
|
|
3060
3562
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3061
3563
|
const snapshot = await this.getAriaSnapshot();
|
|
3062
3564
|
if (snapshot) {
|
|
3063
|
-
await world.attach(
|
|
3565
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3064
3566
|
}
|
|
3065
3567
|
}
|
|
3066
3568
|
}
|
|
@@ -3079,15 +3581,22 @@ class StableBrowser {
|
|
|
3079
3581
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3080
3582
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3081
3583
|
for (let i = 0; i < frames.length; i++) {
|
|
3082
|
-
content.push(`- frame: ${i}`);
|
|
3083
3584
|
const frame = frames[i];
|
|
3084
|
-
|
|
3085
|
-
|
|
3585
|
+
try {
|
|
3586
|
+
// Ensure frame is attached and has body
|
|
3587
|
+
const body = frame.locator("body");
|
|
3588
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3589
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3590
|
+
content.push(`- frame: ${i}`);
|
|
3591
|
+
content.push(snapshot);
|
|
3592
|
+
}
|
|
3593
|
+
catch (innerErr) { }
|
|
3086
3594
|
}
|
|
3087
3595
|
return content.join("\n");
|
|
3088
3596
|
}
|
|
3089
3597
|
catch (e) {
|
|
3090
|
-
console.
|
|
3598
|
+
console.log("Error in getAriaSnapshot");
|
|
3599
|
+
//console.debug(e);
|
|
3091
3600
|
}
|
|
3092
3601
|
return null;
|
|
3093
3602
|
}
|
|
@@ -3098,6 +3607,13 @@ class StableBrowser {
|
|
|
3098
3607
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3099
3608
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3100
3609
|
});
|
|
3610
|
+
if (world && world.attach) {
|
|
3611
|
+
await world.attach(JSON.stringify({
|
|
3612
|
+
type: "trace",
|
|
3613
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3614
|
+
}), "application/json+trace");
|
|
3615
|
+
}
|
|
3616
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3101
3617
|
}
|
|
3102
3618
|
}
|
|
3103
3619
|
if (this.context) {
|
|
@@ -3107,8 +3623,7 @@ class StableBrowser {
|
|
|
3107
3623
|
const snapshot = await this.getAriaSnapshot();
|
|
3108
3624
|
if (snapshot) {
|
|
3109
3625
|
const obj = {};
|
|
3110
|
-
|
|
3111
|
-
await world.attach(obj, "application/json+snapshot");
|
|
3626
|
+
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3112
3627
|
}
|
|
3113
3628
|
}
|
|
3114
3629
|
}
|