automation_model 1.0.370-dev.0 → 1.0.370-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 +12 -0
- package/lib/stable_browser.js +400 -127
- 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,94 @@ 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
|
-
|
|
185
|
-
}
|
|
186
|
-
if (locator.css) {
|
|
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]);
|
|
228
|
+
}
|
|
229
|
+
if (locator.css || locator.engine === "css") {
|
|
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
|
+
// locator object { engine: "internal:text", selector: '"Signup / Login"i', priority: 4 },
|
|
245
|
+
if (locator.engine === "internal:text") {
|
|
246
|
+
// extract the text and the i flag using regex
|
|
247
|
+
const match = locator.selector.match(/"(.*)"(.*)/);
|
|
248
|
+
if (match) {
|
|
249
|
+
const text = match[1];
|
|
250
|
+
const flags = match[2];
|
|
251
|
+
locatorReturn = scope.locator(`text=${text}`, { exact: flags === "i" });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (locator.engine === "internal:attr") {
|
|
255
|
+
selector = `[${selector}]`;
|
|
256
|
+
locatorReturn = scope.locator(`${locator.engine}=${selector}`);
|
|
257
|
+
}
|
|
258
|
+
if (!locatorReturn) {
|
|
259
|
+
console.error(locator);
|
|
260
|
+
throw new Error("Locator " + JSON.stringify(locator) + " not found");
|
|
261
|
+
// } else {
|
|
262
|
+
// const count = locatorReturn.count();
|
|
263
|
+
// if (count === 0) {
|
|
264
|
+
// throw new Error("Elements not found");
|
|
265
|
+
// } else if (count > 1) {
|
|
266
|
+
// throw new Error("Multiple elements found");
|
|
267
|
+
// }
|
|
268
|
+
}
|
|
269
|
+
return locatorReturn;
|
|
190
270
|
}
|
|
191
271
|
async _locateElmentByTextClimbCss(scope, text, climb, css, _params) {
|
|
192
272
|
let result = await this._locateElementByText(scope, this._fixUsingParams(text, _params), "*", false, true, _params);
|
|
@@ -309,10 +389,13 @@ class StableBrowser {
|
|
|
309
389
|
}
|
|
310
390
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
311
391
|
let locatorSearch = selectorHierarchy[index];
|
|
312
|
-
info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
392
|
+
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
313
393
|
let locator = null;
|
|
314
394
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
315
395
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
396
|
+
if (!locatorString) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
316
399
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
317
400
|
}
|
|
318
401
|
else if (locatorSearch.text) {
|
|
@@ -334,7 +417,7 @@ class StableBrowser {
|
|
|
334
417
|
// cssHref = true;
|
|
335
418
|
// }
|
|
336
419
|
let count = await locator.count();
|
|
337
|
-
info.log += "total elements found " + count + "\n";
|
|
420
|
+
//info.log += "total elements found " + count + "\n";
|
|
338
421
|
//let visibleCount = 0;
|
|
339
422
|
let visibleLocator = null;
|
|
340
423
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -344,16 +427,20 @@ class StableBrowser {
|
|
|
344
427
|
for (let j = 0; j < count; j++) {
|
|
345
428
|
let visible = await locator.nth(j).isVisible();
|
|
346
429
|
const enabled = await locator.nth(j).isEnabled();
|
|
347
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
348
430
|
if (!visibleOnly) {
|
|
349
431
|
visible = true;
|
|
350
432
|
}
|
|
351
433
|
if (visible && enabled) {
|
|
352
434
|
foundLocators.push(locator.nth(j));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
if (!info.printMessages) {
|
|
438
|
+
info.printMessages = {};
|
|
439
|
+
}
|
|
440
|
+
if (!info.printMessages[j.toString()]) {
|
|
441
|
+
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
442
|
+
info.printMessages[j.toString()] = true;
|
|
443
|
+
}
|
|
357
444
|
}
|
|
358
445
|
}
|
|
359
446
|
}
|
|
@@ -399,6 +486,10 @@ class StableBrowser {
|
|
|
399
486
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
400
487
|
for (let i = 0; i < 3; i++) {
|
|
401
488
|
info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
|
|
489
|
+
for (let j = 0; j < selectors.locators.length; j++) {
|
|
490
|
+
let selector = selectors.locators[j];
|
|
491
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
492
|
+
}
|
|
402
493
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
403
494
|
if (!element.rerun) {
|
|
404
495
|
return element;
|
|
@@ -493,6 +584,7 @@ class StableBrowser {
|
|
|
493
584
|
let foundElements = result.foundElements;
|
|
494
585
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
495
586
|
info.box = foundElements[0].box;
|
|
587
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
496
588
|
return foundElements[0].locator;
|
|
497
589
|
}
|
|
498
590
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -522,6 +614,7 @@ class StableBrowser {
|
|
|
522
614
|
}
|
|
523
615
|
}
|
|
524
616
|
if (maxCountElement) {
|
|
617
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
525
618
|
info.box = await maxCountElement.locator.boundingBox();
|
|
526
619
|
return maxCountElement.locator;
|
|
527
620
|
}
|
|
@@ -530,9 +623,11 @@ class StableBrowser {
|
|
|
530
623
|
break;
|
|
531
624
|
}
|
|
532
625
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
626
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
533
627
|
highPriorityOnly = false;
|
|
534
628
|
}
|
|
535
629
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
630
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
536
631
|
visibleOnly = false;
|
|
537
632
|
}
|
|
538
633
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -576,6 +671,9 @@ class StableBrowser {
|
|
|
576
671
|
async click(selectors, _params, options = {}, world = null) {
|
|
577
672
|
this._validateSelectors(selectors);
|
|
578
673
|
const startTime = Date.now();
|
|
674
|
+
if (options && options.context) {
|
|
675
|
+
selectors.locators[0].text = options.context;
|
|
676
|
+
}
|
|
579
677
|
const info = {};
|
|
580
678
|
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
581
679
|
info.operation = "click";
|
|
@@ -884,71 +982,45 @@ class StableBrowser {
|
|
|
884
982
|
});
|
|
885
983
|
}
|
|
886
984
|
}
|
|
887
|
-
async
|
|
985
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
986
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
888
987
|
this._validateSelectors(selectors);
|
|
889
988
|
const startTime = Date.now();
|
|
890
|
-
let error = null;
|
|
891
|
-
let screenshotId = null;
|
|
892
|
-
let screenshotPath = null;
|
|
893
989
|
const info = {};
|
|
894
|
-
info.log = "";
|
|
895
|
-
info.operation =
|
|
990
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
991
|
+
info.operation = "setInputValue";
|
|
896
992
|
info.selectors = selectors;
|
|
993
|
+
value = this._fixUsingParams(value, _params);
|
|
897
994
|
info.value = value;
|
|
995
|
+
let error = null;
|
|
996
|
+
let screenshotId = null;
|
|
997
|
+
let screenshotPath = null;
|
|
898
998
|
try {
|
|
899
999
|
value = await this._replaceWithLocalData(value, this);
|
|
900
1000
|
let element = await this._locate(selectors, info, _params);
|
|
901
|
-
//insert red border around the element
|
|
902
1001
|
await this.scrollIfNeeded(element, info);
|
|
903
1002
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
904
1003
|
await this._highlightElements(element);
|
|
905
1004
|
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
|
-
}
|
|
1005
|
+
await element.evaluateHandle((el, value) => {
|
|
1006
|
+
el.value = value;
|
|
1007
|
+
}, value);
|
|
924
1008
|
}
|
|
925
1009
|
catch (error) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
1010
|
+
this.logger.error("setInputValue failed, will try again");
|
|
1011
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
929
1012
|
info.screenshotPath = screenshotPath;
|
|
930
1013
|
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
|
-
}
|
|
1014
|
+
await element.evaluateHandle((el, value) => {
|
|
1015
|
+
el.value = value;
|
|
1016
|
+
});
|
|
949
1017
|
}
|
|
950
1018
|
}
|
|
951
|
-
catch (
|
|
1019
|
+
catch (e) {
|
|
1020
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1021
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1022
|
+
info.screenshotPath = screenshotPath;
|
|
1023
|
+
Object.assign(e, { info: info });
|
|
952
1024
|
error = e;
|
|
953
1025
|
throw e;
|
|
954
1026
|
}
|
|
@@ -956,10 +1028,10 @@ class StableBrowser {
|
|
|
956
1028
|
const endTime = Date.now();
|
|
957
1029
|
this._reportToWorld(world, {
|
|
958
1030
|
element_name: selectors.element_name,
|
|
959
|
-
type: Types.
|
|
960
|
-
|
|
1031
|
+
type: Types.SET_INPUT,
|
|
1032
|
+
text: `Set input value`,
|
|
961
1033
|
value: value,
|
|
962
|
-
|
|
1034
|
+
screenshotId,
|
|
963
1035
|
result: error
|
|
964
1036
|
? {
|
|
965
1037
|
status: "FAILED",
|
|
@@ -976,7 +1048,7 @@ class StableBrowser {
|
|
|
976
1048
|
});
|
|
977
1049
|
}
|
|
978
1050
|
}
|
|
979
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1051
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
980
1052
|
this._validateSelectors(selectors);
|
|
981
1053
|
const startTime = Date.now();
|
|
982
1054
|
let error = null;
|
|
@@ -988,6 +1060,7 @@ class StableBrowser {
|
|
|
988
1060
|
info.selectors = selectors;
|
|
989
1061
|
info.value = value;
|
|
990
1062
|
try {
|
|
1063
|
+
value = await this._replaceWithLocalData(value, this);
|
|
991
1064
|
let element = await this._locate(selectors, info, _params);
|
|
992
1065
|
//insert red border around the element
|
|
993
1066
|
await this.scrollIfNeeded(element, info);
|
|
@@ -996,28 +1069,51 @@ class StableBrowser {
|
|
|
996
1069
|
try {
|
|
997
1070
|
await element.click();
|
|
998
1071
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1072
|
+
if (format) {
|
|
1073
|
+
value = dayjs(value).format(format);
|
|
1074
|
+
await element.fill(value);
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1078
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1079
|
+
el.value = ""; // clear input
|
|
1080
|
+
el.value = dateTimeValue;
|
|
1081
|
+
}, dateTimeValue);
|
|
1082
|
+
}
|
|
1083
|
+
if (enter) {
|
|
1084
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1085
|
+
await this.page.keyboard.press("Enter");
|
|
1086
|
+
await this.waitForPageLoad();
|
|
1087
|
+
}
|
|
1004
1088
|
}
|
|
1005
|
-
catch (
|
|
1089
|
+
catch (err) {
|
|
1006
1090
|
//await this.closeUnexpectedPopups();
|
|
1007
1091
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1008
|
-
this.logger.info("Trying again")
|
|
1092
|
+
this.logger.info("Trying again");
|
|
1093
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1009
1094
|
info.screenshotPath = screenshotPath;
|
|
1010
|
-
Object.assign(
|
|
1095
|
+
Object.assign(err, { info: info });
|
|
1011
1096
|
await element.click();
|
|
1012
1097
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1098
|
+
if (format) {
|
|
1099
|
+
value = dayjs(value).format(format);
|
|
1100
|
+
await element.fill(value);
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1104
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1105
|
+
el.value = ""; // clear input
|
|
1106
|
+
el.value = dateTimeValue;
|
|
1107
|
+
}, dateTimeValue);
|
|
1108
|
+
}
|
|
1109
|
+
if (enter) {
|
|
1110
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1111
|
+
await this.page.keyboard.press("Enter");
|
|
1112
|
+
await this.waitForPageLoad();
|
|
1113
|
+
}
|
|
1018
1114
|
}
|
|
1019
1115
|
}
|
|
1020
|
-
catch (
|
|
1116
|
+
catch (e) {
|
|
1021
1117
|
error = e;
|
|
1022
1118
|
throw e;
|
|
1023
1119
|
}
|
|
@@ -1067,20 +1163,32 @@ class StableBrowser {
|
|
|
1067
1163
|
await this.scrollIfNeeded(element, info);
|
|
1068
1164
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1069
1165
|
await this._highlightElements(element);
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1166
|
+
if (options === null || options === undefined || !options.press) {
|
|
1167
|
+
try {
|
|
1168
|
+
let currentValue = await element.inputValue();
|
|
1169
|
+
if (currentValue) {
|
|
1170
|
+
await element.fill("");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
catch (e) {
|
|
1174
|
+
this.logger.info("unable to clear input value");
|
|
1074
1175
|
}
|
|
1075
1176
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1177
|
+
if (options === null || options === undefined || options.press) {
|
|
1178
|
+
try {
|
|
1179
|
+
await element.click({ timeout: 5000 });
|
|
1180
|
+
}
|
|
1181
|
+
catch (e) {
|
|
1182
|
+
await element.dispatchEvent("click");
|
|
1183
|
+
}
|
|
1081
1184
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1185
|
+
else {
|
|
1186
|
+
try {
|
|
1187
|
+
await element.focus();
|
|
1188
|
+
}
|
|
1189
|
+
catch (e) {
|
|
1190
|
+
await element.dispatchEvent("focus");
|
|
1191
|
+
}
|
|
1084
1192
|
}
|
|
1085
1193
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1086
1194
|
const valueSegment = _value.split("&&");
|
|
@@ -1276,7 +1384,8 @@ class StableBrowser {
|
|
|
1276
1384
|
let screenshotId = null;
|
|
1277
1385
|
let screenshotPath = null;
|
|
1278
1386
|
const info = {};
|
|
1279
|
-
info.log =
|
|
1387
|
+
info.log =
|
|
1388
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1280
1389
|
info.operation = "containsPattern";
|
|
1281
1390
|
info.selectors = selectors;
|
|
1282
1391
|
info.value = text;
|
|
@@ -1446,15 +1555,62 @@ class StableBrowser {
|
|
|
1446
1555
|
// save the data to the file
|
|
1447
1556
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1448
1557
|
}
|
|
1558
|
+
_getDataFilePath(fileName) {
|
|
1559
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1560
|
+
if (fs.existsSync(dataFile)) {
|
|
1561
|
+
return dataFile;
|
|
1562
|
+
}
|
|
1563
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1564
|
+
if (fs.existsSync(dataFile)) {
|
|
1565
|
+
return dataFile;
|
|
1566
|
+
}
|
|
1567
|
+
throw new Error("data file not found " + fileName);
|
|
1568
|
+
}
|
|
1569
|
+
_parseCSVSync(filePath) {
|
|
1570
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1571
|
+
const results = [];
|
|
1572
|
+
return new Promise((resolve, reject) => {
|
|
1573
|
+
const readableStream = new Readable();
|
|
1574
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1575
|
+
readableStream.push(data);
|
|
1576
|
+
readableStream.push(null);
|
|
1577
|
+
readableStream
|
|
1578
|
+
.pipe(csv())
|
|
1579
|
+
.on("data", (data) => results.push(data))
|
|
1580
|
+
.on("end", () => resolve(results))
|
|
1581
|
+
.on("error", (error) => reject(error));
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1449
1584
|
loadTestData(type, dataSelector, world = null) {
|
|
1450
1585
|
switch (type) {
|
|
1451
1586
|
case "users":
|
|
1452
|
-
//
|
|
1453
|
-
|
|
1454
|
-
|
|
1587
|
+
// get the users.json file path
|
|
1588
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1589
|
+
// read the file and return the data
|
|
1590
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1591
|
+
for (let i = 0; i < users.length; i++) {
|
|
1592
|
+
if (users[i].username === dataSelector) {
|
|
1593
|
+
const userObj = {
|
|
1594
|
+
username: users[i].username,
|
|
1595
|
+
password: "secret:" + users[i].password,
|
|
1596
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1597
|
+
};
|
|
1598
|
+
this.setTestData(userObj, world);
|
|
1599
|
+
return userObj;
|
|
1600
|
+
}
|
|
1455
1601
|
}
|
|
1602
|
+
throw new Error("user not found " + dataSelector);
|
|
1603
|
+
default:
|
|
1604
|
+
throw new Error("unknown type " + type);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1608
|
+
switch (type) {
|
|
1609
|
+
case "users": {
|
|
1610
|
+
// get the users.json file path
|
|
1611
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1456
1612
|
// read the file and return the data
|
|
1457
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1613
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1458
1614
|
for (let i = 0; i < users.length; i++) {
|
|
1459
1615
|
if (users[i].username === dataSelector) {
|
|
1460
1616
|
const userObj = {
|
|
@@ -1467,6 +1623,29 @@ class StableBrowser {
|
|
|
1467
1623
|
}
|
|
1468
1624
|
}
|
|
1469
1625
|
throw new Error("user not found " + dataSelector);
|
|
1626
|
+
}
|
|
1627
|
+
case "csv": {
|
|
1628
|
+
// 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
|
|
1629
|
+
const parts = dataSelector.split(":");
|
|
1630
|
+
let rowNumber = 0;
|
|
1631
|
+
if (parts.length > 1) {
|
|
1632
|
+
rowNumber = parseInt(parts[1]);
|
|
1633
|
+
}
|
|
1634
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1635
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1636
|
+
// result stracture:
|
|
1637
|
+
// [
|
|
1638
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1639
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1640
|
+
// ]
|
|
1641
|
+
// verify the row number is within the range
|
|
1642
|
+
if (rowNumber >= results.length) {
|
|
1643
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1644
|
+
}
|
|
1645
|
+
const data = results[rowNumber];
|
|
1646
|
+
this.setTestData(data, world);
|
|
1647
|
+
return data;
|
|
1648
|
+
}
|
|
1470
1649
|
default:
|
|
1471
1650
|
throw new Error("unknown type " + type);
|
|
1472
1651
|
}
|
|
@@ -1571,13 +1750,13 @@ class StableBrowser {
|
|
|
1571
1750
|
])));
|
|
1572
1751
|
const { data } = await client.send("Page.captureScreenshot", {
|
|
1573
1752
|
format: "png",
|
|
1574
|
-
clip: {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
},
|
|
1753
|
+
// clip: {
|
|
1754
|
+
// x: 0,
|
|
1755
|
+
// y: 0,
|
|
1756
|
+
// width: viewportWidth,
|
|
1757
|
+
// height: viewportHeight,
|
|
1758
|
+
// scale: 1,
|
|
1759
|
+
// },
|
|
1581
1760
|
});
|
|
1582
1761
|
if (!screenshotPath) {
|
|
1583
1762
|
return data;
|
|
@@ -1683,7 +1862,8 @@ class StableBrowser {
|
|
|
1683
1862
|
if (world) {
|
|
1684
1863
|
world[variable] = info.value;
|
|
1685
1864
|
}
|
|
1686
|
-
this.
|
|
1865
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1866
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1687
1867
|
return info;
|
|
1688
1868
|
}
|
|
1689
1869
|
catch (e) {
|
|
@@ -1720,6 +1900,91 @@ class StableBrowser {
|
|
|
1720
1900
|
});
|
|
1721
1901
|
}
|
|
1722
1902
|
}
|
|
1903
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1904
|
+
if (!emailAddress) {
|
|
1905
|
+
throw new Error("email address is null");
|
|
1906
|
+
}
|
|
1907
|
+
// check if address contain @
|
|
1908
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1909
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1913
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
const startTime = Date.now();
|
|
1917
|
+
let timeout = 60000;
|
|
1918
|
+
if (options && options.timeout) {
|
|
1919
|
+
timeout = options.timeout;
|
|
1920
|
+
}
|
|
1921
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1922
|
+
const request = {
|
|
1923
|
+
method: "POST",
|
|
1924
|
+
url: serviceUrl,
|
|
1925
|
+
headers: {
|
|
1926
|
+
"Content-Type": "application/json",
|
|
1927
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1928
|
+
},
|
|
1929
|
+
data: JSON.stringify({
|
|
1930
|
+
email: emailAddress,
|
|
1931
|
+
}),
|
|
1932
|
+
};
|
|
1933
|
+
let errorCount = 0;
|
|
1934
|
+
while (true) {
|
|
1935
|
+
try {
|
|
1936
|
+
let result = await this.context.api.request(request);
|
|
1937
|
+
// the response body expected to be the following:
|
|
1938
|
+
// {
|
|
1939
|
+
// "status": true,
|
|
1940
|
+
// "content": {
|
|
1941
|
+
// "url": "",
|
|
1942
|
+
// "code": "112112",
|
|
1943
|
+
// "name": "generate_link_or_code"
|
|
1944
|
+
// }
|
|
1945
|
+
//}
|
|
1946
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1947
|
+
let codeOrUrlFound = false;
|
|
1948
|
+
let emailCode = null;
|
|
1949
|
+
let emailUrl = null;
|
|
1950
|
+
// check if a code is returned
|
|
1951
|
+
if (result.data.content && result.data.content.code) {
|
|
1952
|
+
let code = result.data.content.code;
|
|
1953
|
+
this.setTestData({ emailCode: code }, world);
|
|
1954
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1955
|
+
emailCode = code;
|
|
1956
|
+
codeOrUrlFound = true;
|
|
1957
|
+
}
|
|
1958
|
+
// check if a url is returned
|
|
1959
|
+
if (result.data.content && result.data.content.url) {
|
|
1960
|
+
let url = result.data.content.url;
|
|
1961
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1962
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1963
|
+
emailUrl = url;
|
|
1964
|
+
codeOrUrlFound = true;
|
|
1965
|
+
}
|
|
1966
|
+
if (codeOrUrlFound) {
|
|
1967
|
+
return { emailUrl, emailCode };
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
this.logger.info("an email received but no code or url found");
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
catch (e) {
|
|
1975
|
+
errorCount++;
|
|
1976
|
+
if (errorCount > 3) {
|
|
1977
|
+
throw e;
|
|
1978
|
+
}
|
|
1979
|
+
// ignore
|
|
1980
|
+
}
|
|
1981
|
+
// check if the timeout is reached
|
|
1982
|
+
if (Date.now() - startTime > timeout) {
|
|
1983
|
+
throw new Error("timeout reached");
|
|
1984
|
+
}
|
|
1985
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1723
1988
|
async _highlightElements(scope, css) {
|
|
1724
1989
|
try {
|
|
1725
1990
|
if (!scope) {
|
|
@@ -1901,8 +2166,10 @@ class StableBrowser {
|
|
|
1901
2166
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1902
2167
|
await this._highlightElements(frame, dataAttribute);
|
|
1903
2168
|
const element = await frame.$(dataAttribute);
|
|
1904
|
-
|
|
1905
|
-
|
|
2169
|
+
if (element) {
|
|
2170
|
+
await this.scrollIfNeeded(element, info);
|
|
2171
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2172
|
+
}
|
|
1906
2173
|
}
|
|
1907
2174
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1908
2175
|
return info;
|
|
@@ -1940,6 +2207,16 @@ class StableBrowser {
|
|
|
1940
2207
|
});
|
|
1941
2208
|
}
|
|
1942
2209
|
}
|
|
2210
|
+
_getServerUrl() {
|
|
2211
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2212
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2213
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2214
|
+
}
|
|
2215
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2216
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2217
|
+
}
|
|
2218
|
+
return serviceUrl;
|
|
2219
|
+
}
|
|
1943
2220
|
async visualVerification(text, options = {}, world = null) {
|
|
1944
2221
|
const startTime = Date.now();
|
|
1945
2222
|
let error = null;
|
|
@@ -1954,13 +2231,7 @@ class StableBrowser {
|
|
|
1954
2231
|
throw new Error("TOKEN is not set");
|
|
1955
2232
|
}
|
|
1956
2233
|
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
|
-
}
|
|
2234
|
+
let serviceUrl = this._getServerUrl();
|
|
1964
2235
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1965
2236
|
info.screenshotPath = screenshotPath;
|
|
1966
2237
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2109,7 +2380,16 @@ class StableBrowser {
|
|
|
2109
2380
|
let screenshotId = null;
|
|
2110
2381
|
let screenshotPath = null;
|
|
2111
2382
|
const info = {};
|
|
2112
|
-
info.log =
|
|
2383
|
+
info.log =
|
|
2384
|
+
"***** analyze table " +
|
|
2385
|
+
selectors.element_name +
|
|
2386
|
+
" query " +
|
|
2387
|
+
query +
|
|
2388
|
+
" operator " +
|
|
2389
|
+
operator +
|
|
2390
|
+
" value " +
|
|
2391
|
+
value +
|
|
2392
|
+
" *****\n";
|
|
2113
2393
|
info.operation = "analyzeTable";
|
|
2114
2394
|
info.selectors = selectors;
|
|
2115
2395
|
info.query = query;
|
|
@@ -2343,13 +2623,6 @@ class StableBrowser {
|
|
|
2343
2623
|
const info = {};
|
|
2344
2624
|
try {
|
|
2345
2625
|
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
2626
|
}
|
|
2354
2627
|
catch (e) {
|
|
2355
2628
|
console.log(".");
|