automation_model 1.0.373-dev.0 → 1.0.373-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 +14 -2
- package/lib/stable_browser.js +423 -161
- 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);
|
|
231
|
+
}
|
|
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
|
+
}
|
|
193
257
|
}
|
|
194
|
-
|
|
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,10 +382,13 @@ 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);
|
|
389
|
+
if (!locatorString) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
321
392
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
322
393
|
}
|
|
323
394
|
else if (locatorSearch.text) {
|
|
@@ -339,7 +410,7 @@ class StableBrowser {
|
|
|
339
410
|
// cssHref = true;
|
|
340
411
|
// }
|
|
341
412
|
let count = await locator.count();
|
|
342
|
-
info.log += "total elements found " + count + "\n";
|
|
413
|
+
//info.log += "total elements found " + count + "\n";
|
|
343
414
|
//let visibleCount = 0;
|
|
344
415
|
let visibleLocator = null;
|
|
345
416
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -349,16 +420,20 @@ class StableBrowser {
|
|
|
349
420
|
for (let j = 0; j < count; j++) {
|
|
350
421
|
let visible = await locator.nth(j).isVisible();
|
|
351
422
|
const enabled = await locator.nth(j).isEnabled();
|
|
352
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
353
423
|
if (!visibleOnly) {
|
|
354
424
|
visible = true;
|
|
355
425
|
}
|
|
356
426
|
if (visible && enabled) {
|
|
357
427
|
foundLocators.push(locator.nth(j));
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
437
|
}
|
|
363
438
|
}
|
|
364
439
|
}
|
|
@@ -404,6 +479,10 @@ class StableBrowser {
|
|
|
404
479
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
405
480
|
for (let i = 0; i < 3; i++) {
|
|
406
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
|
+
}
|
|
407
486
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
408
487
|
if (!element.rerun) {
|
|
409
488
|
return element;
|
|
@@ -498,6 +577,7 @@ class StableBrowser {
|
|
|
498
577
|
let foundElements = result.foundElements;
|
|
499
578
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
500
579
|
info.box = foundElements[0].box;
|
|
580
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
501
581
|
return foundElements[0].locator;
|
|
502
582
|
}
|
|
503
583
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -527,6 +607,7 @@ class StableBrowser {
|
|
|
527
607
|
}
|
|
528
608
|
}
|
|
529
609
|
if (maxCountElement) {
|
|
610
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
530
611
|
info.box = await maxCountElement.locator.boundingBox();
|
|
531
612
|
return maxCountElement.locator;
|
|
532
613
|
}
|
|
@@ -535,9 +616,11 @@ class StableBrowser {
|
|
|
535
616
|
break;
|
|
536
617
|
}
|
|
537
618
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
619
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
538
620
|
highPriorityOnly = false;
|
|
539
621
|
}
|
|
540
622
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
623
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
541
624
|
visibleOnly = false;
|
|
542
625
|
}
|
|
543
626
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -581,6 +664,9 @@ class StableBrowser {
|
|
|
581
664
|
async click(selectors, _params, options = {}, world = null) {
|
|
582
665
|
this._validateSelectors(selectors);
|
|
583
666
|
const startTime = Date.now();
|
|
667
|
+
if (options && options.context) {
|
|
668
|
+
selectors.locators[0].text = options.context;
|
|
669
|
+
}
|
|
584
670
|
const info = {};
|
|
585
671
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
586
672
|
info.operation = "click";
|
|
@@ -889,71 +975,45 @@ class StableBrowser {
|
|
|
889
975
|
});
|
|
890
976
|
}
|
|
891
977
|
}
|
|
892
|
-
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.
|
|
893
980
|
this._validateSelectors(selectors);
|
|
894
981
|
const startTime = Date.now();
|
|
895
|
-
let error = null;
|
|
896
|
-
let screenshotId = null;
|
|
897
|
-
let screenshotPath = null;
|
|
898
982
|
const info = {};
|
|
899
|
-
info.log = "";
|
|
900
|
-
info.operation =
|
|
983
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
984
|
+
info.operation = "setInputValue";
|
|
901
985
|
info.selectors = selectors;
|
|
986
|
+
value = this._fixUsingParams(value, _params);
|
|
902
987
|
info.value = value;
|
|
988
|
+
let error = null;
|
|
989
|
+
let screenshotId = null;
|
|
990
|
+
let screenshotPath = null;
|
|
903
991
|
try {
|
|
904
992
|
value = await this._replaceWithLocalData(value, this);
|
|
905
993
|
let element = await this._locate(selectors, info, _params);
|
|
906
|
-
//insert red border around the element
|
|
907
994
|
await this.scrollIfNeeded(element, info);
|
|
908
995
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
909
996
|
await this._highlightElements(element);
|
|
910
997
|
try {
|
|
911
|
-
await element.
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
value = dayjs(value).format(format);
|
|
915
|
-
await element.fill(value);
|
|
916
|
-
}
|
|
917
|
-
else {
|
|
918
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
919
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
920
|
-
el.value = ""; // clear input
|
|
921
|
-
el.value = dateTimeValue;
|
|
922
|
-
}, dateTimeValue);
|
|
923
|
-
}
|
|
924
|
-
if (enter) {
|
|
925
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
926
|
-
await this.page.keyboard.press("Enter");
|
|
927
|
-
await this.waitForPageLoad();
|
|
928
|
-
}
|
|
998
|
+
await element.evaluateHandle((el, value) => {
|
|
999
|
+
el.value = value;
|
|
1000
|
+
}, value);
|
|
929
1001
|
}
|
|
930
1002
|
catch (error) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
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));
|
|
934
1005
|
info.screenshotPath = screenshotPath;
|
|
935
1006
|
Object.assign(error, { info: info });
|
|
936
|
-
await element.
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
value = dayjs(value).format(format);
|
|
940
|
-
await element.fill(value);
|
|
941
|
-
}
|
|
942
|
-
else {
|
|
943
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
944
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
945
|
-
el.value = ""; // clear input
|
|
946
|
-
el.value = dateTimeValue;
|
|
947
|
-
}, dateTimeValue);
|
|
948
|
-
}
|
|
949
|
-
if (enter) {
|
|
950
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
951
|
-
await this.page.keyboard.press("Enter");
|
|
952
|
-
await this.waitForPageLoad();
|
|
953
|
-
}
|
|
1007
|
+
await element.evaluateHandle((el, value) => {
|
|
1008
|
+
el.value = value;
|
|
1009
|
+
});
|
|
954
1010
|
}
|
|
955
1011
|
}
|
|
956
|
-
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 });
|
|
957
1017
|
error = e;
|
|
958
1018
|
throw e;
|
|
959
1019
|
}
|
|
@@ -961,10 +1021,10 @@ class StableBrowser {
|
|
|
961
1021
|
const endTime = Date.now();
|
|
962
1022
|
this._reportToWorld(world, {
|
|
963
1023
|
element_name: selectors.element_name,
|
|
964
|
-
type: Types.
|
|
965
|
-
|
|
1024
|
+
type: Types.SET_INPUT,
|
|
1025
|
+
text: `Set input value`,
|
|
966
1026
|
value: value,
|
|
967
|
-
|
|
1027
|
+
screenshotId,
|
|
968
1028
|
result: error
|
|
969
1029
|
? {
|
|
970
1030
|
status: "FAILED",
|
|
@@ -981,7 +1041,7 @@ class StableBrowser {
|
|
|
981
1041
|
});
|
|
982
1042
|
}
|
|
983
1043
|
}
|
|
984
|
-
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) {
|
|
985
1045
|
this._validateSelectors(selectors);
|
|
986
1046
|
const startTime = Date.now();
|
|
987
1047
|
let error = null;
|
|
@@ -993,6 +1053,7 @@ class StableBrowser {
|
|
|
993
1053
|
info.selectors = selectors;
|
|
994
1054
|
info.value = value;
|
|
995
1055
|
try {
|
|
1056
|
+
value = await this._replaceWithLocalData(value, this);
|
|
996
1057
|
let element = await this._locate(selectors, info, _params);
|
|
997
1058
|
//insert red border around the element
|
|
998
1059
|
await this.scrollIfNeeded(element, info);
|
|
@@ -1001,28 +1062,51 @@ class StableBrowser {
|
|
|
1001
1062
|
try {
|
|
1002
1063
|
await element.click();
|
|
1003
1064
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
+
}
|
|
1009
1081
|
}
|
|
1010
|
-
catch (
|
|
1082
|
+
catch (err) {
|
|
1011
1083
|
//await this.closeUnexpectedPopups();
|
|
1012
1084
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1013
|
-
this.logger.info("Trying again")
|
|
1085
|
+
this.logger.info("Trying again");
|
|
1086
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1014
1087
|
info.screenshotPath = screenshotPath;
|
|
1015
|
-
Object.assign(
|
|
1088
|
+
Object.assign(err, { info: info });
|
|
1016
1089
|
await element.click();
|
|
1017
1090
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
+
}
|
|
1023
1107
|
}
|
|
1024
1108
|
}
|
|
1025
|
-
catch (
|
|
1109
|
+
catch (e) {
|
|
1026
1110
|
error = e;
|
|
1027
1111
|
throw e;
|
|
1028
1112
|
}
|
|
@@ -1072,20 +1156,32 @@ class StableBrowser {
|
|
|
1072
1156
|
await this.scrollIfNeeded(element, info);
|
|
1073
1157
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1074
1158
|
await this._highlightElements(element);
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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");
|
|
1079
1168
|
}
|
|
1080
1169
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
}
|
|
1086
1177
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1178
|
+
else {
|
|
1179
|
+
try {
|
|
1180
|
+
await element.focus();
|
|
1181
|
+
}
|
|
1182
|
+
catch (e) {
|
|
1183
|
+
await element.dispatchEvent("focus");
|
|
1184
|
+
}
|
|
1089
1185
|
}
|
|
1090
1186
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1091
1187
|
const valueSegment = _value.split("&&");
|
|
@@ -1281,7 +1377,8 @@ class StableBrowser {
|
|
|
1281
1377
|
let screenshotId = null;
|
|
1282
1378
|
let screenshotPath = null;
|
|
1283
1379
|
const info = {};
|
|
1284
|
-
info.log =
|
|
1380
|
+
info.log =
|
|
1381
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1285
1382
|
info.operation = "containsPattern";
|
|
1286
1383
|
info.selectors = selectors;
|
|
1287
1384
|
info.value = text;
|
|
@@ -1337,7 +1434,6 @@ class StableBrowser {
|
|
|
1337
1434
|
}
|
|
1338
1435
|
}
|
|
1339
1436
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
1340
|
-
var _a, _b, _c;
|
|
1341
1437
|
this._validateSelectors(selectors);
|
|
1342
1438
|
if (!text) {
|
|
1343
1439
|
throw new Error("text is null");
|
|
@@ -1358,46 +1454,48 @@ class StableBrowser {
|
|
|
1358
1454
|
info.value = text;
|
|
1359
1455
|
let foundObj = null;
|
|
1360
1456
|
try {
|
|
1361
|
-
foundObj = await this._getText(selectors, climb, _params, options, info, world);
|
|
1362
|
-
if (foundObj && foundObj.element) {
|
|
1363
|
-
|
|
1364
|
-
}
|
|
1365
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1366
|
-
const dateAlternatives = findDateAlternatives(text);
|
|
1367
|
-
const numberAlternatives = findNumberAlternatives(text);
|
|
1368
|
-
if (dateAlternatives.date) {
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1457
|
+
// foundObj = await this._getText(selectors, climb, _params, options, info, world);
|
|
1458
|
+
// if (foundObj && foundObj.element) {
|
|
1459
|
+
// await this.scrollIfNeeded(foundObj.element, info);
|
|
1460
|
+
// }
|
|
1461
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1462
|
+
// const dateAlternatives = findDateAlternatives(text);
|
|
1463
|
+
// const numberAlternatives = findNumberAlternatives(text);
|
|
1464
|
+
// if (dateAlternatives.date) {
|
|
1465
|
+
// for (let i = 0; i < dateAlternatives.dates.length; i++) {
|
|
1466
|
+
// if (
|
|
1467
|
+
// foundObj?.text.includes(dateAlternatives.dates[i]) ||
|
|
1468
|
+
// foundObj?.value?.includes(dateAlternatives.dates[i])
|
|
1469
|
+
// ) {
|
|
1470
|
+
// return info;
|
|
1471
|
+
// }
|
|
1472
|
+
// }
|
|
1473
|
+
// throw new Error("element doesn't contain text " + text);
|
|
1474
|
+
// } else if (numberAlternatives.number) {
|
|
1475
|
+
// for (let i = 0; i < numberAlternatives.numbers.length; i++) {
|
|
1476
|
+
// if (
|
|
1477
|
+
// foundObj?.text.includes(numberAlternatives.numbers[i]) ||
|
|
1478
|
+
// foundObj?.value?.includes(numberAlternatives.numbers[i])
|
|
1479
|
+
// ) {
|
|
1480
|
+
// return info;
|
|
1481
|
+
// }
|
|
1482
|
+
// }
|
|
1483
|
+
// throw new Error("element doesn't contain text " + text);
|
|
1484
|
+
// } else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
|
|
1485
|
+
// info.foundText = foundObj?.text;
|
|
1486
|
+
// info.value = foundObj?.value;
|
|
1487
|
+
// throw new Error("element doesn't contain text " + text);
|
|
1488
|
+
// }
|
|
1489
|
+
// return info;
|
|
1392
1490
|
}
|
|
1393
1491
|
catch (e) {
|
|
1394
1492
|
//await this.closeUnexpectedPopups();
|
|
1395
|
-
this.logger.error("verify element contains text failed " + info.log);
|
|
1396
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1397
|
-
info.screenshotPath = screenshotPath;
|
|
1398
|
-
Object.assign(e, { info: info });
|
|
1399
|
-
error = e;
|
|
1400
|
-
throw e;
|
|
1493
|
+
// this.logger.error("verify element contains text failed " + info.log);
|
|
1494
|
+
// ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1495
|
+
// info.screenshotPath = screenshotPath;
|
|
1496
|
+
// Object.assign(e, { info: info });
|
|
1497
|
+
// error = e;
|
|
1498
|
+
// throw e;
|
|
1401
1499
|
}
|
|
1402
1500
|
finally {
|
|
1403
1501
|
const endTime = Date.now();
|
|
@@ -1451,15 +1549,62 @@ class StableBrowser {
|
|
|
1451
1549
|
// save the data to the file
|
|
1452
1550
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1453
1551
|
}
|
|
1552
|
+
_getDataFilePath(fileName) {
|
|
1553
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1554
|
+
if (fs.existsSync(dataFile)) {
|
|
1555
|
+
return dataFile;
|
|
1556
|
+
}
|
|
1557
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1558
|
+
if (fs.existsSync(dataFile)) {
|
|
1559
|
+
return dataFile;
|
|
1560
|
+
}
|
|
1561
|
+
throw new Error("data file not found " + fileName);
|
|
1562
|
+
}
|
|
1563
|
+
_parseCSVSync(filePath) {
|
|
1564
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1565
|
+
const results = [];
|
|
1566
|
+
return new Promise((resolve, reject) => {
|
|
1567
|
+
const readableStream = new Readable();
|
|
1568
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1569
|
+
readableStream.push(data);
|
|
1570
|
+
readableStream.push(null);
|
|
1571
|
+
readableStream
|
|
1572
|
+
.pipe(csv())
|
|
1573
|
+
.on("data", (data) => results.push(data))
|
|
1574
|
+
.on("end", () => resolve(results))
|
|
1575
|
+
.on("error", (error) => reject(error));
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1454
1578
|
loadTestData(type, dataSelector, world = null) {
|
|
1455
1579
|
switch (type) {
|
|
1456
1580
|
case "users":
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
-
|
|
1581
|
+
// get the users.json file path
|
|
1582
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1583
|
+
// read the file and return the data
|
|
1584
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1585
|
+
for (let i = 0; i < users.length; i++) {
|
|
1586
|
+
if (users[i].username === dataSelector) {
|
|
1587
|
+
const userObj = {
|
|
1588
|
+
username: users[i].username,
|
|
1589
|
+
password: "secret:" + users[i].password,
|
|
1590
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1591
|
+
};
|
|
1592
|
+
this.setTestData(userObj, world);
|
|
1593
|
+
return userObj;
|
|
1594
|
+
}
|
|
1460
1595
|
}
|
|
1596
|
+
throw new Error("user not found " + dataSelector);
|
|
1597
|
+
default:
|
|
1598
|
+
throw new Error("unknown type " + type);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1602
|
+
switch (type) {
|
|
1603
|
+
case "users": {
|
|
1604
|
+
// get the users.json file path
|
|
1605
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1461
1606
|
// read the file and return the data
|
|
1462
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1607
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1463
1608
|
for (let i = 0; i < users.length; i++) {
|
|
1464
1609
|
if (users[i].username === dataSelector) {
|
|
1465
1610
|
const userObj = {
|
|
@@ -1472,6 +1617,29 @@ class StableBrowser {
|
|
|
1472
1617
|
}
|
|
1473
1618
|
}
|
|
1474
1619
|
throw new Error("user not found " + dataSelector);
|
|
1620
|
+
}
|
|
1621
|
+
case "csv": {
|
|
1622
|
+
// the dataSelector should start with the file name followed by the row number: data.csv:1, if no row number is provided, it will default to 1
|
|
1623
|
+
const parts = dataSelector.split(":");
|
|
1624
|
+
let rowNumber = 0;
|
|
1625
|
+
if (parts.length > 1) {
|
|
1626
|
+
rowNumber = parseInt(parts[1]);
|
|
1627
|
+
}
|
|
1628
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1629
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1630
|
+
// result stracture:
|
|
1631
|
+
// [
|
|
1632
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1633
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1634
|
+
// ]
|
|
1635
|
+
// verify the row number is within the range
|
|
1636
|
+
if (rowNumber >= results.length) {
|
|
1637
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1638
|
+
}
|
|
1639
|
+
const data = results[rowNumber];
|
|
1640
|
+
this.setTestData(data, world);
|
|
1641
|
+
return data;
|
|
1642
|
+
}
|
|
1475
1643
|
default:
|
|
1476
1644
|
throw new Error("unknown type " + type);
|
|
1477
1645
|
}
|
|
@@ -1576,13 +1744,13 @@ class StableBrowser {
|
|
|
1576
1744
|
])));
|
|
1577
1745
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1578
1746
|
format: "png",
|
|
1579
|
-
clip: {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
},
|
|
1747
|
+
// clip: {
|
|
1748
|
+
// x: 0,
|
|
1749
|
+
// y: 0,
|
|
1750
|
+
// width: viewportWidth,
|
|
1751
|
+
// height: viewportHeight,
|
|
1752
|
+
// scale: 1,
|
|
1753
|
+
// },
|
|
1586
1754
|
});
|
|
1587
1755
|
if (!screenshotPath) {
|
|
1588
1756
|
return data;
|
|
@@ -1688,7 +1856,8 @@ class StableBrowser {
|
|
|
1688
1856
|
if (world) {
|
|
1689
1857
|
world[variable] = info.value;
|
|
1690
1858
|
}
|
|
1691
|
-
this.
|
|
1859
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1860
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1692
1861
|
return info;
|
|
1693
1862
|
}
|
|
1694
1863
|
catch (e) {
|
|
@@ -1725,6 +1894,91 @@ class StableBrowser {
|
|
|
1725
1894
|
});
|
|
1726
1895
|
}
|
|
1727
1896
|
}
|
|
1897
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1898
|
+
if (!emailAddress) {
|
|
1899
|
+
throw new Error("email address is null");
|
|
1900
|
+
}
|
|
1901
|
+
// check if address contain @
|
|
1902
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1903
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1904
|
+
}
|
|
1905
|
+
else {
|
|
1906
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1907
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const startTime = Date.now();
|
|
1911
|
+
let timeout = 60000;
|
|
1912
|
+
if (options && options.timeout) {
|
|
1913
|
+
timeout = options.timeout;
|
|
1914
|
+
}
|
|
1915
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1916
|
+
const request = {
|
|
1917
|
+
method: "POST",
|
|
1918
|
+
url: serviceUrl,
|
|
1919
|
+
headers: {
|
|
1920
|
+
"Content-Type": "application/json",
|
|
1921
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1922
|
+
},
|
|
1923
|
+
data: JSON.stringify({
|
|
1924
|
+
email: emailAddress,
|
|
1925
|
+
}),
|
|
1926
|
+
};
|
|
1927
|
+
let errorCount = 0;
|
|
1928
|
+
while (true) {
|
|
1929
|
+
try {
|
|
1930
|
+
let result = await this.context.api.request(request);
|
|
1931
|
+
// the response body expected to be the following:
|
|
1932
|
+
// {
|
|
1933
|
+
// "status": true,
|
|
1934
|
+
// "content": {
|
|
1935
|
+
// "url": "",
|
|
1936
|
+
// "code": "112112",
|
|
1937
|
+
// "name": "generate_link_or_code"
|
|
1938
|
+
// }
|
|
1939
|
+
//}
|
|
1940
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1941
|
+
let codeOrUrlFound = false;
|
|
1942
|
+
let emailCode = null;
|
|
1943
|
+
let emailUrl = null;
|
|
1944
|
+
// check if a code is returned
|
|
1945
|
+
if (result.data.content && result.data.content.code) {
|
|
1946
|
+
let code = result.data.content.code;
|
|
1947
|
+
this.setTestData({ emailCode: code }, world);
|
|
1948
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1949
|
+
emailCode = code;
|
|
1950
|
+
codeOrUrlFound = true;
|
|
1951
|
+
}
|
|
1952
|
+
// check if a url is returned
|
|
1953
|
+
if (result.data.content && result.data.content.url) {
|
|
1954
|
+
let url = result.data.content.url;
|
|
1955
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1956
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1957
|
+
emailUrl = url;
|
|
1958
|
+
codeOrUrlFound = true;
|
|
1959
|
+
}
|
|
1960
|
+
if (codeOrUrlFound) {
|
|
1961
|
+
return { emailUrl, emailCode };
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
this.logger.info("an email received but no code or url found");
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
catch (e) {
|
|
1969
|
+
errorCount++;
|
|
1970
|
+
if (errorCount > 3) {
|
|
1971
|
+
throw e;
|
|
1972
|
+
}
|
|
1973
|
+
// ignore
|
|
1974
|
+
}
|
|
1975
|
+
// check if the timeout is reached
|
|
1976
|
+
if (Date.now() - startTime > timeout) {
|
|
1977
|
+
throw new Error("timeout reached");
|
|
1978
|
+
}
|
|
1979
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1728
1982
|
async _highlightElements(scope, css) {
|
|
1729
1983
|
try {
|
|
1730
1984
|
if (!scope) {
|
|
@@ -1906,8 +2160,10 @@ class StableBrowser {
|
|
|
1906
2160
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1907
2161
|
await this._highlightElements(frame, dataAttribute);
|
|
1908
2162
|
const element = await frame.$(dataAttribute);
|
|
1909
|
-
|
|
1910
|
-
|
|
2163
|
+
if (element) {
|
|
2164
|
+
await this.scrollIfNeeded(element, info);
|
|
2165
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2166
|
+
}
|
|
1911
2167
|
}
|
|
1912
2168
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1913
2169
|
return info;
|
|
@@ -1945,6 +2201,16 @@ class StableBrowser {
|
|
|
1945
2201
|
});
|
|
1946
2202
|
}
|
|
1947
2203
|
}
|
|
2204
|
+
_getServerUrl() {
|
|
2205
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2206
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2207
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2208
|
+
}
|
|
2209
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2210
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2211
|
+
}
|
|
2212
|
+
return serviceUrl;
|
|
2213
|
+
}
|
|
1948
2214
|
async visualVerification(text, options = {}, world = null) {
|
|
1949
2215
|
const startTime = Date.now();
|
|
1950
2216
|
let error = null;
|
|
@@ -1959,13 +2225,7 @@ class StableBrowser {
|
|
|
1959
2225
|
throw new Error("TOKEN is not set");
|
|
1960
2226
|
}
|
|
1961
2227
|
try {
|
|
1962
|
-
let serviceUrl =
|
|
1963
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1964
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1965
|
-
}
|
|
1966
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1967
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1968
|
-
}
|
|
2228
|
+
let serviceUrl = this._getServerUrl();
|
|
1969
2229
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1970
2230
|
info.screenshotPath = screenshotPath;
|
|
1971
2231
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2114,7 +2374,16 @@ class StableBrowser {
|
|
|
2114
2374
|
let screenshotId = null;
|
|
2115
2375
|
let screenshotPath = null;
|
|
2116
2376
|
const info = {};
|
|
2117
|
-
info.log =
|
|
2377
|
+
info.log =
|
|
2378
|
+
"***** analyze table " +
|
|
2379
|
+
selectors.element_name +
|
|
2380
|
+
" query " +
|
|
2381
|
+
query +
|
|
2382
|
+
" operator " +
|
|
2383
|
+
operator +
|
|
2384
|
+
" value " +
|
|
2385
|
+
value +
|
|
2386
|
+
" *****\n";
|
|
2118
2387
|
info.operation = "analyzeTable";
|
|
2119
2388
|
info.selectors = selectors;
|
|
2120
2389
|
info.query = query;
|
|
@@ -2348,13 +2617,6 @@ class StableBrowser {
|
|
|
2348
2617
|
const info = {};
|
|
2349
2618
|
try {
|
|
2350
2619
|
await this.page.close();
|
|
2351
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2352
|
-
this.context.pages.pop();
|
|
2353
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2354
|
-
this.context.page = this.page;
|
|
2355
|
-
let title = await this.page.title();
|
|
2356
|
-
console.log("Switched to page " + title);
|
|
2357
|
-
}
|
|
2358
2620
|
}
|
|
2359
2621
|
catch (e) {
|
|
2360
2622
|
console.log(".");
|