automation_model 1.0.371-dev.0 → 1.0.371-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.d.ts +1 -1
- package/lib/api.js +2 -9
- package/lib/api.js.map +1 -1
- package/lib/stable_browser.d.ts +13 -1
- package/lib/stable_browser.js +388 -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)) {
|
|
@@ -168,25 +179,85 @@ class StableBrowser {
|
|
|
168
179
|
return text;
|
|
169
180
|
}
|
|
170
181
|
for (let key in _params) {
|
|
171
|
-
|
|
182
|
+
let regValue = key;
|
|
183
|
+
if (key.startsWith("_")) {
|
|
184
|
+
// remove the _ prefix
|
|
185
|
+
regValue = key.substring(1);
|
|
186
|
+
}
|
|
187
|
+
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
172
188
|
}
|
|
173
189
|
return text;
|
|
174
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
|
+
}
|
|
175
216
|
_getLocator(locator, scope, _params) {
|
|
217
|
+
locator = this._fixLocatorUsingParams(locator, _params);
|
|
218
|
+
let locatorReturn;
|
|
176
219
|
if (locator.role) {
|
|
177
220
|
if (locator.role[1].nameReg) {
|
|
178
221
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
179
222
|
delete locator.role[1].nameReg;
|
|
180
223
|
}
|
|
181
|
-
if (locator.role[1].name) {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
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]);
|
|
185
228
|
}
|
|
186
229
|
if (locator.css) {
|
|
187
|
-
|
|
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
|
+
}
|
|
188
243
|
}
|
|
189
|
-
|
|
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
|
+
selector = `[${selector}]`;
|
|
252
|
+
}
|
|
253
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!locatorReturn) {
|
|
257
|
+
console.error(locator);
|
|
258
|
+
throw new Error("Locator undefined");
|
|
259
|
+
}
|
|
260
|
+
return locatorReturn;
|
|
190
261
|
}
|
|
191
262
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
192
263
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -309,10 +380,13 @@ class StableBrowser {
|
|
|
309
380
|
}
|
|
310
381
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
311
382
|
let locatorSearch = selectorHierarchy[index];
|
|
312
|
-
info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
383
|
+
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
313
384
|
let locator = null;
|
|
314
385
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
315
386
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
387
|
+
if (!locatorString) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
316
390
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
317
391
|
}
|
|
318
392
|
else if (locatorSearch.text) {
|
|
@@ -334,7 +408,7 @@ class StableBrowser {
|
|
|
334
408
|
// cssHref = true;
|
|
335
409
|
// }
|
|
336
410
|
let count = await locator.count();
|
|
337
|
-
info.log += "total elements found " + count + "\n";
|
|
411
|
+
//info.log += "total elements found " + count + "\n";
|
|
338
412
|
//let visibleCount = 0;
|
|
339
413
|
let visibleLocator = null;
|
|
340
414
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -344,16 +418,20 @@ class StableBrowser {
|
|
|
344
418
|
for (let j = 0; j < count; j++) {
|
|
345
419
|
let visible = await locator.nth(j).isVisible();
|
|
346
420
|
const enabled = await locator.nth(j).isEnabled();
|
|
347
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
348
421
|
if (!visibleOnly) {
|
|
349
422
|
visible = true;
|
|
350
423
|
}
|
|
351
424
|
if (visible && enabled) {
|
|
352
425
|
foundLocators.push(locator.nth(j));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
if (!info.printMessages) {
|
|
429
|
+
info.printMessages = {};
|
|
430
|
+
}
|
|
431
|
+
if (!info.printMessages[j.toString()]) {
|
|
432
|
+
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
433
|
+
info.printMessages[j.toString()] = true;
|
|
434
|
+
}
|
|
357
435
|
}
|
|
358
436
|
}
|
|
359
437
|
}
|
|
@@ -399,6 +477,10 @@ class StableBrowser {
|
|
|
399
477
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
400
478
|
for (let i = 0; i < 3; i++) {
|
|
401
479
|
info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
|
|
480
|
+
for (let j = 0; j < selectors.locators.length; j++) {
|
|
481
|
+
let selector = selectors.locators[j];
|
|
482
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
483
|
+
}
|
|
402
484
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
403
485
|
if (!element.rerun) {
|
|
404
486
|
return element;
|
|
@@ -493,6 +575,7 @@ class StableBrowser {
|
|
|
493
575
|
let foundElements = result.foundElements;
|
|
494
576
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
495
577
|
info.box = foundElements[0].box;
|
|
578
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
496
579
|
return foundElements[0].locator;
|
|
497
580
|
}
|
|
498
581
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -522,6 +605,7 @@ class StableBrowser {
|
|
|
522
605
|
}
|
|
523
606
|
}
|
|
524
607
|
if (maxCountElement) {
|
|
608
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
525
609
|
info.box = await maxCountElement.locator.boundingBox();
|
|
526
610
|
return maxCountElement.locator;
|
|
527
611
|
}
|
|
@@ -530,9 +614,11 @@ class StableBrowser {
|
|
|
530
614
|
break;
|
|
531
615
|
}
|
|
532
616
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
617
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
533
618
|
highPriorityOnly = false;
|
|
534
619
|
}
|
|
535
620
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
621
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
536
622
|
visibleOnly = false;
|
|
537
623
|
}
|
|
538
624
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -576,6 +662,9 @@ class StableBrowser {
|
|
|
576
662
|
async click(selectors, _params, options = {}, world = null) {
|
|
577
663
|
this._validateSelectors(selectors);
|
|
578
664
|
const startTime = Date.now();
|
|
665
|
+
if (options && options.context) {
|
|
666
|
+
selectors.locators[0].text = options.context;
|
|
667
|
+
}
|
|
579
668
|
const info = {};
|
|
580
669
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
581
670
|
info.operation = "click";
|
|
@@ -884,71 +973,45 @@ class StableBrowser {
|
|
|
884
973
|
});
|
|
885
974
|
}
|
|
886
975
|
}
|
|
887
|
-
async
|
|
976
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
977
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
888
978
|
this._validateSelectors(selectors);
|
|
889
979
|
const startTime = Date.now();
|
|
890
|
-
let error = null;
|
|
891
|
-
let screenshotId = null;
|
|
892
|
-
let screenshotPath = null;
|
|
893
980
|
const info = {};
|
|
894
|
-
info.log = "";
|
|
895
|
-
info.operation =
|
|
981
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
982
|
+
info.operation = "setInputValue";
|
|
896
983
|
info.selectors = selectors;
|
|
984
|
+
value = this._fixUsingParams(value, _params);
|
|
897
985
|
info.value = value;
|
|
986
|
+
let error = null;
|
|
987
|
+
let screenshotId = null;
|
|
988
|
+
let screenshotPath = null;
|
|
898
989
|
try {
|
|
899
990
|
value = await this._replaceWithLocalData(value, this);
|
|
900
991
|
let element = await this._locate(selectors, info, _params);
|
|
901
|
-
//insert red border around the element
|
|
902
992
|
await this.scrollIfNeeded(element, info);
|
|
903
993
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
904
994
|
await this._highlightElements(element);
|
|
905
995
|
try {
|
|
906
|
-
await element.
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
value = dayjs(value).format(format);
|
|
910
|
-
await element.fill(value);
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
914
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
915
|
-
el.value = ""; // clear input
|
|
916
|
-
el.value = dateTimeValue;
|
|
917
|
-
}, dateTimeValue);
|
|
918
|
-
}
|
|
919
|
-
if (enter) {
|
|
920
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
921
|
-
await this.page.keyboard.press("Enter");
|
|
922
|
-
await this.waitForPageLoad();
|
|
923
|
-
}
|
|
996
|
+
await element.evaluateHandle((el, value) => {
|
|
997
|
+
el.value = value;
|
|
998
|
+
}, value);
|
|
924
999
|
}
|
|
925
1000
|
catch (error) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1001
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1002
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
929
1003
|
info.screenshotPath = screenshotPath;
|
|
930
1004
|
Object.assign(error, { info: info });
|
|
931
|
-
await element.
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
value = dayjs(value).format(format);
|
|
935
|
-
await element.fill(value);
|
|
936
|
-
}
|
|
937
|
-
else {
|
|
938
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
939
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
940
|
-
el.value = ""; // clear input
|
|
941
|
-
el.value = dateTimeValue;
|
|
942
|
-
}, dateTimeValue);
|
|
943
|
-
}
|
|
944
|
-
if (enter) {
|
|
945
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
946
|
-
await this.page.keyboard.press("Enter");
|
|
947
|
-
await this.waitForPageLoad();
|
|
948
|
-
}
|
|
1005
|
+
await element.evaluateHandle((el, value) => {
|
|
1006
|
+
el.value = value;
|
|
1007
|
+
});
|
|
949
1008
|
}
|
|
950
1009
|
}
|
|
951
|
-
catch (
|
|
1010
|
+
catch (e) {
|
|
1011
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1012
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1013
|
+
info.screenshotPath = screenshotPath;
|
|
1014
|
+
Object.assign(e, { info: info });
|
|
952
1015
|
error = e;
|
|
953
1016
|
throw e;
|
|
954
1017
|
}
|
|
@@ -956,10 +1019,10 @@ class StableBrowser {
|
|
|
956
1019
|
const endTime = Date.now();
|
|
957
1020
|
this._reportToWorld(world, {
|
|
958
1021
|
element_name: selectors.element_name,
|
|
959
|
-
type: Types.
|
|
960
|
-
|
|
1022
|
+
type: Types.SET_INPUT,
|
|
1023
|
+
text: `Set input value`,
|
|
961
1024
|
value: value,
|
|
962
|
-
|
|
1025
|
+
screenshotId,
|
|
963
1026
|
result: error
|
|
964
1027
|
? {
|
|
965
1028
|
status: "FAILED",
|
|
@@ -976,7 +1039,7 @@ class StableBrowser {
|
|
|
976
1039
|
});
|
|
977
1040
|
}
|
|
978
1041
|
}
|
|
979
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1042
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
980
1043
|
this._validateSelectors(selectors);
|
|
981
1044
|
const startTime = Date.now();
|
|
982
1045
|
let error = null;
|
|
@@ -988,6 +1051,7 @@ class StableBrowser {
|
|
|
988
1051
|
info.selectors = selectors;
|
|
989
1052
|
info.value = value;
|
|
990
1053
|
try {
|
|
1054
|
+
value = await this._replaceWithLocalData(value, this);
|
|
991
1055
|
let element = await this._locate(selectors, info, _params);
|
|
992
1056
|
//insert red border around the element
|
|
993
1057
|
await this.scrollIfNeeded(element, info);
|
|
@@ -996,28 +1060,51 @@ class StableBrowser {
|
|
|
996
1060
|
try {
|
|
997
1061
|
await element.click();
|
|
998
1062
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1063
|
+
if (format) {
|
|
1064
|
+
value = dayjs(value).format(format);
|
|
1065
|
+
await element.fill(value);
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1069
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1070
|
+
el.value = ""; // clear input
|
|
1071
|
+
el.value = dateTimeValue;
|
|
1072
|
+
}, dateTimeValue);
|
|
1073
|
+
}
|
|
1074
|
+
if (enter) {
|
|
1075
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1076
|
+
await this.page.keyboard.press("Enter");
|
|
1077
|
+
await this.waitForPageLoad();
|
|
1078
|
+
}
|
|
1004
1079
|
}
|
|
1005
|
-
catch (
|
|
1080
|
+
catch (err) {
|
|
1006
1081
|
//await this.closeUnexpectedPopups();
|
|
1007
1082
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1008
|
-
this.logger.info("Trying again")
|
|
1083
|
+
this.logger.info("Trying again");
|
|
1084
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1009
1085
|
info.screenshotPath = screenshotPath;
|
|
1010
|
-
Object.assign(
|
|
1086
|
+
Object.assign(err, { info: info });
|
|
1011
1087
|
await element.click();
|
|
1012
1088
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1089
|
+
if (format) {
|
|
1090
|
+
value = dayjs(value).format(format);
|
|
1091
|
+
await element.fill(value);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1095
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1096
|
+
el.value = ""; // clear input
|
|
1097
|
+
el.value = dateTimeValue;
|
|
1098
|
+
}, dateTimeValue);
|
|
1099
|
+
}
|
|
1100
|
+
if (enter) {
|
|
1101
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1102
|
+
await this.page.keyboard.press("Enter");
|
|
1103
|
+
await this.waitForPageLoad();
|
|
1104
|
+
}
|
|
1018
1105
|
}
|
|
1019
1106
|
}
|
|
1020
|
-
catch (
|
|
1107
|
+
catch (e) {
|
|
1021
1108
|
error = e;
|
|
1022
1109
|
throw e;
|
|
1023
1110
|
}
|
|
@@ -1067,20 +1154,32 @@ class StableBrowser {
|
|
|
1067
1154
|
await this.scrollIfNeeded(element, info);
|
|
1068
1155
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1069
1156
|
await this._highlightElements(element);
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1157
|
+
if (options === null || options === undefined || !options.press) {
|
|
1158
|
+
try {
|
|
1159
|
+
let currentValue = await element.inputValue();
|
|
1160
|
+
if (currentValue) {
|
|
1161
|
+
await element.fill("");
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
catch (e) {
|
|
1165
|
+
this.logger.info("unable to clear input value");
|
|
1074
1166
|
}
|
|
1075
1167
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1168
|
+
if (options === null || options === undefined || options.press) {
|
|
1169
|
+
try {
|
|
1170
|
+
await element.click({ timeout: 5000 });
|
|
1171
|
+
}
|
|
1172
|
+
catch (e) {
|
|
1173
|
+
await element.dispatchEvent("click");
|
|
1174
|
+
}
|
|
1081
1175
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1176
|
+
else {
|
|
1177
|
+
try {
|
|
1178
|
+
await element.focus();
|
|
1179
|
+
}
|
|
1180
|
+
catch (e) {
|
|
1181
|
+
await element.dispatchEvent("focus");
|
|
1182
|
+
}
|
|
1084
1183
|
}
|
|
1085
1184
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1086
1185
|
const valueSegment = _value.split("&&");
|
|
@@ -1276,7 +1375,8 @@ class StableBrowser {
|
|
|
1276
1375
|
let screenshotId = null;
|
|
1277
1376
|
let screenshotPath = null;
|
|
1278
1377
|
const info = {};
|
|
1279
|
-
info.log =
|
|
1378
|
+
info.log =
|
|
1379
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1280
1380
|
info.operation = "containsPattern";
|
|
1281
1381
|
info.selectors = selectors;
|
|
1282
1382
|
info.value = text;
|
|
@@ -1446,15 +1546,62 @@ class StableBrowser {
|
|
|
1446
1546
|
// save the data to the file
|
|
1447
1547
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1448
1548
|
}
|
|
1549
|
+
_getDataFilePath(fileName) {
|
|
1550
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1551
|
+
if (fs.existsSync(dataFile)) {
|
|
1552
|
+
return dataFile;
|
|
1553
|
+
}
|
|
1554
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1555
|
+
if (fs.existsSync(dataFile)) {
|
|
1556
|
+
return dataFile;
|
|
1557
|
+
}
|
|
1558
|
+
throw new Error("data file not found " + fileName);
|
|
1559
|
+
}
|
|
1560
|
+
_parseCSVSync(filePath) {
|
|
1561
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1562
|
+
const results = [];
|
|
1563
|
+
return new Promise((resolve, reject) => {
|
|
1564
|
+
const readableStream = new Readable();
|
|
1565
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1566
|
+
readableStream.push(data);
|
|
1567
|
+
readableStream.push(null);
|
|
1568
|
+
readableStream
|
|
1569
|
+
.pipe(csv())
|
|
1570
|
+
.on("data", (data) => results.push(data))
|
|
1571
|
+
.on("end", () => resolve(results))
|
|
1572
|
+
.on("error", (error) => reject(error));
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1449
1575
|
loadTestData(type, dataSelector, world = null) {
|
|
1450
1576
|
switch (type) {
|
|
1451
1577
|
case "users":
|
|
1452
|
-
//
|
|
1453
|
-
|
|
1454
|
-
|
|
1578
|
+
// get the users.json file path
|
|
1579
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1580
|
+
// read the file and return the data
|
|
1581
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1582
|
+
for (let i = 0; i < users.length; i++) {
|
|
1583
|
+
if (users[i].username === dataSelector) {
|
|
1584
|
+
const userObj = {
|
|
1585
|
+
username: users[i].username,
|
|
1586
|
+
password: "secret:" + users[i].password,
|
|
1587
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1588
|
+
};
|
|
1589
|
+
this.setTestData(userObj, world);
|
|
1590
|
+
return userObj;
|
|
1591
|
+
}
|
|
1455
1592
|
}
|
|
1593
|
+
throw new Error("user not found " + dataSelector);
|
|
1594
|
+
default:
|
|
1595
|
+
throw new Error("unknown type " + type);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1599
|
+
switch (type) {
|
|
1600
|
+
case "users": {
|
|
1601
|
+
// get the users.json file path
|
|
1602
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1456
1603
|
// read the file and return the data
|
|
1457
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1604
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1458
1605
|
for (let i = 0; i < users.length; i++) {
|
|
1459
1606
|
if (users[i].username === dataSelector) {
|
|
1460
1607
|
const userObj = {
|
|
@@ -1467,6 +1614,29 @@ class StableBrowser {
|
|
|
1467
1614
|
}
|
|
1468
1615
|
}
|
|
1469
1616
|
throw new Error("user not found " + dataSelector);
|
|
1617
|
+
}
|
|
1618
|
+
case "csv": {
|
|
1619
|
+
// 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
|
|
1620
|
+
const parts = dataSelector.split(":");
|
|
1621
|
+
let rowNumber = 0;
|
|
1622
|
+
if (parts.length > 1) {
|
|
1623
|
+
rowNumber = parseInt(parts[1]);
|
|
1624
|
+
}
|
|
1625
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1626
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1627
|
+
// result stracture:
|
|
1628
|
+
// [
|
|
1629
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1630
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1631
|
+
// ]
|
|
1632
|
+
// verify the row number is within the range
|
|
1633
|
+
if (rowNumber >= results.length) {
|
|
1634
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1635
|
+
}
|
|
1636
|
+
const data = results[rowNumber];
|
|
1637
|
+
this.setTestData(data, world);
|
|
1638
|
+
return data;
|
|
1639
|
+
}
|
|
1470
1640
|
default:
|
|
1471
1641
|
throw new Error("unknown type " + type);
|
|
1472
1642
|
}
|
|
@@ -1571,13 +1741,13 @@ class StableBrowser {
|
|
|
1571
1741
|
])));
|
|
1572
1742
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1573
1743
|
format: "png",
|
|
1574
|
-
clip: {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
},
|
|
1744
|
+
// clip: {
|
|
1745
|
+
// x: 0,
|
|
1746
|
+
// y: 0,
|
|
1747
|
+
// width: viewportWidth,
|
|
1748
|
+
// height: viewportHeight,
|
|
1749
|
+
// scale: 1,
|
|
1750
|
+
// },
|
|
1581
1751
|
});
|
|
1582
1752
|
if (!screenshotPath) {
|
|
1583
1753
|
return data;
|
|
@@ -1683,7 +1853,8 @@ class StableBrowser {
|
|
|
1683
1853
|
if (world) {
|
|
1684
1854
|
world[variable] = info.value;
|
|
1685
1855
|
}
|
|
1686
|
-
this.
|
|
1856
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1857
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1687
1858
|
return info;
|
|
1688
1859
|
}
|
|
1689
1860
|
catch (e) {
|
|
@@ -1720,6 +1891,91 @@ class StableBrowser {
|
|
|
1720
1891
|
});
|
|
1721
1892
|
}
|
|
1722
1893
|
}
|
|
1894
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1895
|
+
if (!emailAddress) {
|
|
1896
|
+
throw new Error("email address is null");
|
|
1897
|
+
}
|
|
1898
|
+
// check if address contain @
|
|
1899
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1900
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1901
|
+
}
|
|
1902
|
+
else {
|
|
1903
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1904
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const startTime = Date.now();
|
|
1908
|
+
let timeout = 60000;
|
|
1909
|
+
if (options && options.timeout) {
|
|
1910
|
+
timeout = options.timeout;
|
|
1911
|
+
}
|
|
1912
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1913
|
+
const request = {
|
|
1914
|
+
method: "POST",
|
|
1915
|
+
url: serviceUrl,
|
|
1916
|
+
headers: {
|
|
1917
|
+
"Content-Type": "application/json",
|
|
1918
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1919
|
+
},
|
|
1920
|
+
data: JSON.stringify({
|
|
1921
|
+
email: emailAddress,
|
|
1922
|
+
}),
|
|
1923
|
+
};
|
|
1924
|
+
let errorCount = 0;
|
|
1925
|
+
while (true) {
|
|
1926
|
+
try {
|
|
1927
|
+
let result = await this.context.api.request(request);
|
|
1928
|
+
// the response body expected to be the following:
|
|
1929
|
+
// {
|
|
1930
|
+
// "status": true,
|
|
1931
|
+
// "content": {
|
|
1932
|
+
// "url": "",
|
|
1933
|
+
// "code": "112112",
|
|
1934
|
+
// "name": "generate_link_or_code"
|
|
1935
|
+
// }
|
|
1936
|
+
//}
|
|
1937
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1938
|
+
let codeOrUrlFound = false;
|
|
1939
|
+
let emailCode = null;
|
|
1940
|
+
let emailUrl = null;
|
|
1941
|
+
// check if a code is returned
|
|
1942
|
+
if (result.data.content && result.data.content.code) {
|
|
1943
|
+
let code = result.data.content.code;
|
|
1944
|
+
this.setTestData({ emailCode: code }, world);
|
|
1945
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1946
|
+
emailCode = code;
|
|
1947
|
+
codeOrUrlFound = true;
|
|
1948
|
+
}
|
|
1949
|
+
// check if a url is returned
|
|
1950
|
+
if (result.data.content && result.data.content.url) {
|
|
1951
|
+
let url = result.data.content.url;
|
|
1952
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1953
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1954
|
+
emailUrl = url;
|
|
1955
|
+
codeOrUrlFound = true;
|
|
1956
|
+
}
|
|
1957
|
+
if (codeOrUrlFound) {
|
|
1958
|
+
return { emailUrl, emailCode };
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
this.logger.info("an email received but no code or url found");
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
catch (e) {
|
|
1966
|
+
errorCount++;
|
|
1967
|
+
if (errorCount > 3) {
|
|
1968
|
+
throw e;
|
|
1969
|
+
}
|
|
1970
|
+
// ignore
|
|
1971
|
+
}
|
|
1972
|
+
// check if the timeout is reached
|
|
1973
|
+
if (Date.now() - startTime > timeout) {
|
|
1974
|
+
throw new Error("timeout reached");
|
|
1975
|
+
}
|
|
1976
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1723
1979
|
async _highlightElements(scope, css) {
|
|
1724
1980
|
try {
|
|
1725
1981
|
if (!scope) {
|
|
@@ -1901,8 +2157,10 @@ class StableBrowser {
|
|
|
1901
2157
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1902
2158
|
await this._highlightElements(frame, dataAttribute);
|
|
1903
2159
|
const element = await frame.$(dataAttribute);
|
|
1904
|
-
|
|
1905
|
-
|
|
2160
|
+
if (element) {
|
|
2161
|
+
await this.scrollIfNeeded(element, info);
|
|
2162
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2163
|
+
}
|
|
1906
2164
|
}
|
|
1907
2165
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1908
2166
|
return info;
|
|
@@ -1940,6 +2198,16 @@ class StableBrowser {
|
|
|
1940
2198
|
});
|
|
1941
2199
|
}
|
|
1942
2200
|
}
|
|
2201
|
+
_getServerUrl() {
|
|
2202
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2203
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2204
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2205
|
+
}
|
|
2206
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2207
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2208
|
+
}
|
|
2209
|
+
return serviceUrl;
|
|
2210
|
+
}
|
|
1943
2211
|
async visualVerification(text, options = {}, world = null) {
|
|
1944
2212
|
const startTime = Date.now();
|
|
1945
2213
|
let error = null;
|
|
@@ -1954,13 +2222,7 @@ class StableBrowser {
|
|
|
1954
2222
|
throw new Error("TOKEN is not set");
|
|
1955
2223
|
}
|
|
1956
2224
|
try {
|
|
1957
|
-
let serviceUrl =
|
|
1958
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1959
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1960
|
-
}
|
|
1961
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1962
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1963
|
-
}
|
|
2225
|
+
let serviceUrl = this._getServerUrl();
|
|
1964
2226
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1965
2227
|
info.screenshotPath = screenshotPath;
|
|
1966
2228
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2109,7 +2371,16 @@ class StableBrowser {
|
|
|
2109
2371
|
let screenshotId = null;
|
|
2110
2372
|
let screenshotPath = null;
|
|
2111
2373
|
const info = {};
|
|
2112
|
-
info.log =
|
|
2374
|
+
info.log =
|
|
2375
|
+
"***** analyze table " +
|
|
2376
|
+
selectors.element_name +
|
|
2377
|
+
" query " +
|
|
2378
|
+
query +
|
|
2379
|
+
" operator " +
|
|
2380
|
+
operator +
|
|
2381
|
+
" value " +
|
|
2382
|
+
value +
|
|
2383
|
+
" *****\n";
|
|
2113
2384
|
info.operation = "analyzeTable";
|
|
2114
2385
|
info.selectors = selectors;
|
|
2115
2386
|
info.query = query;
|
|
@@ -2343,13 +2614,6 @@ class StableBrowser {
|
|
|
2343
2614
|
const info = {};
|
|
2344
2615
|
try {
|
|
2345
2616
|
await this.page.close();
|
|
2346
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2347
|
-
this.context.pages.pop();
|
|
2348
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2349
|
-
this.context.page = this.page;
|
|
2350
|
-
let title = await this.page.title();
|
|
2351
|
-
console.log("Switched to page " + title);
|
|
2352
|
-
}
|
|
2353
2617
|
}
|
|
2354
2618
|
catch (e) {
|
|
2355
2619
|
console.log(".");
|