automation_model 1.0.388-dev → 1.0.388-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 +6 -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/browser_manager.js +45 -8
- package/lib/browser_manager.js.map +1 -1
- package/lib/stable_browser.d.ts +14 -1
- package/lib/stable_browser.js +393 -145
- 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,21 +194,78 @@ class StableBrowser {
|
|
|
177
194
|
}
|
|
178
195
|
return text;
|
|
179
196
|
}
|
|
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
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
180
222
|
_getLocator(locator, scope, _params) {
|
|
223
|
+
locator = this._fixLocatorUsingParams(locator, _params);
|
|
224
|
+
let locatorReturn;
|
|
181
225
|
if (locator.role) {
|
|
182
226
|
if (locator.role[1].nameReg) {
|
|
183
227
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
184
228
|
delete locator.role[1].nameReg;
|
|
185
229
|
}
|
|
186
|
-
if (locator.role[1].name) {
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
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]);
|
|
190
234
|
}
|
|
191
235
|
if (locator.css) {
|
|
192
|
-
|
|
236
|
+
locatorReturn = scope.locator(locator.css);
|
|
193
237
|
}
|
|
194
|
-
|
|
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
|
+
}
|
|
249
|
+
}
|
|
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;
|
|
195
269
|
}
|
|
196
270
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
197
271
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -410,7 +484,7 @@ class StableBrowser {
|
|
|
410
484
|
}
|
|
411
485
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
412
486
|
for (let i = 0; i < 3; i++) {
|
|
413
|
-
info.log += "attempt " + i + ":
|
|
487
|
+
info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
|
|
414
488
|
for (let j = 0; j < selectors.locators.length; j++) {
|
|
415
489
|
let selector = selectors.locators[j];
|
|
416
490
|
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
@@ -596,6 +670,9 @@ class StableBrowser {
|
|
|
596
670
|
async click(selectors, _params, options = {}, world = null) {
|
|
597
671
|
this._validateSelectors(selectors);
|
|
598
672
|
const startTime = Date.now();
|
|
673
|
+
if (options && options.context) {
|
|
674
|
+
selectors.locators[0].text = options.context;
|
|
675
|
+
}
|
|
599
676
|
const info = {};
|
|
600
677
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
601
678
|
info.operation = "click";
|
|
@@ -609,14 +686,14 @@ class StableBrowser {
|
|
|
609
686
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
610
687
|
try {
|
|
611
688
|
await this._highlightElements(element);
|
|
612
|
-
await element.click(
|
|
689
|
+
await element.click();
|
|
613
690
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
614
691
|
}
|
|
615
692
|
catch (e) {
|
|
616
693
|
// await this.closeUnexpectedPopups();
|
|
617
694
|
info.log += "click failed, will try again" + "\n";
|
|
618
695
|
element = await this._locate(selectors, info, _params);
|
|
619
|
-
await element.click
|
|
696
|
+
await element.dispatchEvent("click");
|
|
620
697
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
621
698
|
}
|
|
622
699
|
await this.waitForPageLoad();
|
|
@@ -669,7 +746,7 @@ class StableBrowser {
|
|
|
669
746
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
670
747
|
try {
|
|
671
748
|
await this._highlightElements(element);
|
|
672
|
-
await element.setChecked(checked
|
|
749
|
+
await element.setChecked(checked);
|
|
673
750
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
674
751
|
}
|
|
675
752
|
catch (e) {
|
|
@@ -733,7 +810,7 @@ class StableBrowser {
|
|
|
733
810
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
734
811
|
try {
|
|
735
812
|
await this._highlightElements(element);
|
|
736
|
-
await element.hover(
|
|
813
|
+
await element.hover();
|
|
737
814
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
738
815
|
}
|
|
739
816
|
catch (e) {
|
|
@@ -795,7 +872,7 @@ class StableBrowser {
|
|
|
795
872
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
796
873
|
try {
|
|
797
874
|
await this._highlightElements(element);
|
|
798
|
-
await element.selectOption(values
|
|
875
|
+
await element.selectOption(values);
|
|
799
876
|
}
|
|
800
877
|
catch (e) {
|
|
801
878
|
//await this.closeUnexpectedPopups();
|
|
@@ -904,71 +981,45 @@ class StableBrowser {
|
|
|
904
981
|
});
|
|
905
982
|
}
|
|
906
983
|
}
|
|
907
|
-
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.
|
|
908
986
|
this._validateSelectors(selectors);
|
|
909
987
|
const startTime = Date.now();
|
|
910
|
-
let error = null;
|
|
911
|
-
let screenshotId = null;
|
|
912
|
-
let screenshotPath = null;
|
|
913
988
|
const info = {};
|
|
914
|
-
info.log = "";
|
|
915
|
-
info.operation =
|
|
989
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
990
|
+
info.operation = "setInputValue";
|
|
916
991
|
info.selectors = selectors;
|
|
992
|
+
value = this._fixUsingParams(value, _params);
|
|
917
993
|
info.value = value;
|
|
994
|
+
let error = null;
|
|
995
|
+
let screenshotId = null;
|
|
996
|
+
let screenshotPath = null;
|
|
918
997
|
try {
|
|
919
998
|
value = await this._replaceWithLocalData(value, this);
|
|
920
999
|
let element = await this._locate(selectors, info, _params);
|
|
921
|
-
//insert red border around the element
|
|
922
1000
|
await this.scrollIfNeeded(element, info);
|
|
923
1001
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
924
1002
|
await this._highlightElements(element);
|
|
925
1003
|
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
|
-
}
|
|
1004
|
+
await element.evaluateHandle((el, value) => {
|
|
1005
|
+
el.value = value;
|
|
1006
|
+
}, value);
|
|
944
1007
|
}
|
|
945
1008
|
catch (error) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
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));
|
|
949
1011
|
info.screenshotPath = screenshotPath;
|
|
950
1012
|
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
|
-
}
|
|
1013
|
+
await element.evaluateHandle((el, value) => {
|
|
1014
|
+
el.value = value;
|
|
1015
|
+
});
|
|
969
1016
|
}
|
|
970
1017
|
}
|
|
971
|
-
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 });
|
|
972
1023
|
error = e;
|
|
973
1024
|
throw e;
|
|
974
1025
|
}
|
|
@@ -976,10 +1027,10 @@ class StableBrowser {
|
|
|
976
1027
|
const endTime = Date.now();
|
|
977
1028
|
this._reportToWorld(world, {
|
|
978
1029
|
element_name: selectors.element_name,
|
|
979
|
-
type: Types.
|
|
980
|
-
|
|
1030
|
+
type: Types.SET_INPUT,
|
|
1031
|
+
text: `Set input value`,
|
|
981
1032
|
value: value,
|
|
982
|
-
|
|
1033
|
+
screenshotId,
|
|
983
1034
|
result: error
|
|
984
1035
|
? {
|
|
985
1036
|
status: "FAILED",
|
|
@@ -996,7 +1047,7 @@ class StableBrowser {
|
|
|
996
1047
|
});
|
|
997
1048
|
}
|
|
998
1049
|
}
|
|
999
|
-
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) {
|
|
1000
1051
|
this._validateSelectors(selectors);
|
|
1001
1052
|
const startTime = Date.now();
|
|
1002
1053
|
let error = null;
|
|
@@ -1008,6 +1059,7 @@ class StableBrowser {
|
|
|
1008
1059
|
info.selectors = selectors;
|
|
1009
1060
|
info.value = value;
|
|
1010
1061
|
try {
|
|
1062
|
+
value = await this._replaceWithLocalData(value, this);
|
|
1011
1063
|
let element = await this._locate(selectors, info, _params);
|
|
1012
1064
|
//insert red border around the element
|
|
1013
1065
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1016,28 +1068,51 @@ class StableBrowser {
|
|
|
1016
1068
|
try {
|
|
1017
1069
|
await element.click();
|
|
1018
1070
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
+
}
|
|
1024
1087
|
}
|
|
1025
|
-
catch (
|
|
1088
|
+
catch (err) {
|
|
1026
1089
|
//await this.closeUnexpectedPopups();
|
|
1027
1090
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1028
|
-
this.logger.info("Trying again")
|
|
1091
|
+
this.logger.info("Trying again");
|
|
1092
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1029
1093
|
info.screenshotPath = screenshotPath;
|
|
1030
|
-
Object.assign(
|
|
1094
|
+
Object.assign(err, { info: info });
|
|
1031
1095
|
await element.click();
|
|
1032
1096
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
+
}
|
|
1038
1113
|
}
|
|
1039
1114
|
}
|
|
1040
|
-
catch (
|
|
1115
|
+
catch (e) {
|
|
1041
1116
|
error = e;
|
|
1042
1117
|
throw e;
|
|
1043
1118
|
}
|
|
@@ -1087,20 +1162,32 @@ class StableBrowser {
|
|
|
1087
1162
|
await this.scrollIfNeeded(element, info);
|
|
1088
1163
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1089
1164
|
await this._highlightElements(element);
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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");
|
|
1094
1174
|
}
|
|
1095
1175
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
+
}
|
|
1101
1183
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1184
|
+
else {
|
|
1185
|
+
try {
|
|
1186
|
+
await element.focus();
|
|
1187
|
+
}
|
|
1188
|
+
catch (e) {
|
|
1189
|
+
await element.dispatchEvent("focus");
|
|
1190
|
+
}
|
|
1104
1191
|
}
|
|
1105
1192
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1106
1193
|
const valueSegment = _value.split("&&");
|
|
@@ -1188,7 +1275,7 @@ class StableBrowser {
|
|
|
1188
1275
|
let element = await this._locate(selectors, info, _params);
|
|
1189
1276
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1190
1277
|
await this._highlightElements(element);
|
|
1191
|
-
await element.fill(value
|
|
1278
|
+
await element.fill(value);
|
|
1192
1279
|
await element.dispatchEvent("change");
|
|
1193
1280
|
if (enter) {
|
|
1194
1281
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -1407,7 +1494,7 @@ class StableBrowser {
|
|
|
1407
1494
|
return info;
|
|
1408
1495
|
}
|
|
1409
1496
|
catch (e) {
|
|
1410
|
-
|
|
1497
|
+
await this.closeUnexpectedPopups();
|
|
1411
1498
|
this.logger.error("verify element contains text failed " + info.log);
|
|
1412
1499
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1413
1500
|
info.screenshotPath = screenshotPath;
|
|
@@ -1455,6 +1542,29 @@ class StableBrowser {
|
|
|
1455
1542
|
}
|
|
1456
1543
|
return dataFile;
|
|
1457
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
|
+
}
|
|
1458
1568
|
setTestData(testData, world = null) {
|
|
1459
1569
|
if (!testData) {
|
|
1460
1570
|
return;
|
|
@@ -1467,15 +1577,62 @@ class StableBrowser {
|
|
|
1467
1577
|
// save the data to the file
|
|
1468
1578
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1469
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
|
+
}
|
|
1470
1606
|
loadTestData(type, dataSelector, world = null) {
|
|
1471
1607
|
switch (type) {
|
|
1472
1608
|
case "users":
|
|
1473
|
-
//
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
+
}
|
|
1476
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");
|
|
1477
1634
|
// read the file and return the data
|
|
1478
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1635
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1479
1636
|
for (let i = 0; i < users.length; i++) {
|
|
1480
1637
|
if (users[i].username === dataSelector) {
|
|
1481
1638
|
const userObj = {
|
|
@@ -1488,6 +1645,29 @@ class StableBrowser {
|
|
|
1488
1645
|
}
|
|
1489
1646
|
}
|
|
1490
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
|
+
}
|
|
1491
1671
|
default:
|
|
1492
1672
|
throw new Error("unknown type " + type);
|
|
1493
1673
|
}
|
|
@@ -1592,13 +1772,13 @@ class StableBrowser {
|
|
|
1592
1772
|
])));
|
|
1593
1773
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1594
1774
|
format: "png",
|
|
1595
|
-
clip: {
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
},
|
|
1775
|
+
// clip: {
|
|
1776
|
+
// x: 0,
|
|
1777
|
+
// y: 0,
|
|
1778
|
+
// width: viewportWidth,
|
|
1779
|
+
// height: viewportHeight,
|
|
1780
|
+
// scale: 1,
|
|
1781
|
+
// },
|
|
1602
1782
|
});
|
|
1603
1783
|
if (!screenshotPath) {
|
|
1604
1784
|
return data;
|
|
@@ -1704,7 +1884,8 @@ class StableBrowser {
|
|
|
1704
1884
|
if (world) {
|
|
1705
1885
|
world[variable] = info.value;
|
|
1706
1886
|
}
|
|
1707
|
-
this.
|
|
1887
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1888
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1708
1889
|
return info;
|
|
1709
1890
|
}
|
|
1710
1891
|
catch (e) {
|
|
@@ -1741,6 +1922,91 @@ class StableBrowser {
|
|
|
1741
1922
|
});
|
|
1742
1923
|
}
|
|
1743
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
|
+
}
|
|
1744
2010
|
async _highlightElements(scope, css) {
|
|
1745
2011
|
try {
|
|
1746
2012
|
if (!scope) {
|
|
@@ -1963,6 +2229,16 @@ class StableBrowser {
|
|
|
1963
2229
|
});
|
|
1964
2230
|
}
|
|
1965
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
|
+
}
|
|
1966
2242
|
async visualVerification(text, options = {}, world = null) {
|
|
1967
2243
|
const startTime = Date.now();
|
|
1968
2244
|
let error = null;
|
|
@@ -1977,13 +2253,7 @@ class StableBrowser {
|
|
|
1977
2253
|
throw new Error("TOKEN is not set");
|
|
1978
2254
|
}
|
|
1979
2255
|
try {
|
|
1980
|
-
let serviceUrl =
|
|
1981
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1982
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1983
|
-
}
|
|
1984
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1985
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1986
|
-
}
|
|
2256
|
+
let serviceUrl = this._getServerUrl();
|
|
1987
2257
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1988
2258
|
info.screenshotPath = screenshotPath;
|
|
1989
2259
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2334,13 +2604,13 @@ class StableBrowser {
|
|
|
2334
2604
|
}
|
|
2335
2605
|
catch (e) {
|
|
2336
2606
|
if (e.label === "networkidle") {
|
|
2337
|
-
console.log("
|
|
2607
|
+
console.log("waited for the network to be idle timeout");
|
|
2338
2608
|
}
|
|
2339
2609
|
else if (e.label === "load") {
|
|
2340
|
-
console.log("
|
|
2610
|
+
console.log("waited for the load timeout");
|
|
2341
2611
|
}
|
|
2342
2612
|
else if (e.label === "domcontentloaded") {
|
|
2343
|
-
console.log("
|
|
2613
|
+
console.log("waited for the domcontent loaded timeout");
|
|
2344
2614
|
}
|
|
2345
2615
|
console.log(".");
|
|
2346
2616
|
}
|
|
@@ -2375,13 +2645,6 @@ class StableBrowser {
|
|
|
2375
2645
|
const info = {};
|
|
2376
2646
|
try {
|
|
2377
2647
|
await this.page.close();
|
|
2378
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2379
|
-
this.context.pages.pop();
|
|
2380
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2381
|
-
this.context.page = this.page;
|
|
2382
|
-
let title = await this.page.title();
|
|
2383
|
-
console.log("Switched to page " + title);
|
|
2384
|
-
}
|
|
2385
2648
|
}
|
|
2386
2649
|
catch (e) {
|
|
2387
2650
|
console.log(".");
|
|
@@ -2490,33 +2753,18 @@ class StableBrowser {
|
|
|
2490
2753
|
}
|
|
2491
2754
|
async scrollIfNeeded(element, info) {
|
|
2492
2755
|
try {
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
if (rect &&
|
|
2496
|
-
rect.top >= 0 &&
|
|
2497
|
-
rect.left >= 0 &&
|
|
2498
|
-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
2499
|
-
rect.right <= (window.innerWidth || document.documentElement.clientWidth)) {
|
|
2500
|
-
return false;
|
|
2501
|
-
}
|
|
2502
|
-
else {
|
|
2503
|
-
node.scrollIntoView({
|
|
2504
|
-
behavior: "smooth",
|
|
2505
|
-
block: "center",
|
|
2506
|
-
inline: "center",
|
|
2507
|
-
});
|
|
2508
|
-
return true;
|
|
2509
|
-
}
|
|
2756
|
+
await element.scrollIntoViewIfNeeded({
|
|
2757
|
+
timeout: 2000,
|
|
2510
2758
|
});
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
}
|
|
2759
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2760
|
+
if (info) {
|
|
2761
|
+
info.box = await element.boundingBox({
|
|
2762
|
+
timeout: 1000,
|
|
2763
|
+
});
|
|
2516
2764
|
}
|
|
2517
2765
|
}
|
|
2518
2766
|
catch (e) {
|
|
2519
|
-
console.log("
|
|
2767
|
+
console.log("#-#");
|
|
2520
2768
|
}
|
|
2521
2769
|
}
|
|
2522
2770
|
_reportToWorld(world, properties) {
|