automation_model 1.0.367 → 1.0.368-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 +376 -118
- package/lib/stable_browser.js.map +1 -1
- package/package.json +7 -5
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,23 +179,77 @@ 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);
|
|
176
218
|
if (locator.role) {
|
|
177
219
|
if (locator.role[1].nameReg) {
|
|
178
220
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
179
221
|
delete locator.role[1].nameReg;
|
|
180
222
|
}
|
|
181
|
-
if (locator.role[1].name) {
|
|
182
|
-
|
|
183
|
-
}
|
|
223
|
+
// if (locator.role[1].name) {
|
|
224
|
+
// locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
|
|
225
|
+
// }
|
|
184
226
|
return scope.getByRole(locator.role[0], locator.role[1]);
|
|
185
227
|
}
|
|
186
228
|
if (locator.css) {
|
|
187
|
-
return scope.locator(
|
|
229
|
+
return scope.locator(locator.css);
|
|
230
|
+
}
|
|
231
|
+
// handle role/name locators
|
|
232
|
+
// locator.selector will be something like: textbox[name="Username"i]
|
|
233
|
+
if (locator.engine === "internal:role") {
|
|
234
|
+
// extract the role, name and the i/s flags using regex
|
|
235
|
+
const match = locator.selector.match(/(.*)\[(.*)="(.*)"(.*)\]/);
|
|
236
|
+
if (match) {
|
|
237
|
+
const role = match[1];
|
|
238
|
+
const name = match[3];
|
|
239
|
+
const flags = match[4];
|
|
240
|
+
return scope.getByRole(role, { name }, { exact: flags === "i" });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// handle css locators
|
|
244
|
+
if (locator.engine === "css") {
|
|
245
|
+
return scope.locator(locator.selector);
|
|
246
|
+
}
|
|
247
|
+
if ((locator === null || locator === void 0 ? void 0 : locator.engine) && (locator === null || locator === void 0 ? void 0 : locator.score) <= 520) {
|
|
248
|
+
if (locator.engine === "internal:attr") {
|
|
249
|
+
selector = `[${selector}]`;
|
|
250
|
+
}
|
|
251
|
+
const newLocator = scope.locator(`${locator.engine}=${selector}`);
|
|
252
|
+
return newLocator;
|
|
188
253
|
}
|
|
189
254
|
throw new Error("unknown locator type");
|
|
190
255
|
}
|
|
@@ -309,10 +374,13 @@ class StableBrowser {
|
|
|
309
374
|
}
|
|
310
375
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
311
376
|
let locatorSearch = selectorHierarchy[index];
|
|
312
|
-
info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
377
|
+
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
313
378
|
let locator = null;
|
|
314
379
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
315
380
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
381
|
+
if (!locatorString) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
316
384
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
317
385
|
}
|
|
318
386
|
else if (locatorSearch.text) {
|
|
@@ -334,7 +402,7 @@ class StableBrowser {
|
|
|
334
402
|
// cssHref = true;
|
|
335
403
|
// }
|
|
336
404
|
let count = await locator.count();
|
|
337
|
-
info.log += "total elements found " + count + "\n";
|
|
405
|
+
//info.log += "total elements found " + count + "\n";
|
|
338
406
|
//let visibleCount = 0;
|
|
339
407
|
let visibleLocator = null;
|
|
340
408
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -344,16 +412,20 @@ class StableBrowser {
|
|
|
344
412
|
for (let j = 0; j < count; j++) {
|
|
345
413
|
let visible = await locator.nth(j).isVisible();
|
|
346
414
|
const enabled = await locator.nth(j).isEnabled();
|
|
347
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
348
415
|
if (!visibleOnly) {
|
|
349
416
|
visible = true;
|
|
350
417
|
}
|
|
351
418
|
if (visible && enabled) {
|
|
352
419
|
foundLocators.push(locator.nth(j));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
if (!info.printMessages) {
|
|
423
|
+
info.printMessages = {};
|
|
424
|
+
}
|
|
425
|
+
if (!info.printMessages[j.toString()]) {
|
|
426
|
+
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
427
|
+
info.printMessages[j.toString()] = true;
|
|
428
|
+
}
|
|
357
429
|
}
|
|
358
430
|
}
|
|
359
431
|
}
|
|
@@ -399,6 +471,10 @@ class StableBrowser {
|
|
|
399
471
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
400
472
|
for (let i = 0; i < 3; i++) {
|
|
401
473
|
info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
|
|
474
|
+
for (let j = 0; j < selectors.locators.length; j++) {
|
|
475
|
+
let selector = selectors.locators[j];
|
|
476
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
477
|
+
}
|
|
402
478
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
403
479
|
if (!element.rerun) {
|
|
404
480
|
return element;
|
|
@@ -482,14 +558,18 @@ class StableBrowser {
|
|
|
482
558
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
483
559
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
484
560
|
}
|
|
485
|
-
|
|
486
|
-
if (result.foundElements.length === 0 && shouldTestPriority3) {
|
|
487
|
-
// info.log += "scanning locators in priority 3" + "\n";
|
|
561
|
+
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
488
562
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
489
563
|
}
|
|
564
|
+
else {
|
|
565
|
+
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
566
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
490
569
|
let foundElements = result.foundElements;
|
|
491
570
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
492
571
|
info.box = foundElements[0].box;
|
|
572
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
493
573
|
return foundElements[0].locator;
|
|
494
574
|
}
|
|
495
575
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -519,6 +599,7 @@ class StableBrowser {
|
|
|
519
599
|
}
|
|
520
600
|
}
|
|
521
601
|
if (maxCountElement) {
|
|
602
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
522
603
|
info.box = await maxCountElement.locator.boundingBox();
|
|
523
604
|
return maxCountElement.locator;
|
|
524
605
|
}
|
|
@@ -527,9 +608,11 @@ class StableBrowser {
|
|
|
527
608
|
break;
|
|
528
609
|
}
|
|
529
610
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
611
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
530
612
|
highPriorityOnly = false;
|
|
531
613
|
}
|
|
532
614
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
615
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
533
616
|
visibleOnly = false;
|
|
534
617
|
}
|
|
535
618
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -881,71 +964,45 @@ class StableBrowser {
|
|
|
881
964
|
});
|
|
882
965
|
}
|
|
883
966
|
}
|
|
884
|
-
async
|
|
967
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
968
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
885
969
|
this._validateSelectors(selectors);
|
|
886
970
|
const startTime = Date.now();
|
|
887
|
-
let error = null;
|
|
888
|
-
let screenshotId = null;
|
|
889
|
-
let screenshotPath = null;
|
|
890
971
|
const info = {};
|
|
891
|
-
info.log = "";
|
|
892
|
-
info.operation =
|
|
972
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
973
|
+
info.operation = "setInputValue";
|
|
893
974
|
info.selectors = selectors;
|
|
975
|
+
value = this._fixUsingParams(value, _params);
|
|
894
976
|
info.value = value;
|
|
977
|
+
let error = null;
|
|
978
|
+
let screenshotId = null;
|
|
979
|
+
let screenshotPath = null;
|
|
895
980
|
try {
|
|
896
981
|
value = await this._replaceWithLocalData(value, this);
|
|
897
982
|
let element = await this._locate(selectors, info, _params);
|
|
898
|
-
//insert red border around the element
|
|
899
983
|
await this.scrollIfNeeded(element, info);
|
|
900
984
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
901
985
|
await this._highlightElements(element);
|
|
902
986
|
try {
|
|
903
|
-
await element.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
value = dayjs(value).format(format);
|
|
907
|
-
await element.fill(value);
|
|
908
|
-
}
|
|
909
|
-
else {
|
|
910
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
911
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
912
|
-
el.value = ""; // clear input
|
|
913
|
-
el.value = dateTimeValue;
|
|
914
|
-
}, dateTimeValue);
|
|
915
|
-
}
|
|
916
|
-
if (enter) {
|
|
917
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
918
|
-
await this.page.keyboard.press("Enter");
|
|
919
|
-
await this.waitForPageLoad();
|
|
920
|
-
}
|
|
987
|
+
await element.evaluateHandle((el, value) => {
|
|
988
|
+
el.value = value;
|
|
989
|
+
}, value);
|
|
921
990
|
}
|
|
922
991
|
catch (error) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
992
|
+
this.logger.error("setInputValue failed, will try again");
|
|
993
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
926
994
|
info.screenshotPath = screenshotPath;
|
|
927
995
|
Object.assign(error, { info: info });
|
|
928
|
-
await element.
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
value = dayjs(value).format(format);
|
|
932
|
-
await element.fill(value);
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
936
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
937
|
-
el.value = ""; // clear input
|
|
938
|
-
el.value = dateTimeValue;
|
|
939
|
-
}, dateTimeValue);
|
|
940
|
-
}
|
|
941
|
-
if (enter) {
|
|
942
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
943
|
-
await this.page.keyboard.press("Enter");
|
|
944
|
-
await this.waitForPageLoad();
|
|
945
|
-
}
|
|
996
|
+
await element.evaluateHandle((el, value) => {
|
|
997
|
+
el.value = value;
|
|
998
|
+
});
|
|
946
999
|
}
|
|
947
1000
|
}
|
|
948
|
-
catch (
|
|
1001
|
+
catch (e) {
|
|
1002
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
1003
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1004
|
+
info.screenshotPath = screenshotPath;
|
|
1005
|
+
Object.assign(e, { info: info });
|
|
949
1006
|
error = e;
|
|
950
1007
|
throw e;
|
|
951
1008
|
}
|
|
@@ -953,10 +1010,10 @@ class StableBrowser {
|
|
|
953
1010
|
const endTime = Date.now();
|
|
954
1011
|
this._reportToWorld(world, {
|
|
955
1012
|
element_name: selectors.element_name,
|
|
956
|
-
type: Types.
|
|
957
|
-
|
|
1013
|
+
type: Types.SET_INPUT,
|
|
1014
|
+
text: `Set input value`,
|
|
958
1015
|
value: value,
|
|
959
|
-
|
|
1016
|
+
screenshotId,
|
|
960
1017
|
result: error
|
|
961
1018
|
? {
|
|
962
1019
|
status: "FAILED",
|
|
@@ -973,7 +1030,7 @@ class StableBrowser {
|
|
|
973
1030
|
});
|
|
974
1031
|
}
|
|
975
1032
|
}
|
|
976
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1033
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
977
1034
|
this._validateSelectors(selectors);
|
|
978
1035
|
const startTime = Date.now();
|
|
979
1036
|
let error = null;
|
|
@@ -985,6 +1042,7 @@ class StableBrowser {
|
|
|
985
1042
|
info.selectors = selectors;
|
|
986
1043
|
info.value = value;
|
|
987
1044
|
try {
|
|
1045
|
+
value = await this._replaceWithLocalData(value, this);
|
|
988
1046
|
let element = await this._locate(selectors, info, _params);
|
|
989
1047
|
//insert red border around the element
|
|
990
1048
|
await this.scrollIfNeeded(element, info);
|
|
@@ -993,28 +1051,51 @@ class StableBrowser {
|
|
|
993
1051
|
try {
|
|
994
1052
|
await element.click();
|
|
995
1053
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1054
|
+
if (format) {
|
|
1055
|
+
value = dayjs(value).format(format);
|
|
1056
|
+
await element.fill(value);
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1060
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1061
|
+
el.value = ""; // clear input
|
|
1062
|
+
el.value = dateTimeValue;
|
|
1063
|
+
}, dateTimeValue);
|
|
1064
|
+
}
|
|
1065
|
+
if (enter) {
|
|
1066
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1067
|
+
await this.page.keyboard.press("Enter");
|
|
1068
|
+
await this.waitForPageLoad();
|
|
1069
|
+
}
|
|
1001
1070
|
}
|
|
1002
|
-
catch (
|
|
1071
|
+
catch (err) {
|
|
1003
1072
|
//await this.closeUnexpectedPopups();
|
|
1004
1073
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
1005
|
-
this.logger.info("Trying again")
|
|
1074
|
+
this.logger.info("Trying again");
|
|
1075
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1006
1076
|
info.screenshotPath = screenshotPath;
|
|
1007
|
-
Object.assign(
|
|
1077
|
+
Object.assign(err, { info: info });
|
|
1008
1078
|
await element.click();
|
|
1009
1079
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1080
|
+
if (format) {
|
|
1081
|
+
value = dayjs(value).format(format);
|
|
1082
|
+
await element.fill(value);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1086
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1087
|
+
el.value = ""; // clear input
|
|
1088
|
+
el.value = dateTimeValue;
|
|
1089
|
+
}, dateTimeValue);
|
|
1090
|
+
}
|
|
1091
|
+
if (enter) {
|
|
1092
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1093
|
+
await this.page.keyboard.press("Enter");
|
|
1094
|
+
await this.waitForPageLoad();
|
|
1095
|
+
}
|
|
1015
1096
|
}
|
|
1016
1097
|
}
|
|
1017
|
-
catch (
|
|
1098
|
+
catch (e) {
|
|
1018
1099
|
error = e;
|
|
1019
1100
|
throw e;
|
|
1020
1101
|
}
|
|
@@ -1064,20 +1145,32 @@ class StableBrowser {
|
|
|
1064
1145
|
await this.scrollIfNeeded(element, info);
|
|
1065
1146
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1066
1147
|
await this._highlightElements(element);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1148
|
+
if (options === null || options === undefined || !options.press) {
|
|
1149
|
+
try {
|
|
1150
|
+
let currentValue = await element.inputValue();
|
|
1151
|
+
if (currentValue) {
|
|
1152
|
+
await element.fill("");
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
catch (e) {
|
|
1156
|
+
this.logger.info("unable to clear input value");
|
|
1071
1157
|
}
|
|
1072
1158
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1159
|
+
if (options === null || options === undefined || options.press) {
|
|
1160
|
+
try {
|
|
1161
|
+
await element.click({ timeout: 5000 });
|
|
1162
|
+
}
|
|
1163
|
+
catch (e) {
|
|
1164
|
+
await element.dispatchEvent("click");
|
|
1165
|
+
}
|
|
1078
1166
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1167
|
+
else {
|
|
1168
|
+
try {
|
|
1169
|
+
await element.focus();
|
|
1170
|
+
}
|
|
1171
|
+
catch (e) {
|
|
1172
|
+
await element.dispatchEvent("focus");
|
|
1173
|
+
}
|
|
1081
1174
|
}
|
|
1082
1175
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1083
1176
|
const valueSegment = _value.split("&&");
|
|
@@ -1273,7 +1366,8 @@ class StableBrowser {
|
|
|
1273
1366
|
let screenshotId = null;
|
|
1274
1367
|
let screenshotPath = null;
|
|
1275
1368
|
const info = {};
|
|
1276
|
-
info.log =
|
|
1369
|
+
info.log =
|
|
1370
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1277
1371
|
info.operation = "containsPattern";
|
|
1278
1372
|
info.selectors = selectors;
|
|
1279
1373
|
info.value = text;
|
|
@@ -1443,15 +1537,62 @@ class StableBrowser {
|
|
|
1443
1537
|
// save the data to the file
|
|
1444
1538
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1445
1539
|
}
|
|
1540
|
+
_getDataFilePath(fileName) {
|
|
1541
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1542
|
+
if (fs.existsSync(dataFile)) {
|
|
1543
|
+
return dataFile;
|
|
1544
|
+
}
|
|
1545
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1546
|
+
if (fs.existsSync(dataFile)) {
|
|
1547
|
+
return dataFile;
|
|
1548
|
+
}
|
|
1549
|
+
throw new Error("data file not found " + fileName);
|
|
1550
|
+
}
|
|
1551
|
+
_parseCSVSync(filePath) {
|
|
1552
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1553
|
+
const results = [];
|
|
1554
|
+
return new Promise((resolve, reject) => {
|
|
1555
|
+
const readableStream = new Readable();
|
|
1556
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1557
|
+
readableStream.push(data);
|
|
1558
|
+
readableStream.push(null);
|
|
1559
|
+
readableStream
|
|
1560
|
+
.pipe(csv())
|
|
1561
|
+
.on("data", (data) => results.push(data))
|
|
1562
|
+
.on("end", () => resolve(results))
|
|
1563
|
+
.on("error", (error) => reject(error));
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1446
1566
|
loadTestData(type, dataSelector, world = null) {
|
|
1447
1567
|
switch (type) {
|
|
1448
1568
|
case "users":
|
|
1449
|
-
//
|
|
1450
|
-
|
|
1451
|
-
|
|
1569
|
+
// get the users.json file path
|
|
1570
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1571
|
+
// read the file and return the data
|
|
1572
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1573
|
+
for (let i = 0; i < users.length; i++) {
|
|
1574
|
+
if (users[i].username === dataSelector) {
|
|
1575
|
+
const userObj = {
|
|
1576
|
+
username: users[i].username,
|
|
1577
|
+
password: "secret:" + users[i].password,
|
|
1578
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1579
|
+
};
|
|
1580
|
+
this.setTestData(userObj, world);
|
|
1581
|
+
return userObj;
|
|
1582
|
+
}
|
|
1452
1583
|
}
|
|
1584
|
+
throw new Error("user not found " + dataSelector);
|
|
1585
|
+
default:
|
|
1586
|
+
throw new Error("unknown type " + type);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1590
|
+
switch (type) {
|
|
1591
|
+
case "users": {
|
|
1592
|
+
// get the users.json file path
|
|
1593
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1453
1594
|
// read the file and return the data
|
|
1454
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1595
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1455
1596
|
for (let i = 0; i < users.length; i++) {
|
|
1456
1597
|
if (users[i].username === dataSelector) {
|
|
1457
1598
|
const userObj = {
|
|
@@ -1464,6 +1605,29 @@ class StableBrowser {
|
|
|
1464
1605
|
}
|
|
1465
1606
|
}
|
|
1466
1607
|
throw new Error("user not found " + dataSelector);
|
|
1608
|
+
}
|
|
1609
|
+
case "csv": {
|
|
1610
|
+
// 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
|
|
1611
|
+
const parts = dataSelector.split(":");
|
|
1612
|
+
let rowNumber = 0;
|
|
1613
|
+
if (parts.length > 1) {
|
|
1614
|
+
rowNumber = parseInt(parts[1]);
|
|
1615
|
+
}
|
|
1616
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1617
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1618
|
+
// result stracture:
|
|
1619
|
+
// [
|
|
1620
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1621
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1622
|
+
// ]
|
|
1623
|
+
// verify the row number is within the range
|
|
1624
|
+
if (rowNumber >= results.length) {
|
|
1625
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1626
|
+
}
|
|
1627
|
+
const data = results[rowNumber];
|
|
1628
|
+
this.setTestData(data, world);
|
|
1629
|
+
return data;
|
|
1630
|
+
}
|
|
1467
1631
|
default:
|
|
1468
1632
|
throw new Error("unknown type " + type);
|
|
1469
1633
|
}
|
|
@@ -1680,7 +1844,8 @@ class StableBrowser {
|
|
|
1680
1844
|
if (world) {
|
|
1681
1845
|
world[variable] = info.value;
|
|
1682
1846
|
}
|
|
1683
|
-
this.
|
|
1847
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1848
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1684
1849
|
return info;
|
|
1685
1850
|
}
|
|
1686
1851
|
catch (e) {
|
|
@@ -1717,6 +1882,91 @@ class StableBrowser {
|
|
|
1717
1882
|
});
|
|
1718
1883
|
}
|
|
1719
1884
|
}
|
|
1885
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1886
|
+
if (!emailAddress) {
|
|
1887
|
+
throw new Error("email address is null");
|
|
1888
|
+
}
|
|
1889
|
+
// check if address contain @
|
|
1890
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1891
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1892
|
+
}
|
|
1893
|
+
else {
|
|
1894
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1895
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const startTime = Date.now();
|
|
1899
|
+
let timeout = 60000;
|
|
1900
|
+
if (options && options.timeout) {
|
|
1901
|
+
timeout = options.timeout;
|
|
1902
|
+
}
|
|
1903
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1904
|
+
const request = {
|
|
1905
|
+
method: "POST",
|
|
1906
|
+
url: serviceUrl,
|
|
1907
|
+
headers: {
|
|
1908
|
+
"Content-Type": "application/json",
|
|
1909
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1910
|
+
},
|
|
1911
|
+
data: JSON.stringify({
|
|
1912
|
+
email: emailAddress,
|
|
1913
|
+
}),
|
|
1914
|
+
};
|
|
1915
|
+
let errorCount = 0;
|
|
1916
|
+
while (true) {
|
|
1917
|
+
try {
|
|
1918
|
+
let result = await this.context.api.request(request);
|
|
1919
|
+
// the response body expected to be the following:
|
|
1920
|
+
// {
|
|
1921
|
+
// "status": true,
|
|
1922
|
+
// "content": {
|
|
1923
|
+
// "url": "",
|
|
1924
|
+
// "code": "112112",
|
|
1925
|
+
// "name": "generate_link_or_code"
|
|
1926
|
+
// }
|
|
1927
|
+
//}
|
|
1928
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1929
|
+
let codeOrUrlFound = false;
|
|
1930
|
+
let emailCode = null;
|
|
1931
|
+
let emailUrl = null;
|
|
1932
|
+
// check if a code is returned
|
|
1933
|
+
if (result.data.content && result.data.content.code) {
|
|
1934
|
+
let code = result.data.content.code;
|
|
1935
|
+
this.setTestData({ emailCode: code }, world);
|
|
1936
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1937
|
+
emailCode = code;
|
|
1938
|
+
codeOrUrlFound = true;
|
|
1939
|
+
}
|
|
1940
|
+
// check if a url is returned
|
|
1941
|
+
if (result.data.content && result.data.content.url) {
|
|
1942
|
+
let url = result.data.content.url;
|
|
1943
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1944
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1945
|
+
emailUrl = url;
|
|
1946
|
+
codeOrUrlFound = true;
|
|
1947
|
+
}
|
|
1948
|
+
if (codeOrUrlFound) {
|
|
1949
|
+
return { emailUrl, emailCode };
|
|
1950
|
+
}
|
|
1951
|
+
else {
|
|
1952
|
+
this.logger.info("an email received but no code or url found");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
catch (e) {
|
|
1957
|
+
errorCount++;
|
|
1958
|
+
if (errorCount > 3) {
|
|
1959
|
+
throw e;
|
|
1960
|
+
}
|
|
1961
|
+
// ignore
|
|
1962
|
+
}
|
|
1963
|
+
// check if the timeout is reached
|
|
1964
|
+
if (Date.now() - startTime > timeout) {
|
|
1965
|
+
throw new Error("timeout reached");
|
|
1966
|
+
}
|
|
1967
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1720
1970
|
async _highlightElements(scope, css) {
|
|
1721
1971
|
try {
|
|
1722
1972
|
if (!scope) {
|
|
@@ -1898,8 +2148,10 @@ class StableBrowser {
|
|
|
1898
2148
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1899
2149
|
await this._highlightElements(frame, dataAttribute);
|
|
1900
2150
|
const element = await frame.$(dataAttribute);
|
|
1901
|
-
|
|
1902
|
-
|
|
2151
|
+
if (element) {
|
|
2152
|
+
await this.scrollIfNeeded(element, info);
|
|
2153
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2154
|
+
}
|
|
1903
2155
|
}
|
|
1904
2156
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1905
2157
|
return info;
|
|
@@ -1937,6 +2189,16 @@ class StableBrowser {
|
|
|
1937
2189
|
});
|
|
1938
2190
|
}
|
|
1939
2191
|
}
|
|
2192
|
+
_getServerUrl() {
|
|
2193
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2194
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2195
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2196
|
+
}
|
|
2197
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2198
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2199
|
+
}
|
|
2200
|
+
return serviceUrl;
|
|
2201
|
+
}
|
|
1940
2202
|
async visualVerification(text, options = {}, world = null) {
|
|
1941
2203
|
const startTime = Date.now();
|
|
1942
2204
|
let error = null;
|
|
@@ -1951,13 +2213,7 @@ class StableBrowser {
|
|
|
1951
2213
|
throw new Error("TOKEN is not set");
|
|
1952
2214
|
}
|
|
1953
2215
|
try {
|
|
1954
|
-
let serviceUrl =
|
|
1955
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1956
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1957
|
-
}
|
|
1958
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1959
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1960
|
-
}
|
|
2216
|
+
let serviceUrl = this._getServerUrl();
|
|
1961
2217
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1962
2218
|
info.screenshotPath = screenshotPath;
|
|
1963
2219
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2106,7 +2362,16 @@ class StableBrowser {
|
|
|
2106
2362
|
let screenshotId = null;
|
|
2107
2363
|
let screenshotPath = null;
|
|
2108
2364
|
const info = {};
|
|
2109
|
-
info.log =
|
|
2365
|
+
info.log =
|
|
2366
|
+
"***** analyze table " +
|
|
2367
|
+
selectors.element_name +
|
|
2368
|
+
" query " +
|
|
2369
|
+
query +
|
|
2370
|
+
" operator " +
|
|
2371
|
+
operator +
|
|
2372
|
+
" value " +
|
|
2373
|
+
value +
|
|
2374
|
+
" *****\n";
|
|
2110
2375
|
info.operation = "analyzeTable";
|
|
2111
2376
|
info.selectors = selectors;
|
|
2112
2377
|
info.query = query;
|
|
@@ -2340,13 +2605,6 @@ class StableBrowser {
|
|
|
2340
2605
|
const info = {};
|
|
2341
2606
|
try {
|
|
2342
2607
|
await this.page.close();
|
|
2343
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2344
|
-
this.context.pages.pop();
|
|
2345
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2346
|
-
this.context.page = this.page;
|
|
2347
|
-
let title = await this.page.title();
|
|
2348
|
-
console.log("Switched to page " + title);
|
|
2349
|
-
}
|
|
2350
2608
|
}
|
|
2351
2609
|
catch (e) {
|
|
2352
2610
|
console.log(".");
|