automation_model 1.0.372-dev.0 → 1.0.372-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/stable_browser.d.ts +13 -1
- package/lib/stable_browser.js +391 -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,88 @@ 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.engine === "internal:text") {
|
|
245
|
+
// extract the text and the i flag using regex
|
|
246
|
+
const match = locator.selector.match(/"(.*)"(.*)/);
|
|
247
|
+
if (match) {
|
|
248
|
+
const text = match[1];
|
|
249
|
+
const flags = match[2];
|
|
250
|
+
locatorReturn = scope.locator(`text=${text}`, { exact: flags === "i" });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (locator.engine === "internal:attr") {
|
|
254
|
+
if (!selector.startsWith("[")) {
|
|
255
|
+
selector = `[${selector}]`;
|
|
256
|
+
}
|
|
257
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
258
|
+
}
|
|
259
|
+
if (!locatorReturn) {
|
|
260
|
+
console.error(locator);
|
|
261
|
+
throw new Error("Locator " + JSON.stringify(locator) + " not found");
|
|
262
|
+
}
|
|
263
|
+
return locatorReturn;
|
|
190
264
|
}
|
|
191
265
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
192
266
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -309,10 +383,13 @@ class StableBrowser {
|
|
|
309
383
|
}
|
|
310
384
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
311
385
|
let locatorSearch = selectorHierarchy[index];
|
|
312
|
-
info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
386
|
+
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
313
387
|
let locator = null;
|
|
314
388
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
315
389
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
390
|
+
if (!locatorString) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
316
393
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
317
394
|
}
|
|
318
395
|
else if (locatorSearch.text) {
|
|
@@ -334,7 +411,7 @@ class StableBrowser {
|
|
|
334
411
|
// cssHref = true;
|
|
335
412
|
// }
|
|
336
413
|
let count = await locator.count();
|
|
337
|
-
info.log += "total elements found " + count + "\n";
|
|
414
|
+
//info.log += "total elements found " + count + "\n";
|
|
338
415
|
//let visibleCount = 0;
|
|
339
416
|
let visibleLocator = null;
|
|
340
417
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -344,16 +421,20 @@ class StableBrowser {
|
|
|
344
421
|
for (let j = 0; j < count; j++) {
|
|
345
422
|
let visible = await locator.nth(j).isVisible();
|
|
346
423
|
const enabled = await locator.nth(j).isEnabled();
|
|
347
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
348
424
|
if (!visibleOnly) {
|
|
349
425
|
visible = true;
|
|
350
426
|
}
|
|
351
427
|
if (visible && enabled) {
|
|
352
428
|
foundLocators.push(locator.nth(j));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
if (!info.printMessages) {
|
|
432
|
+
info.printMessages = {};
|
|
433
|
+
}
|
|
434
|
+
if (!info.printMessages[j.toString()]) {
|
|
435
|
+
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
436
|
+
info.printMessages[j.toString()] = true;
|
|
437
|
+
}
|
|
357
438
|
}
|
|
358
439
|
}
|
|
359
440
|
}
|
|
@@ -399,6 +480,10 @@ class StableBrowser {
|
|
|
399
480
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
400
481
|
for (let i = 0; i < 3; i++) {
|
|
401
482
|
info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
|
|
483
|
+
for (let j = 0; j < selectors.locators.length; j++) {
|
|
484
|
+
let selector = selectors.locators[j];
|
|
485
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
486
|
+
}
|
|
402
487
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
403
488
|
if (!element.rerun) {
|
|
404
489
|
return element;
|
|
@@ -493,6 +578,7 @@ class StableBrowser {
|
|
|
493
578
|
let foundElements = result.foundElements;
|
|
494
579
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
495
580
|
info.box = foundElements[0].box;
|
|
581
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
496
582
|
return foundElements[0].locator;
|
|
497
583
|
}
|
|
498
584
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -522,6 +608,7 @@ class StableBrowser {
|
|
|
522
608
|
}
|
|
523
609
|
}
|
|
524
610
|
if (maxCountElement) {
|
|
611
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
525
612
|
info.box = await maxCountElement.locator.boundingBox();
|
|
526
613
|
return maxCountElement.locator;
|
|
527
614
|
}
|
|
@@ -530,9 +617,11 @@ class StableBrowser {
|
|
|
530
617
|
break;
|
|
531
618
|
}
|
|
532
619
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
620
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
533
621
|
highPriorityOnly = false;
|
|
534
622
|
}
|
|
535
623
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
624
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
536
625
|
visibleOnly = false;
|
|
537
626
|
}
|
|
538
627
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -576,6 +665,9 @@ class StableBrowser {
|
|
|
576
665
|
async click(selectors, _params, options = {}, world = null) {
|
|
577
666
|
this._validateSelectors(selectors);
|
|
578
667
|
const startTime = Date.now();
|
|
668
|
+
if (options && options.context) {
|
|
669
|
+
selectors.locators[0].text = options.context;
|
|
670
|
+
}
|
|
579
671
|
const info = {};
|
|
580
672
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
581
673
|
info.operation = "click";
|
|
@@ -884,71 +976,45 @@ class StableBrowser {
|
|
|
884
976
|
});
|
|
885
977
|
}
|
|
886
978
|
}
|
|
887
|
-
async
|
|
979
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
980
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
888
981
|
this._validateSelectors(selectors);
|
|
889
982
|
const startTime = Date.now();
|
|
890
|
-
let error = null;
|
|
891
|
-
let screenshotId = null;
|
|
892
|
-
let screenshotPath = null;
|
|
893
983
|
const info = {};
|
|
894
|
-
info.log = "";
|
|
895
|
-
info.operation =
|
|
984
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
985
|
+
info.operation = "setInputValue";
|
|
896
986
|
info.selectors = selectors;
|
|
987
|
+
value = this._fixUsingParams(value, _params);
|
|
897
988
|
info.value = value;
|
|
989
|
+
let error = null;
|
|
990
|
+
let screenshotId = null;
|
|
991
|
+
let screenshotPath = null;
|
|
898
992
|
try {
|
|
899
993
|
value = await this._replaceWithLocalData(value, this);
|
|
900
994
|
let element = await this._locate(selectors, info, _params);
|
|
901
|
-
//insert red border around the element
|
|
902
995
|
await this.scrollIfNeeded(element, info);
|
|
903
996
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
904
997
|
await this._highlightElements(element);
|
|
905
998
|
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
|
-
}
|
|
999
|
+
await element.evaluateHandle((el, value) => {
|
|
1000
|
+
el.value = value;
|
|
1001
|
+
}, value);
|
|
924
1002
|
}
|
|
925
1003
|
catch (error) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1004
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1005
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
929
1006
|
info.screenshotPath = screenshotPath;
|
|
930
1007
|
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
|
-
}
|
|
1008
|
+
await element.evaluateHandle((el, value) => {
|
|
1009
|
+
el.value = value;
|
|
1010
|
+
});
|
|
949
1011
|
}
|
|
950
1012
|
}
|
|
951
|
-
catch (
|
|
1013
|
+
catch (e) {
|
|
1014
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1015
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1016
|
+
info.screenshotPath = screenshotPath;
|
|
1017
|
+
Object.assign(e, { info: info });
|
|
952
1018
|
error = e;
|
|
953
1019
|
throw e;
|
|
954
1020
|
}
|
|
@@ -956,10 +1022,10 @@ class StableBrowser {
|
|
|
956
1022
|
const endTime = Date.now();
|
|
957
1023
|
this._reportToWorld(world, {
|
|
958
1024
|
element_name: selectors.element_name,
|
|
959
|
-
type: Types.
|
|
960
|
-
|
|
1025
|
+
type: Types.SET_INPUT,
|
|
1026
|
+
text: `Set input value`,
|
|
961
1027
|
value: value,
|
|
962
|
-
|
|
1028
|
+
screenshotId,
|
|
963
1029
|
result: error
|
|
964
1030
|
? {
|
|
965
1031
|
status: "FAILED",
|
|
@@ -976,7 +1042,7 @@ class StableBrowser {
|
|
|
976
1042
|
});
|
|
977
1043
|
}
|
|
978
1044
|
}
|
|
979
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1045
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
980
1046
|
this._validateSelectors(selectors);
|
|
981
1047
|
const startTime = Date.now();
|
|
982
1048
|
let error = null;
|
|
@@ -988,6 +1054,7 @@ class StableBrowser {
|
|
|
988
1054
|
info.selectors = selectors;
|
|
989
1055
|
info.value = value;
|
|
990
1056
|
try {
|
|
1057
|
+
value = await this._replaceWithLocalData(value, this);
|
|
991
1058
|
let element = await this._locate(selectors, info, _params);
|
|
992
1059
|
//insert red border around the element
|
|
993
1060
|
await this.scrollIfNeeded(element, info);
|
|
@@ -996,28 +1063,51 @@ class StableBrowser {
|
|
|
996
1063
|
try {
|
|
997
1064
|
await element.click();
|
|
998
1065
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1066
|
+
if (format) {
|
|
1067
|
+
value = dayjs(value).format(format);
|
|
1068
|
+
await element.fill(value);
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1072
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1073
|
+
el.value = ""; // clear input
|
|
1074
|
+
el.value = dateTimeValue;
|
|
1075
|
+
}, dateTimeValue);
|
|
1076
|
+
}
|
|
1077
|
+
if (enter) {
|
|
1078
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1079
|
+
await this.page.keyboard.press("Enter");
|
|
1080
|
+
await this.waitForPageLoad();
|
|
1081
|
+
}
|
|
1004
1082
|
}
|
|
1005
|
-
catch (
|
|
1083
|
+
catch (err) {
|
|
1006
1084
|
//await this.closeUnexpectedPopups();
|
|
1007
1085
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1008
|
-
this.logger.info("Trying again")
|
|
1086
|
+
this.logger.info("Trying again");
|
|
1087
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1009
1088
|
info.screenshotPath = screenshotPath;
|
|
1010
|
-
Object.assign(
|
|
1089
|
+
Object.assign(err, { info: info });
|
|
1011
1090
|
await element.click();
|
|
1012
1091
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1092
|
+
if (format) {
|
|
1093
|
+
value = dayjs(value).format(format);
|
|
1094
|
+
await element.fill(value);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1098
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1099
|
+
el.value = ""; // clear input
|
|
1100
|
+
el.value = dateTimeValue;
|
|
1101
|
+
}, dateTimeValue);
|
|
1102
|
+
}
|
|
1103
|
+
if (enter) {
|
|
1104
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1105
|
+
await this.page.keyboard.press("Enter");
|
|
1106
|
+
await this.waitForPageLoad();
|
|
1107
|
+
}
|
|
1018
1108
|
}
|
|
1019
1109
|
}
|
|
1020
|
-
catch (
|
|
1110
|
+
catch (e) {
|
|
1021
1111
|
error = e;
|
|
1022
1112
|
throw e;
|
|
1023
1113
|
}
|
|
@@ -1067,20 +1157,32 @@ class StableBrowser {
|
|
|
1067
1157
|
await this.scrollIfNeeded(element, info);
|
|
1068
1158
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1069
1159
|
await this._highlightElements(element);
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1160
|
+
if (options === null || options === undefined || !options.press) {
|
|
1161
|
+
try {
|
|
1162
|
+
let currentValue = await element.inputValue();
|
|
1163
|
+
if (currentValue) {
|
|
1164
|
+
await element.fill("");
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
catch (e) {
|
|
1168
|
+
this.logger.info("unable to clear input value");
|
|
1074
1169
|
}
|
|
1075
1170
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1171
|
+
if (options === null || options === undefined || options.press) {
|
|
1172
|
+
try {
|
|
1173
|
+
await element.click({ timeout: 5000 });
|
|
1174
|
+
}
|
|
1175
|
+
catch (e) {
|
|
1176
|
+
await element.dispatchEvent("click");
|
|
1177
|
+
}
|
|
1081
1178
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1179
|
+
else {
|
|
1180
|
+
try {
|
|
1181
|
+
await element.focus();
|
|
1182
|
+
}
|
|
1183
|
+
catch (e) {
|
|
1184
|
+
await element.dispatchEvent("focus");
|
|
1185
|
+
}
|
|
1084
1186
|
}
|
|
1085
1187
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1086
1188
|
const valueSegment = _value.split("&&");
|
|
@@ -1276,7 +1378,8 @@ class StableBrowser {
|
|
|
1276
1378
|
let screenshotId = null;
|
|
1277
1379
|
let screenshotPath = null;
|
|
1278
1380
|
const info = {};
|
|
1279
|
-
info.log =
|
|
1381
|
+
info.log =
|
|
1382
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1280
1383
|
info.operation = "containsPattern";
|
|
1281
1384
|
info.selectors = selectors;
|
|
1282
1385
|
info.value = text;
|
|
@@ -1446,15 +1549,62 @@ class StableBrowser {
|
|
|
1446
1549
|
// save the data to the file
|
|
1447
1550
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1448
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
|
+
}
|
|
1449
1578
|
loadTestData(type, dataSelector, world = null) {
|
|
1450
1579
|
switch (type) {
|
|
1451
1580
|
case "users":
|
|
1452
|
-
//
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
+
}
|
|
1455
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");
|
|
1456
1606
|
// read the file and return the data
|
|
1457
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1607
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1458
1608
|
for (let i = 0; i < users.length; i++) {
|
|
1459
1609
|
if (users[i].username === dataSelector) {
|
|
1460
1610
|
const userObj = {
|
|
@@ -1467,6 +1617,29 @@ class StableBrowser {
|
|
|
1467
1617
|
}
|
|
1468
1618
|
}
|
|
1469
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
|
+
}
|
|
1470
1643
|
default:
|
|
1471
1644
|
throw new Error("unknown type " + type);
|
|
1472
1645
|
}
|
|
@@ -1571,13 +1744,13 @@ class StableBrowser {
|
|
|
1571
1744
|
])));
|
|
1572
1745
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1573
1746
|
format: "png",
|
|
1574
|
-
clip: {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
},
|
|
1747
|
+
// clip: {
|
|
1748
|
+
// x: 0,
|
|
1749
|
+
// y: 0,
|
|
1750
|
+
// width: viewportWidth,
|
|
1751
|
+
// height: viewportHeight,
|
|
1752
|
+
// scale: 1,
|
|
1753
|
+
// },
|
|
1581
1754
|
});
|
|
1582
1755
|
if (!screenshotPath) {
|
|
1583
1756
|
return data;
|
|
@@ -1683,7 +1856,8 @@ class StableBrowser {
|
|
|
1683
1856
|
if (world) {
|
|
1684
1857
|
world[variable] = info.value;
|
|
1685
1858
|
}
|
|
1686
|
-
this.
|
|
1859
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1860
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1687
1861
|
return info;
|
|
1688
1862
|
}
|
|
1689
1863
|
catch (e) {
|
|
@@ -1720,6 +1894,91 @@ class StableBrowser {
|
|
|
1720
1894
|
});
|
|
1721
1895
|
}
|
|
1722
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
|
+
}
|
|
1723
1982
|
async _highlightElements(scope, css) {
|
|
1724
1983
|
try {
|
|
1725
1984
|
if (!scope) {
|
|
@@ -1901,8 +2160,10 @@ class StableBrowser {
|
|
|
1901
2160
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1902
2161
|
await this._highlightElements(frame, dataAttribute);
|
|
1903
2162
|
const element = await frame.$(dataAttribute);
|
|
1904
|
-
|
|
1905
|
-
|
|
2163
|
+
if (element) {
|
|
2164
|
+
await this.scrollIfNeeded(element, info);
|
|
2165
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2166
|
+
}
|
|
1906
2167
|
}
|
|
1907
2168
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1908
2169
|
return info;
|
|
@@ -1940,6 +2201,16 @@ class StableBrowser {
|
|
|
1940
2201
|
});
|
|
1941
2202
|
}
|
|
1942
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
|
+
}
|
|
1943
2214
|
async visualVerification(text, options = {}, world = null) {
|
|
1944
2215
|
const startTime = Date.now();
|
|
1945
2216
|
let error = null;
|
|
@@ -1954,13 +2225,7 @@ class StableBrowser {
|
|
|
1954
2225
|
throw new Error("TOKEN is not set");
|
|
1955
2226
|
}
|
|
1956
2227
|
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
|
-
}
|
|
2228
|
+
let serviceUrl = this._getServerUrl();
|
|
1964
2229
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1965
2230
|
info.screenshotPath = screenshotPath;
|
|
1966
2231
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2109,7 +2374,16 @@ class StableBrowser {
|
|
|
2109
2374
|
let screenshotId = null;
|
|
2110
2375
|
let screenshotPath = null;
|
|
2111
2376
|
const info = {};
|
|
2112
|
-
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";
|
|
2113
2387
|
info.operation = "analyzeTable";
|
|
2114
2388
|
info.selectors = selectors;
|
|
2115
2389
|
info.query = query;
|
|
@@ -2343,13 +2617,6 @@ class StableBrowser {
|
|
|
2343
2617
|
const info = {};
|
|
2344
2618
|
try {
|
|
2345
2619
|
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
2620
|
}
|
|
2354
2621
|
catch (e) {
|
|
2355
2622
|
console.log(".");
|