automation_model 1.0.626-dev → 1.0.626-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 +33 -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 +280 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +20 -1
- package/lib/stable_browser.js +587 -108
- 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 +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) => {
|
|
@@ -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
|
}
|
|
@@ -944,19 +1027,7 @@ class StableBrowser {
|
|
|
944
1027
|
};
|
|
945
1028
|
try {
|
|
946
1029
|
await _preCommand(state, this);
|
|
947
|
-
|
|
948
|
-
// state.selectors.locators[0].text = state.options.context;
|
|
949
|
-
// }
|
|
950
|
-
try {
|
|
951
|
-
await state.element.click();
|
|
952
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
953
|
-
}
|
|
954
|
-
catch (e) {
|
|
955
|
-
// await this.closeUnexpectedPopups();
|
|
956
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
957
|
-
await state.element.dispatchEvent("click");
|
|
958
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
959
|
-
}
|
|
1030
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
960
1031
|
await this.waitForPageLoad();
|
|
961
1032
|
return state.info;
|
|
962
1033
|
}
|
|
@@ -964,9 +1035,41 @@ class StableBrowser {
|
|
|
964
1035
|
await _commandError(state, e, this);
|
|
965
1036
|
}
|
|
966
1037
|
finally {
|
|
967
|
-
_commandFinally(state, this);
|
|
1038
|
+
await _commandFinally(state, this);
|
|
968
1039
|
}
|
|
969
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);
|
|
1070
|
+
}
|
|
1071
|
+
return found;
|
|
1072
|
+
}
|
|
970
1073
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
971
1074
|
const state = {
|
|
972
1075
|
selectors,
|
|
@@ -987,7 +1090,7 @@ class StableBrowser {
|
|
|
987
1090
|
try {
|
|
988
1091
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
989
1092
|
// console.log(`Highlighting while running from recorder`);
|
|
990
|
-
await this._highlightElements(element);
|
|
1093
|
+
await this._highlightElements(state.element);
|
|
991
1094
|
await state.element.setChecked(checked);
|
|
992
1095
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
993
1096
|
// await this._unHighlightElements(element);
|
|
@@ -1014,7 +1117,7 @@ class StableBrowser {
|
|
|
1014
1117
|
await _commandError(state, e, this);
|
|
1015
1118
|
}
|
|
1016
1119
|
finally {
|
|
1017
|
-
_commandFinally(state, this);
|
|
1120
|
+
await _commandFinally(state, this);
|
|
1018
1121
|
}
|
|
1019
1122
|
}
|
|
1020
1123
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1031,19 +1134,7 @@ class StableBrowser {
|
|
|
1031
1134
|
};
|
|
1032
1135
|
try {
|
|
1033
1136
|
await _preCommand(state, this);
|
|
1034
|
-
|
|
1035
|
-
await state.element.hover();
|
|
1036
|
-
// await _screenshot(state, this);
|
|
1037
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1038
|
-
}
|
|
1039
|
-
catch (e) {
|
|
1040
|
-
//await this.closeUnexpectedPopups();
|
|
1041
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1042
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1043
|
-
await state.element.hover({ timeout: 10000 });
|
|
1044
|
-
// await _screenshot(state, this);
|
|
1045
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1046
|
-
}
|
|
1137
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1047
1138
|
await _screenshot(state, this);
|
|
1048
1139
|
await this.waitForPageLoad();
|
|
1049
1140
|
return state.info;
|
|
@@ -1052,7 +1143,7 @@ class StableBrowser {
|
|
|
1052
1143
|
await _commandError(state, e, this);
|
|
1053
1144
|
}
|
|
1054
1145
|
finally {
|
|
1055
|
-
_commandFinally(state, this);
|
|
1146
|
+
await _commandFinally(state, this);
|
|
1056
1147
|
}
|
|
1057
1148
|
}
|
|
1058
1149
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1088,7 +1179,7 @@ class StableBrowser {
|
|
|
1088
1179
|
await _commandError(state, e, this);
|
|
1089
1180
|
}
|
|
1090
1181
|
finally {
|
|
1091
|
-
_commandFinally(state, this);
|
|
1182
|
+
await _commandFinally(state, this);
|
|
1092
1183
|
}
|
|
1093
1184
|
}
|
|
1094
1185
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1134,7 +1225,7 @@ class StableBrowser {
|
|
|
1134
1225
|
await _commandError(state, e, this);
|
|
1135
1226
|
}
|
|
1136
1227
|
finally {
|
|
1137
|
-
_commandFinally(state, this);
|
|
1228
|
+
await _commandFinally(state, this);
|
|
1138
1229
|
}
|
|
1139
1230
|
}
|
|
1140
1231
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1170,7 +1261,7 @@ class StableBrowser {
|
|
|
1170
1261
|
await _commandError(state, e, this);
|
|
1171
1262
|
}
|
|
1172
1263
|
finally {
|
|
1173
|
-
_commandFinally(state, this);
|
|
1264
|
+
await _commandFinally(state, this);
|
|
1174
1265
|
}
|
|
1175
1266
|
}
|
|
1176
1267
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1190,7 +1281,7 @@ class StableBrowser {
|
|
|
1190
1281
|
try {
|
|
1191
1282
|
await _preCommand(state, this);
|
|
1192
1283
|
try {
|
|
1193
|
-
await state.element
|
|
1284
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1194
1285
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1195
1286
|
if (format) {
|
|
1196
1287
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1239,7 +1330,7 @@ class StableBrowser {
|
|
|
1239
1330
|
await _commandError(state, e, this);
|
|
1240
1331
|
}
|
|
1241
1332
|
finally {
|
|
1242
|
-
_commandFinally(state, this);
|
|
1333
|
+
await _commandFinally(state, this);
|
|
1243
1334
|
}
|
|
1244
1335
|
}
|
|
1245
1336
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1258,6 +1349,9 @@ class StableBrowser {
|
|
|
1258
1349
|
operation: "clickType",
|
|
1259
1350
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1260
1351
|
};
|
|
1352
|
+
if (!options) {
|
|
1353
|
+
options = {};
|
|
1354
|
+
}
|
|
1261
1355
|
if (newValue !== _value) {
|
|
1262
1356
|
//this.logger.info(_value + "=" + newValue);
|
|
1263
1357
|
_value = newValue;
|
|
@@ -1265,7 +1359,7 @@ class StableBrowser {
|
|
|
1265
1359
|
try {
|
|
1266
1360
|
await _preCommand(state, this);
|
|
1267
1361
|
state.info.value = _value;
|
|
1268
|
-
if (
|
|
1362
|
+
if (!options.press) {
|
|
1269
1363
|
try {
|
|
1270
1364
|
let currentValue = await state.element.inputValue();
|
|
1271
1365
|
if (currentValue) {
|
|
@@ -1276,13 +1370,9 @@ class StableBrowser {
|
|
|
1276
1370
|
this.logger.info("unable to clear input value");
|
|
1277
1371
|
}
|
|
1278
1372
|
}
|
|
1279
|
-
if (options
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
}
|
|
1283
|
-
catch (e) {
|
|
1284
|
-
await state.element.dispatchEvent("click");
|
|
1285
|
-
}
|
|
1373
|
+
if (options.press) {
|
|
1374
|
+
options.timeout = 5000;
|
|
1375
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1286
1376
|
}
|
|
1287
1377
|
else {
|
|
1288
1378
|
try {
|
|
@@ -1340,7 +1430,7 @@ class StableBrowser {
|
|
|
1340
1430
|
await _commandError(state, e, this);
|
|
1341
1431
|
}
|
|
1342
1432
|
finally {
|
|
1343
|
-
_commandFinally(state, this);
|
|
1433
|
+
await _commandFinally(state, this);
|
|
1344
1434
|
}
|
|
1345
1435
|
}
|
|
1346
1436
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1370,7 +1460,42 @@ class StableBrowser {
|
|
|
1370
1460
|
await _commandError(state, e, this);
|
|
1371
1461
|
}
|
|
1372
1462
|
finally {
|
|
1373
|
-
_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);
|
|
1374
1499
|
}
|
|
1375
1500
|
}
|
|
1376
1501
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1486,7 +1611,7 @@ class StableBrowser {
|
|
|
1486
1611
|
await _commandError(state, e, this);
|
|
1487
1612
|
}
|
|
1488
1613
|
finally {
|
|
1489
|
-
_commandFinally(state, this);
|
|
1614
|
+
await _commandFinally(state, this);
|
|
1490
1615
|
}
|
|
1491
1616
|
}
|
|
1492
1617
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1521,7 +1646,7 @@ class StableBrowser {
|
|
|
1521
1646
|
while (Date.now() - startTime < timeout) {
|
|
1522
1647
|
try {
|
|
1523
1648
|
await _preCommand(state, this);
|
|
1524
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1649
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1525
1650
|
if (foundObj && foundObj.element) {
|
|
1526
1651
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1527
1652
|
}
|
|
@@ -1563,7 +1688,79 @@ class StableBrowser {
|
|
|
1563
1688
|
throw e;
|
|
1564
1689
|
}
|
|
1565
1690
|
finally {
|
|
1566
|
-
_commandFinally(state, this);
|
|
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({ nestFrmLoc: 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);
|
|
1746
|
+
}
|
|
1747
|
+
// highlight and screenshot
|
|
1748
|
+
return state.info;
|
|
1749
|
+
}
|
|
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
|
|
1755
|
+
}
|
|
1756
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1757
|
+
}
|
|
1758
|
+
catch (e) {
|
|
1759
|
+
await _commandError(state, e, this);
|
|
1760
|
+
throw e;
|
|
1761
|
+
}
|
|
1762
|
+
finally {
|
|
1763
|
+
await _commandFinally(state, this);
|
|
1567
1764
|
}
|
|
1568
1765
|
}
|
|
1569
1766
|
async waitForUserInput(message, world = null) {
|
|
@@ -1601,6 +1798,15 @@ class StableBrowser {
|
|
|
1601
1798
|
// save the data to the file
|
|
1602
1799
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1603
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
|
+
}
|
|
1604
1810
|
_getDataFilePath(fileName) {
|
|
1605
1811
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1606
1812
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1853,7 +2059,7 @@ class StableBrowser {
|
|
|
1853
2059
|
await _commandError(state, e, this);
|
|
1854
2060
|
}
|
|
1855
2061
|
finally {
|
|
1856
|
-
_commandFinally(state, this);
|
|
2062
|
+
await _commandFinally(state, this);
|
|
1857
2063
|
}
|
|
1858
2064
|
}
|
|
1859
2065
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1884,10 +2090,31 @@ class StableBrowser {
|
|
|
1884
2090
|
case "value":
|
|
1885
2091
|
state.value = await state.element.inputValue();
|
|
1886
2092
|
break;
|
|
2093
|
+
case "text":
|
|
2094
|
+
state.value = await state.element.textContent();
|
|
2095
|
+
break;
|
|
1887
2096
|
default:
|
|
1888
2097
|
state.value = await state.element.getAttribute(attribute);
|
|
1889
2098
|
break;
|
|
1890
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
|
+
}
|
|
1891
2118
|
state.info.value = state.value;
|
|
1892
2119
|
this.setTestData({ [variable]: state.value }, world);
|
|
1893
2120
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1898,7 +2125,7 @@ class StableBrowser {
|
|
|
1898
2125
|
await _commandError(state, e, this);
|
|
1899
2126
|
}
|
|
1900
2127
|
finally {
|
|
1901
|
-
_commandFinally(state, this);
|
|
2128
|
+
await _commandFinally(state, this);
|
|
1902
2129
|
}
|
|
1903
2130
|
}
|
|
1904
2131
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1923,12 +2150,15 @@ class StableBrowser {
|
|
|
1923
2150
|
let expectedValue;
|
|
1924
2151
|
try {
|
|
1925
2152
|
await _preCommand(state, this);
|
|
1926
|
-
expectedValue = state.value;
|
|
2153
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1927
2154
|
state.info.expectedValue = expectedValue;
|
|
1928
2155
|
switch (attribute) {
|
|
1929
2156
|
case "innerText":
|
|
1930
2157
|
val = String(await state.element.innerText());
|
|
1931
2158
|
break;
|
|
2159
|
+
case "text":
|
|
2160
|
+
val = String(await state.element.textContent());
|
|
2161
|
+
break;
|
|
1932
2162
|
case "value":
|
|
1933
2163
|
val = String(await state.element.inputValue());
|
|
1934
2164
|
break;
|
|
@@ -1950,17 +2180,42 @@ class StableBrowser {
|
|
|
1950
2180
|
let regex;
|
|
1951
2181
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1952
2182
|
const patternBody = expectedValue.slice(1, -1);
|
|
1953
|
-
|
|
2183
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2184
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2185
|
+
state.info.regex = true;
|
|
1954
2186
|
}
|
|
1955
2187
|
else {
|
|
1956
2188
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1957
2189
|
regex = new RegExp(escapedPattern, "g");
|
|
1958
2190
|
}
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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
|
+
}
|
|
1964
2219
|
}
|
|
1965
2220
|
return state.info;
|
|
1966
2221
|
}
|
|
@@ -1968,7 +2223,7 @@ class StableBrowser {
|
|
|
1968
2223
|
await _commandError(state, e, this);
|
|
1969
2224
|
}
|
|
1970
2225
|
finally {
|
|
1971
|
-
_commandFinally(state, this);
|
|
2226
|
+
await _commandFinally(state, this);
|
|
1972
2227
|
}
|
|
1973
2228
|
}
|
|
1974
2229
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2213,7 +2468,7 @@ class StableBrowser {
|
|
|
2213
2468
|
Object.assign(e, { info: info });
|
|
2214
2469
|
error = e;
|
|
2215
2470
|
// throw e;
|
|
2216
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2471
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
|
|
2217
2472
|
}
|
|
2218
2473
|
finally {
|
|
2219
2474
|
const endTime = Date.now();
|
|
@@ -2238,27 +2493,89 @@ class StableBrowser {
|
|
|
2238
2493
|
});
|
|
2239
2494
|
}
|
|
2240
2495
|
}
|
|
2241
|
-
async
|
|
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,
|
|
2541
|
+
screenshotId,
|
|
2542
|
+
result: error
|
|
2543
|
+
? {
|
|
2544
|
+
status: "FAILED",
|
|
2545
|
+
startTime,
|
|
2546
|
+
endTime,
|
|
2547
|
+
message: error?.message,
|
|
2548
|
+
}
|
|
2549
|
+
: {
|
|
2550
|
+
status: "PASSED",
|
|
2551
|
+
startTime,
|
|
2552
|
+
endTime,
|
|
2553
|
+
},
|
|
2554
|
+
info: info,
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2242
2559
|
const frames = this.page.frames();
|
|
2243
2560
|
let results = [];
|
|
2244
|
-
let ignoreCase = false;
|
|
2561
|
+
// let ignoreCase = false;
|
|
2245
2562
|
for (let i = 0; i < frames.length; i++) {
|
|
2246
2563
|
if (dateAlternatives.date) {
|
|
2247
2564
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2248
|
-
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, {});
|
|
2249
2566
|
result.frame = frames[i];
|
|
2250
2567
|
results.push(result);
|
|
2251
2568
|
}
|
|
2252
2569
|
}
|
|
2253
2570
|
else if (numberAlternatives.number) {
|
|
2254
2571
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2255
|
-
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, {});
|
|
2256
2573
|
result.frame = frames[i];
|
|
2257
2574
|
results.push(result);
|
|
2258
2575
|
}
|
|
2259
2576
|
}
|
|
2260
2577
|
else {
|
|
2261
|
-
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, {});
|
|
2262
2579
|
result.frame = frames[i];
|
|
2263
2580
|
results.push(result);
|
|
2264
2581
|
}
|
|
@@ -2277,11 +2594,14 @@ class StableBrowser {
|
|
|
2277
2594
|
scroll: false,
|
|
2278
2595
|
highlight: false,
|
|
2279
2596
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2280
|
-
text: `Verify text exists in page`,
|
|
2597
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2281
2598
|
_text: `Verify the text '${text}' exists in page`,
|
|
2282
2599
|
operation: "verifyTextExistInPage",
|
|
2283
2600
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2284
2601
|
};
|
|
2602
|
+
if (testForRegex(text)) {
|
|
2603
|
+
text = text.replace(/\\"/g, '"');
|
|
2604
|
+
}
|
|
2285
2605
|
const timeout = this._getFindElementTimeout(options);
|
|
2286
2606
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2287
2607
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2352,7 +2672,7 @@ class StableBrowser {
|
|
|
2352
2672
|
await _commandError(state, e, this);
|
|
2353
2673
|
}
|
|
2354
2674
|
finally {
|
|
2355
|
-
_commandFinally(state, this);
|
|
2675
|
+
await _commandFinally(state, this);
|
|
2356
2676
|
}
|
|
2357
2677
|
}
|
|
2358
2678
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2365,11 +2685,14 @@ class StableBrowser {
|
|
|
2365
2685
|
scroll: false,
|
|
2366
2686
|
highlight: false,
|
|
2367
2687
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2368
|
-
text: `Verify text does not exist in page`,
|
|
2688
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2369
2689
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2370
2690
|
operation: "verifyTextNotExistInPage",
|
|
2371
2691
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2372
2692
|
};
|
|
2693
|
+
if (testForRegex(text)) {
|
|
2694
|
+
text = text.replace(/\\"/g, '"');
|
|
2695
|
+
}
|
|
2373
2696
|
const timeout = this._getFindElementTimeout(options);
|
|
2374
2697
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2375
2698
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2406,7 +2729,7 @@ class StableBrowser {
|
|
|
2406
2729
|
await _commandError(state, e, this);
|
|
2407
2730
|
}
|
|
2408
2731
|
finally {
|
|
2409
|
-
_commandFinally(state, this);
|
|
2732
|
+
await _commandFinally(state, this);
|
|
2410
2733
|
}
|
|
2411
2734
|
}
|
|
2412
2735
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2448,7 +2771,7 @@ class StableBrowser {
|
|
|
2448
2771
|
};
|
|
2449
2772
|
while (true) {
|
|
2450
2773
|
try {
|
|
2451
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2774
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2452
2775
|
}
|
|
2453
2776
|
catch (error) {
|
|
2454
2777
|
// ignore
|
|
@@ -2476,7 +2799,7 @@ class StableBrowser {
|
|
|
2476
2799
|
const count = await frame.locator(css).count();
|
|
2477
2800
|
for (let j = 0; j < count; j++) {
|
|
2478
2801
|
const continer = await frame.locator(css).nth(j);
|
|
2479
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2802
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2480
2803
|
if (result.elementCount > 0) {
|
|
2481
2804
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2482
2805
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2517,9 +2840,33 @@ class StableBrowser {
|
|
|
2517
2840
|
await _commandError(state, e, this);
|
|
2518
2841
|
}
|
|
2519
2842
|
finally {
|
|
2520
|
-
_commandFinally(state, this);
|
|
2843
|
+
await _commandFinally(state, this);
|
|
2521
2844
|
}
|
|
2522
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
|
+
}
|
|
2523
2870
|
async visualVerification(text, options = {}, world = null) {
|
|
2524
2871
|
const startTime = Date.now();
|
|
2525
2872
|
let error = null;
|
|
@@ -2836,7 +3183,13 @@ class StableBrowser {
|
|
|
2836
3183
|
}
|
|
2837
3184
|
}
|
|
2838
3185
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2839
|
-
|
|
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
|
+
}
|
|
2840
3193
|
}
|
|
2841
3194
|
_getLoadTimeout(options) {
|
|
2842
3195
|
let timeout = 15000;
|
|
@@ -2873,6 +3226,9 @@ class StableBrowser {
|
|
|
2873
3226
|
this.registerEventListeners(this.context);
|
|
2874
3227
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2875
3228
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3229
|
+
if (this.onRestoreSaveState) {
|
|
3230
|
+
this.onRestoreSaveState(path);
|
|
3231
|
+
}
|
|
2876
3232
|
}
|
|
2877
3233
|
async waitForPageLoad(options = {}, world = null) {
|
|
2878
3234
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2955,11 +3311,98 @@ class StableBrowser {
|
|
|
2955
3311
|
await _commandError(state, e, this);
|
|
2956
3312
|
}
|
|
2957
3313
|
finally {
|
|
2958
|
-
_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);
|
|
2959
3402
|
}
|
|
2960
3403
|
}
|
|
2961
3404
|
saveTestDataAsGlobal(options, world) {
|
|
2962
|
-
const dataFile =
|
|
3405
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2963
3406
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2964
3407
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2965
3408
|
}
|
|
@@ -3060,7 +3503,39 @@ class StableBrowser {
|
|
|
3060
3503
|
console.log("#-#");
|
|
3061
3504
|
}
|
|
3062
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) { }
|
|
3063
3535
|
async beforeStep(world, step) {
|
|
3536
|
+
if (!this.beforeScenarioCalled) {
|
|
3537
|
+
this.beforeScenario(world, step);
|
|
3538
|
+
}
|
|
3064
3539
|
if (this.stepIndex === undefined) {
|
|
3065
3540
|
this.stepIndex = 0;
|
|
3066
3541
|
}
|
|
@@ -3077,21 +3552,11 @@ class StableBrowser {
|
|
|
3077
3552
|
else {
|
|
3078
3553
|
this.stepName = "step " + this.stepIndex;
|
|
3079
3554
|
}
|
|
3080
|
-
if (this.context) {
|
|
3081
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3082
|
-
}
|
|
3083
3555
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3084
3556
|
if (this.context.browserObject.context) {
|
|
3085
3557
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3086
3558
|
}
|
|
3087
3559
|
}
|
|
3088
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3089
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3090
|
-
// check if @global_test_data tag is present
|
|
3091
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3092
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
3560
|
if (this.initSnapshotTaken === false) {
|
|
3096
3561
|
this.initSnapshotTaken = true;
|
|
3097
3562
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
@@ -3116,15 +3581,22 @@ class StableBrowser {
|
|
|
3116
3581
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3117
3582
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3118
3583
|
for (let i = 0; i < frames.length; i++) {
|
|
3119
|
-
content.push(`- frame: ${i}`);
|
|
3120
3584
|
const frame = frames[i];
|
|
3121
|
-
|
|
3122
|
-
|
|
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) { }
|
|
3123
3594
|
}
|
|
3124
3595
|
return content.join("\n");
|
|
3125
3596
|
}
|
|
3126
3597
|
catch (e) {
|
|
3127
|
-
console.
|
|
3598
|
+
console.log("Error in getAriaSnapshot");
|
|
3599
|
+
//console.debug(e);
|
|
3128
3600
|
}
|
|
3129
3601
|
return null;
|
|
3130
3602
|
}
|
|
@@ -3135,6 +3607,13 @@ class StableBrowser {
|
|
|
3135
3607
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3136
3608
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3137
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`);
|
|
3138
3617
|
}
|
|
3139
3618
|
}
|
|
3140
3619
|
if (this.context) {
|