automation_model 1.0.374-dev.0 → 1.0.374-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/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 +13 -1
- package/lib/stable_browser.js +382 -124
- 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);
|
|
@@ -314,7 +382,7 @@ class StableBrowser {
|
|
|
314
382
|
}
|
|
315
383
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
316
384
|
let locatorSearch = selectorHierarchy[index];
|
|
317
|
-
info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
385
|
+
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
318
386
|
let locator = null;
|
|
319
387
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
320
388
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
@@ -342,7 +410,7 @@ class StableBrowser {
|
|
|
342
410
|
// cssHref = true;
|
|
343
411
|
// }
|
|
344
412
|
let count = await locator.count();
|
|
345
|
-
info.log += "total elements found " + count + "\n";
|
|
413
|
+
//info.log += "total elements found " + count + "\n";
|
|
346
414
|
//let visibleCount = 0;
|
|
347
415
|
let visibleLocator = null;
|
|
348
416
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -352,16 +420,20 @@ class StableBrowser {
|
|
|
352
420
|
for (let j = 0; j < count; j++) {
|
|
353
421
|
let visible = await locator.nth(j).isVisible();
|
|
354
422
|
const enabled = await locator.nth(j).isEnabled();
|
|
355
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
356
423
|
if (!visibleOnly) {
|
|
357
424
|
visible = true;
|
|
358
425
|
}
|
|
359
426
|
if (visible && enabled) {
|
|
360
427
|
foundLocators.push(locator.nth(j));
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
if (!info.printMessages) {
|
|
431
|
+
info.printMessages = {};
|
|
432
|
+
}
|
|
433
|
+
if (!info.printMessages[j.toString()]) {
|
|
434
|
+
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
435
|
+
info.printMessages[j.toString()] = true;
|
|
436
|
+
}
|
|
365
437
|
}
|
|
366
438
|
}
|
|
367
439
|
}
|
|
@@ -407,6 +479,10 @@ class StableBrowser {
|
|
|
407
479
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
408
480
|
for (let i = 0; i < 3; i++) {
|
|
409
481
|
info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
|
|
482
|
+
for (let j = 0; j < selectors.locators.length; j++) {
|
|
483
|
+
let selector = selectors.locators[j];
|
|
484
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
485
|
+
}
|
|
410
486
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
411
487
|
if (!element.rerun) {
|
|
412
488
|
return element;
|
|
@@ -501,6 +577,7 @@ class StableBrowser {
|
|
|
501
577
|
let foundElements = result.foundElements;
|
|
502
578
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
503
579
|
info.box = foundElements[0].box;
|
|
580
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
504
581
|
return foundElements[0].locator;
|
|
505
582
|
}
|
|
506
583
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -530,6 +607,7 @@ class StableBrowser {
|
|
|
530
607
|
}
|
|
531
608
|
}
|
|
532
609
|
if (maxCountElement) {
|
|
610
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
533
611
|
info.box = await maxCountElement.locator.boundingBox();
|
|
534
612
|
return maxCountElement.locator;
|
|
535
613
|
}
|
|
@@ -538,9 +616,11 @@ class StableBrowser {
|
|
|
538
616
|
break;
|
|
539
617
|
}
|
|
540
618
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
619
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
541
620
|
highPriorityOnly = false;
|
|
542
621
|
}
|
|
543
622
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
623
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
544
624
|
visibleOnly = false;
|
|
545
625
|
}
|
|
546
626
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -584,6 +664,9 @@ class StableBrowser {
|
|
|
584
664
|
async click(selectors, _params, options = {}, world = null) {
|
|
585
665
|
this._validateSelectors(selectors);
|
|
586
666
|
const startTime = Date.now();
|
|
667
|
+
if (options && options.context) {
|
|
668
|
+
selectors.locators[0].text = options.context;
|
|
669
|
+
}
|
|
587
670
|
const info = {};
|
|
588
671
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
589
672
|
info.operation = "click";
|
|
@@ -892,71 +975,45 @@ class StableBrowser {
|
|
|
892
975
|
});
|
|
893
976
|
}
|
|
894
977
|
}
|
|
895
|
-
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.
|
|
896
980
|
this._validateSelectors(selectors);
|
|
897
981
|
const startTime = Date.now();
|
|
898
|
-
let error = null;
|
|
899
|
-
let screenshotId = null;
|
|
900
|
-
let screenshotPath = null;
|
|
901
982
|
const info = {};
|
|
902
|
-
info.log = "";
|
|
903
|
-
info.operation =
|
|
983
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
984
|
+
info.operation = "setInputValue";
|
|
904
985
|
info.selectors = selectors;
|
|
986
|
+
value = this._fixUsingParams(value, _params);
|
|
905
987
|
info.value = value;
|
|
988
|
+
let error = null;
|
|
989
|
+
let screenshotId = null;
|
|
990
|
+
let screenshotPath = null;
|
|
906
991
|
try {
|
|
907
992
|
value = await this._replaceWithLocalData(value, this);
|
|
908
993
|
let element = await this._locate(selectors, info, _params);
|
|
909
|
-
//insert red border around the element
|
|
910
994
|
await this.scrollIfNeeded(element, info);
|
|
911
995
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
912
996
|
await this._highlightElements(element);
|
|
913
997
|
try {
|
|
914
|
-
await element.
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
value = dayjs(value).format(format);
|
|
918
|
-
await element.fill(value);
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
922
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
923
|
-
el.value = ""; // clear input
|
|
924
|
-
el.value = dateTimeValue;
|
|
925
|
-
}, dateTimeValue);
|
|
926
|
-
}
|
|
927
|
-
if (enter) {
|
|
928
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
929
|
-
await this.page.keyboard.press("Enter");
|
|
930
|
-
await this.waitForPageLoad();
|
|
931
|
-
}
|
|
998
|
+
await element.evaluateHandle((el, value) => {
|
|
999
|
+
el.value = value;
|
|
1000
|
+
}, value);
|
|
932
1001
|
}
|
|
933
1002
|
catch (error) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
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));
|
|
937
1005
|
info.screenshotPath = screenshotPath;
|
|
938
1006
|
Object.assign(error, { info: info });
|
|
939
|
-
await element.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
value = dayjs(value).format(format);
|
|
943
|
-
await element.fill(value);
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
947
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
948
|
-
el.value = ""; // clear input
|
|
949
|
-
el.value = dateTimeValue;
|
|
950
|
-
}, dateTimeValue);
|
|
951
|
-
}
|
|
952
|
-
if (enter) {
|
|
953
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
954
|
-
await this.page.keyboard.press("Enter");
|
|
955
|
-
await this.waitForPageLoad();
|
|
956
|
-
}
|
|
1007
|
+
await element.evaluateHandle((el, value) => {
|
|
1008
|
+
el.value = value;
|
|
1009
|
+
});
|
|
957
1010
|
}
|
|
958
1011
|
}
|
|
959
|
-
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 });
|
|
960
1017
|
error = e;
|
|
961
1018
|
throw e;
|
|
962
1019
|
}
|
|
@@ -964,10 +1021,10 @@ class StableBrowser {
|
|
|
964
1021
|
const endTime = Date.now();
|
|
965
1022
|
this._reportToWorld(world, {
|
|
966
1023
|
element_name: selectors.element_name,
|
|
967
|
-
type: Types.
|
|
968
|
-
|
|
1024
|
+
type: Types.SET_INPUT,
|
|
1025
|
+
text: `Set input value`,
|
|
969
1026
|
value: value,
|
|
970
|
-
|
|
1027
|
+
screenshotId,
|
|
971
1028
|
result: error
|
|
972
1029
|
? {
|
|
973
1030
|
status: "FAILED",
|
|
@@ -984,7 +1041,7 @@ class StableBrowser {
|
|
|
984
1041
|
});
|
|
985
1042
|
}
|
|
986
1043
|
}
|
|
987
|
-
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) {
|
|
988
1045
|
this._validateSelectors(selectors);
|
|
989
1046
|
const startTime = Date.now();
|
|
990
1047
|
let error = null;
|
|
@@ -996,6 +1053,7 @@ class StableBrowser {
|
|
|
996
1053
|
info.selectors = selectors;
|
|
997
1054
|
info.value = value;
|
|
998
1055
|
try {
|
|
1056
|
+
value = await this._replaceWithLocalData(value, this);
|
|
999
1057
|
let element = await this._locate(selectors, info, _params);
|
|
1000
1058
|
//insert red border around the element
|
|
1001
1059
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1004,28 +1062,51 @@ class StableBrowser {
|
|
|
1004
1062
|
try {
|
|
1005
1063
|
await element.click();
|
|
1006
1064
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
+
}
|
|
1012
1081
|
}
|
|
1013
|
-
catch (
|
|
1082
|
+
catch (err) {
|
|
1014
1083
|
//await this.closeUnexpectedPopups();
|
|
1015
1084
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1016
|
-
this.logger.info("Trying again")
|
|
1085
|
+
this.logger.info("Trying again");
|
|
1086
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1017
1087
|
info.screenshotPath = screenshotPath;
|
|
1018
|
-
Object.assign(
|
|
1088
|
+
Object.assign(err, { info: info });
|
|
1019
1089
|
await element.click();
|
|
1020
1090
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
+
}
|
|
1026
1107
|
}
|
|
1027
1108
|
}
|
|
1028
|
-
catch (
|
|
1109
|
+
catch (e) {
|
|
1029
1110
|
error = e;
|
|
1030
1111
|
throw e;
|
|
1031
1112
|
}
|
|
@@ -1075,20 +1156,32 @@ class StableBrowser {
|
|
|
1075
1156
|
await this.scrollIfNeeded(element, info);
|
|
1076
1157
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1077
1158
|
await this._highlightElements(element);
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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");
|
|
1082
1168
|
}
|
|
1083
1169
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
+
}
|
|
1089
1177
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1178
|
+
else {
|
|
1179
|
+
try {
|
|
1180
|
+
await element.focus();
|
|
1181
|
+
}
|
|
1182
|
+
catch (e) {
|
|
1183
|
+
await element.dispatchEvent("focus");
|
|
1184
|
+
}
|
|
1092
1185
|
}
|
|
1093
1186
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1094
1187
|
const valueSegment = _value.split("&&");
|
|
@@ -1284,7 +1377,8 @@ class StableBrowser {
|
|
|
1284
1377
|
let screenshotId = null;
|
|
1285
1378
|
let screenshotPath = null;
|
|
1286
1379
|
const info = {};
|
|
1287
|
-
info.log =
|
|
1380
|
+
info.log =
|
|
1381
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1288
1382
|
info.operation = "containsPattern";
|
|
1289
1383
|
info.selectors = selectors;
|
|
1290
1384
|
info.value = text;
|
|
@@ -1394,7 +1488,7 @@ class StableBrowser {
|
|
|
1394
1488
|
return info;
|
|
1395
1489
|
}
|
|
1396
1490
|
catch (e) {
|
|
1397
|
-
|
|
1491
|
+
await this.closeUnexpectedPopups();
|
|
1398
1492
|
this.logger.error("verify element contains text failed " + info.log);
|
|
1399
1493
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1400
1494
|
info.screenshotPath = screenshotPath;
|
|
@@ -1454,15 +1548,62 @@ class StableBrowser {
|
|
|
1454
1548
|
// save the data to the file
|
|
1455
1549
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1456
1550
|
}
|
|
1551
|
+
_getDataFilePath(fileName) {
|
|
1552
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1553
|
+
if (fs.existsSync(dataFile)) {
|
|
1554
|
+
return dataFile;
|
|
1555
|
+
}
|
|
1556
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1557
|
+
if (fs.existsSync(dataFile)) {
|
|
1558
|
+
return dataFile;
|
|
1559
|
+
}
|
|
1560
|
+
throw new Error("data file not found " + fileName);
|
|
1561
|
+
}
|
|
1562
|
+
_parseCSVSync(filePath) {
|
|
1563
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1564
|
+
const results = [];
|
|
1565
|
+
return new Promise((resolve, reject) => {
|
|
1566
|
+
const readableStream = new Readable();
|
|
1567
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1568
|
+
readableStream.push(data);
|
|
1569
|
+
readableStream.push(null);
|
|
1570
|
+
readableStream
|
|
1571
|
+
.pipe(csv())
|
|
1572
|
+
.on("data", (data) => results.push(data))
|
|
1573
|
+
.on("end", () => resolve(results))
|
|
1574
|
+
.on("error", (error) => reject(error));
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1457
1577
|
loadTestData(type, dataSelector, world = null) {
|
|
1458
1578
|
switch (type) {
|
|
1459
1579
|
case "users":
|
|
1460
|
-
//
|
|
1461
|
-
|
|
1462
|
-
|
|
1580
|
+
// get the users.json file path
|
|
1581
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1582
|
+
// read the file and return the data
|
|
1583
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1584
|
+
for (let i = 0; i < users.length; i++) {
|
|
1585
|
+
if (users[i].username === dataSelector) {
|
|
1586
|
+
const userObj = {
|
|
1587
|
+
username: users[i].username,
|
|
1588
|
+
password: "secret:" + users[i].password,
|
|
1589
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1590
|
+
};
|
|
1591
|
+
this.setTestData(userObj, world);
|
|
1592
|
+
return userObj;
|
|
1593
|
+
}
|
|
1463
1594
|
}
|
|
1595
|
+
throw new Error("user not found " + dataSelector);
|
|
1596
|
+
default:
|
|
1597
|
+
throw new Error("unknown type " + type);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1601
|
+
switch (type) {
|
|
1602
|
+
case "users": {
|
|
1603
|
+
// get the users.json file path
|
|
1604
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1464
1605
|
// read the file and return the data
|
|
1465
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1606
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1466
1607
|
for (let i = 0; i < users.length; i++) {
|
|
1467
1608
|
if (users[i].username === dataSelector) {
|
|
1468
1609
|
const userObj = {
|
|
@@ -1475,6 +1616,29 @@ class StableBrowser {
|
|
|
1475
1616
|
}
|
|
1476
1617
|
}
|
|
1477
1618
|
throw new Error("user not found " + dataSelector);
|
|
1619
|
+
}
|
|
1620
|
+
case "csv": {
|
|
1621
|
+
// 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
|
|
1622
|
+
const parts = dataSelector.split(":");
|
|
1623
|
+
let rowNumber = 0;
|
|
1624
|
+
if (parts.length > 1) {
|
|
1625
|
+
rowNumber = parseInt(parts[1]);
|
|
1626
|
+
}
|
|
1627
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1628
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1629
|
+
// result stracture:
|
|
1630
|
+
// [
|
|
1631
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1632
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1633
|
+
// ]
|
|
1634
|
+
// verify the row number is within the range
|
|
1635
|
+
if (rowNumber >= results.length) {
|
|
1636
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1637
|
+
}
|
|
1638
|
+
const data = results[rowNumber];
|
|
1639
|
+
this.setTestData(data, world);
|
|
1640
|
+
return data;
|
|
1641
|
+
}
|
|
1478
1642
|
default:
|
|
1479
1643
|
throw new Error("unknown type " + type);
|
|
1480
1644
|
}
|
|
@@ -1579,13 +1743,13 @@ class StableBrowser {
|
|
|
1579
1743
|
])));
|
|
1580
1744
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1581
1745
|
format: "png",
|
|
1582
|
-
clip: {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
},
|
|
1746
|
+
// clip: {
|
|
1747
|
+
// x: 0,
|
|
1748
|
+
// y: 0,
|
|
1749
|
+
// width: viewportWidth,
|
|
1750
|
+
// height: viewportHeight,
|
|
1751
|
+
// scale: 1,
|
|
1752
|
+
// },
|
|
1589
1753
|
});
|
|
1590
1754
|
if (!screenshotPath) {
|
|
1591
1755
|
return data;
|
|
@@ -1691,7 +1855,8 @@ class StableBrowser {
|
|
|
1691
1855
|
if (world) {
|
|
1692
1856
|
world[variable] = info.value;
|
|
1693
1857
|
}
|
|
1694
|
-
this.
|
|
1858
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1859
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1695
1860
|
return info;
|
|
1696
1861
|
}
|
|
1697
1862
|
catch (e) {
|
|
@@ -1728,6 +1893,91 @@ class StableBrowser {
|
|
|
1728
1893
|
});
|
|
1729
1894
|
}
|
|
1730
1895
|
}
|
|
1896
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1897
|
+
if (!emailAddress) {
|
|
1898
|
+
throw new Error("email address is null");
|
|
1899
|
+
}
|
|
1900
|
+
// check if address contain @
|
|
1901
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1902
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1906
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
const startTime = Date.now();
|
|
1910
|
+
let timeout = 60000;
|
|
1911
|
+
if (options && options.timeout) {
|
|
1912
|
+
timeout = options.timeout;
|
|
1913
|
+
}
|
|
1914
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1915
|
+
const request = {
|
|
1916
|
+
method: "POST",
|
|
1917
|
+
url: serviceUrl,
|
|
1918
|
+
headers: {
|
|
1919
|
+
"Content-Type": "application/json",
|
|
1920
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1921
|
+
},
|
|
1922
|
+
data: JSON.stringify({
|
|
1923
|
+
email: emailAddress,
|
|
1924
|
+
}),
|
|
1925
|
+
};
|
|
1926
|
+
let errorCount = 0;
|
|
1927
|
+
while (true) {
|
|
1928
|
+
try {
|
|
1929
|
+
let result = await this.context.api.request(request);
|
|
1930
|
+
// the response body expected to be the following:
|
|
1931
|
+
// {
|
|
1932
|
+
// "status": true,
|
|
1933
|
+
// "content": {
|
|
1934
|
+
// "url": "",
|
|
1935
|
+
// "code": "112112",
|
|
1936
|
+
// "name": "generate_link_or_code"
|
|
1937
|
+
// }
|
|
1938
|
+
//}
|
|
1939
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1940
|
+
let codeOrUrlFound = false;
|
|
1941
|
+
let emailCode = null;
|
|
1942
|
+
let emailUrl = null;
|
|
1943
|
+
// check if a code is returned
|
|
1944
|
+
if (result.data.content && result.data.content.code) {
|
|
1945
|
+
let code = result.data.content.code;
|
|
1946
|
+
this.setTestData({ emailCode: code }, world);
|
|
1947
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1948
|
+
emailCode = code;
|
|
1949
|
+
codeOrUrlFound = true;
|
|
1950
|
+
}
|
|
1951
|
+
// check if a url is returned
|
|
1952
|
+
if (result.data.content && result.data.content.url) {
|
|
1953
|
+
let url = result.data.content.url;
|
|
1954
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1955
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1956
|
+
emailUrl = url;
|
|
1957
|
+
codeOrUrlFound = true;
|
|
1958
|
+
}
|
|
1959
|
+
if (codeOrUrlFound) {
|
|
1960
|
+
return { emailUrl, emailCode };
|
|
1961
|
+
}
|
|
1962
|
+
else {
|
|
1963
|
+
this.logger.info("an email received but no code or url found");
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
catch (e) {
|
|
1968
|
+
errorCount++;
|
|
1969
|
+
if (errorCount > 3) {
|
|
1970
|
+
throw e;
|
|
1971
|
+
}
|
|
1972
|
+
// ignore
|
|
1973
|
+
}
|
|
1974
|
+
// check if the timeout is reached
|
|
1975
|
+
if (Date.now() - startTime > timeout) {
|
|
1976
|
+
throw new Error("timeout reached");
|
|
1977
|
+
}
|
|
1978
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1731
1981
|
async _highlightElements(scope, css) {
|
|
1732
1982
|
try {
|
|
1733
1983
|
if (!scope) {
|
|
@@ -1909,8 +2159,10 @@ class StableBrowser {
|
|
|
1909
2159
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1910
2160
|
await this._highlightElements(frame, dataAttribute);
|
|
1911
2161
|
const element = await frame.$(dataAttribute);
|
|
1912
|
-
|
|
1913
|
-
|
|
2162
|
+
if (element) {
|
|
2163
|
+
await this.scrollIfNeeded(element, info);
|
|
2164
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2165
|
+
}
|
|
1914
2166
|
}
|
|
1915
2167
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1916
2168
|
return info;
|
|
@@ -1948,6 +2200,16 @@ class StableBrowser {
|
|
|
1948
2200
|
});
|
|
1949
2201
|
}
|
|
1950
2202
|
}
|
|
2203
|
+
_getServerUrl() {
|
|
2204
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2205
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2206
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2207
|
+
}
|
|
2208
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2209
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2210
|
+
}
|
|
2211
|
+
return serviceUrl;
|
|
2212
|
+
}
|
|
1951
2213
|
async visualVerification(text, options = {}, world = null) {
|
|
1952
2214
|
const startTime = Date.now();
|
|
1953
2215
|
let error = null;
|
|
@@ -1962,13 +2224,7 @@ class StableBrowser {
|
|
|
1962
2224
|
throw new Error("TOKEN is not set");
|
|
1963
2225
|
}
|
|
1964
2226
|
try {
|
|
1965
|
-
let serviceUrl =
|
|
1966
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1967
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1968
|
-
}
|
|
1969
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1970
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1971
|
-
}
|
|
2227
|
+
let serviceUrl = this._getServerUrl();
|
|
1972
2228
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1973
2229
|
info.screenshotPath = screenshotPath;
|
|
1974
2230
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2117,7 +2373,16 @@ class StableBrowser {
|
|
|
2117
2373
|
let screenshotId = null;
|
|
2118
2374
|
let screenshotPath = null;
|
|
2119
2375
|
const info = {};
|
|
2120
|
-
info.log =
|
|
2376
|
+
info.log =
|
|
2377
|
+
"***** analyze table " +
|
|
2378
|
+
selectors.element_name +
|
|
2379
|
+
" query " +
|
|
2380
|
+
query +
|
|
2381
|
+
" operator " +
|
|
2382
|
+
operator +
|
|
2383
|
+
" value " +
|
|
2384
|
+
value +
|
|
2385
|
+
" *****\n";
|
|
2121
2386
|
info.operation = "analyzeTable";
|
|
2122
2387
|
info.selectors = selectors;
|
|
2123
2388
|
info.query = query;
|
|
@@ -2351,13 +2616,6 @@ class StableBrowser {
|
|
|
2351
2616
|
const info = {};
|
|
2352
2617
|
try {
|
|
2353
2618
|
await this.page.close();
|
|
2354
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2355
|
-
this.context.pages.pop();
|
|
2356
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2357
|
-
this.context.page = this.page;
|
|
2358
|
-
let title = await this.page.title();
|
|
2359
|
-
console.log("Switched to page " + title);
|
|
2360
|
-
}
|
|
2361
2619
|
}
|
|
2362
2620
|
catch (e) {
|
|
2363
2621
|
console.log(".");
|