automation_model 1.0.392-dev → 1.0.392-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/api.js +12 -6
- 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/browser_manager.js +3 -2
- package/lib/browser_manager.js.map +1 -1
- package/lib/stable_browser.d.ts +14 -1
- package/lib/stable_browser.js +393 -148
- 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 +11 -6
package/lib/stable_browser.js
CHANGED
|
@@ -12,6 +12,9 @@ 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";
|
|
17
|
+
import readline from "readline";
|
|
15
18
|
const Types = {
|
|
16
19
|
CLICK: "click_element",
|
|
17
20
|
NAVIGATE: "navigate",
|
|
@@ -27,6 +30,7 @@ const Types = {
|
|
|
27
30
|
SELECT: "select_combobox",
|
|
28
31
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
29
32
|
TYPE_PRESS: "type_press",
|
|
33
|
+
PRESS: "press_key",
|
|
30
34
|
HOVER: "hover_element",
|
|
31
35
|
CHECK: "check_element",
|
|
32
36
|
UNCHECK: "uncheck_element",
|
|
@@ -35,6 +39,8 @@ const Types = {
|
|
|
35
39
|
SET_DATE_TIME: "set_date_time",
|
|
36
40
|
SET_VIEWPORT: "set_viewport",
|
|
37
41
|
VERIFY_VISUAL: "verify_visual",
|
|
42
|
+
LOAD_DATA: "load_data",
|
|
43
|
+
SET_INPUT: "set_input",
|
|
38
44
|
};
|
|
39
45
|
class StableBrowser {
|
|
40
46
|
constructor(browser, page, logger = null, context = null) {
|
|
@@ -80,9 +86,20 @@ class StableBrowser {
|
|
|
80
86
|
this.page = page;
|
|
81
87
|
context.page = page;
|
|
82
88
|
context.pages.push(page);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
page.on("close", async () => {
|
|
90
|
+
if (this.context && this.context.pages && this.context.pages.length > 1) {
|
|
91
|
+
this.context.pages.pop();
|
|
92
|
+
this.page = this.context.pages[this.context.pages.length - 1];
|
|
93
|
+
this.context.page = this.page;
|
|
94
|
+
try {
|
|
95
|
+
let title = await this.page.title();
|
|
96
|
+
console.log("Switched to page " + title);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error("Error on page close", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
86
103
|
try {
|
|
87
104
|
await this.waitForPageLoad();
|
|
88
105
|
console.log("Switch page: " + (await page.title()));
|
|
@@ -177,24 +194,78 @@ class StableBrowser {
|
|
|
177
194
|
}
|
|
178
195
|
return text;
|
|
179
196
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
197
|
+
_fixLocatorUsingParams(locator, _params) {
|
|
198
|
+
// check if not null
|
|
199
|
+
if (!locator) {
|
|
200
|
+
return locator;
|
|
201
|
+
}
|
|
202
|
+
// clone the locator
|
|
203
|
+
locator = JSON.parse(JSON.stringify(locator));
|
|
204
|
+
this.scanAndManipulate(locator, _params);
|
|
205
|
+
return locator;
|
|
206
|
+
}
|
|
207
|
+
_isObject(value) {
|
|
208
|
+
return value && typeof value === "object" && value.constructor === Object;
|
|
209
|
+
}
|
|
210
|
+
scanAndManipulate(currentObj, _params) {
|
|
211
|
+
for (const key in currentObj) {
|
|
212
|
+
if (typeof currentObj[key] === "string") {
|
|
213
|
+
// Perform string manipulation
|
|
214
|
+
currentObj[key] = this._fixUsingParams(currentObj[key], _params);
|
|
215
|
+
}
|
|
216
|
+
else if (this._isObject(currentObj[key])) {
|
|
217
|
+
// Recursively scan nested objects
|
|
218
|
+
this.scanAndManipulate(currentObj[key], _params);
|
|
219
|
+
}
|
|
183
220
|
}
|
|
221
|
+
}
|
|
222
|
+
_getLocator(locator, scope, _params) {
|
|
223
|
+
locator = this._fixLocatorUsingParams(locator, _params);
|
|
224
|
+
let locatorReturn;
|
|
184
225
|
if (locator.role) {
|
|
185
226
|
if (locator.role[1].nameReg) {
|
|
186
227
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
187
228
|
delete locator.role[1].nameReg;
|
|
188
229
|
}
|
|
189
|
-
if (locator.role[1].name) {
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
230
|
+
// if (locator.role[1].name) {
|
|
231
|
+
// locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
|
|
232
|
+
// }
|
|
233
|
+
locatorReturn = scope.getByRole(locator.role[0], locator.role[1]);
|
|
193
234
|
}
|
|
194
235
|
if (locator.css) {
|
|
195
|
-
|
|
236
|
+
locatorReturn = scope.locator(locator.css);
|
|
237
|
+
}
|
|
238
|
+
// handle role/name locators
|
|
239
|
+
// locator.selector will be something like: textbox[name="Username"i]
|
|
240
|
+
if (locator.engine === "internal:role") {
|
|
241
|
+
// extract the role, name and the i/s flags using regex
|
|
242
|
+
const match = locator.selector.match(/(.*)\[(.*)="(.*)"(.*)\]/);
|
|
243
|
+
if (match) {
|
|
244
|
+
const role = match[1];
|
|
245
|
+
const name = match[3];
|
|
246
|
+
const flags = match[4];
|
|
247
|
+
locatorReturn = scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
248
|
+
}
|
|
196
249
|
}
|
|
197
|
-
|
|
250
|
+
if (locator === null || locator === void 0 ? void 0 : locator.engine) {
|
|
251
|
+
if (locator.engine === "css") {
|
|
252
|
+
locatorReturn = scope.locator(locator.selector);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
let selector = locator.selector;
|
|
256
|
+
if (locator.engine === "internal:attr") {
|
|
257
|
+
if (!selector.startsWith("[")) {
|
|
258
|
+
selector = `[${selector}]`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!locatorReturn) {
|
|
265
|
+
console.error(locator);
|
|
266
|
+
throw new Error("Locator undefined");
|
|
267
|
+
}
|
|
268
|
+
return locatorReturn;
|
|
198
269
|
}
|
|
199
270
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
200
271
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -413,7 +484,7 @@ class StableBrowser {
|
|
|
413
484
|
}
|
|
414
485
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
415
486
|
for (let i = 0; i < 3; i++) {
|
|
416
|
-
info.log += "attempt " + i + ":
|
|
487
|
+
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
417
488
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
418
489
|
let selector = selectors.locators[j];
|
|
419
490
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
@@ -599,6 +670,9 @@ class StableBrowser {
|
|
|
599
670
|
async click(selectors, _params, options = {}, world = null) {
|
|
600
671
|
this._validateSelectors(selectors);
|
|
601
672
|
const startTime = Date.now();
|
|
673
|
+
if (options && options.context) {
|
|
674
|
+
selectors.locators[0].text = options.context;
|
|
675
|
+
}
|
|
602
676
|
const info = {};
|
|
603
677
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
604
678
|
info.operation = "click";
|
|
@@ -612,14 +686,14 @@ class StableBrowser {
|
|
|
612
686
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
613
687
|
try {
|
|
614
688
|
await this._highlightElements(element);
|
|
615
|
-
await element.click(
|
|
689
|
+
await element.click();
|
|
616
690
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
617
691
|
}
|
|
618
692
|
catch (e) {
|
|
619
693
|
// await this.closeUnexpectedPopups();
|
|
620
694
|
info.log += "click failed, will try again" + "\n";
|
|
621
695
|
element = await this._locate(selectors, info, _params);
|
|
622
|
-
await element.click
|
|
696
|
+
await element.dispatchEvent("click");
|
|
623
697
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
624
698
|
}
|
|
625
699
|
await this.waitForPageLoad();
|
|
@@ -672,7 +746,7 @@ class StableBrowser {
|
|
|
672
746
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
673
747
|
try {
|
|
674
748
|
await this._highlightElements(element);
|
|
675
|
-
await element.setChecked(checked
|
|
749
|
+
await element.setChecked(checked);
|
|
676
750
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
677
751
|
}
|
|
678
752
|
catch (e) {
|
|
@@ -736,7 +810,7 @@ class StableBrowser {
|
|
|
736
810
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
737
811
|
try {
|
|
738
812
|
await this._highlightElements(element);
|
|
739
|
-
await element.hover(
|
|
813
|
+
await element.hover();
|
|
740
814
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
741
815
|
}
|
|
742
816
|
catch (e) {
|
|
@@ -798,7 +872,7 @@ class StableBrowser {
|
|
|
798
872
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
799
873
|
try {
|
|
800
874
|
await this._highlightElements(element);
|
|
801
|
-
await element.selectOption(values
|
|
875
|
+
await element.selectOption(values);
|
|
802
876
|
}
|
|
803
877
|
catch (e) {
|
|
804
878
|
//await this.closeUnexpectedPopups();
|
|
@@ -907,71 +981,45 @@ class StableBrowser {
|
|
|
907
981
|
});
|
|
908
982
|
}
|
|
909
983
|
}
|
|
910
|
-
async
|
|
984
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
985
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
911
986
|
this._validateSelectors(selectors);
|
|
912
987
|
const startTime = Date.now();
|
|
913
|
-
let error = null;
|
|
914
|
-
let screenshotId = null;
|
|
915
|
-
let screenshotPath = null;
|
|
916
988
|
const info = {};
|
|
917
|
-
info.log = "";
|
|
918
|
-
info.operation =
|
|
989
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
990
|
+
info.operation = "setInputValue";
|
|
919
991
|
info.selectors = selectors;
|
|
992
|
+
value = this._fixUsingParams(value, _params);
|
|
920
993
|
info.value = value;
|
|
994
|
+
let error = null;
|
|
995
|
+
let screenshotId = null;
|
|
996
|
+
let screenshotPath = null;
|
|
921
997
|
try {
|
|
922
998
|
value = await this._replaceWithLocalData(value, this);
|
|
923
999
|
let element = await this._locate(selectors, info, _params);
|
|
924
|
-
//insert red border around the element
|
|
925
1000
|
await this.scrollIfNeeded(element, info);
|
|
926
1001
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
927
1002
|
await this._highlightElements(element);
|
|
928
1003
|
try {
|
|
929
|
-
await element.
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
value = dayjs(value).format(format);
|
|
933
|
-
await element.fill(value);
|
|
934
|
-
}
|
|
935
|
-
else {
|
|
936
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
937
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
938
|
-
el.value = ""; // clear input
|
|
939
|
-
el.value = dateTimeValue;
|
|
940
|
-
}, dateTimeValue);
|
|
941
|
-
}
|
|
942
|
-
if (enter) {
|
|
943
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
944
|
-
await this.page.keyboard.press("Enter");
|
|
945
|
-
await this.waitForPageLoad();
|
|
946
|
-
}
|
|
1004
|
+
await element.evaluateHandle((el, value) => {
|
|
1005
|
+
el.value = value;
|
|
1006
|
+
}, value);
|
|
947
1007
|
}
|
|
948
1008
|
catch (error) {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1009
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1010
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
952
1011
|
info.screenshotPath = screenshotPath;
|
|
953
1012
|
Object.assign(error, { info: info });
|
|
954
|
-
await element.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
value = dayjs(value).format(format);
|
|
958
|
-
await element.fill(value);
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
962
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
963
|
-
el.value = ""; // clear input
|
|
964
|
-
el.value = dateTimeValue;
|
|
965
|
-
}, dateTimeValue);
|
|
966
|
-
}
|
|
967
|
-
if (enter) {
|
|
968
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
969
|
-
await this.page.keyboard.press("Enter");
|
|
970
|
-
await this.waitForPageLoad();
|
|
971
|
-
}
|
|
1013
|
+
await element.evaluateHandle((el, value) => {
|
|
1014
|
+
el.value = value;
|
|
1015
|
+
});
|
|
972
1016
|
}
|
|
973
1017
|
}
|
|
974
|
-
catch (
|
|
1018
|
+
catch (e) {
|
|
1019
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1020
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1021
|
+
info.screenshotPath = screenshotPath;
|
|
1022
|
+
Object.assign(e, { info: info });
|
|
975
1023
|
error = e;
|
|
976
1024
|
throw e;
|
|
977
1025
|
}
|
|
@@ -979,10 +1027,10 @@ class StableBrowser {
|
|
|
979
1027
|
const endTime = Date.now();
|
|
980
1028
|
this._reportToWorld(world, {
|
|
981
1029
|
element_name: selectors.element_name,
|
|
982
|
-
type: Types.
|
|
983
|
-
|
|
1030
|
+
type: Types.SET_INPUT,
|
|
1031
|
+
text: `Set input value`,
|
|
984
1032
|
value: value,
|
|
985
|
-
|
|
1033
|
+
screenshotId,
|
|
986
1034
|
result: error
|
|
987
1035
|
? {
|
|
988
1036
|
status: "FAILED",
|
|
@@ -999,7 +1047,7 @@ class StableBrowser {
|
|
|
999
1047
|
});
|
|
1000
1048
|
}
|
|
1001
1049
|
}
|
|
1002
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1050
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
1003
1051
|
this._validateSelectors(selectors);
|
|
1004
1052
|
const startTime = Date.now();
|
|
1005
1053
|
let error = null;
|
|
@@ -1011,6 +1059,7 @@ class StableBrowser {
|
|
|
1011
1059
|
info.selectors = selectors;
|
|
1012
1060
|
info.value = value;
|
|
1013
1061
|
try {
|
|
1062
|
+
value = await this._replaceWithLocalData(value, this);
|
|
1014
1063
|
let element = await this._locate(selectors, info, _params);
|
|
1015
1064
|
//insert red border around the element
|
|
1016
1065
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1019,28 +1068,51 @@ class StableBrowser {
|
|
|
1019
1068
|
try {
|
|
1020
1069
|
await element.click();
|
|
1021
1070
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1071
|
+
if (format) {
|
|
1072
|
+
value = dayjs(value).format(format);
|
|
1073
|
+
await element.fill(value);
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1077
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1078
|
+
el.value = ""; // clear input
|
|
1079
|
+
el.value = dateTimeValue;
|
|
1080
|
+
}, dateTimeValue);
|
|
1081
|
+
}
|
|
1082
|
+
if (enter) {
|
|
1083
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1084
|
+
await this.page.keyboard.press("Enter");
|
|
1085
|
+
await this.waitForPageLoad();
|
|
1086
|
+
}
|
|
1027
1087
|
}
|
|
1028
|
-
catch (
|
|
1088
|
+
catch (err) {
|
|
1029
1089
|
//await this.closeUnexpectedPopups();
|
|
1030
1090
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1031
|
-
this.logger.info("Trying again")
|
|
1091
|
+
this.logger.info("Trying again");
|
|
1092
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1032
1093
|
info.screenshotPath = screenshotPath;
|
|
1033
|
-
Object.assign(
|
|
1094
|
+
Object.assign(err, { info: info });
|
|
1034
1095
|
await element.click();
|
|
1035
1096
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1097
|
+
if (format) {
|
|
1098
|
+
value = dayjs(value).format(format);
|
|
1099
|
+
await element.fill(value);
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1103
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1104
|
+
el.value = ""; // clear input
|
|
1105
|
+
el.value = dateTimeValue;
|
|
1106
|
+
}, dateTimeValue);
|
|
1107
|
+
}
|
|
1108
|
+
if (enter) {
|
|
1109
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1110
|
+
await this.page.keyboard.press("Enter");
|
|
1111
|
+
await this.waitForPageLoad();
|
|
1112
|
+
}
|
|
1041
1113
|
}
|
|
1042
1114
|
}
|
|
1043
|
-
catch (
|
|
1115
|
+
catch (e) {
|
|
1044
1116
|
error = e;
|
|
1045
1117
|
throw e;
|
|
1046
1118
|
}
|
|
@@ -1090,20 +1162,32 @@ class StableBrowser {
|
|
|
1090
1162
|
await this.scrollIfNeeded(element, info);
|
|
1091
1163
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1092
1164
|
await this._highlightElements(element);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1165
|
+
if (options === null || options === undefined || !options.press) {
|
|
1166
|
+
try {
|
|
1167
|
+
let currentValue = await element.inputValue();
|
|
1168
|
+
if (currentValue) {
|
|
1169
|
+
await element.fill("");
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
catch (e) {
|
|
1173
|
+
this.logger.info("unable to clear input value");
|
|
1097
1174
|
}
|
|
1098
1175
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1176
|
+
if (options === null || options === undefined || options.press) {
|
|
1177
|
+
try {
|
|
1178
|
+
await element.click({ timeout: 5000 });
|
|
1179
|
+
}
|
|
1180
|
+
catch (e) {
|
|
1181
|
+
await element.dispatchEvent("click");
|
|
1182
|
+
}
|
|
1104
1183
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1184
|
+
else {
|
|
1185
|
+
try {
|
|
1186
|
+
await element.focus();
|
|
1187
|
+
}
|
|
1188
|
+
catch (e) {
|
|
1189
|
+
await element.dispatchEvent("focus");
|
|
1190
|
+
}
|
|
1107
1191
|
}
|
|
1108
1192
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1109
1193
|
const valueSegment = _value.split("&&");
|
|
@@ -1191,7 +1275,7 @@ class StableBrowser {
|
|
|
1191
1275
|
let element = await this._locate(selectors, info, _params);
|
|
1192
1276
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1193
1277
|
await this._highlightElements(element);
|
|
1194
|
-
await element.fill(value
|
|
1278
|
+
await element.fill(value);
|
|
1195
1279
|
await element.dispatchEvent("change");
|
|
1196
1280
|
if (enter) {
|
|
1197
1281
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -1410,7 +1494,7 @@ class StableBrowser {
|
|
|
1410
1494
|
return info;
|
|
1411
1495
|
}
|
|
1412
1496
|
catch (e) {
|
|
1413
|
-
|
|
1497
|
+
await this.closeUnexpectedPopups();
|
|
1414
1498
|
this.logger.error("verify element contains text failed " + info.log);
|
|
1415
1499
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1416
1500
|
info.screenshotPath = screenshotPath;
|
|
@@ -1458,6 +1542,29 @@ class StableBrowser {
|
|
|
1458
1542
|
}
|
|
1459
1543
|
return dataFile;
|
|
1460
1544
|
}
|
|
1545
|
+
async waitForUserInput(message, world = null) {
|
|
1546
|
+
if (!message) {
|
|
1547
|
+
message = "# Wait for user input. Press any key to continue";
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
message = "# Wait for user input. " + message;
|
|
1551
|
+
}
|
|
1552
|
+
message += "\n";
|
|
1553
|
+
const value = await new Promise((resolve) => {
|
|
1554
|
+
const rl = readline.createInterface({
|
|
1555
|
+
input: process.stdin,
|
|
1556
|
+
output: process.stdout,
|
|
1557
|
+
});
|
|
1558
|
+
rl.question(message, (answer) => {
|
|
1559
|
+
rl.close();
|
|
1560
|
+
resolve(answer);
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1563
|
+
if (value) {
|
|
1564
|
+
this.logger.info(`{{userInput}} was set to: ${value}`);
|
|
1565
|
+
}
|
|
1566
|
+
this.setTestData({ userInput: value }, world);
|
|
1567
|
+
}
|
|
1461
1568
|
setTestData(testData, world = null) {
|
|
1462
1569
|
if (!testData) {
|
|
1463
1570
|
return;
|
|
@@ -1470,15 +1577,62 @@ class StableBrowser {
|
|
|
1470
1577
|
// save the data to the file
|
|
1471
1578
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1472
1579
|
}
|
|
1580
|
+
_getDataFilePath(fileName) {
|
|
1581
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1582
|
+
if (fs.existsSync(dataFile)) {
|
|
1583
|
+
return dataFile;
|
|
1584
|
+
}
|
|
1585
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1586
|
+
if (fs.existsSync(dataFile)) {
|
|
1587
|
+
return dataFile;
|
|
1588
|
+
}
|
|
1589
|
+
throw new Error("data file not found " + fileName);
|
|
1590
|
+
}
|
|
1591
|
+
_parseCSVSync(filePath) {
|
|
1592
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1593
|
+
const results = [];
|
|
1594
|
+
return new Promise((resolve, reject) => {
|
|
1595
|
+
const readableStream = new Readable();
|
|
1596
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1597
|
+
readableStream.push(data);
|
|
1598
|
+
readableStream.push(null);
|
|
1599
|
+
readableStream
|
|
1600
|
+
.pipe(csv())
|
|
1601
|
+
.on("data", (data) => results.push(data))
|
|
1602
|
+
.on("end", () => resolve(results))
|
|
1603
|
+
.on("error", (error) => reject(error));
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1473
1606
|
loadTestData(type, dataSelector, world = null) {
|
|
1474
1607
|
switch (type) {
|
|
1475
1608
|
case "users":
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1478
|
-
|
|
1609
|
+
// get the users.json file path
|
|
1610
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1611
|
+
// read the file and return the data
|
|
1612
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1613
|
+
for (let i = 0; i < users.length; i++) {
|
|
1614
|
+
if (users[i].username === dataSelector) {
|
|
1615
|
+
const userObj = {
|
|
1616
|
+
username: users[i].username,
|
|
1617
|
+
password: "secret:" + users[i].password,
|
|
1618
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1619
|
+
};
|
|
1620
|
+
this.setTestData(userObj, world);
|
|
1621
|
+
return userObj;
|
|
1622
|
+
}
|
|
1479
1623
|
}
|
|
1624
|
+
throw new Error("user not found " + dataSelector);
|
|
1625
|
+
default:
|
|
1626
|
+
throw new Error("unknown type " + type);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1630
|
+
switch (type) {
|
|
1631
|
+
case "users": {
|
|
1632
|
+
// get the users.json file path
|
|
1633
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1480
1634
|
// read the file and return the data
|
|
1481
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1635
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1482
1636
|
for (let i = 0; i < users.length; i++) {
|
|
1483
1637
|
if (users[i].username === dataSelector) {
|
|
1484
1638
|
const userObj = {
|
|
@@ -1491,6 +1645,29 @@ class StableBrowser {
|
|
|
1491
1645
|
}
|
|
1492
1646
|
}
|
|
1493
1647
|
throw new Error("user not found " + dataSelector);
|
|
1648
|
+
}
|
|
1649
|
+
case "csv": {
|
|
1650
|
+
// 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
|
|
1651
|
+
const parts = dataSelector.split(":");
|
|
1652
|
+
let rowNumber = 0;
|
|
1653
|
+
if (parts.length > 1) {
|
|
1654
|
+
rowNumber = parseInt(parts[1]);
|
|
1655
|
+
}
|
|
1656
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1657
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1658
|
+
// result stracture:
|
|
1659
|
+
// [
|
|
1660
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1661
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1662
|
+
// ]
|
|
1663
|
+
// verify the row number is within the range
|
|
1664
|
+
if (rowNumber >= results.length) {
|
|
1665
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1666
|
+
}
|
|
1667
|
+
const data = results[rowNumber];
|
|
1668
|
+
this.setTestData(data, world);
|
|
1669
|
+
return data;
|
|
1670
|
+
}
|
|
1494
1671
|
default:
|
|
1495
1672
|
throw new Error("unknown type " + type);
|
|
1496
1673
|
}
|
|
@@ -1595,13 +1772,13 @@ class StableBrowser {
|
|
|
1595
1772
|
])));
|
|
1596
1773
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1597
1774
|
format: "png",
|
|
1598
|
-
clip: {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
},
|
|
1775
|
+
// clip: {
|
|
1776
|
+
// x: 0,
|
|
1777
|
+
// y: 0,
|
|
1778
|
+
// width: viewportWidth,
|
|
1779
|
+
// height: viewportHeight,
|
|
1780
|
+
// scale: 1,
|
|
1781
|
+
// },
|
|
1605
1782
|
});
|
|
1606
1783
|
if (!screenshotPath) {
|
|
1607
1784
|
return data;
|
|
@@ -1707,7 +1884,8 @@ class StableBrowser {
|
|
|
1707
1884
|
if (world) {
|
|
1708
1885
|
world[variable] = info.value;
|
|
1709
1886
|
}
|
|
1710
|
-
this.
|
|
1887
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1888
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1711
1889
|
return info;
|
|
1712
1890
|
}
|
|
1713
1891
|
catch (e) {
|
|
@@ -1744,6 +1922,91 @@ class StableBrowser {
|
|
|
1744
1922
|
});
|
|
1745
1923
|
}
|
|
1746
1924
|
}
|
|
1925
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1926
|
+
if (!emailAddress) {
|
|
1927
|
+
throw new Error("email address is null");
|
|
1928
|
+
}
|
|
1929
|
+
// check if address contain @
|
|
1930
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1931
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1935
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
const startTime = Date.now();
|
|
1939
|
+
let timeout = 60000;
|
|
1940
|
+
if (options && options.timeout) {
|
|
1941
|
+
timeout = options.timeout;
|
|
1942
|
+
}
|
|
1943
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1944
|
+
const request = {
|
|
1945
|
+
method: "POST",
|
|
1946
|
+
url: serviceUrl,
|
|
1947
|
+
headers: {
|
|
1948
|
+
"Content-Type": "application/json",
|
|
1949
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1950
|
+
},
|
|
1951
|
+
data: JSON.stringify({
|
|
1952
|
+
email: emailAddress,
|
|
1953
|
+
}),
|
|
1954
|
+
};
|
|
1955
|
+
let errorCount = 0;
|
|
1956
|
+
while (true) {
|
|
1957
|
+
try {
|
|
1958
|
+
let result = await this.context.api.request(request);
|
|
1959
|
+
// the response body expected to be the following:
|
|
1960
|
+
// {
|
|
1961
|
+
// "status": true,
|
|
1962
|
+
// "content": {
|
|
1963
|
+
// "url": "",
|
|
1964
|
+
// "code": "112112",
|
|
1965
|
+
// "name": "generate_link_or_code"
|
|
1966
|
+
// }
|
|
1967
|
+
//}
|
|
1968
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1969
|
+
let codeOrUrlFound = false;
|
|
1970
|
+
let emailCode = null;
|
|
1971
|
+
let emailUrl = null;
|
|
1972
|
+
// check if a code is returned
|
|
1973
|
+
if (result.data.content && result.data.content.code) {
|
|
1974
|
+
let code = result.data.content.code;
|
|
1975
|
+
this.setTestData({ emailCode: code }, world);
|
|
1976
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1977
|
+
emailCode = code;
|
|
1978
|
+
codeOrUrlFound = true;
|
|
1979
|
+
}
|
|
1980
|
+
// check if a url is returned
|
|
1981
|
+
if (result.data.content && result.data.content.url) {
|
|
1982
|
+
let url = result.data.content.url;
|
|
1983
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1984
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1985
|
+
emailUrl = url;
|
|
1986
|
+
codeOrUrlFound = true;
|
|
1987
|
+
}
|
|
1988
|
+
if (codeOrUrlFound) {
|
|
1989
|
+
return { emailUrl, emailCode };
|
|
1990
|
+
}
|
|
1991
|
+
else {
|
|
1992
|
+
this.logger.info("an email received but no code or url found");
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
catch (e) {
|
|
1997
|
+
errorCount++;
|
|
1998
|
+
if (errorCount > 3) {
|
|
1999
|
+
throw e;
|
|
2000
|
+
}
|
|
2001
|
+
// ignore
|
|
2002
|
+
}
|
|
2003
|
+
// check if the timeout is reached
|
|
2004
|
+
if (Date.now() - startTime > timeout) {
|
|
2005
|
+
throw new Error("timeout reached");
|
|
2006
|
+
}
|
|
2007
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1747
2010
|
async _highlightElements(scope, css) {
|
|
1748
2011
|
try {
|
|
1749
2012
|
if (!scope) {
|
|
@@ -1966,6 +2229,16 @@ class StableBrowser {
|
|
|
1966
2229
|
});
|
|
1967
2230
|
}
|
|
1968
2231
|
}
|
|
2232
|
+
_getServerUrl() {
|
|
2233
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2234
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2235
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2236
|
+
}
|
|
2237
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2238
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2239
|
+
}
|
|
2240
|
+
return serviceUrl;
|
|
2241
|
+
}
|
|
1969
2242
|
async visualVerification(text, options = {}, world = null) {
|
|
1970
2243
|
const startTime = Date.now();
|
|
1971
2244
|
let error = null;
|
|
@@ -1980,13 +2253,7 @@ class StableBrowser {
|
|
|
1980
2253
|
throw new Error("TOKEN is not set");
|
|
1981
2254
|
}
|
|
1982
2255
|
try {
|
|
1983
|
-
let serviceUrl =
|
|
1984
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1985
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1986
|
-
}
|
|
1987
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1988
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1989
|
-
}
|
|
2256
|
+
let serviceUrl = this._getServerUrl();
|
|
1990
2257
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1991
2258
|
info.screenshotPath = screenshotPath;
|
|
1992
2259
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2337,13 +2604,13 @@ class StableBrowser {
|
|
|
2337
2604
|
}
|
|
2338
2605
|
catch (e) {
|
|
2339
2606
|
if (e.label === "networkidle") {
|
|
2340
|
-
console.log("
|
|
2607
|
+
console.log("waited for the network to be idle timeout");
|
|
2341
2608
|
}
|
|
2342
2609
|
else if (e.label === "load") {
|
|
2343
|
-
console.log("
|
|
2610
|
+
console.log("waited for the load timeout");
|
|
2344
2611
|
}
|
|
2345
2612
|
else if (e.label === "domcontentloaded") {
|
|
2346
|
-
console.log("
|
|
2613
|
+
console.log("waited for the domcontent loaded timeout");
|
|
2347
2614
|
}
|
|
2348
2615
|
console.log(".");
|
|
2349
2616
|
}
|
|
@@ -2378,13 +2645,6 @@ class StableBrowser {
|
|
|
2378
2645
|
const info = {};
|
|
2379
2646
|
try {
|
|
2380
2647
|
await this.page.close();
|
|
2381
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2382
|
-
this.context.pages.pop();
|
|
2383
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2384
|
-
this.context.page = this.page;
|
|
2385
|
-
let title = await this.page.title();
|
|
2386
|
-
console.log("Switched to page " + title);
|
|
2387
|
-
}
|
|
2388
2648
|
}
|
|
2389
2649
|
catch (e) {
|
|
2390
2650
|
console.log(".");
|
|
@@ -2493,33 +2753,18 @@ class StableBrowser {
|
|
|
2493
2753
|
}
|
|
2494
2754
|
async scrollIfNeeded(element, info) {
|
|
2495
2755
|
try {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
if (rect &&
|
|
2499
|
-
rect.top >= 0 &&
|
|
2500
|
-
rect.left >= 0 &&
|
|
2501
|
-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
2502
|
-
rect.right <= (window.innerWidth || document.documentElement.clientWidth)) {
|
|
2503
|
-
return false;
|
|
2504
|
-
}
|
|
2505
|
-
else {
|
|
2506
|
-
node.scrollIntoView({
|
|
2507
|
-
behavior: "smooth",
|
|
2508
|
-
block: "center",
|
|
2509
|
-
inline: "center",
|
|
2510
|
-
});
|
|
2511
|
-
return true;
|
|
2512
|
-
}
|
|
2756
|
+
await element.scrollIntoViewIfNeeded({
|
|
2757
|
+
timeout: 2000,
|
|
2513
2758
|
});
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
}
|
|
2759
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2760
|
+
if (info) {
|
|
2761
|
+
info.box = await element.boundingBox({
|
|
2762
|
+
timeout: 1000,
|
|
2763
|
+
});
|
|
2519
2764
|
}
|
|
2520
2765
|
}
|
|
2521
2766
|
catch (e) {
|
|
2522
|
-
console.log("
|
|
2767
|
+
console.log("#-#");
|
|
2523
2768
|
}
|
|
2524
2769
|
}
|
|
2525
2770
|
_reportToWorld(world, properties) {
|