automation_model 1.0.384-dev.0 → 1.0.384-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/lib/api.js +3 -2
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +28 -1
- package/lib/auto_page.js.map +1 -1
- package/lib/stable_browser.d.ts +13 -1
- package/lib/stable_browser.js +386 -150
- package/lib/stable_browser.js.map +1 -1
- package/lib/test_context.d.ts +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.js +2 -2
- package/lib/utils.js.map +1 -1
- package/package.json +10 -8
package/lib/stable_browser.js
CHANGED
|
@@ -12,6 +12,8 @@ import drawRectangle from "./drawRect.js";
|
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
13
|
import objectPath from "object-path";
|
|
14
14
|
import { decrypt } from "./utils.js";
|
|
15
|
+
import csv from "csv-parser";
|
|
16
|
+
import { Readable } from "node:stream";
|
|
15
17
|
const Types = {
|
|
16
18
|
CLICK: "click_element",
|
|
17
19
|
NAVIGATE: "navigate",
|
|
@@ -27,6 +29,7 @@ const Types = {
|
|
|
27
29
|
SELECT: "select_combobox",
|
|
28
30
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
29
31
|
TYPE_PRESS: "type_press",
|
|
32
|
+
PRESS: "press_key",
|
|
30
33
|
HOVER: "hover_element",
|
|
31
34
|
CHECK: "check_element",
|
|
32
35
|
UNCHECK: "uncheck_element",
|
|
@@ -35,6 +38,8 @@ const Types = {
|
|
|
35
38
|
SET_DATE_TIME: "set_date_time",
|
|
36
39
|
SET_VIEWPORT: "set_viewport",
|
|
37
40
|
VERIFY_VISUAL: "verify_visual",
|
|
41
|
+
LOAD_DATA: "load_data",
|
|
42
|
+
SET_INPUT: "set_input",
|
|
38
43
|
};
|
|
39
44
|
class StableBrowser {
|
|
40
45
|
constructor(browser, page, logger = null, context = null) {
|
|
@@ -80,9 +85,20 @@ class StableBrowser {
|
|
|
80
85
|
this.page = page;
|
|
81
86
|
context.page = page;
|
|
82
87
|
context.pages.push(page);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
page.on("close", async () => {
|
|
89
|
+
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
90
|
+
this.context.pages.pop();
|
|
91
|
+
this.page = this.context.pages[this.context.pages.length - 1];
|
|
92
|
+
this.context.page = this.page;
|
|
93
|
+
try {
|
|
94
|
+
let title = await this.page.title();
|
|
95
|
+
console.log("Switched to page " + title);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error("Error on page close", error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
86
102
|
try {
|
|
87
103
|
await this.waitForPageLoad();
|
|
88
104
|
console.log("Switch page: " + (await page.title()));
|
|
@@ -91,7 +107,7 @@ class StableBrowser {
|
|
|
91
107
|
this.logger.error("error on page load " + e);
|
|
92
108
|
}
|
|
93
109
|
context.pageLoading.status = false;
|
|
94
|
-
});
|
|
110
|
+
}.bind(this));
|
|
95
111
|
}
|
|
96
112
|
getWebLogFile(logFolder) {
|
|
97
113
|
if (!fs.existsSync(logFolder)) {
|
|
@@ -177,21 +193,78 @@ class StableBrowser {
|
|
|
177
193
|
}
|
|
178
194
|
return text;
|
|
179
195
|
}
|
|
196
|
+
_fixLocatorUsingParams(locator, _params) {
|
|
197
|
+
// check if not null
|
|
198
|
+
if (!locator) {
|
|
199
|
+
return locator;
|
|
200
|
+
}
|
|
201
|
+
// clone the locator
|
|
202
|
+
locator = JSON.parse(JSON.stringify(locator));
|
|
203
|
+
this.scanAndManipulate(locator, _params);
|
|
204
|
+
return locator;
|
|
205
|
+
}
|
|
206
|
+
_isObject(value) {
|
|
207
|
+
return value && typeof value === "object" && value.constructor === Object;
|
|
208
|
+
}
|
|
209
|
+
scanAndManipulate(currentObj, _params) {
|
|
210
|
+
for (const key in currentObj) {
|
|
211
|
+
if (typeof currentObj[key] === "string") {
|
|
212
|
+
// Perform string manipulation
|
|
213
|
+
currentObj[key] = this._fixUsingParams(currentObj[key], _params);
|
|
214
|
+
}
|
|
215
|
+
else if (this._isObject(currentObj[key])) {
|
|
216
|
+
// Recursively scan nested objects
|
|
217
|
+
this.scanAndManipulate(currentObj[key], _params);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
180
221
|
_getLocator(locator, scope, _params) {
|
|
222
|
+
locator = this._fixLocatorUsingParams(locator, _params);
|
|
223
|
+
let locatorReturn;
|
|
181
224
|
if (locator.role) {
|
|
182
225
|
if (locator.role[1].nameReg) {
|
|
183
226
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
184
227
|
delete locator.role[1].nameReg;
|
|
185
228
|
}
|
|
186
|
-
if (locator.role[1].name) {
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
229
|
+
// if (locator.role[1].name) {
|
|
230
|
+
// locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
|
|
231
|
+
// }
|
|
232
|
+
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
190
233
|
}
|
|
191
234
|
if (locator.css) {
|
|
192
|
-
|
|
235
|
+
locatorReturn = scope.locator(locator.css);
|
|
236
|
+
}
|
|
237
|
+
// handle role/name locators
|
|
238
|
+
// locator.selector will be something like: textbox[name="Username"i]
|
|
239
|
+
if (locator.engine === "internal:role") {
|
|
240
|
+
// extract the role, name and the i/s flags using regex
|
|
241
|
+
const match = locator.selector.match(/(.*)\[(.*)="(.*)"(.*)\]/);
|
|
242
|
+
if (match) {
|
|
243
|
+
const role = match[1];
|
|
244
|
+
const name = match[3];
|
|
245
|
+
const flags = match[4];
|
|
246
|
+
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (locator === null || locator === void 0 ? void 0 : locator.engine) {
|
|
250
|
+
if (locator.engine === "css") {
|
|
251
|
+
locatorReturn = scope.locator(locator.selector);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
let selector = locator.selector;
|
|
255
|
+
if (locator.engine === "internal:attr") {
|
|
256
|
+
if (!selector.startsWith("[")) {
|
|
257
|
+
selector = `[${selector}]`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
261
|
+
}
|
|
193
262
|
}
|
|
194
|
-
|
|
263
|
+
if (!locatorReturn) {
|
|
264
|
+
console.error(locator);
|
|
265
|
+
throw new Error("Locator undefined");
|
|
266
|
+
}
|
|
267
|
+
return locatorReturn;
|
|
195
268
|
}
|
|
196
269
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
197
270
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -410,7 +483,7 @@ class StableBrowser {
|
|
|
410
483
|
}
|
|
411
484
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
412
485
|
for (let i = 0; i < 3; i++) {
|
|
413
|
-
info.log += "attempt " + i + ":
|
|
486
|
+
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
414
487
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
415
488
|
let selector = selectors.locators[j];
|
|
416
489
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
@@ -596,6 +669,9 @@ class StableBrowser {
|
|
|
596
669
|
async click(selectors, _params, options = {}, world = null) {
|
|
597
670
|
this._validateSelectors(selectors);
|
|
598
671
|
const startTime = Date.now();
|
|
672
|
+
if (options && options.context) {
|
|
673
|
+
selectors.locators[0].text = options.context;
|
|
674
|
+
}
|
|
599
675
|
const info = {};
|
|
600
676
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
601
677
|
info.operation = "click";
|
|
@@ -609,14 +685,14 @@ class StableBrowser {
|
|
|
609
685
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
610
686
|
try {
|
|
611
687
|
await this._highlightElements(element);
|
|
612
|
-
await element.click(
|
|
688
|
+
await element.click();
|
|
613
689
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
614
690
|
}
|
|
615
691
|
catch (e) {
|
|
616
692
|
// await this.closeUnexpectedPopups();
|
|
617
693
|
info.log += "click failed, will try again" + "\n";
|
|
618
694
|
element = await this._locate(selectors, info, _params);
|
|
619
|
-
await element.click
|
|
695
|
+
await element.dispatchEvent("click");
|
|
620
696
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
621
697
|
}
|
|
622
698
|
await this.waitForPageLoad();
|
|
@@ -669,7 +745,7 @@ class StableBrowser {
|
|
|
669
745
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
670
746
|
try {
|
|
671
747
|
await this._highlightElements(element);
|
|
672
|
-
await element.setChecked(checked
|
|
748
|
+
await element.setChecked(checked);
|
|
673
749
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
674
750
|
}
|
|
675
751
|
catch (e) {
|
|
@@ -733,7 +809,7 @@ class StableBrowser {
|
|
|
733
809
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
734
810
|
try {
|
|
735
811
|
await this._highlightElements(element);
|
|
736
|
-
await element.hover(
|
|
812
|
+
await element.hover();
|
|
737
813
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
738
814
|
}
|
|
739
815
|
catch (e) {
|
|
@@ -795,7 +871,7 @@ class StableBrowser {
|
|
|
795
871
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
796
872
|
try {
|
|
797
873
|
await this._highlightElements(element);
|
|
798
|
-
await element.selectOption(values
|
|
874
|
+
await element.selectOption(values);
|
|
799
875
|
}
|
|
800
876
|
catch (e) {
|
|
801
877
|
//await this.closeUnexpectedPopups();
|
|
@@ -904,71 +980,45 @@ class StableBrowser {
|
|
|
904
980
|
});
|
|
905
981
|
}
|
|
906
982
|
}
|
|
907
|
-
async
|
|
983
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
984
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
908
985
|
this._validateSelectors(selectors);
|
|
909
986
|
const startTime = Date.now();
|
|
910
|
-
let error = null;
|
|
911
|
-
let screenshotId = null;
|
|
912
|
-
let screenshotPath = null;
|
|
913
987
|
const info = {};
|
|
914
|
-
info.log = "";
|
|
915
|
-
info.operation =
|
|
988
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
989
|
+
info.operation = "setInputValue";
|
|
916
990
|
info.selectors = selectors;
|
|
991
|
+
value = this._fixUsingParams(value, _params);
|
|
917
992
|
info.value = value;
|
|
993
|
+
let error = null;
|
|
994
|
+
let screenshotId = null;
|
|
995
|
+
let screenshotPath = null;
|
|
918
996
|
try {
|
|
919
997
|
value = await this._replaceWithLocalData(value, this);
|
|
920
998
|
let element = await this._locate(selectors, info, _params);
|
|
921
|
-
//insert red border around the element
|
|
922
999
|
await this.scrollIfNeeded(element, info);
|
|
923
1000
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
924
1001
|
await this._highlightElements(element);
|
|
925
1002
|
try {
|
|
926
|
-
await element.
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
value = dayjs(value).format(format);
|
|
930
|
-
await element.fill(value);
|
|
931
|
-
}
|
|
932
|
-
else {
|
|
933
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
934
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
935
|
-
el.value = ""; // clear input
|
|
936
|
-
el.value = dateTimeValue;
|
|
937
|
-
}, dateTimeValue);
|
|
938
|
-
}
|
|
939
|
-
if (enter) {
|
|
940
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
941
|
-
await this.page.keyboard.press("Enter");
|
|
942
|
-
await this.waitForPageLoad();
|
|
943
|
-
}
|
|
1003
|
+
await element.evaluateHandle((el, value) => {
|
|
1004
|
+
el.value = value;
|
|
1005
|
+
}, value);
|
|
944
1006
|
}
|
|
945
1007
|
catch (error) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1008
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1009
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
949
1010
|
info.screenshotPath = screenshotPath;
|
|
950
1011
|
Object.assign(error, { info: info });
|
|
951
|
-
await element.
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
value = dayjs(value).format(format);
|
|
955
|
-
await element.fill(value);
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
959
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
960
|
-
el.value = ""; // clear input
|
|
961
|
-
el.value = dateTimeValue;
|
|
962
|
-
}, dateTimeValue);
|
|
963
|
-
}
|
|
964
|
-
if (enter) {
|
|
965
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
966
|
-
await this.page.keyboard.press("Enter");
|
|
967
|
-
await this.waitForPageLoad();
|
|
968
|
-
}
|
|
1012
|
+
await element.evaluateHandle((el, value) => {
|
|
1013
|
+
el.value = value;
|
|
1014
|
+
});
|
|
969
1015
|
}
|
|
970
1016
|
}
|
|
971
|
-
catch (
|
|
1017
|
+
catch (e) {
|
|
1018
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1019
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1020
|
+
info.screenshotPath = screenshotPath;
|
|
1021
|
+
Object.assign(e, { info: info });
|
|
972
1022
|
error = e;
|
|
973
1023
|
throw e;
|
|
974
1024
|
}
|
|
@@ -976,10 +1026,10 @@ class StableBrowser {
|
|
|
976
1026
|
const endTime = Date.now();
|
|
977
1027
|
this._reportToWorld(world, {
|
|
978
1028
|
element_name: selectors.element_name,
|
|
979
|
-
type: Types.
|
|
980
|
-
|
|
1029
|
+
type: Types.SET_INPUT,
|
|
1030
|
+
text: `Set input value`,
|
|
981
1031
|
value: value,
|
|
982
|
-
|
|
1032
|
+
screenshotId,
|
|
983
1033
|
result: error
|
|
984
1034
|
? {
|
|
985
1035
|
status: "FAILED",
|
|
@@ -996,7 +1046,7 @@ class StableBrowser {
|
|
|
996
1046
|
});
|
|
997
1047
|
}
|
|
998
1048
|
}
|
|
999
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1049
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1000
1050
|
this._validateSelectors(selectors);
|
|
1001
1051
|
const startTime = Date.now();
|
|
1002
1052
|
let error = null;
|
|
@@ -1008,6 +1058,7 @@ class StableBrowser {
|
|
|
1008
1058
|
info.selectors = selectors;
|
|
1009
1059
|
info.value = value;
|
|
1010
1060
|
try {
|
|
1061
|
+
value = await this._replaceWithLocalData(value, this);
|
|
1011
1062
|
let element = await this._locate(selectors, info, _params);
|
|
1012
1063
|
//insert red border around the element
|
|
1013
1064
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1016,28 +1067,51 @@ class StableBrowser {
|
|
|
1016
1067
|
try {
|
|
1017
1068
|
await element.click();
|
|
1018
1069
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1070
|
+
if (format) {
|
|
1071
|
+
value = dayjs(value).format(format);
|
|
1072
|
+
await element.fill(value);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1076
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1077
|
+
el.value = ""; // clear input
|
|
1078
|
+
el.value = dateTimeValue;
|
|
1079
|
+
}, dateTimeValue);
|
|
1080
|
+
}
|
|
1081
|
+
if (enter) {
|
|
1082
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1083
|
+
await this.page.keyboard.press("Enter");
|
|
1084
|
+
await this.waitForPageLoad();
|
|
1085
|
+
}
|
|
1024
1086
|
}
|
|
1025
|
-
catch (
|
|
1087
|
+
catch (err) {
|
|
1026
1088
|
//await this.closeUnexpectedPopups();
|
|
1027
1089
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1028
|
-
this.logger.info("Trying again")
|
|
1090
|
+
this.logger.info("Trying again");
|
|
1091
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1029
1092
|
info.screenshotPath = screenshotPath;
|
|
1030
|
-
Object.assign(
|
|
1093
|
+
Object.assign(err, { info: info });
|
|
1031
1094
|
await element.click();
|
|
1032
1095
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1096
|
+
if (format) {
|
|
1097
|
+
value = dayjs(value).format(format);
|
|
1098
|
+
await element.fill(value);
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1102
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1103
|
+
el.value = ""; // clear input
|
|
1104
|
+
el.value = dateTimeValue;
|
|
1105
|
+
}, dateTimeValue);
|
|
1106
|
+
}
|
|
1107
|
+
if (enter) {
|
|
1108
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1109
|
+
await this.page.keyboard.press("Enter");
|
|
1110
|
+
await this.waitForPageLoad();
|
|
1111
|
+
}
|
|
1038
1112
|
}
|
|
1039
1113
|
}
|
|
1040
|
-
catch (
|
|
1114
|
+
catch (e) {
|
|
1041
1115
|
error = e;
|
|
1042
1116
|
throw e;
|
|
1043
1117
|
}
|
|
@@ -1087,20 +1161,32 @@ class StableBrowser {
|
|
|
1087
1161
|
await this.scrollIfNeeded(element, info);
|
|
1088
1162
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1089
1163
|
await this._highlightElements(element);
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1164
|
+
if (options === null || options === undefined || !options.press) {
|
|
1165
|
+
try {
|
|
1166
|
+
let currentValue = await element.inputValue();
|
|
1167
|
+
if (currentValue) {
|
|
1168
|
+
await element.fill("");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
catch (e) {
|
|
1172
|
+
this.logger.info("unable to clear input value");
|
|
1094
1173
|
}
|
|
1095
1174
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1175
|
+
if (options === null || options === undefined || options.press) {
|
|
1176
|
+
try {
|
|
1177
|
+
await element.click({ timeout: 5000 });
|
|
1178
|
+
}
|
|
1179
|
+
catch (e) {
|
|
1180
|
+
await element.dispatchEvent("click");
|
|
1181
|
+
}
|
|
1101
1182
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1183
|
+
else {
|
|
1184
|
+
try {
|
|
1185
|
+
await element.focus();
|
|
1186
|
+
}
|
|
1187
|
+
catch (e) {
|
|
1188
|
+
await element.dispatchEvent("focus");
|
|
1189
|
+
}
|
|
1104
1190
|
}
|
|
1105
1191
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1106
1192
|
const valueSegment = _value.split("&&");
|
|
@@ -1188,7 +1274,7 @@ class StableBrowser {
|
|
|
1188
1274
|
let element = await this._locate(selectors, info, _params);
|
|
1189
1275
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1190
1276
|
await this._highlightElements(element);
|
|
1191
|
-
await element.fill(value
|
|
1277
|
+
await element.fill(value);
|
|
1192
1278
|
await element.dispatchEvent("change");
|
|
1193
1279
|
if (enter) {
|
|
1194
1280
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -1296,7 +1382,8 @@ class StableBrowser {
|
|
|
1296
1382
|
let screenshotId = null;
|
|
1297
1383
|
let screenshotPath = null;
|
|
1298
1384
|
const info = {};
|
|
1299
|
-
info.log =
|
|
1385
|
+
info.log =
|
|
1386
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1300
1387
|
info.operation = "containsPattern";
|
|
1301
1388
|
info.selectors = selectors;
|
|
1302
1389
|
info.value = text;
|
|
@@ -1406,7 +1493,7 @@ class StableBrowser {
|
|
|
1406
1493
|
return info;
|
|
1407
1494
|
}
|
|
1408
1495
|
catch (e) {
|
|
1409
|
-
|
|
1496
|
+
await this.closeUnexpectedPopups();
|
|
1410
1497
|
this.logger.error("verify element contains text failed " + info.log);
|
|
1411
1498
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1412
1499
|
info.screenshotPath = screenshotPath;
|
|
@@ -1466,15 +1553,62 @@ class StableBrowser {
|
|
|
1466
1553
|
// save the data to the file
|
|
1467
1554
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1468
1555
|
}
|
|
1556
|
+
_getDataFilePath(fileName) {
|
|
1557
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1558
|
+
if (fs.existsSync(dataFile)) {
|
|
1559
|
+
return dataFile;
|
|
1560
|
+
}
|
|
1561
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1562
|
+
if (fs.existsSync(dataFile)) {
|
|
1563
|
+
return dataFile;
|
|
1564
|
+
}
|
|
1565
|
+
throw new Error("data file not found " + fileName);
|
|
1566
|
+
}
|
|
1567
|
+
_parseCSVSync(filePath) {
|
|
1568
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1569
|
+
const results = [];
|
|
1570
|
+
return new Promise((resolve, reject) => {
|
|
1571
|
+
const readableStream = new Readable();
|
|
1572
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1573
|
+
readableStream.push(data);
|
|
1574
|
+
readableStream.push(null);
|
|
1575
|
+
readableStream
|
|
1576
|
+
.pipe(csv())
|
|
1577
|
+
.on("data", (data) => results.push(data))
|
|
1578
|
+
.on("end", () => resolve(results))
|
|
1579
|
+
.on("error", (error) => reject(error));
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1469
1582
|
loadTestData(type, dataSelector, world = null) {
|
|
1470
1583
|
switch (type) {
|
|
1471
1584
|
case "users":
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1474
|
-
|
|
1585
|
+
// get the users.json file path
|
|
1586
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1587
|
+
// read the file and return the data
|
|
1588
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1589
|
+
for (let i = 0; i < users.length; i++) {
|
|
1590
|
+
if (users[i].username === dataSelector) {
|
|
1591
|
+
const userObj = {
|
|
1592
|
+
username: users[i].username,
|
|
1593
|
+
password: "secret:" + users[i].password,
|
|
1594
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1595
|
+
};
|
|
1596
|
+
this.setTestData(userObj, world);
|
|
1597
|
+
return userObj;
|
|
1598
|
+
}
|
|
1475
1599
|
}
|
|
1600
|
+
throw new Error("user not found " + dataSelector);
|
|
1601
|
+
default:
|
|
1602
|
+
throw new Error("unknown type " + type);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1606
|
+
switch (type) {
|
|
1607
|
+
case "users": {
|
|
1608
|
+
// get the users.json file path
|
|
1609
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1476
1610
|
// read the file and return the data
|
|
1477
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1611
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1478
1612
|
for (let i = 0; i < users.length; i++) {
|
|
1479
1613
|
if (users[i].username === dataSelector) {
|
|
1480
1614
|
const userObj = {
|
|
@@ -1487,6 +1621,29 @@ class StableBrowser {
|
|
|
1487
1621
|
}
|
|
1488
1622
|
}
|
|
1489
1623
|
throw new Error("user not found " + dataSelector);
|
|
1624
|
+
}
|
|
1625
|
+
case "csv": {
|
|
1626
|
+
// the dataSelector should start with the file name followed by the row number: data.csv:1, if no row number is provided, it will default to 1
|
|
1627
|
+
const parts = dataSelector.split(":");
|
|
1628
|
+
let rowNumber = 0;
|
|
1629
|
+
if (parts.length > 1) {
|
|
1630
|
+
rowNumber = parseInt(parts[1]);
|
|
1631
|
+
}
|
|
1632
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1633
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1634
|
+
// result stracture:
|
|
1635
|
+
// [
|
|
1636
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1637
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1638
|
+
// ]
|
|
1639
|
+
// verify the row number is within the range
|
|
1640
|
+
if (rowNumber >= results.length) {
|
|
1641
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1642
|
+
}
|
|
1643
|
+
const data = results[rowNumber];
|
|
1644
|
+
this.setTestData(data, world);
|
|
1645
|
+
return data;
|
|
1646
|
+
}
|
|
1490
1647
|
default:
|
|
1491
1648
|
throw new Error("unknown type " + type);
|
|
1492
1649
|
}
|
|
@@ -1591,13 +1748,13 @@ class StableBrowser {
|
|
|
1591
1748
|
])));
|
|
1592
1749
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1593
1750
|
format: "png",
|
|
1594
|
-
clip: {
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
},
|
|
1751
|
+
// clip: {
|
|
1752
|
+
// x: 0,
|
|
1753
|
+
// y: 0,
|
|
1754
|
+
// width: viewportWidth,
|
|
1755
|
+
// height: viewportHeight,
|
|
1756
|
+
// scale: 1,
|
|
1757
|
+
// },
|
|
1601
1758
|
});
|
|
1602
1759
|
if (!screenshotPath) {
|
|
1603
1760
|
return data;
|
|
@@ -1703,7 +1860,8 @@ class StableBrowser {
|
|
|
1703
1860
|
if (world) {
|
|
1704
1861
|
world[variable] = info.value;
|
|
1705
1862
|
}
|
|
1706
|
-
this.
|
|
1863
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1864
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1707
1865
|
return info;
|
|
1708
1866
|
}
|
|
1709
1867
|
catch (e) {
|
|
@@ -1740,6 +1898,91 @@ class StableBrowser {
|
|
|
1740
1898
|
});
|
|
1741
1899
|
}
|
|
1742
1900
|
}
|
|
1901
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1902
|
+
if (!emailAddress) {
|
|
1903
|
+
throw new Error("email address is null");
|
|
1904
|
+
}
|
|
1905
|
+
// check if address contain @
|
|
1906
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1907
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1911
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const startTime = Date.now();
|
|
1915
|
+
let timeout = 60000;
|
|
1916
|
+
if (options && options.timeout) {
|
|
1917
|
+
timeout = options.timeout;
|
|
1918
|
+
}
|
|
1919
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1920
|
+
const request = {
|
|
1921
|
+
method: "POST",
|
|
1922
|
+
url: serviceUrl,
|
|
1923
|
+
headers: {
|
|
1924
|
+
"Content-Type": "application/json",
|
|
1925
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1926
|
+
},
|
|
1927
|
+
data: JSON.stringify({
|
|
1928
|
+
email: emailAddress,
|
|
1929
|
+
}),
|
|
1930
|
+
};
|
|
1931
|
+
let errorCount = 0;
|
|
1932
|
+
while (true) {
|
|
1933
|
+
try {
|
|
1934
|
+
let result = await this.context.api.request(request);
|
|
1935
|
+
// the response body expected to be the following:
|
|
1936
|
+
// {
|
|
1937
|
+
// "status": true,
|
|
1938
|
+
// "content": {
|
|
1939
|
+
// "url": "",
|
|
1940
|
+
// "code": "112112",
|
|
1941
|
+
// "name": "generate_link_or_code"
|
|
1942
|
+
// }
|
|
1943
|
+
//}
|
|
1944
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1945
|
+
let codeOrUrlFound = false;
|
|
1946
|
+
let emailCode = null;
|
|
1947
|
+
let emailUrl = null;
|
|
1948
|
+
// check if a code is returned
|
|
1949
|
+
if (result.data.content && result.data.content.code) {
|
|
1950
|
+
let code = result.data.content.code;
|
|
1951
|
+
this.setTestData({ emailCode: code }, world);
|
|
1952
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1953
|
+
emailCode = code;
|
|
1954
|
+
codeOrUrlFound = true;
|
|
1955
|
+
}
|
|
1956
|
+
// check if a url is returned
|
|
1957
|
+
if (result.data.content && result.data.content.url) {
|
|
1958
|
+
let url = result.data.content.url;
|
|
1959
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1960
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1961
|
+
emailUrl = url;
|
|
1962
|
+
codeOrUrlFound = true;
|
|
1963
|
+
}
|
|
1964
|
+
if (codeOrUrlFound) {
|
|
1965
|
+
return { emailUrl, emailCode };
|
|
1966
|
+
}
|
|
1967
|
+
else {
|
|
1968
|
+
this.logger.info("an email received but no code or url found");
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
catch (e) {
|
|
1973
|
+
errorCount++;
|
|
1974
|
+
if (errorCount > 3) {
|
|
1975
|
+
throw e;
|
|
1976
|
+
}
|
|
1977
|
+
// ignore
|
|
1978
|
+
}
|
|
1979
|
+
// check if the timeout is reached
|
|
1980
|
+
if (Date.now() - startTime > timeout) {
|
|
1981
|
+
throw new Error("timeout reached");
|
|
1982
|
+
}
|
|
1983
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1743
1986
|
async _highlightElements(scope, css) {
|
|
1744
1987
|
try {
|
|
1745
1988
|
if (!scope) {
|
|
@@ -1921,8 +2164,10 @@ class StableBrowser {
|
|
|
1921
2164
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1922
2165
|
await this._highlightElements(frame, dataAttribute);
|
|
1923
2166
|
const element = await frame.$(dataAttribute);
|
|
1924
|
-
|
|
1925
|
-
|
|
2167
|
+
if (element) {
|
|
2168
|
+
await this.scrollIfNeeded(element, info);
|
|
2169
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2170
|
+
}
|
|
1926
2171
|
}
|
|
1927
2172
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1928
2173
|
return info;
|
|
@@ -1960,6 +2205,16 @@ class StableBrowser {
|
|
|
1960
2205
|
});
|
|
1961
2206
|
}
|
|
1962
2207
|
}
|
|
2208
|
+
_getServerUrl() {
|
|
2209
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2210
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2211
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2212
|
+
}
|
|
2213
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2214
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2215
|
+
}
|
|
2216
|
+
return serviceUrl;
|
|
2217
|
+
}
|
|
1963
2218
|
async visualVerification(text, options = {}, world = null) {
|
|
1964
2219
|
const startTime = Date.now();
|
|
1965
2220
|
let error = null;
|
|
@@ -1974,13 +2229,7 @@ class StableBrowser {
|
|
|
1974
2229
|
throw new Error("TOKEN is not set");
|
|
1975
2230
|
}
|
|
1976
2231
|
try {
|
|
1977
|
-
let serviceUrl =
|
|
1978
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1979
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1980
|
-
}
|
|
1981
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1982
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1983
|
-
}
|
|
2232
|
+
let serviceUrl = this._getServerUrl();
|
|
1984
2233
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1985
2234
|
info.screenshotPath = screenshotPath;
|
|
1986
2235
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2129,7 +2378,16 @@ class StableBrowser {
|
|
|
2129
2378
|
let screenshotId = null;
|
|
2130
2379
|
let screenshotPath = null;
|
|
2131
2380
|
const info = {};
|
|
2132
|
-
info.log =
|
|
2381
|
+
info.log =
|
|
2382
|
+
"***** analyze table " +
|
|
2383
|
+
selectors.element_name +
|
|
2384
|
+
" query " +
|
|
2385
|
+
query +
|
|
2386
|
+
" operator " +
|
|
2387
|
+
operator +
|
|
2388
|
+
" value " +
|
|
2389
|
+
value +
|
|
2390
|
+
" *****\n";
|
|
2133
2391
|
info.operation = "analyzeTable";
|
|
2134
2392
|
info.selectors = selectors;
|
|
2135
2393
|
info.query = query;
|
|
@@ -2322,13 +2580,13 @@ class StableBrowser {
|
|
|
2322
2580
|
}
|
|
2323
2581
|
catch (e) {
|
|
2324
2582
|
if (e.label === "networkidle") {
|
|
2325
|
-
console.log("
|
|
2583
|
+
console.log("waited for the network to be idle timeout");
|
|
2326
2584
|
}
|
|
2327
2585
|
else if (e.label === "load") {
|
|
2328
|
-
console.log("
|
|
2586
|
+
console.log("waited for the load timeout");
|
|
2329
2587
|
}
|
|
2330
2588
|
else if (e.label === "domcontentloaded") {
|
|
2331
|
-
console.log("
|
|
2589
|
+
console.log("waited for the domcontent loaded timeout");
|
|
2332
2590
|
}
|
|
2333
2591
|
console.log(".");
|
|
2334
2592
|
}
|
|
@@ -2363,13 +2621,6 @@ class StableBrowser {
|
|
|
2363
2621
|
const info = {};
|
|
2364
2622
|
try {
|
|
2365
2623
|
await this.page.close();
|
|
2366
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2367
|
-
this.context.pages.pop();
|
|
2368
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2369
|
-
this.context.page = this.page;
|
|
2370
|
-
let title = await this.page.title();
|
|
2371
|
-
console.log("Switched to page " + title);
|
|
2372
|
-
}
|
|
2373
2624
|
}
|
|
2374
2625
|
catch (e) {
|
|
2375
2626
|
console.log(".");
|
|
@@ -2478,33 +2729,18 @@ class StableBrowser {
|
|
|
2478
2729
|
}
|
|
2479
2730
|
async scrollIfNeeded(element, info) {
|
|
2480
2731
|
try {
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
if (rect &&
|
|
2484
|
-
rect.top >= 0 &&
|
|
2485
|
-
rect.left >= 0 &&
|
|
2486
|
-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
2487
|
-
rect.right <= (window.innerWidth || document.documentElement.clientWidth)) {
|
|
2488
|
-
return false;
|
|
2489
|
-
}
|
|
2490
|
-
else {
|
|
2491
|
-
node.scrollIntoView({
|
|
2492
|
-
behavior: "smooth",
|
|
2493
|
-
block: "center",
|
|
2494
|
-
inline: "center",
|
|
2495
|
-
});
|
|
2496
|
-
return true;
|
|
2497
|
-
}
|
|
2732
|
+
await element.scrollIntoViewIfNeeded({
|
|
2733
|
+
timeout: 2000,
|
|
2498
2734
|
});
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
}
|
|
2735
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2736
|
+
if (info) {
|
|
2737
|
+
info.box = await element.boundingBox({
|
|
2738
|
+
timeout: 1000,
|
|
2739
|
+
});
|
|
2504
2740
|
}
|
|
2505
2741
|
}
|
|
2506
2742
|
catch (e) {
|
|
2507
|
-
console.log("
|
|
2743
|
+
console.log("#-#");
|
|
2508
2744
|
}
|
|
2509
2745
|
}
|
|
2510
2746
|
_reportToWorld(world, properties) {
|