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