automation_model 1.0.365-dev.0 → 1.0.365-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 +11 -0
- package/lib/stable_browser.js +329 -137
- 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,7 @@ 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",
|
|
38
42
|
};
|
|
39
43
|
class StableBrowser {
|
|
40
44
|
constructor(browser, page, logger = null, context = null) {
|
|
@@ -80,9 +84,15 @@ class StableBrowser {
|
|
|
80
84
|
this.page = page;
|
|
81
85
|
context.page = page;
|
|
82
86
|
context.pages.push(page);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
page.on("close", async () => {
|
|
88
|
+
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
89
|
+
this.context.pages.pop();
|
|
90
|
+
this.page = this.context.pages[this.context.pages.length - 1];
|
|
91
|
+
this.context.page = this.page;
|
|
92
|
+
let title = await this.page.title();
|
|
93
|
+
console.log("Switched to page " + title);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
86
96
|
try {
|
|
87
97
|
await this.waitForPageLoad();
|
|
88
98
|
console.log("Switch page: " + (await page.title()));
|
|
@@ -91,7 +101,7 @@ class StableBrowser {
|
|
|
91
101
|
this.logger.error("error on page load " + e);
|
|
92
102
|
}
|
|
93
103
|
context.pageLoading.status = false;
|
|
94
|
-
});
|
|
104
|
+
}.bind(this));
|
|
95
105
|
}
|
|
96
106
|
getWebLogFile(logFolder) {
|
|
97
107
|
if (!fs.existsSync(logFolder)) {
|
|
@@ -168,23 +178,65 @@ class StableBrowser {
|
|
|
168
178
|
return text;
|
|
169
179
|
}
|
|
170
180
|
for (let key in _params) {
|
|
171
|
-
|
|
181
|
+
let regValue = key;
|
|
182
|
+
if (key.startsWith("_")) {
|
|
183
|
+
// remove the _ prefix
|
|
184
|
+
regValue = key.substring(1);
|
|
185
|
+
}
|
|
186
|
+
text = text.replaceAll(new RegExp("{" + regValue + "}", "g"), _params[key]);
|
|
172
187
|
}
|
|
173
188
|
return text;
|
|
174
189
|
}
|
|
190
|
+
_fixLocatorUsingParams(locator, _params) {
|
|
191
|
+
// check if not null
|
|
192
|
+
if (!locator) {
|
|
193
|
+
return locator;
|
|
194
|
+
}
|
|
195
|
+
// clone the locator
|
|
196
|
+
locator = JSON.parse(JSON.stringify(locator));
|
|
197
|
+
this.scanAndManipulate(locator, _params);
|
|
198
|
+
return locator;
|
|
199
|
+
}
|
|
200
|
+
_isObject(value) {
|
|
201
|
+
return value && typeof value === "object" && value.constructor === Object;
|
|
202
|
+
}
|
|
203
|
+
scanAndManipulate(currentObj, _params) {
|
|
204
|
+
for (const key in currentObj) {
|
|
205
|
+
if (typeof currentObj[key] === "string") {
|
|
206
|
+
// Perform string manipulation
|
|
207
|
+
currentObj[key] = this._fixUsingParams(currentObj[key], _params);
|
|
208
|
+
}
|
|
209
|
+
else if (this._isObject(currentObj[key])) {
|
|
210
|
+
// Recursively scan nested objects
|
|
211
|
+
this.scanAndManipulate(currentObj[key], _params);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
175
215
|
_getLocator(locator, scope, _params) {
|
|
216
|
+
locator = this._fixLocatorUsingParams(locator, _params);
|
|
217
|
+
if (locator.type === "pw_selector") {
|
|
218
|
+
return scope.locator(locator.selector);
|
|
219
|
+
}
|
|
176
220
|
if (locator.role) {
|
|
177
221
|
if (locator.role[1].nameReg) {
|
|
178
222
|
locator.role[1].name = reg_parser(locator.role[1].nameReg);
|
|
179
223
|
delete locator.role[1].nameReg;
|
|
180
224
|
}
|
|
181
|
-
if (locator.role[1].name) {
|
|
182
|
-
|
|
183
|
-
}
|
|
225
|
+
// if (locator.role[1].name) {
|
|
226
|
+
// locator.role[1].name = this._fixUsingParams(locator.role[1].name, _params);
|
|
227
|
+
// }
|
|
184
228
|
return scope.getByRole(locator.role[0], locator.role[1]);
|
|
185
229
|
}
|
|
186
230
|
if (locator.css) {
|
|
187
|
-
return scope.locator(
|
|
231
|
+
return scope.locator(locator.css);
|
|
232
|
+
}
|
|
233
|
+
if ((locator === null || locator === void 0 ? void 0 : locator.engine) && (locator === null || locator === void 0 ? void 0 : locator.score) <= 520) {
|
|
234
|
+
let selector = locator.selector.replace(/"/g, '\\"');
|
|
235
|
+
if (locator.engine === "internal:att") {
|
|
236
|
+
selector = `[${selector}]`;
|
|
237
|
+
}
|
|
238
|
+
const locator = scope.locator(`${locator.engine}="${selector}"`);
|
|
239
|
+
return locator;
|
|
188
240
|
}
|
|
189
241
|
throw new Error("unknown locator type");
|
|
190
242
|
}
|
|
@@ -309,10 +361,13 @@ class StableBrowser {
|
|
|
309
361
|
}
|
|
310
362
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
311
363
|
let locatorSearch = selectorHierarchy[index];
|
|
312
|
-
info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
364
|
+
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
313
365
|
let locator = null;
|
|
314
366
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
315
367
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
368
|
+
if (!locatorString) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
316
371
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
317
372
|
}
|
|
318
373
|
else if (locatorSearch.text) {
|
|
@@ -334,7 +389,7 @@ class StableBrowser {
|
|
|
334
389
|
// cssHref = true;
|
|
335
390
|
// }
|
|
336
391
|
let count = await locator.count();
|
|
337
|
-
info.log += "total elements found " + count + "\n";
|
|
392
|
+
//info.log += "total elements found " + count + "\n";
|
|
338
393
|
//let visibleCount = 0;
|
|
339
394
|
let visibleLocator = null;
|
|
340
395
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -344,16 +399,20 @@ class StableBrowser {
|
|
|
344
399
|
for (let j = 0; j < count; j++) {
|
|
345
400
|
let visible = await locator.nth(j).isVisible();
|
|
346
401
|
const enabled = await locator.nth(j).isEnabled();
|
|
347
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
348
402
|
if (!visibleOnly) {
|
|
349
403
|
visible = true;
|
|
350
404
|
}
|
|
351
405
|
if (visible && enabled) {
|
|
352
406
|
foundLocators.push(locator.nth(j));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
if (!info.printMessages) {
|
|
410
|
+
info.printMessages = {};
|
|
411
|
+
}
|
|
412
|
+
if (!info.printMessages[j.toString()]) {
|
|
413
|
+
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
414
|
+
info.printMessages[j.toString()] = true;
|
|
415
|
+
}
|
|
357
416
|
}
|
|
358
417
|
}
|
|
359
418
|
}
|
|
@@ -379,15 +438,30 @@ class StableBrowser {
|
|
|
379
438
|
}
|
|
380
439
|
if (result.foundElements.length > 0) {
|
|
381
440
|
// need to handle popup
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
441
|
+
const closeHandlerGroup = [];
|
|
442
|
+
closeHandlerGroup.push(this.configuration.popupHandlers[result.locatorIndex].close_dialog_locator);
|
|
443
|
+
for (let i = 0; i < scopes.length; i++) {
|
|
444
|
+
result = await this._scanLocatorsGroup(closeHandlerGroup, scopes[i], _params, info, true);
|
|
445
|
+
if (result.foundElements.length > 0) {
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (result.foundElements.length > 0) {
|
|
450
|
+
let dialogCloseLocator = result.foundElements[0].locator;
|
|
451
|
+
await dialogCloseLocator.click();
|
|
452
|
+
return { rerun: true };
|
|
453
|
+
}
|
|
385
454
|
}
|
|
386
455
|
}
|
|
387
456
|
return { rerun: false };
|
|
388
457
|
}
|
|
389
458
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
390
459
|
for (let i = 0; i < 3; i++) {
|
|
460
|
+
info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
|
|
461
|
+
for (let j = 0; j < selectors.locators.length; j++) {
|
|
462
|
+
let selector = selectors.locators[j];
|
|
463
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
464
|
+
}
|
|
391
465
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
392
466
|
if (!element.rerun) {
|
|
393
467
|
return element;
|
|
@@ -403,6 +477,7 @@ class StableBrowser {
|
|
|
403
477
|
//let arrayMode = Array.isArray(selectors);
|
|
404
478
|
let scope = this.page;
|
|
405
479
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
480
|
+
info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
|
|
406
481
|
while (true) {
|
|
407
482
|
let frameFound = false;
|
|
408
483
|
if (selectors.frameLocators) {
|
|
@@ -463,19 +538,25 @@ class StableBrowser {
|
|
|
463
538
|
if (popupResult.rerun) {
|
|
464
539
|
return popupResult;
|
|
465
540
|
}
|
|
466
|
-
info.log += "scanning locators in priority 1" + "\n";
|
|
541
|
+
// info.log += "scanning locators in priority 1" + "\n";
|
|
542
|
+
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
467
543
|
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
468
544
|
if (result.foundElements.length === 0) {
|
|
469
|
-
info.log += "scanning locators in priority 2" + "\n";
|
|
545
|
+
// info.log += "scanning locators in priority 2" + "\n";
|
|
470
546
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
471
547
|
}
|
|
472
|
-
if (result.foundElements.length === 0 &&
|
|
473
|
-
info.log += "scanning locators in priority 3" + "\n";
|
|
548
|
+
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
474
549
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
475
550
|
}
|
|
551
|
+
else {
|
|
552
|
+
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
553
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
476
556
|
let foundElements = result.foundElements;
|
|
477
557
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
478
558
|
info.box = foundElements[0].box;
|
|
559
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
479
560
|
return foundElements[0].locator;
|
|
480
561
|
}
|
|
481
562
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -505,6 +586,7 @@ class StableBrowser {
|
|
|
505
586
|
}
|
|
506
587
|
}
|
|
507
588
|
if (maxCountElement) {
|
|
589
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
508
590
|
info.box = await maxCountElement.locator.boundingBox();
|
|
509
591
|
return maxCountElement.locator;
|
|
510
592
|
}
|
|
@@ -513,9 +595,11 @@ class StableBrowser {
|
|
|
513
595
|
break;
|
|
514
596
|
}
|
|
515
597
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
598
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
516
599
|
highPriorityOnly = false;
|
|
517
600
|
}
|
|
518
601
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
602
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
519
603
|
visibleOnly = false;
|
|
520
604
|
}
|
|
521
605
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -560,7 +644,7 @@ class StableBrowser {
|
|
|
560
644
|
this._validateSelectors(selectors);
|
|
561
645
|
const startTime = Date.now();
|
|
562
646
|
const info = {};
|
|
563
|
-
info.log = "";
|
|
647
|
+
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
564
648
|
info.operation = "click";
|
|
565
649
|
info.selectors = selectors;
|
|
566
650
|
let error = null;
|
|
@@ -905,12 +989,12 @@ class StableBrowser {
|
|
|
905
989
|
await this.waitForPageLoad();
|
|
906
990
|
}
|
|
907
991
|
}
|
|
908
|
-
catch (
|
|
992
|
+
catch (err) {
|
|
909
993
|
//await this.closeUnexpectedPopups();
|
|
910
994
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
911
995
|
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
912
996
|
info.screenshotPath = screenshotPath;
|
|
913
|
-
Object.assign(
|
|
997
|
+
Object.assign(err, { info: info });
|
|
914
998
|
await element.click();
|
|
915
999
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
916
1000
|
if (format) {
|
|
@@ -931,76 +1015,7 @@ class StableBrowser {
|
|
|
931
1015
|
}
|
|
932
1016
|
}
|
|
933
1017
|
}
|
|
934
|
-
catch (
|
|
935
|
-
error = e;
|
|
936
|
-
throw e;
|
|
937
|
-
}
|
|
938
|
-
finally {
|
|
939
|
-
const endTime = Date.now();
|
|
940
|
-
this._reportToWorld(world, {
|
|
941
|
-
element_name: selectors.element_name,
|
|
942
|
-
type: Types.SET_DATE_TIME,
|
|
943
|
-
screenshotId,
|
|
944
|
-
value: value,
|
|
945
|
-
text: `setDateTime input with value: ${value}`,
|
|
946
|
-
result: error
|
|
947
|
-
? {
|
|
948
|
-
status: "FAILED",
|
|
949
|
-
startTime,
|
|
950
|
-
endTime,
|
|
951
|
-
message: error === null || error === void 0 ? void 0 : error.message,
|
|
952
|
-
}
|
|
953
|
-
: {
|
|
954
|
-
status: "PASSED",
|
|
955
|
-
startTime,
|
|
956
|
-
endTime,
|
|
957
|
-
},
|
|
958
|
-
info: info,
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
963
|
-
this._validateSelectors(selectors);
|
|
964
|
-
const startTime = Date.now();
|
|
965
|
-
let error = null;
|
|
966
|
-
let screenshotId = null;
|
|
967
|
-
let screenshotPath = null;
|
|
968
|
-
const info = {};
|
|
969
|
-
info.log = "";
|
|
970
|
-
info.operation = Types.SET_DATE_TIME;
|
|
971
|
-
info.selectors = selectors;
|
|
972
|
-
info.value = value;
|
|
973
|
-
try {
|
|
974
|
-
let element = await this._locate(selectors, info, _params);
|
|
975
|
-
//insert red border around the element
|
|
976
|
-
await this.scrollIfNeeded(element, info);
|
|
977
|
-
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
978
|
-
await this._highlightElements(element);
|
|
979
|
-
try {
|
|
980
|
-
await element.click();
|
|
981
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
982
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
983
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
984
|
-
el.value = ""; // clear input
|
|
985
|
-
el.value = dateTimeValue;
|
|
986
|
-
}, dateTimeValue);
|
|
987
|
-
}
|
|
988
|
-
catch (error) {
|
|
989
|
-
//await this.closeUnexpectedPopups();
|
|
990
|
-
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
991
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
992
|
-
info.screenshotPath = screenshotPath;
|
|
993
|
-
Object.assign(error, { info: info });
|
|
994
|
-
await element.click();
|
|
995
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
996
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
997
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
998
|
-
el.value = ""; // clear input
|
|
999
|
-
el.value = dateTimeValue;
|
|
1000
|
-
}, dateTimeValue);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
catch (error) {
|
|
1018
|
+
catch (e) {
|
|
1004
1019
|
error = e;
|
|
1005
1020
|
throw e;
|
|
1006
1021
|
}
|
|
@@ -1035,7 +1050,7 @@ class StableBrowser {
|
|
|
1035
1050
|
let screenshotId = null;
|
|
1036
1051
|
let screenshotPath = null;
|
|
1037
1052
|
const info = {};
|
|
1038
|
-
info.log = "";
|
|
1053
|
+
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1039
1054
|
info.operation = "clickType";
|
|
1040
1055
|
info.selectors = selectors;
|
|
1041
1056
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
@@ -1050,20 +1065,32 @@ class StableBrowser {
|
|
|
1050
1065
|
await this.scrollIfNeeded(element, info);
|
|
1051
1066
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1052
1067
|
await this._highlightElements(element);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1068
|
+
if (options === null || options === undefined || !options.press) {
|
|
1069
|
+
try {
|
|
1070
|
+
let currentValue = await element.inputValue();
|
|
1071
|
+
if (currentValue) {
|
|
1072
|
+
await element.fill("");
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
catch (e) {
|
|
1076
|
+
this.logger.info("unable to clear input value");
|
|
1057
1077
|
}
|
|
1058
1078
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1079
|
+
if (options === null || options === undefined || options.press) {
|
|
1080
|
+
try {
|
|
1081
|
+
await element.click({ timeout: 5000 });
|
|
1082
|
+
}
|
|
1083
|
+
catch (e) {
|
|
1084
|
+
await element.dispatchEvent("click");
|
|
1085
|
+
}
|
|
1064
1086
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1087
|
+
else {
|
|
1088
|
+
try {
|
|
1089
|
+
await element.focus();
|
|
1090
|
+
}
|
|
1091
|
+
catch (e) {
|
|
1092
|
+
await element.dispatchEvent("focus");
|
|
1093
|
+
}
|
|
1067
1094
|
}
|
|
1068
1095
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1069
1096
|
const valueSegment = _value.split("&&");
|
|
@@ -1143,7 +1170,7 @@ class StableBrowser {
|
|
|
1143
1170
|
let screenshotId = null;
|
|
1144
1171
|
let screenshotPath = null;
|
|
1145
1172
|
const info = {};
|
|
1146
|
-
info.log = "";
|
|
1173
|
+
info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
|
|
1147
1174
|
info.operation = "fill";
|
|
1148
1175
|
info.selectors = selectors;
|
|
1149
1176
|
info.value = value;
|
|
@@ -1259,7 +1286,8 @@ class StableBrowser {
|
|
|
1259
1286
|
let screenshotId = null;
|
|
1260
1287
|
let screenshotPath = null;
|
|
1261
1288
|
const info = {};
|
|
1262
|
-
info.log =
|
|
1289
|
+
info.log =
|
|
1290
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1263
1291
|
info.operation = "containsPattern";
|
|
1264
1292
|
info.selectors = selectors;
|
|
1265
1293
|
info.value = text;
|
|
@@ -1325,7 +1353,7 @@ class StableBrowser {
|
|
|
1325
1353
|
let screenshotId = null;
|
|
1326
1354
|
let screenshotPath = null;
|
|
1327
1355
|
const info = {};
|
|
1328
|
-
info.log = "";
|
|
1356
|
+
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1329
1357
|
info.operation = "containsText";
|
|
1330
1358
|
info.selectors = selectors;
|
|
1331
1359
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -1429,15 +1457,62 @@ class StableBrowser {
|
|
|
1429
1457
|
// save the data to the file
|
|
1430
1458
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1431
1459
|
}
|
|
1460
|
+
_getDataFilePath(fileName) {
|
|
1461
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1462
|
+
if (fs.existsSync(dataFile)) {
|
|
1463
|
+
return dataFile;
|
|
1464
|
+
}
|
|
1465
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1466
|
+
if (fs.existsSync(dataFile)) {
|
|
1467
|
+
return dataFile;
|
|
1468
|
+
}
|
|
1469
|
+
throw new Error("data file not found " + fileName);
|
|
1470
|
+
}
|
|
1471
|
+
_parseCSVSync(filePath) {
|
|
1472
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1473
|
+
const results = [];
|
|
1474
|
+
return new Promise((resolve, reject) => {
|
|
1475
|
+
const readableStream = new Readable();
|
|
1476
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1477
|
+
readableStream.push(data);
|
|
1478
|
+
readableStream.push(null);
|
|
1479
|
+
readableStream
|
|
1480
|
+
.pipe(csv())
|
|
1481
|
+
.on("data", (data) => results.push(data))
|
|
1482
|
+
.on("end", () => resolve(results))
|
|
1483
|
+
.on("error", (error) => reject(error));
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1432
1486
|
loadTestData(type, dataSelector, world = null) {
|
|
1433
1487
|
switch (type) {
|
|
1434
1488
|
case "users":
|
|
1435
|
-
//
|
|
1436
|
-
|
|
1437
|
-
|
|
1489
|
+
// get the users.json file path
|
|
1490
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1491
|
+
// read the file and return the data
|
|
1492
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1493
|
+
for (let i = 0; i < users.length; i++) {
|
|
1494
|
+
if (users[i].username === dataSelector) {
|
|
1495
|
+
const userObj = {
|
|
1496
|
+
username: users[i].username,
|
|
1497
|
+
password: "secret:" + users[i].password,
|
|
1498
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1499
|
+
};
|
|
1500
|
+
this.setTestData(userObj, world);
|
|
1501
|
+
return userObj;
|
|
1502
|
+
}
|
|
1438
1503
|
}
|
|
1504
|
+
throw new Error("user not found " + dataSelector);
|
|
1505
|
+
default:
|
|
1506
|
+
throw new Error("unknown type " + type);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1510
|
+
switch (type) {
|
|
1511
|
+
case "users": {
|
|
1512
|
+
// get the users.json file path
|
|
1513
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1439
1514
|
// read the file and return the data
|
|
1440
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1515
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1441
1516
|
for (let i = 0; i < users.length; i++) {
|
|
1442
1517
|
if (users[i].username === dataSelector) {
|
|
1443
1518
|
const userObj = {
|
|
@@ -1450,6 +1525,29 @@ class StableBrowser {
|
|
|
1450
1525
|
}
|
|
1451
1526
|
}
|
|
1452
1527
|
throw new Error("user not found " + dataSelector);
|
|
1528
|
+
}
|
|
1529
|
+
case "csv": {
|
|
1530
|
+
// 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
|
|
1531
|
+
const parts = dataSelector.split(":");
|
|
1532
|
+
let rowNumber = 0;
|
|
1533
|
+
if (parts.length > 1) {
|
|
1534
|
+
rowNumber = parseInt(parts[1]);
|
|
1535
|
+
}
|
|
1536
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1537
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1538
|
+
// result stracture:
|
|
1539
|
+
// [
|
|
1540
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1541
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1542
|
+
// ]
|
|
1543
|
+
// verify the row number is within the range
|
|
1544
|
+
if (rowNumber >= results.length) {
|
|
1545
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1546
|
+
}
|
|
1547
|
+
const data = results[rowNumber];
|
|
1548
|
+
this.setTestData(data, world);
|
|
1549
|
+
return data;
|
|
1550
|
+
}
|
|
1453
1551
|
default:
|
|
1454
1552
|
throw new Error("unknown type " + type);
|
|
1455
1553
|
}
|
|
@@ -1588,7 +1686,7 @@ class StableBrowser {
|
|
|
1588
1686
|
let screenshotPath = null;
|
|
1589
1687
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1590
1688
|
const info = {};
|
|
1591
|
-
info.log = "";
|
|
1689
|
+
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1592
1690
|
info.operation = "verify";
|
|
1593
1691
|
info.selectors = selectors;
|
|
1594
1692
|
try {
|
|
@@ -1641,7 +1739,7 @@ class StableBrowser {
|
|
|
1641
1739
|
let screenshotPath = null;
|
|
1642
1740
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1643
1741
|
const info = {};
|
|
1644
|
-
info.log = "";
|
|
1742
|
+
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1645
1743
|
info.operation = "extract";
|
|
1646
1744
|
info.selectors = selectors;
|
|
1647
1745
|
try {
|
|
@@ -1666,7 +1764,8 @@ class StableBrowser {
|
|
|
1666
1764
|
if (world) {
|
|
1667
1765
|
world[variable] = info.value;
|
|
1668
1766
|
}
|
|
1669
|
-
this.
|
|
1767
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1768
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1670
1769
|
return info;
|
|
1671
1770
|
}
|
|
1672
1771
|
catch (e) {
|
|
@@ -1703,6 +1802,91 @@ class StableBrowser {
|
|
|
1703
1802
|
});
|
|
1704
1803
|
}
|
|
1705
1804
|
}
|
|
1805
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1806
|
+
if (!emailAddress) {
|
|
1807
|
+
throw new Error("email address is null");
|
|
1808
|
+
}
|
|
1809
|
+
// check if address contain @
|
|
1810
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1811
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1815
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const startTime = Date.now();
|
|
1819
|
+
let timeout = 60000;
|
|
1820
|
+
if (options && options.timeout) {
|
|
1821
|
+
timeout = options.timeout;
|
|
1822
|
+
}
|
|
1823
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1824
|
+
const request = {
|
|
1825
|
+
method: "POST",
|
|
1826
|
+
url: serviceUrl,
|
|
1827
|
+
headers: {
|
|
1828
|
+
"Content-Type": "application/json",
|
|
1829
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1830
|
+
},
|
|
1831
|
+
data: JSON.stringify({
|
|
1832
|
+
email: emailAddress,
|
|
1833
|
+
}),
|
|
1834
|
+
};
|
|
1835
|
+
let errorCount = 0;
|
|
1836
|
+
while (true) {
|
|
1837
|
+
try {
|
|
1838
|
+
let result = await this.context.api.request(request);
|
|
1839
|
+
// the response body expected to be the following:
|
|
1840
|
+
// {
|
|
1841
|
+
// "status": true,
|
|
1842
|
+
// "content": {
|
|
1843
|
+
// "url": "",
|
|
1844
|
+
// "code": "112112",
|
|
1845
|
+
// "name": "generate_link_or_code"
|
|
1846
|
+
// }
|
|
1847
|
+
//}
|
|
1848
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1849
|
+
let codeOrUrlFound = false;
|
|
1850
|
+
let emailCode = null;
|
|
1851
|
+
let emailUrl = null;
|
|
1852
|
+
// check if a code is returned
|
|
1853
|
+
if (result.data.content && result.data.content.code) {
|
|
1854
|
+
let code = result.data.content.code;
|
|
1855
|
+
this.setTestData({ emailCode: code }, world);
|
|
1856
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1857
|
+
emailCode = code;
|
|
1858
|
+
codeOrUrlFound = true;
|
|
1859
|
+
}
|
|
1860
|
+
// check if a url is returned
|
|
1861
|
+
if (result.data.content && result.data.content.url) {
|
|
1862
|
+
let url = result.data.content.url;
|
|
1863
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1864
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1865
|
+
emailUrl = url;
|
|
1866
|
+
codeOrUrlFound = true;
|
|
1867
|
+
}
|
|
1868
|
+
if (codeOrUrlFound) {
|
|
1869
|
+
return { emailUrl, emailCode };
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
this.logger.info("an email received but no code or url found");
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
catch (e) {
|
|
1877
|
+
errorCount++;
|
|
1878
|
+
if (errorCount > 3) {
|
|
1879
|
+
throw e;
|
|
1880
|
+
}
|
|
1881
|
+
// ignore
|
|
1882
|
+
}
|
|
1883
|
+
// check if the timeout is reached
|
|
1884
|
+
if (Date.now() - startTime > timeout) {
|
|
1885
|
+
throw new Error("timeout reached");
|
|
1886
|
+
}
|
|
1887
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1706
1890
|
async _highlightElements(scope, css) {
|
|
1707
1891
|
try {
|
|
1708
1892
|
if (!scope) {
|
|
@@ -1774,7 +1958,7 @@ class StableBrowser {
|
|
|
1774
1958
|
let screenshotPath = null;
|
|
1775
1959
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1776
1960
|
const info = {};
|
|
1777
|
-
info.log = "";
|
|
1961
|
+
info.log = "***** verify page path " + pathPart + " *****\n";
|
|
1778
1962
|
info.operation = "verifyPagePath";
|
|
1779
1963
|
const newValue = await this._replaceWithLocalData(pathPart, world);
|
|
1780
1964
|
if (newValue !== pathPart) {
|
|
@@ -1835,7 +2019,7 @@ class StableBrowser {
|
|
|
1835
2019
|
let screenshotPath = null;
|
|
1836
2020
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1837
2021
|
const info = {};
|
|
1838
|
-
info.log = "";
|
|
2022
|
+
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
1839
2023
|
info.operation = "verifyTextExistInPage";
|
|
1840
2024
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1841
2025
|
if (newValue !== text) {
|
|
@@ -1884,8 +2068,10 @@ class StableBrowser {
|
|
|
1884
2068
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1885
2069
|
await this._highlightElements(frame, dataAttribute);
|
|
1886
2070
|
const element = await frame.$(dataAttribute);
|
|
1887
|
-
|
|
1888
|
-
|
|
2071
|
+
if (element) {
|
|
2072
|
+
await this.scrollIfNeeded(element, info);
|
|
2073
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2074
|
+
}
|
|
1889
2075
|
}
|
|
1890
2076
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1891
2077
|
return info;
|
|
@@ -1923,6 +2109,16 @@ class StableBrowser {
|
|
|
1923
2109
|
});
|
|
1924
2110
|
}
|
|
1925
2111
|
}
|
|
2112
|
+
_getServerUrl() {
|
|
2113
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2114
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2115
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2116
|
+
}
|
|
2117
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2118
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2119
|
+
}
|
|
2120
|
+
return serviceUrl;
|
|
2121
|
+
}
|
|
1926
2122
|
async visualVerification(text, options = {}, world = null) {
|
|
1927
2123
|
const startTime = Date.now();
|
|
1928
2124
|
let error = null;
|
|
@@ -1937,13 +2133,7 @@ class StableBrowser {
|
|
|
1937
2133
|
throw new Error("TOKEN is not set");
|
|
1938
2134
|
}
|
|
1939
2135
|
try {
|
|
1940
|
-
let serviceUrl =
|
|
1941
|
-
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
1942
|
-
serviceUrl = "https://dev.api.blinq.io";
|
|
1943
|
-
}
|
|
1944
|
-
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
1945
|
-
serviceUrl = "https://stage.api.blinq.io";
|
|
1946
|
-
}
|
|
2136
|
+
let serviceUrl = this._getServerUrl();
|
|
1947
2137
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1948
2138
|
info.screenshotPath = screenshotPath;
|
|
1949
2139
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2092,7 +2282,16 @@ class StableBrowser {
|
|
|
2092
2282
|
let screenshotId = null;
|
|
2093
2283
|
let screenshotPath = null;
|
|
2094
2284
|
const info = {};
|
|
2095
|
-
info.log =
|
|
2285
|
+
info.log =
|
|
2286
|
+
"***** analyze table " +
|
|
2287
|
+
selectors.element_name +
|
|
2288
|
+
" query " +
|
|
2289
|
+
query +
|
|
2290
|
+
" operator " +
|
|
2291
|
+
operator +
|
|
2292
|
+
" value " +
|
|
2293
|
+
value +
|
|
2294
|
+
" *****\n";
|
|
2096
2295
|
info.operation = "analyzeTable";
|
|
2097
2296
|
info.selectors = selectors;
|
|
2098
2297
|
info.query = query;
|
|
@@ -2326,13 +2525,6 @@ class StableBrowser {
|
|
|
2326
2525
|
const info = {};
|
|
2327
2526
|
try {
|
|
2328
2527
|
await this.page.close();
|
|
2329
|
-
if (this.context && this.context.pages && this.context.pages.length > 0) {
|
|
2330
|
-
this.context.pages.pop();
|
|
2331
|
-
this.page = this.context.pages[this.context.pages.length - 1];
|
|
2332
|
-
this.context.page = this.page;
|
|
2333
|
-
let title = await this.page.title();
|
|
2334
|
-
console.log("Switched to page " + title);
|
|
2335
|
-
}
|
|
2336
2528
|
}
|
|
2337
2529
|
catch (e) {
|
|
2338
2530
|
console.log(".");
|