automation_model 1.0.365 → 1.0.366-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 +390 -130
- 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,65 @@ 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
|
+
if ((locator === null || locator === void 0 ? void 0 : locator.engine) && (locator === null || locator === void 0 ? void 0 : locator.score) <= 520) {
|
|
235
|
+
let selector = locator.selector.replace(/"/g, '\\"');
|
|
236
|
+
if (locator.engine === "internal:att") {
|
|
237
|
+
selector = `[${selector}]`;
|
|
238
|
+
}
|
|
239
|
+
const locator = scope.locator(`${locator.engine}="${selector}"`);
|
|
240
|
+
return locator;
|
|
188
241
|
}
|
|
189
242
|
throw new Error("unknown locator type");
|
|
190
243
|
}
|
|
@@ -309,10 +362,13 @@ class StableBrowser {
|
|
|
309
362
|
}
|
|
310
363
|
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
|
|
311
364
|
let locatorSearch = selectorHierarchy[index];
|
|
312
|
-
info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
365
|
+
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
313
366
|
let locator = null;
|
|
314
367
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
315
368
|
let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
|
|
369
|
+
if (!locatorString) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
316
372
|
locator = this._getLocator({ css: locatorString }, scope, _params);
|
|
317
373
|
}
|
|
318
374
|
else if (locatorSearch.text) {
|
|
@@ -334,7 +390,7 @@ class StableBrowser {
|
|
|
334
390
|
// cssHref = true;
|
|
335
391
|
// }
|
|
336
392
|
let count = await locator.count();
|
|
337
|
-
info.log += "total elements found " + count + "\n";
|
|
393
|
+
//info.log += "total elements found " + count + "\n";
|
|
338
394
|
//let visibleCount = 0;
|
|
339
395
|
let visibleLocator = null;
|
|
340
396
|
if (locatorSearch.index && locatorSearch.index < count) {
|
|
@@ -344,16 +400,20 @@ class StableBrowser {
|
|
|
344
400
|
for (let j = 0; j < count; j++) {
|
|
345
401
|
let visible = await locator.nth(j).isVisible();
|
|
346
402
|
const enabled = await locator.nth(j).isEnabled();
|
|
347
|
-
info.log += "element " + j + " visible " + visible + " enabled " + enabled + "\n";
|
|
348
403
|
if (!visibleOnly) {
|
|
349
404
|
visible = true;
|
|
350
405
|
}
|
|
351
406
|
if (visible && enabled) {
|
|
352
407
|
foundLocators.push(locator.nth(j));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
if (!info.printMessages) {
|
|
411
|
+
info.printMessages = {};
|
|
412
|
+
}
|
|
413
|
+
if (!info.printMessages[j.toString()]) {
|
|
414
|
+
info.log += "element " + locator + " visible " + visible + " enabled " + enabled + "\n";
|
|
415
|
+
info.printMessages[j.toString()] = true;
|
|
416
|
+
}
|
|
357
417
|
}
|
|
358
418
|
}
|
|
359
419
|
}
|
|
@@ -379,15 +439,30 @@ class StableBrowser {
|
|
|
379
439
|
}
|
|
380
440
|
if (result.foundElements.length > 0) {
|
|
381
441
|
// need to handle popup
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
442
|
+
const closeHandlerGroup = [];
|
|
443
|
+
closeHandlerGroup.push(this.configuration.popupHandlers[result.locatorIndex].close_dialog_locator);
|
|
444
|
+
for (let i = 0; i < scopes.length; i++) {
|
|
445
|
+
result = await this._scanLocatorsGroup(closeHandlerGroup, scopes[i], _params, info, true);
|
|
446
|
+
if (result.foundElements.length > 0) {
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (result.foundElements.length > 0) {
|
|
451
|
+
let dialogCloseLocator = result.foundElements[0].locator;
|
|
452
|
+
await dialogCloseLocator.click();
|
|
453
|
+
return { rerun: true };
|
|
454
|
+
}
|
|
385
455
|
}
|
|
386
456
|
}
|
|
387
457
|
return { rerun: false };
|
|
388
458
|
}
|
|
389
459
|
async _locate(selectors, info, _params, timeout = 30000) {
|
|
390
460
|
for (let i = 0; i < 3; i++) {
|
|
461
|
+
info.log += "attempt " + i + ": totoal locators " + selectors.locators.length + "\n";
|
|
462
|
+
for (let j = 0; j < selectors.locators.length; j++) {
|
|
463
|
+
let selector = selectors.locators[j];
|
|
464
|
+
info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
|
|
465
|
+
}
|
|
391
466
|
let element = await this._locate_internal(selectors, info, _params, timeout);
|
|
392
467
|
if (!element.rerun) {
|
|
393
468
|
return element;
|
|
@@ -403,6 +478,7 @@ class StableBrowser {
|
|
|
403
478
|
//let arrayMode = Array.isArray(selectors);
|
|
404
479
|
let scope = this.page;
|
|
405
480
|
if (selectors.iframe_src || selectors.frameLocators) {
|
|
481
|
+
info.log += "searching for iframe " + selectors.iframe_src + "/" + selectors.frameLocators + "\n";
|
|
406
482
|
while (true) {
|
|
407
483
|
let frameFound = false;
|
|
408
484
|
if (selectors.frameLocators) {
|
|
@@ -463,19 +539,25 @@ class StableBrowser {
|
|
|
463
539
|
if (popupResult.rerun) {
|
|
464
540
|
return popupResult;
|
|
465
541
|
}
|
|
466
|
-
info.log += "scanning locators in priority 1" + "\n";
|
|
542
|
+
// info.log += "scanning locators in priority 1" + "\n";
|
|
543
|
+
let onlyPriority3 = selectorsLocators[0].priority === 3;
|
|
467
544
|
result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
|
|
468
545
|
if (result.foundElements.length === 0) {
|
|
469
|
-
info.log += "scanning locators in priority 2" + "\n";
|
|
546
|
+
// info.log += "scanning locators in priority 2" + "\n";
|
|
470
547
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
|
|
471
548
|
}
|
|
472
|
-
if (result.foundElements.length === 0 &&
|
|
473
|
-
info.log += "scanning locators in priority 3" + "\n";
|
|
549
|
+
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
474
550
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
475
551
|
}
|
|
552
|
+
else {
|
|
553
|
+
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
554
|
+
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
476
557
|
let foundElements = result.foundElements;
|
|
477
558
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
478
559
|
info.box = foundElements[0].box;
|
|
560
|
+
info.log += "unique element was found, locator: " + foundElements[0].locator + "\n";
|
|
479
561
|
return foundElements[0].locator;
|
|
480
562
|
}
|
|
481
563
|
//info.log += "total elements found " + foundElements.length);
|
|
@@ -505,6 +587,7 @@ class StableBrowser {
|
|
|
505
587
|
}
|
|
506
588
|
}
|
|
507
589
|
if (maxCountElement) {
|
|
590
|
+
info.log += "unique element was found, locator: " + maxCountElement.locator + "\n";
|
|
508
591
|
info.box = await maxCountElement.locator.boundingBox();
|
|
509
592
|
return maxCountElement.locator;
|
|
510
593
|
}
|
|
@@ -513,9 +596,11 @@ class StableBrowser {
|
|
|
513
596
|
break;
|
|
514
597
|
}
|
|
515
598
|
if (performance.now() - startTime > highPriorityTimeout) {
|
|
599
|
+
info.log += "high priority timeout, will try all elements" + "\n";
|
|
516
600
|
highPriorityOnly = false;
|
|
517
601
|
}
|
|
518
602
|
if (performance.now() - startTime > visibleOnlyTimeout) {
|
|
603
|
+
info.log += "visible only timeout, will try all elements" + "\n";
|
|
519
604
|
visibleOnly = false;
|
|
520
605
|
}
|
|
521
606
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -560,7 +645,7 @@ class StableBrowser {
|
|
|
560
645
|
this._validateSelectors(selectors);
|
|
561
646
|
const startTime = Date.now();
|
|
562
647
|
const info = {};
|
|
563
|
-
info.log = "";
|
|
648
|
+
info.log = "***** click on " + selectors.element_name + " *****\n";
|
|
564
649
|
info.operation = "click";
|
|
565
650
|
info.selectors = selectors;
|
|
566
651
|
let error = null;
|
|
@@ -867,71 +952,45 @@ class StableBrowser {
|
|
|
867
952
|
});
|
|
868
953
|
}
|
|
869
954
|
}
|
|
870
|
-
async
|
|
955
|
+
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
956
|
+
// set input value for non fillable inputs like date, time, range, color, etc.
|
|
871
957
|
this._validateSelectors(selectors);
|
|
872
958
|
const startTime = Date.now();
|
|
873
|
-
let error = null;
|
|
874
|
-
let screenshotId = null;
|
|
875
|
-
let screenshotPath = null;
|
|
876
959
|
const info = {};
|
|
877
|
-
info.log = "";
|
|
878
|
-
info.operation =
|
|
960
|
+
info.log = "***** set input value " + selectors.element_name + " *****\n";
|
|
961
|
+
info.operation = "setInputValue";
|
|
879
962
|
info.selectors = selectors;
|
|
963
|
+
value = this._fixUsingParams(value, _params);
|
|
880
964
|
info.value = value;
|
|
965
|
+
let error = null;
|
|
966
|
+
let screenshotId = null;
|
|
967
|
+
let screenshotPath = null;
|
|
881
968
|
try {
|
|
882
969
|
value = await this._replaceWithLocalData(value, this);
|
|
883
970
|
let element = await this._locate(selectors, info, _params);
|
|
884
|
-
//insert red border around the element
|
|
885
971
|
await this.scrollIfNeeded(element, info);
|
|
886
972
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
887
973
|
await this._highlightElements(element);
|
|
888
974
|
try {
|
|
889
|
-
await element.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
value = dayjs(value).format(format);
|
|
893
|
-
await element.fill(value);
|
|
894
|
-
}
|
|
895
|
-
else {
|
|
896
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
897
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
898
|
-
el.value = ""; // clear input
|
|
899
|
-
el.value = dateTimeValue;
|
|
900
|
-
}, dateTimeValue);
|
|
901
|
-
}
|
|
902
|
-
if (enter) {
|
|
903
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
904
|
-
await this.page.keyboard.press("Enter");
|
|
905
|
-
await this.waitForPageLoad();
|
|
906
|
-
}
|
|
975
|
+
await element.evaluateHandle((el, value) => {
|
|
976
|
+
el.value = value;
|
|
977
|
+
}, value);
|
|
907
978
|
}
|
|
908
979
|
catch (error) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
this.logger.info("Trying again")(({ screenshotId, screenshotPath } = await this._screenShot(options, world, info)));
|
|
980
|
+
this.logger.error("setInputValue failed, will try again");
|
|
981
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
912
982
|
info.screenshotPath = screenshotPath;
|
|
913
983
|
Object.assign(error, { info: info });
|
|
914
|
-
await element.
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
value = dayjs(value).format(format);
|
|
918
|
-
await element.fill(value);
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
922
|
-
await element.evaluateHandle((el, dateTimeValue) => {
|
|
923
|
-
el.value = ""; // clear input
|
|
924
|
-
el.value = dateTimeValue;
|
|
925
|
-
}, dateTimeValue);
|
|
926
|
-
}
|
|
927
|
-
if (enter) {
|
|
928
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
929
|
-
await this.page.keyboard.press("Enter");
|
|
930
|
-
await this.waitForPageLoad();
|
|
931
|
-
}
|
|
984
|
+
await element.evaluateHandle((el, value) => {
|
|
985
|
+
el.value = value;
|
|
986
|
+
});
|
|
932
987
|
}
|
|
933
988
|
}
|
|
934
|
-
catch (
|
|
989
|
+
catch (e) {
|
|
990
|
+
this.logger.error("setInputValue failed " + info.log);
|
|
991
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
992
|
+
info.screenshotPath = screenshotPath;
|
|
993
|
+
Object.assign(e, { info: info });
|
|
935
994
|
error = e;
|
|
936
995
|
throw e;
|
|
937
996
|
}
|
|
@@ -939,10 +998,10 @@ class StableBrowser {
|
|
|
939
998
|
const endTime = Date.now();
|
|
940
999
|
this._reportToWorld(world, {
|
|
941
1000
|
element_name: selectors.element_name,
|
|
942
|
-
type: Types.
|
|
943
|
-
|
|
1001
|
+
type: Types.SET_INPUT,
|
|
1002
|
+
text: `Set input value`,
|
|
944
1003
|
value: value,
|
|
945
|
-
|
|
1004
|
+
screenshotId,
|
|
946
1005
|
result: error
|
|
947
1006
|
? {
|
|
948
1007
|
status: "FAILED",
|
|
@@ -959,7 +1018,7 @@ class StableBrowser {
|
|
|
959
1018
|
});
|
|
960
1019
|
}
|
|
961
1020
|
}
|
|
962
|
-
async setDateTime(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
1021
|
+
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
963
1022
|
this._validateSelectors(selectors);
|
|
964
1023
|
const startTime = Date.now();
|
|
965
1024
|
let error = null;
|
|
@@ -971,6 +1030,7 @@ class StableBrowser {
|
|
|
971
1030
|
info.selectors = selectors;
|
|
972
1031
|
info.value = value;
|
|
973
1032
|
try {
|
|
1033
|
+
value = await this._replaceWithLocalData(value, this);
|
|
974
1034
|
let element = await this._locate(selectors, info, _params);
|
|
975
1035
|
//insert red border around the element
|
|
976
1036
|
await this.scrollIfNeeded(element, info);
|
|
@@ -979,28 +1039,51 @@ class StableBrowser {
|
|
|
979
1039
|
try {
|
|
980
1040
|
await element.click();
|
|
981
1041
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1042
|
+
if (format) {
|
|
1043
|
+
value = dayjs(value).format(format);
|
|
1044
|
+
await element.fill(value);
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1048
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1049
|
+
el.value = ""; // clear input
|
|
1050
|
+
el.value = dateTimeValue;
|
|
1051
|
+
}, dateTimeValue);
|
|
1052
|
+
}
|
|
1053
|
+
if (enter) {
|
|
1054
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1055
|
+
await this.page.keyboard.press("Enter");
|
|
1056
|
+
await this.waitForPageLoad();
|
|
1057
|
+
}
|
|
987
1058
|
}
|
|
988
|
-
catch (
|
|
1059
|
+
catch (err) {
|
|
989
1060
|
//await this.closeUnexpectedPopups();
|
|
990
1061
|
this.logger.error("setting date time input failed " + JSON.stringify(info));
|
|
991
|
-
this.logger.info("Trying again")
|
|
1062
|
+
this.logger.info("Trying again");
|
|
1063
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
992
1064
|
info.screenshotPath = screenshotPath;
|
|
993
|
-
Object.assign(
|
|
1065
|
+
Object.assign(err, { info: info });
|
|
994
1066
|
await element.click();
|
|
995
1067
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1068
|
+
if (format) {
|
|
1069
|
+
value = dayjs(value).format(format);
|
|
1070
|
+
await element.fill(value);
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
const dateTimeValue = await getDateTimeValue({ value, element });
|
|
1074
|
+
await element.evaluateHandle((el, dateTimeValue) => {
|
|
1075
|
+
el.value = ""; // clear input
|
|
1076
|
+
el.value = dateTimeValue;
|
|
1077
|
+
}, dateTimeValue);
|
|
1078
|
+
}
|
|
1079
|
+
if (enter) {
|
|
1080
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1081
|
+
await this.page.keyboard.press("Enter");
|
|
1082
|
+
await this.waitForPageLoad();
|
|
1083
|
+
}
|
|
1001
1084
|
}
|
|
1002
1085
|
}
|
|
1003
|
-
catch (
|
|
1086
|
+
catch (e) {
|
|
1004
1087
|
error = e;
|
|
1005
1088
|
throw e;
|
|
1006
1089
|
}
|
|
@@ -1035,7 +1118,7 @@ class StableBrowser {
|
|
|
1035
1118
|
let screenshotId = null;
|
|
1036
1119
|
let screenshotPath = null;
|
|
1037
1120
|
const info = {};
|
|
1038
|
-
info.log = "";
|
|
1121
|
+
info.log = "***** clickType on " + selectors.element_name + " with value " + _value + "*****\n";
|
|
1039
1122
|
info.operation = "clickType";
|
|
1040
1123
|
info.selectors = selectors;
|
|
1041
1124
|
const newValue = await this._replaceWithLocalData(_value, world);
|
|
@@ -1050,20 +1133,32 @@ class StableBrowser {
|
|
|
1050
1133
|
await this.scrollIfNeeded(element, info);
|
|
1051
1134
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1052
1135
|
await this._highlightElements(element);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1136
|
+
if (options === null || options === undefined || !options.press) {
|
|
1137
|
+
try {
|
|
1138
|
+
let currentValue = await element.inputValue();
|
|
1139
|
+
if (currentValue) {
|
|
1140
|
+
await element.fill("");
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
catch (e) {
|
|
1144
|
+
this.logger.info("unable to clear input value");
|
|
1057
1145
|
}
|
|
1058
1146
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1147
|
+
if (options === null || options === undefined || options.press) {
|
|
1148
|
+
try {
|
|
1149
|
+
await element.click({ timeout: 5000 });
|
|
1150
|
+
}
|
|
1151
|
+
catch (e) {
|
|
1152
|
+
await element.dispatchEvent("click");
|
|
1153
|
+
}
|
|
1064
1154
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1155
|
+
else {
|
|
1156
|
+
try {
|
|
1157
|
+
await element.focus();
|
|
1158
|
+
}
|
|
1159
|
+
catch (e) {
|
|
1160
|
+
await element.dispatchEvent("focus");
|
|
1161
|
+
}
|
|
1067
1162
|
}
|
|
1068
1163
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1069
1164
|
const valueSegment = _value.split("&&");
|
|
@@ -1143,7 +1238,7 @@ class StableBrowser {
|
|
|
1143
1238
|
let screenshotId = null;
|
|
1144
1239
|
let screenshotPath = null;
|
|
1145
1240
|
const info = {};
|
|
1146
|
-
info.log = "";
|
|
1241
|
+
info.log = "***** fill on " + selectors.element_name + " with value " + value + "*****\n";
|
|
1147
1242
|
info.operation = "fill";
|
|
1148
1243
|
info.selectors = selectors;
|
|
1149
1244
|
info.value = value;
|
|
@@ -1259,7 +1354,8 @@ class StableBrowser {
|
|
|
1259
1354
|
let screenshotId = null;
|
|
1260
1355
|
let screenshotPath = null;
|
|
1261
1356
|
const info = {};
|
|
1262
|
-
info.log =
|
|
1357
|
+
info.log =
|
|
1358
|
+
"***** verify element " + selectors.element_name + " contains pattern " + pattern + "/" + text + " *****\n";
|
|
1263
1359
|
info.operation = "containsPattern";
|
|
1264
1360
|
info.selectors = selectors;
|
|
1265
1361
|
info.value = text;
|
|
@@ -1325,7 +1421,7 @@ class StableBrowser {
|
|
|
1325
1421
|
let screenshotId = null;
|
|
1326
1422
|
let screenshotPath = null;
|
|
1327
1423
|
const info = {};
|
|
1328
|
-
info.log = "";
|
|
1424
|
+
info.log = "***** verify element " + selectors.element_name + " contains text " + text + " *****\n";
|
|
1329
1425
|
info.operation = "containsText";
|
|
1330
1426
|
info.selectors = selectors;
|
|
1331
1427
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -1429,15 +1525,62 @@ class StableBrowser {
|
|
|
1429
1525
|
// save the data to the file
|
|
1430
1526
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1431
1527
|
}
|
|
1528
|
+
_getDataFilePath(fileName) {
|
|
1529
|
+
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1530
|
+
if (fs.existsSync(dataFile)) {
|
|
1531
|
+
return dataFile;
|
|
1532
|
+
}
|
|
1533
|
+
dataFile = path.join(this.project_path, fileName);
|
|
1534
|
+
if (fs.existsSync(dataFile)) {
|
|
1535
|
+
return dataFile;
|
|
1536
|
+
}
|
|
1537
|
+
throw new Error("data file not found " + fileName);
|
|
1538
|
+
}
|
|
1539
|
+
_parseCSVSync(filePath) {
|
|
1540
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
1541
|
+
const results = [];
|
|
1542
|
+
return new Promise((resolve, reject) => {
|
|
1543
|
+
const readableStream = new Readable();
|
|
1544
|
+
readableStream._read = () => { }; // _read is required but you can noop it
|
|
1545
|
+
readableStream.push(data);
|
|
1546
|
+
readableStream.push(null);
|
|
1547
|
+
readableStream
|
|
1548
|
+
.pipe(csv())
|
|
1549
|
+
.on("data", (data) => results.push(data))
|
|
1550
|
+
.on("end", () => resolve(results))
|
|
1551
|
+
.on("error", (error) => reject(error));
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1432
1554
|
loadTestData(type, dataSelector, world = null) {
|
|
1433
1555
|
switch (type) {
|
|
1434
1556
|
case "users":
|
|
1435
|
-
//
|
|
1436
|
-
|
|
1437
|
-
|
|
1557
|
+
// get the users.json file path
|
|
1558
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1559
|
+
// read the file and return the data
|
|
1560
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1561
|
+
for (let i = 0; i < users.length; i++) {
|
|
1562
|
+
if (users[i].username === dataSelector) {
|
|
1563
|
+
const userObj = {
|
|
1564
|
+
username: users[i].username,
|
|
1565
|
+
password: "secret:" + users[i].password,
|
|
1566
|
+
totp: users[i].secretKey ? "totp:" + users[i].secretKey : null,
|
|
1567
|
+
};
|
|
1568
|
+
this.setTestData(userObj, world);
|
|
1569
|
+
return userObj;
|
|
1570
|
+
}
|
|
1438
1571
|
}
|
|
1572
|
+
throw new Error("user not found " + dataSelector);
|
|
1573
|
+
default:
|
|
1574
|
+
throw new Error("unknown type " + type);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async loadTestDataAsync(type, dataSelector, world = null) {
|
|
1578
|
+
switch (type) {
|
|
1579
|
+
case "users": {
|
|
1580
|
+
// get the users.json file path
|
|
1581
|
+
let dataFile = this._getDataFilePath("users.json");
|
|
1439
1582
|
// read the file and return the data
|
|
1440
|
-
const users = JSON.parse(fs.readFileSync(
|
|
1583
|
+
const users = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1441
1584
|
for (let i = 0; i < users.length; i++) {
|
|
1442
1585
|
if (users[i].username === dataSelector) {
|
|
1443
1586
|
const userObj = {
|
|
@@ -1450,6 +1593,29 @@ class StableBrowser {
|
|
|
1450
1593
|
}
|
|
1451
1594
|
}
|
|
1452
1595
|
throw new Error("user not found " + dataSelector);
|
|
1596
|
+
}
|
|
1597
|
+
case "csv": {
|
|
1598
|
+
// 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
|
|
1599
|
+
const parts = dataSelector.split(":");
|
|
1600
|
+
let rowNumber = 0;
|
|
1601
|
+
if (parts.length > 1) {
|
|
1602
|
+
rowNumber = parseInt(parts[1]);
|
|
1603
|
+
}
|
|
1604
|
+
let dataFile = this._getDataFilePath(parts[0]);
|
|
1605
|
+
const results = await this._parseCSVSync(dataFile);
|
|
1606
|
+
// result stracture:
|
|
1607
|
+
// [
|
|
1608
|
+
// { NAME: 'Daffy Duck', AGE: '24' },
|
|
1609
|
+
// { NAME: 'Bugs Bunny', AGE: '22' }
|
|
1610
|
+
// ]
|
|
1611
|
+
// verify the row number is within the range
|
|
1612
|
+
if (rowNumber >= results.length) {
|
|
1613
|
+
throw new Error("row number is out of range " + rowNumber);
|
|
1614
|
+
}
|
|
1615
|
+
const data = results[rowNumber];
|
|
1616
|
+
this.setTestData(data, world);
|
|
1617
|
+
return data;
|
|
1618
|
+
}
|
|
1453
1619
|
default:
|
|
1454
1620
|
throw new Error("unknown type " + type);
|
|
1455
1621
|
}
|
|
@@ -1588,7 +1754,7 @@ class StableBrowser {
|
|
|
1588
1754
|
let screenshotPath = null;
|
|
1589
1755
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1590
1756
|
const info = {};
|
|
1591
|
-
info.log = "";
|
|
1757
|
+
info.log = "***** verify element " + selectors.element_name + " exists in page *****\n";
|
|
1592
1758
|
info.operation = "verify";
|
|
1593
1759
|
info.selectors = selectors;
|
|
1594
1760
|
try {
|
|
@@ -1641,7 +1807,7 @@ class StableBrowser {
|
|
|
1641
1807
|
let screenshotPath = null;
|
|
1642
1808
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1643
1809
|
const info = {};
|
|
1644
|
-
info.log = "";
|
|
1810
|
+
info.log = "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n";
|
|
1645
1811
|
info.operation = "extract";
|
|
1646
1812
|
info.selectors = selectors;
|
|
1647
1813
|
try {
|
|
@@ -1666,7 +1832,8 @@ class StableBrowser {
|
|
|
1666
1832
|
if (world) {
|
|
1667
1833
|
world[variable] = info.value;
|
|
1668
1834
|
}
|
|
1669
|
-
this.
|
|
1835
|
+
this.setTestData({ [variable]: info.value }, world);
|
|
1836
|
+
this.logger.info("set test data: " + variable + "=" + info.value);
|
|
1670
1837
|
return info;
|
|
1671
1838
|
}
|
|
1672
1839
|
catch (e) {
|
|
@@ -1703,6 +1870,91 @@ class StableBrowser {
|
|
|
1703
1870
|
});
|
|
1704
1871
|
}
|
|
1705
1872
|
}
|
|
1873
|
+
async extractEmailData(emailAddress, options, world) {
|
|
1874
|
+
if (!emailAddress) {
|
|
1875
|
+
throw new Error("email address is null");
|
|
1876
|
+
}
|
|
1877
|
+
// check if address contain @
|
|
1878
|
+
if (emailAddress.indexOf("@") === -1) {
|
|
1879
|
+
emailAddress = emailAddress + "@blinq-mail.io";
|
|
1880
|
+
}
|
|
1881
|
+
else {
|
|
1882
|
+
if (!emailAddress.toLowerCase().endsWith("@blinq-mail.io")) {
|
|
1883
|
+
throw new Error("email address should end with @blinq-mail.io");
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const startTime = Date.now();
|
|
1887
|
+
let timeout = 60000;
|
|
1888
|
+
if (options && options.timeout) {
|
|
1889
|
+
timeout = options.timeout;
|
|
1890
|
+
}
|
|
1891
|
+
const serviceUrl = this._getServerUrl() + "/api/mail/createLinkOrCodeFromEmail";
|
|
1892
|
+
const request = {
|
|
1893
|
+
method: "POST",
|
|
1894
|
+
url: serviceUrl,
|
|
1895
|
+
headers: {
|
|
1896
|
+
"Content-Type": "application/json",
|
|
1897
|
+
Authorization: `Bearer ${process.env.TOKEN}`,
|
|
1898
|
+
},
|
|
1899
|
+
data: JSON.stringify({
|
|
1900
|
+
email: emailAddress,
|
|
1901
|
+
}),
|
|
1902
|
+
};
|
|
1903
|
+
let errorCount = 0;
|
|
1904
|
+
while (true) {
|
|
1905
|
+
try {
|
|
1906
|
+
let result = await this.context.api.request(request);
|
|
1907
|
+
// the response body expected to be the following:
|
|
1908
|
+
// {
|
|
1909
|
+
// "status": true,
|
|
1910
|
+
// "content": {
|
|
1911
|
+
// "url": "",
|
|
1912
|
+
// "code": "112112",
|
|
1913
|
+
// "name": "generate_link_or_code"
|
|
1914
|
+
// }
|
|
1915
|
+
//}
|
|
1916
|
+
if ((result && result.data, result.data.status === true)) {
|
|
1917
|
+
let codeOrUrlFound = false;
|
|
1918
|
+
let emailCode = null;
|
|
1919
|
+
let emailUrl = null;
|
|
1920
|
+
// check if a code is returned
|
|
1921
|
+
if (result.data.content && result.data.content.code) {
|
|
1922
|
+
let code = result.data.content.code;
|
|
1923
|
+
this.setTestData({ emailCode: code }, world);
|
|
1924
|
+
this.logger.info("set test data: emailCode = " + code);
|
|
1925
|
+
emailCode = code;
|
|
1926
|
+
codeOrUrlFound = true;
|
|
1927
|
+
}
|
|
1928
|
+
// check if a url is returned
|
|
1929
|
+
if (result.data.content && result.data.content.url) {
|
|
1930
|
+
let url = result.data.content.url;
|
|
1931
|
+
this.setTestData({ emailUrl: url }, world);
|
|
1932
|
+
this.logger.info("set test data: emailUrl = " + url);
|
|
1933
|
+
emailUrl = url;
|
|
1934
|
+
codeOrUrlFound = true;
|
|
1935
|
+
}
|
|
1936
|
+
if (codeOrUrlFound) {
|
|
1937
|
+
return { emailUrl, emailCode };
|
|
1938
|
+
}
|
|
1939
|
+
else {
|
|
1940
|
+
this.logger.info("an email received but no code or url found");
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
catch (e) {
|
|
1945
|
+
errorCount++;
|
|
1946
|
+
if (errorCount > 3) {
|
|
1947
|
+
throw e;
|
|
1948
|
+
}
|
|
1949
|
+
// ignore
|
|
1950
|
+
}
|
|
1951
|
+
// check if the timeout is reached
|
|
1952
|
+
if (Date.now() - startTime > timeout) {
|
|
1953
|
+
throw new Error("timeout reached");
|
|
1954
|
+
}
|
|
1955
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1706
1958
|
async _highlightElements(scope, css) {
|
|
1707
1959
|
try {
|
|
1708
1960
|
if (!scope) {
|
|
@@ -1774,7 +2026,7 @@ class StableBrowser {
|
|
|
1774
2026
|
let screenshotPath = null;
|
|
1775
2027
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1776
2028
|
const info = {};
|
|
1777
|
-
info.log = "";
|
|
2029
|
+
info.log = "***** verify page path " + pathPart + " *****\n";
|
|
1778
2030
|
info.operation = "verifyPagePath";
|
|
1779
2031
|
const newValue = await this._replaceWithLocalData(pathPart, world);
|
|
1780
2032
|
if (newValue !== pathPart) {
|
|
@@ -1835,7 +2087,7 @@ class StableBrowser {
|
|
|
1835
2087
|
let screenshotPath = null;
|
|
1836
2088
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1837
2089
|
const info = {};
|
|
1838
|
-
info.log = "";
|
|
2090
|
+
info.log = "***** verify text " + text + " exists in page *****\n";
|
|
1839
2091
|
info.operation = "verifyTextExistInPage";
|
|
1840
2092
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
1841
2093
|
if (newValue !== text) {
|
|
@@ -1884,8 +2136,10 @@ class StableBrowser {
|
|
|
1884
2136
|
const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
|
|
1885
2137
|
await this._highlightElements(frame, dataAttribute);
|
|
1886
2138
|
const element = await frame.$(dataAttribute);
|
|
1887
|
-
|
|
1888
|
-
|
|
2139
|
+
if (element) {
|
|
2140
|
+
await this.scrollIfNeeded(element, info);
|
|
2141
|
+
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2142
|
+
}
|
|
1889
2143
|
}
|
|
1890
2144
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1891
2145
|
return info;
|
|
@@ -1923,6 +2177,16 @@ class StableBrowser {
|
|
|
1923
2177
|
});
|
|
1924
2178
|
}
|
|
1925
2179
|
}
|
|
2180
|
+
_getServerUrl() {
|
|
2181
|
+
let serviceUrl = "https://api.blinq.io";
|
|
2182
|
+
if (process.env.NODE_ENV_BLINQ === "dev") {
|
|
2183
|
+
serviceUrl = "https://dev.api.blinq.io";
|
|
2184
|
+
}
|
|
2185
|
+
else if (process.env.NODE_ENV_BLINQ === "stage") {
|
|
2186
|
+
serviceUrl = "https://stage.api.blinq.io";
|
|
2187
|
+
}
|
|
2188
|
+
return serviceUrl;
|
|
2189
|
+
}
|
|
1926
2190
|
async visualVerification(text, options = {}, world = null) {
|
|
1927
2191
|
const startTime = Date.now();
|
|
1928
2192
|
let error = null;
|
|
@@ -1937,13 +2201,7 @@ class StableBrowser {
|
|
|
1937
2201
|
throw new Error("TOKEN is not set");
|
|
1938
2202
|
}
|
|
1939
2203
|
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
|
-
}
|
|
2204
|
+
let serviceUrl = this._getServerUrl();
|
|
1947
2205
|
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
1948
2206
|
info.screenshotPath = screenshotPath;
|
|
1949
2207
|
const screenshot = await this.takeScreenshot();
|
|
@@ -2092,7 +2350,16 @@ class StableBrowser {
|
|
|
2092
2350
|
let screenshotId = null;
|
|
2093
2351
|
let screenshotPath = null;
|
|
2094
2352
|
const info = {};
|
|
2095
|
-
info.log =
|
|
2353
|
+
info.log =
|
|
2354
|
+
"***** analyze table " +
|
|
2355
|
+
selectors.element_name +
|
|
2356
|
+
" query " +
|
|
2357
|
+
query +
|
|
2358
|
+
" operator " +
|
|
2359
|
+
operator +
|
|
2360
|
+
" value " +
|
|
2361
|
+
value +
|
|
2362
|
+
" *****\n";
|
|
2096
2363
|
info.operation = "analyzeTable";
|
|
2097
2364
|
info.selectors = selectors;
|
|
2098
2365
|
info.query = query;
|
|
@@ -2326,13 +2593,6 @@ class StableBrowser {
|
|
|
2326
2593
|
const info = {};
|
|
2327
2594
|
try {
|
|
2328
2595
|
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
2596
|
}
|
|
2337
2597
|
catch (e) {
|
|
2338
2598
|
console.log(".");
|