automation_model 1.0.385-dev → 1.0.385-main
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/auto_page.d.ts +1 -1
- package/lib/auto_page.js +24 -1
- package/lib/auto_page.js.map +1 -1
- package/lib/stable_browser.d.ts +14 -2
- package/lib/stable_browser.js +401 -154
- package/lib/stable_browser.js.map +1 -1
- package/package.json +7 -3
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,15 @@ 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 > 0) {
|
|
90
|
+
this.context.pages.pop();
|
|
91
|
+
this.page = this.context.pages[this.context.pages.length - 1];
|
|
92
|
+
this.context.page = this.page;
|
|
93
|
+
let title = await this.page.title();
|
|
94
|
+
console.log("Switched to page " + title);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
86
97
|
try {
|
|
87
98
|
await this.waitForPageLoad();
|
|
88
99
|
console.log("Switch page: " + (await page.title()));
|
|
@@ -91,7 +102,7 @@ class StableBrowser {
|
|
|
91
102
|
this.logger.error("error on page load " + e);
|
|
92
103
|
}
|
|
93
104
|
context.pageLoading.status = false;
|
|
94
|
-
});
|
|
105
|
+
}.bind(this));
|
|
95
106
|
}
|
|
96
107
|
getWebLogFile(logFolder) {
|
|
97
108
|
if (!fs.existsSync(logFolder)) {
|
|
@@ -177,21 +188,78 @@ class StableBrowser {
|
|
|
177
188
|
}
|
|
178
189
|
return text;
|
|
179
190
|
}
|
|
191
|
+
_fixLocatorUsingParams(locator, _params) {
|
|
192
|
+
// check if not null
|
|
193
|
+
if (!locator) {
|
|
194
|
+
return locator;
|
|
195
|
+
}
|
|
196
|
+
// clone the locator
|
|
197
|
+
locator = JSON.parse(JSON.stringify(locator));
|
|
198
|
+
this.scanAndManipulate(locator, _params);
|
|
199
|
+
return locator;
|
|
200
|
+
}
|
|
201
|
+
_isObject(value) {
|
|
202
|
+
return value && typeof value === "object" && value.constructor === Object;
|
|
203
|
+
}
|
|
204
|
+
scanAndManipulate(currentObj, _params) {
|
|
205
|
+
for (const key in currentObj) {
|
|
206
|
+
if (typeof currentObj[key] === "string") {
|
|
207
|
+
// Perform string manipulation
|
|
208
|
+
currentObj[key] = this._fixUsingParams(currentObj[key], _params);
|
|
209
|
+
}
|
|
210
|
+
else if (this._isObject(currentObj[key])) {
|
|
211
|
+
// Recursively scan nested objects
|
|
212
|
+
this.scanAndManipulate(currentObj[key], _params);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
180
216
|
_getLocator(locator, scope, _params) {
|
|
217
|
+
locator = this._fixLocatorUsingParams(locator, _params);
|
|
218
|
+
let locatorReturn;
|
|
181
219
|
if (locator.role) {
|
|
182
220
|
if (locator.role[1].nameReg) {
|
|
183
221
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
184
222
|
delete locator.role[1].nameReg;
|
|
185
223
|
}
|
|
186
|
-
if (locator.role[1].name) {
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
224
|
+
// if (locator.role[1].name) {
|
|
225
|
+
// locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
|
|
226
|
+
// }
|
|
227
|
+
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
190
228
|
}
|
|
191
229
|
if (locator.css) {
|
|
192
|
-
|
|
230
|
+
locatorReturn = scope.locator(locator.css);
|
|
193
231
|
}
|
|
194
|
-
|
|
232
|
+
// handle role/name locators
|
|
233
|
+
// locator.selector will be something like: textbox[name="Username"i]
|
|
234
|
+
if (locator.engine === "internal:role") {
|
|
235
|
+
// extract the role, name and the i/s flags using regex
|
|
236
|
+
const match = locator.selector.match(/(.*)\[(.*)="(.*)"(.*)\]/);
|
|
237
|
+
if (match) {
|
|
238
|
+
const role = match[1];
|
|
239
|
+
const name = match[3];
|
|
240
|
+
const flags = match[4];
|
|
241
|
+
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (locator === null || locator === void 0 ? void 0 : locator.engine) {
|
|
245
|
+
if (locator.engine === "css") {
|
|
246
|
+
locatorReturn = scope.locator(locator.selector);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
let selector = locator.selector;
|
|
250
|
+
if (locator.engine === "internal:attr") {
|
|
251
|
+
if (!selector.startsWith("[")) {
|
|
252
|
+
selector = `[${selector}]`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!locatorReturn) {
|
|
259
|
+
console.error(locator);
|
|
260
|
+
throw new Error("Locator undefined");
|
|
261
|
+
}
|
|
262
|
+
return locatorReturn;
|
|
195
263
|
}
|
|
196
264
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
197
265
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -596,6 +664,9 @@ class StableBrowser {
|
|
|
596
664
|
async click(selectors, _params, options = {}, world = null) {
|
|
597
665
|
this._validateSelectors(selectors);
|
|
598
666
|
const startTime = Date.now();
|
|
667
|
+
if (options && options.context) {
|
|
668
|
+
selectors.locators[0].text = options.context;
|
|
669
|
+
}
|
|
599
670
|
const info = {};
|
|
600
671
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
601
672
|
info.operation = "click";
|
|
@@ -904,71 +975,45 @@ class StableBrowser {
|
|
|
904
975
|
});
|
|
905
976
|
}
|
|
906
977
|
}
|
|
907
|
-
async
|
|
978
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
979
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
908
980
|
this._validateSelectors(selectors);
|
|
909
981
|
const startTime = Date.now();
|
|
910
|
-
let error = null;
|
|
911
|
-
let screenshotId = null;
|
|
912
|
-
let screenshotPath = null;
|
|
913
982
|
const info = {};
|
|
914
|
-
info.log = "";
|
|
915
|
-
info.operation =
|
|
983
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
984
|
+
info.operation = "setInputValue";
|
|
916
985
|
info.selectors = selectors;
|
|
986
|
+
value = this._fixUsingParams(value, _params);
|
|
917
987
|
info.value = value;
|
|
988
|
+
let error = null;
|
|
989
|
+
let screenshotId = null;
|
|
990
|
+
let screenshotPath = null;
|
|
918
991
|
try {
|
|
919
992
|
value = await this._replaceWithLocalData(value, this);
|
|
920
993
|
let element = await this._locate(selectors, info, _params);
|
|
921
|
-
//insert red border around the element
|
|
922
994
|
await this.scrollIfNeeded(element, info);
|
|
923
995
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
924
996
|
await this._highlightElements(element);
|
|
925
997
|
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
|
-
}
|
|
998
|
+
await element.evaluateHandle((el, value) => {
|
|
999
|
+
el.value = value;
|
|
1000
|
+
}, value);
|
|
944
1001
|
}
|
|
945
1002
|
catch (error) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1003
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1004
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
949
1005
|
info.screenshotPath = screenshotPath;
|
|
950
1006
|
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
|
-
}
|
|
1007
|
+
await element.evaluateHandle((el, value) => {
|
|
1008
|
+
el.value = value;
|
|
1009
|
+
});
|
|
969
1010
|
}
|
|
970
1011
|
}
|
|
971
|
-
catch (
|
|
1012
|
+
catch (e) {
|
|
1013
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1014
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1015
|
+
info.screenshotPath = screenshotPath;
|
|
1016
|
+
Object.assign(e, { info: info });
|
|
972
1017
|
error = e;
|
|
973
1018
|
throw e;
|
|
974
1019
|
}
|
|
@@ -976,10 +1021,10 @@ class StableBrowser {
|
|
|
976
1021
|
const endTime = Date.now();
|
|
977
1022
|
this._reportToWorld(world, {
|
|
978
1023
|
element_name: selectors.element_name,
|
|
979
|
-
type: Types.
|
|
980
|
-
|
|
1024
|
+
type: Types.SET_INPUT,
|
|
1025
|
+
text: `Set input value`,
|
|
981
1026
|
value: value,
|
|
982
|
-
|
|
1027
|
+
screenshotId,
|
|
983
1028
|
result: error
|
|
984
1029
|
? {
|
|
985
1030
|
status: "FAILED",
|
|
@@ -996,7 +1041,7 @@ class StableBrowser {
|
|
|
996
1041
|
});
|
|
997
1042
|
}
|
|
998
1043
|
}
|
|
999
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1044
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1000
1045
|
this._validateSelectors(selectors);
|
|
1001
1046
|
const startTime = Date.now();
|
|
1002
1047
|
let error = null;
|
|
@@ -1008,6 +1053,7 @@ class StableBrowser {
|
|
|
1008
1053
|
info.selectors = selectors;
|
|
1009
1054
|
info.value = value;
|
|
1010
1055
|
try {
|
|
1056
|
+
value = await this._replaceWithLocalData(value, this);
|
|
1011
1057
|
let element = await this._locate(selectors, info, _params);
|
|
1012
1058
|
//insert red border around the element
|
|
1013
1059
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1016,28 +1062,51 @@ class StableBrowser {
|
|
|
1016
1062
|
try {
|
|
1017
1063
|
await element.click();
|
|
1018
1064
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1065
|
+
if (format) {
|
|
1066
|
+
value = dayjs(value).format(format);
|
|
1067
|
+
await element.fill(value);
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1071
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1072
|
+
el.value = ""; // clear input
|
|
1073
|
+
el.value = dateTimeValue;
|
|
1074
|
+
}, dateTimeValue);
|
|
1075
|
+
}
|
|
1076
|
+
if (enter) {
|
|
1077
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1078
|
+
await this.page.keyboard.press("Enter");
|
|
1079
|
+
await this.waitForPageLoad();
|
|
1080
|
+
}
|
|
1024
1081
|
}
|
|
1025
|
-
catch (
|
|
1082
|
+
catch (err) {
|
|
1026
1083
|
//await this.closeUnexpectedPopups();
|
|
1027
1084
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1028
|
-
this.logger.info("Trying again")
|
|
1085
|
+
this.logger.info("Trying again");
|
|
1086
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1029
1087
|
info.screenshotPath = screenshotPath;
|
|
1030
|
-
Object.assign(
|
|
1088
|
+
Object.assign(err, { info: info });
|
|
1031
1089
|
await element.click();
|
|
1032
1090
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1091
|
+
if (format) {
|
|
1092
|
+
value = dayjs(value).format(format);
|
|
1093
|
+
await element.fill(value);
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1097
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1098
|
+
el.value = ""; // clear input
|
|
1099
|
+
el.value = dateTimeValue;
|
|
1100
|
+
}, dateTimeValue);
|
|
1101
|
+
}
|
|
1102
|
+
if (enter) {
|
|
1103
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1104
|
+
await this.page.keyboard.press("Enter");
|
|
1105
|
+
await this.waitForPageLoad();
|
|
1106
|
+
}
|
|
1038
1107
|
}
|
|
1039
1108
|
}
|
|
1040
|
-
catch (
|
|
1109
|
+
catch (e) {
|
|
1041
1110
|
error = e;
|
|
1042
1111
|
throw e;
|
|
1043
1112
|
}
|
|
@@ -1087,20 +1156,32 @@ class StableBrowser {
|
|
|
1087
1156
|
await this.scrollIfNeeded(element, info);
|
|
1088
1157
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1089
1158
|
await this._highlightElements(element);
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1159
|
+
if (options === null || options === undefined || !options.press) {
|
|
1160
|
+
try {
|
|
1161
|
+
let currentValue = await element.inputValue();
|
|
1162
|
+
if (currentValue) {
|
|
1163
|
+
await element.fill("");
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
catch (e) {
|
|
1167
|
+
this.logger.info("unable to clear input value");
|
|
1094
1168
|
}
|
|
1095
1169
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1170
|
+
if (options === null || options === undefined || options.press) {
|
|
1171
|
+
try {
|
|
1172
|
+
await element.click({ timeout: 5000 });
|
|
1173
|
+
}
|
|
1174
|
+
catch (e) {
|
|
1175
|
+
await element.dispatchEvent("click");
|
|
1176
|
+
}
|
|
1101
1177
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1178
|
+
else {
|
|
1179
|
+
try {
|
|
1180
|
+
await element.focus();
|
|
1181
|
+
}
|
|
1182
|
+
catch (e) {
|
|
1183
|
+
await element.dispatchEvent("focus");
|
|
1184
|
+
}
|
|
1104
1185
|
}
|
|
1105
1186
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1106
1187
|
const valueSegment = _value.split("&&");
|
|
@@ -1296,7 +1377,8 @@ class StableBrowser {
|
|
|
1296
1377
|
let screenshotId = null;
|
|
1297
1378
|
let screenshotPath = null;
|
|
1298
1379
|
const info = {};
|
|
1299
|
-
info.log =
|
|
1380
|
+
info.log =
|
|
1381
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1300
1382
|
info.operation = "containsPattern";
|
|
1301
1383
|
info.selectors = selectors;
|
|
1302
1384
|
info.value = text;
|
|
@@ -1352,7 +1434,6 @@ class StableBrowser {
|
|
|
1352
1434
|
}
|
|
1353
1435
|
}
|
|
1354
1436
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1355
|
-
var _a, _b, _c;
|
|
1356
1437
|
this._validateSelectors(selectors);
|
|
1357
1438
|
if (!text) {
|
|
1358
1439
|
throw new Error("text is null");
|
|
@@ -1373,46 +1454,48 @@ class StableBrowser {
|
|
|
1373
1454
|
info.value = text;
|
|
1374
1455
|
let foundObj = null;
|
|
1375
1456
|
try {
|
|
1376
|
-
foundObj = await this._getText(selectors, climb, _params, options, info, world);
|
|
1377
|
-
if (foundObj && foundObj.element) {
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1381
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1382
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1383
|
-
if (dateAlternatives.date) {
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1457
|
+
// foundObj = await this._getText(selectors, climb, _params, options, info, world);
|
|
1458
|
+
// if (foundObj && foundObj.element) {
|
|
1459
|
+
// await this.scrollIfNeeded(foundObj.element, info);
|
|
1460
|
+
// }
|
|
1461
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1462
|
+
// const dateAlternatives = findDateAlternatives(text);
|
|
1463
|
+
// const numberAlternatives = findNumberAlternatives(text);
|
|
1464
|
+
// if (dateAlternatives.date) {
|
|
1465
|
+
// for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1466
|
+
// if (
|
|
1467
|
+
// foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1468
|
+
// foundObj?.value?.includes(dateAlternatives.dates[i])
|
|
1469
|
+
// ) {
|
|
1470
|
+
// return info;
|
|
1471
|
+
// }
|
|
1472
|
+
// }
|
|
1473
|
+
// throw new Error("element doesn't contain text " + text);
|
|
1474
|
+
// } else if (numberAlternatives.number) {
|
|
1475
|
+
// for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1476
|
+
// if (
|
|
1477
|
+
// foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1478
|
+
// foundObj?.value?.includes(numberAlternatives.numbers[i])
|
|
1479
|
+
// ) {
|
|
1480
|
+
// return info;
|
|
1481
|
+
// }
|
|
1482
|
+
// }
|
|
1483
|
+
// throw new Error("element doesn't contain text " + text);
|
|
1484
|
+
// } else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
|
|
1485
|
+
// info.foundText = foundObj?.text;
|
|
1486
|
+
// info.value = foundObj?.value;
|
|
1487
|
+
// throw new Error("element doesn't contain text " + text);
|
|
1488
|
+
// }
|
|
1489
|
+
// return info;
|
|
1407
1490
|
}
|
|
1408
1491
|
catch (e) {
|
|
1409
1492
|
//await this.closeUnexpectedPopups();
|
|
1410
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1411
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1412
|
-
info.screenshotPath = screenshotPath;
|
|
1413
|
-
Object.assign(e, { info: info });
|
|
1414
|
-
error = e;
|
|
1415
|
-
throw e;
|
|
1493
|
+
// this.logger.error("verify element contains text failed " + info.log);
|
|
1494
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1495
|
+
// info.screenshotPath = screenshotPath;
|
|
1496
|
+
// Object.assign(e, { info: info });
|
|
1497
|
+
// error = e;
|
|
1498
|
+
// throw e;
|
|
1416
1499
|
}
|
|
1417
1500
|
finally {
|
|
1418
1501
|
const endTime = Date.now();
|
|
@@ -1466,15 +1549,62 @@ class StableBrowser {
|
|
|
1466
1549
|
// save the data to the file
|
|
1467
1550
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1468
1551
|
}
|
|
1552
|
+
_getDataFilePath(fileName) {
|
|
1553
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1554
|
+
if (fs.existsSync(dataFile)) {
|
|
1555
|
+
return dataFile;
|
|
1556
|
+
}
|
|
1557
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1558
|
+
if (fs.existsSync(dataFile)) {
|
|
1559
|
+
return dataFile;
|
|
1560
|
+
}
|
|
1561
|
+
throw new Error("data file not found " + fileName);
|
|
1562
|
+
}
|
|
1563
|
+
_parseCSVSync(filePath) {
|
|
1564
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1565
|
+
const results = [];
|
|
1566
|
+
return new Promise((resolve, reject) => {
|
|
1567
|
+
const readableStream = new Readable();
|
|
1568
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1569
|
+
readableStream.push(data);
|
|
1570
|
+
readableStream.push(null);
|
|
1571
|
+
readableStream
|
|
1572
|
+
.pipe(csv())
|
|
1573
|
+
.on("data", (data) => results.push(data))
|
|
1574
|
+
.on("end", () => resolve(results))
|
|
1575
|
+
.on("error", (error) => reject(error));
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1469
1578
|
loadTestData(type, dataSelector, world = null) {
|
|
1470
1579
|
switch (type) {
|
|
1471
1580
|
case "users":
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1474
|
-
|
|
1581
|
+
// get the users.json file path
|
|
1582
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1583
|
+
// read the file and return the data
|
|
1584
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1585
|
+
for (let i = 0; i < users.length; i++) {
|
|
1586
|
+
if (users[i].username === dataSelector) {
|
|
1587
|
+
const userObj = {
|
|
1588
|
+
username: users[i].username,
|
|
1589
|
+
password: "secret:" + users[i].password,
|
|
1590
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1591
|
+
};
|
|
1592
|
+
this.setTestData(userObj, world);
|
|
1593
|
+
return userObj;
|
|
1594
|
+
}
|
|
1475
1595
|
}
|
|
1596
|
+
throw new Error("user not found " + dataSelector);
|
|
1597
|
+
default:
|
|
1598
|
+
throw new Error("unknown type " + type);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1602
|
+
switch (type) {
|
|
1603
|
+
case "users": {
|
|
1604
|
+
// get the users.json file path
|
|
1605
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1476
1606
|
// read the file and return the data
|
|
1477
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1607
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1478
1608
|
for (let i = 0; i < users.length; i++) {
|
|
1479
1609
|
if (users[i].username === dataSelector) {
|
|
1480
1610
|
const userObj = {
|
|
@@ -1487,6 +1617,29 @@ class StableBrowser {
|
|
|
1487
1617
|
}
|
|
1488
1618
|
}
|
|
1489
1619
|
throw new Error("user not found " + dataSelector);
|
|
1620
|
+
}
|
|
1621
|
+
case "csv": {
|
|
1622
|
+
// 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
|
|
1623
|
+
const parts = dataSelector.split(":");
|
|
1624
|
+
let rowNumber = 0;
|
|
1625
|
+
if (parts.length > 1) {
|
|
1626
|
+
rowNumber = parseInt(parts[1]);
|
|
1627
|
+
}
|
|
1628
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1629
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1630
|
+
// result stracture:
|
|
1631
|
+
// [
|
|
1632
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1633
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1634
|
+
// ]
|
|
1635
|
+
// verify the row number is within the range
|
|
1636
|
+
if (rowNumber >= results.length) {
|
|
1637
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1638
|
+
}
|
|
1639
|
+
const data = results[rowNumber];
|
|
1640
|
+
this.setTestData(data, world);
|
|
1641
|
+
return data;
|
|
1642
|
+
}
|
|
1490
1643
|
default:
|
|
1491
1644
|
throw new Error("unknown type " + type);
|
|
1492
1645
|
}
|
|
@@ -1591,13 +1744,13 @@ class StableBrowser {
|
|
|
1591
1744
|
])));
|
|
1592
1745
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1593
1746
|
format: "png",
|
|
1594
|
-
clip: {
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
},
|
|
1747
|
+
// clip: {
|
|
1748
|
+
// x: 0,
|
|
1749
|
+
// y: 0,
|
|
1750
|
+
// width: viewportWidth,
|
|
1751
|
+
// height: viewportHeight,
|
|
1752
|
+
// scale: 1,
|
|
1753
|
+
// },
|
|
1601
1754
|
});
|
|
1602
1755
|
if (!screenshotPath) {
|
|
1603
1756
|
return data;
|
|
@@ -1703,7 +1856,8 @@ class StableBrowser {
|
|
|
1703
1856
|
if (world) {
|
|
1704
1857
|
world[variable] = info.value;
|
|
1705
1858
|
}
|
|
1706
|
-
this.
|
|
1859
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1860
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1707
1861
|
return info;
|
|
1708
1862
|
}
|
|
1709
1863
|
catch (e) {
|
|
@@ -1740,6 +1894,91 @@ class StableBrowser {
|
|
|
1740
1894
|
});
|
|
1741
1895
|
}
|
|
1742
1896
|
}
|
|
1897
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1898
|
+
if (!emailAddress) {
|
|
1899
|
+
throw new Error("email address is null");
|
|
1900
|
+
}
|
|
1901
|
+
// check if address contain @
|
|
1902
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1903
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1904
|
+
}
|
|
1905
|
+
else {
|
|
1906
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1907
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const startTime = Date.now();
|
|
1911
|
+
let timeout = 60000;
|
|
1912
|
+
if (options && options.timeout) {
|
|
1913
|
+
timeout = options.timeout;
|
|
1914
|
+
}
|
|
1915
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1916
|
+
const request = {
|
|
1917
|
+
method: "POST",
|
|
1918
|
+
url: serviceUrl,
|
|
1919
|
+
headers: {
|
|
1920
|
+
"Content-Type": "application/json",
|
|
1921
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1922
|
+
},
|
|
1923
|
+
data: JSON.stringify({
|
|
1924
|
+
email: emailAddress,
|
|
1925
|
+
}),
|
|
1926
|
+
};
|
|
1927
|
+
let errorCount = 0;
|
|
1928
|
+
while (true) {
|
|
1929
|
+
try {
|
|
1930
|
+
let result = await this.context.api.request(request);
|
|
1931
|
+
// the response body expected to be the following:
|
|
1932
|
+
// {
|
|
1933
|
+
// "status": true,
|
|
1934
|
+
// "content": {
|
|
1935
|
+
// "url": "",
|
|
1936
|
+
// "code": "112112",
|
|
1937
|
+
// "name": "generate_link_or_code"
|
|
1938
|
+
// }
|
|
1939
|
+
//}
|
|
1940
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1941
|
+
let codeOrUrlFound = false;
|
|
1942
|
+
let emailCode = null;
|
|
1943
|
+
let emailUrl = null;
|
|
1944
|
+
// check if a code is returned
|
|
1945
|
+
if (result.data.content && result.data.content.code) {
|
|
1946
|
+
let code = result.data.content.code;
|
|
1947
|
+
this.setTestData({ emailCode: code }, world);
|
|
1948
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1949
|
+
emailCode = code;
|
|
1950
|
+
codeOrUrlFound = true;
|
|
1951
|
+
}
|
|
1952
|
+
// check if a url is returned
|
|
1953
|
+
if (result.data.content && result.data.content.url) {
|
|
1954
|
+
let url = result.data.content.url;
|
|
1955
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1956
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1957
|
+
emailUrl = url;
|
|
1958
|
+
codeOrUrlFound = true;
|
|
1959
|
+
}
|
|
1960
|
+
if (codeOrUrlFound) {
|
|
1961
|
+
return { emailUrl, emailCode };
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
this.logger.info("an email received but no code or url found");
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
catch (e) {
|
|
1969
|
+
errorCount++;
|
|
1970
|
+
if (errorCount > 3) {
|
|
1971
|
+
throw e;
|
|
1972
|
+
}
|
|
1973
|
+
// ignore
|
|
1974
|
+
}
|
|
1975
|
+
// check if the timeout is reached
|
|
1976
|
+
if (Date.now() - startTime > timeout) {
|
|
1977
|
+
throw new Error("timeout reached");
|
|
1978
|
+
}
|
|
1979
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1743
1982
|
async _highlightElements(scope, css) {
|
|
1744
1983
|
try {
|
|
1745
1984
|
if (!scope) {
|
|
@@ -1921,8 +2160,10 @@ class StableBrowser {
|
|
|
1921
2160
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1922
2161
|
await this._highlightElements(frame, dataAttribute);
|
|
1923
2162
|
const element = await frame.$(dataAttribute);
|
|
1924
|
-
|
|
1925
|
-
|
|
2163
|
+
if (element) {
|
|
2164
|
+
await this.scrollIfNeeded(element, info);
|
|
2165
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2166
|
+
}
|
|
1926
2167
|
}
|
|
1927
2168
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1928
2169
|
return info;
|
|
@@ -1960,6 +2201,16 @@ class StableBrowser {
|
|
|
1960
2201
|
});
|
|
1961
2202
|
}
|
|
1962
2203
|
}
|
|
2204
|
+
_getServerUrl() {
|
|
2205
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2206
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2207
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2208
|
+
}
|
|
2209
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2210
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2211
|
+
}
|
|
2212
|
+
return serviceUrl;
|
|
2213
|
+
}
|
|
1963
2214
|
async visualVerification(text, options = {}, world = null) {
|
|
1964
2215
|
const startTime = Date.now();
|
|
1965
2216
|
let error = null;
|
|
@@ -1974,13 +2225,7 @@ class StableBrowser {
|
|
|
1974
2225
|
throw new Error("TOKEN is not set");
|
|
1975
2226
|
}
|
|
1976
2227
|
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
|
-
}
|
|
2228
|
+
let serviceUrl = this._getServerUrl();
|
|
1984
2229
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1985
2230
|
info.screenshotPath = screenshotPath;
|
|
1986
2231
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2129,7 +2374,16 @@ class StableBrowser {
|
|
|
2129
2374
|
let screenshotId = null;
|
|
2130
2375
|
let screenshotPath = null;
|
|
2131
2376
|
const info = {};
|
|
2132
|
-
info.log =
|
|
2377
|
+
info.log =
|
|
2378
|
+
"***** analyze table " +
|
|
2379
|
+
selectors.element_name +
|
|
2380
|
+
" query " +
|
|
2381
|
+
query +
|
|
2382
|
+
" operator " +
|
|
2383
|
+
operator +
|
|
2384
|
+
" value " +
|
|
2385
|
+
value +
|
|
2386
|
+
" *****\n";
|
|
2133
2387
|
info.operation = "analyzeTable";
|
|
2134
2388
|
info.selectors = selectors;
|
|
2135
2389
|
info.query = query;
|
|
@@ -2363,13 +2617,6 @@ class StableBrowser {
|
|
|
2363
2617
|
const info = {};
|
|
2364
2618
|
try {
|
|
2365
2619
|
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
2620
|
}
|
|
2374
2621
|
catch (e) {
|
|
2375
2622
|
console.log(".");
|