automation_model 1.0.627-dev → 1.0.627-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/README.md +130 -0
- package/lib/api.js +35 -21
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +99 -28
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +68 -23
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.d.ts +2 -0
- package/lib/bruno.js +381 -0
- package/lib/bruno.js.map +1 -0
- package/lib/command_common.d.ts +4 -4
- package/lib/command_common.js +33 -16
- package/lib/command_common.js.map +1 -1
- package/lib/environment.d.ts +1 -0
- package/lib/environment.js +1 -0
- package/lib/environment.js.map +1 -1
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +61 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +2 -2
- package/lib/init_browser.js +33 -27
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js +2 -2
- package/lib/locate_element.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +1 -1
- package/lib/network.js +5 -5
- package/lib/network.js.map +1 -1
- package/lib/snapshot_validation.d.ts +37 -0
- package/lib/snapshot_validation.js +280 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +22 -3
- package/lib/stable_browser.js +594 -113
- package/lib/stable_browser.js.map +1 -1
- package/lib/table_helper.d.ts +19 -0
- package/lib/table_helper.js +116 -0
- package/lib/table_helper.js.map +1 -0
- package/lib/test_context.d.ts +2 -0
- package/lib/test_context.js +2 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +8 -4
- package/lib/utils.js +221 -20
- package/lib/utils.js.map +1 -1
- package/package.json +9 -8
package/lib/stable_browser.js
CHANGED
|
@@ -10,19 +10,24 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
10
|
import drawRectangle from "./drawRect.js";
|
|
11
11
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
|
|
14
14
|
import csv from "csv-parser";
|
|
15
15
|
import { Readable } from "node:stream";
|
|
16
16
|
import readline from "readline";
|
|
17
17
|
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
18
|
+
import { getTestData } from "./auto_page.js";
|
|
18
19
|
import { locate_element } from "./locate_element.js";
|
|
19
20
|
import { randomUUID } from "crypto";
|
|
20
21
|
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
21
22
|
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
22
23
|
import { LocatorLog } from "./locator_log.js";
|
|
23
24
|
import axios from "axios";
|
|
25
|
+
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
27
|
+
import { snapshotValidation } from "./snapshot_validation.js";
|
|
24
28
|
export const Types = {
|
|
25
29
|
CLICK: "click_element",
|
|
30
|
+
WAIT_ELEMENT: "wait_element",
|
|
26
31
|
NAVIGATE: "navigate",
|
|
27
32
|
FILL: "fill_element",
|
|
28
33
|
EXECUTE: "execute_page_method",
|
|
@@ -44,6 +49,7 @@ export const Types = {
|
|
|
44
49
|
UNCHECK: "uncheck_element",
|
|
45
50
|
EXTRACT: "extract_attribute",
|
|
46
51
|
CLOSE_PAGE: "close_page",
|
|
52
|
+
TABLE_OPERATION: "table_operation",
|
|
47
53
|
SET_DATE_TIME: "set_date_time",
|
|
48
54
|
SET_VIEWPORT: "set_viewport",
|
|
49
55
|
VERIFY_VISUAL: "verify_visual",
|
|
@@ -52,6 +58,10 @@ export const Types = {
|
|
|
52
58
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
53
59
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
54
60
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
61
|
+
BRUNO: "bruno",
|
|
62
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
63
|
+
SET_INPUT_FILES: "set_input_files",
|
|
64
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
55
65
|
};
|
|
56
66
|
export const apps = {};
|
|
57
67
|
const formatElementName = (elementName) => {
|
|
@@ -177,6 +187,30 @@ class StableBrowser {
|
|
|
177
187
|
await this.waitForPageLoad();
|
|
178
188
|
}
|
|
179
189
|
}
|
|
190
|
+
async switchTab(tabTitleOrIndex) {
|
|
191
|
+
// first check if the tabNameOrIndex is a number
|
|
192
|
+
let index = parseInt(tabTitleOrIndex);
|
|
193
|
+
if (!isNaN(index)) {
|
|
194
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
195
|
+
this.page = this.context.pages[index];
|
|
196
|
+
this.context.page = this.page;
|
|
197
|
+
await this.page.bringToFront();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
202
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
203
|
+
let page = this.context.pages[i];
|
|
204
|
+
let title = await page.title();
|
|
205
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
206
|
+
this.page = page;
|
|
207
|
+
this.context.page = this.page;
|
|
208
|
+
await this.page.bringToFront();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
213
|
+
}
|
|
180
214
|
registerConsoleLogListener(page, context) {
|
|
181
215
|
if (!this.context.webLogger) {
|
|
182
216
|
this.context.webLogger = [];
|
|
@@ -272,7 +306,7 @@ class StableBrowser {
|
|
|
272
306
|
_commandError(state, error, this);
|
|
273
307
|
}
|
|
274
308
|
finally {
|
|
275
|
-
_commandFinally(state, this);
|
|
309
|
+
await _commandFinally(state, this);
|
|
276
310
|
}
|
|
277
311
|
}
|
|
278
312
|
async _getLocator(locator, scope, _params) {
|
|
@@ -353,7 +387,7 @@ class StableBrowser {
|
|
|
353
387
|
return resultCss;
|
|
354
388
|
}
|
|
355
389
|
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
356
|
-
const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase)
|
|
390
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
357
391
|
const locator = scope.locator(query);
|
|
358
392
|
const count = await locator.count();
|
|
359
393
|
if (!tag1) {
|
|
@@ -373,6 +407,12 @@ class StableBrowser {
|
|
|
373
407
|
if (!el.setAttribute) {
|
|
374
408
|
el = el.parentElement;
|
|
375
409
|
}
|
|
410
|
+
// remove any attributes start with data-blinq-id
|
|
411
|
+
// for (let i = 0; i < el.attributes.length; i++) {
|
|
412
|
+
// if (el.attributes[i].name.startsWith("data-blinq-id")) {
|
|
413
|
+
// el.removeAttribute(el.attributes[i].name);
|
|
414
|
+
// }
|
|
415
|
+
// }
|
|
376
416
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
377
417
|
return true;
|
|
378
418
|
}, [tag1, randomToken]))) {
|
|
@@ -382,7 +422,7 @@ class StableBrowser {
|
|
|
382
422
|
}
|
|
383
423
|
return { elementCount: tagCount, randomToken };
|
|
384
424
|
}
|
|
385
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
425
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
386
426
|
if (!info) {
|
|
387
427
|
info = {};
|
|
388
428
|
}
|
|
@@ -449,7 +489,7 @@ class StableBrowser {
|
|
|
449
489
|
}
|
|
450
490
|
return;
|
|
451
491
|
}
|
|
452
|
-
if (info.locatorLog && count === 0) {
|
|
492
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
453
493
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
454
494
|
}
|
|
455
495
|
for (let j = 0; j < count; j++) {
|
|
@@ -464,7 +504,7 @@ class StableBrowser {
|
|
|
464
504
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
465
505
|
}
|
|
466
506
|
}
|
|
467
|
-
else {
|
|
507
|
+
else if (logErrors) {
|
|
468
508
|
info.failCause.visible = visible;
|
|
469
509
|
info.failCause.enabled = enabled;
|
|
470
510
|
if (!info.printMessages) {
|
|
@@ -559,12 +599,24 @@ class StableBrowser {
|
|
|
559
599
|
element.evaluate((el, randomToken) => {
|
|
560
600
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
561
601
|
}, randomToken);
|
|
562
|
-
if (element._frame) {
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
const scope = element.page();
|
|
566
|
-
|
|
567
|
-
|
|
602
|
+
// if (element._frame) {
|
|
603
|
+
// return element;
|
|
604
|
+
// }
|
|
605
|
+
const scope = element._frame ?? element.page();
|
|
606
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
607
|
+
let prefixSelector = "";
|
|
608
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
609
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
610
|
+
if (frameSelectorIndex !== -1) {
|
|
611
|
+
// remove everything after the >> internal:control=enter-frame
|
|
612
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
613
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
614
|
+
}
|
|
615
|
+
// if (element?._frame?._selector) {
|
|
616
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
617
|
+
// }
|
|
618
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
619
|
+
return scope.locator(newSelector);
|
|
568
620
|
}
|
|
569
621
|
}
|
|
570
622
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -716,14 +768,9 @@ class StableBrowser {
|
|
|
716
768
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
717
769
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
718
770
|
}
|
|
719
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
771
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
720
772
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
721
773
|
}
|
|
722
|
-
else {
|
|
723
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
724
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
774
|
let foundElements = result.foundElements;
|
|
728
775
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
729
776
|
info.box = foundElements[0].box;
|
|
@@ -778,6 +825,11 @@ class StableBrowser {
|
|
|
778
825
|
visibleOnly = false;
|
|
779
826
|
}
|
|
780
827
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
828
|
+
// sheck of more of half of the timeout has passed
|
|
829
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
830
|
+
highPriorityOnly = false;
|
|
831
|
+
visibleOnly = false;
|
|
832
|
+
}
|
|
781
833
|
}
|
|
782
834
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
783
835
|
// if (info.locatorLog) {
|
|
@@ -793,7 +845,7 @@ class StableBrowser {
|
|
|
793
845
|
}
|
|
794
846
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
795
847
|
}
|
|
796
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
848
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
797
849
|
let foundElements = [];
|
|
798
850
|
const result = {
|
|
799
851
|
foundElements: foundElements,
|
|
@@ -812,7 +864,9 @@ class StableBrowser {
|
|
|
812
864
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
813
865
|
}
|
|
814
866
|
catch (e) {
|
|
815
|
-
|
|
867
|
+
if (logErrors) {
|
|
868
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
869
|
+
}
|
|
816
870
|
}
|
|
817
871
|
}
|
|
818
872
|
if (foundLocators.length === 1) {
|
|
@@ -824,9 +878,40 @@ class StableBrowser {
|
|
|
824
878
|
result.locatorIndex = i;
|
|
825
879
|
}
|
|
826
880
|
if (foundLocators.length > 1) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
881
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
882
|
+
const boxes = [];
|
|
883
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
884
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
885
|
+
}
|
|
886
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
887
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
888
|
+
if (j === k) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
892
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
893
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
894
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
895
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
896
|
+
// as the element is not unique, will remove it
|
|
897
|
+
boxes.splice(k, 1);
|
|
898
|
+
k--;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (boxes.length === 1) {
|
|
903
|
+
result.foundElements.push({
|
|
904
|
+
locator: boxes[0].locator.first(),
|
|
905
|
+
box: boxes[0].box,
|
|
906
|
+
unique: true,
|
|
907
|
+
});
|
|
908
|
+
result.locatorIndex = i;
|
|
909
|
+
}
|
|
910
|
+
else if (logErrors) {
|
|
911
|
+
info.failCause.foundMultiple = true;
|
|
912
|
+
if (info.locatorLog) {
|
|
913
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
914
|
+
}
|
|
830
915
|
}
|
|
831
916
|
}
|
|
832
917
|
}
|
|
@@ -874,7 +959,7 @@ class StableBrowser {
|
|
|
874
959
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
875
960
|
}
|
|
876
961
|
finally {
|
|
877
|
-
_commandFinally(state, this);
|
|
962
|
+
await _commandFinally(state, this);
|
|
878
963
|
}
|
|
879
964
|
}
|
|
880
965
|
}
|
|
@@ -923,7 +1008,7 @@ class StableBrowser {
|
|
|
923
1008
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
924
1009
|
}
|
|
925
1010
|
finally {
|
|
926
|
-
_commandFinally(state, this);
|
|
1011
|
+
await _commandFinally(state, this);
|
|
927
1012
|
}
|
|
928
1013
|
}
|
|
929
1014
|
}
|
|
@@ -944,19 +1029,7 @@ class StableBrowser {
|
|
|
944
1029
|
};
|
|
945
1030
|
try {
|
|
946
1031
|
await _preCommand(state, this);
|
|
947
|
-
|
|
948
|
-
// state.selectors.locators[0].text = state.options.context;
|
|
949
|
-
// }
|
|
950
|
-
try {
|
|
951
|
-
await state.element.click();
|
|
952
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
953
|
-
}
|
|
954
|
-
catch (e) {
|
|
955
|
-
// await this.closeUnexpectedPopups();
|
|
956
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
957
|
-
await state.element.dispatchEvent("click");
|
|
958
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
959
|
-
}
|
|
1032
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
960
1033
|
await this.waitForPageLoad();
|
|
961
1034
|
return state.info;
|
|
962
1035
|
}
|
|
@@ -964,9 +1037,41 @@ class StableBrowser {
|
|
|
964
1037
|
await _commandError(state, e, this);
|
|
965
1038
|
}
|
|
966
1039
|
finally {
|
|
967
|
-
_commandFinally(state, this);
|
|
1040
|
+
await _commandFinally(state, this);
|
|
968
1041
|
}
|
|
969
1042
|
}
|
|
1043
|
+
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
1044
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1045
|
+
const state = {
|
|
1046
|
+
selectors,
|
|
1047
|
+
_params,
|
|
1048
|
+
options,
|
|
1049
|
+
world,
|
|
1050
|
+
text: "Wait for element",
|
|
1051
|
+
_text: "Wait for " + selectors.element_name,
|
|
1052
|
+
type: Types.WAIT_ELEMENT,
|
|
1053
|
+
operation: "waitForElement",
|
|
1054
|
+
log: "***** wait for " + selectors.element_name + " *****\n",
|
|
1055
|
+
};
|
|
1056
|
+
let found = false;
|
|
1057
|
+
try {
|
|
1058
|
+
await _preCommand(state, this);
|
|
1059
|
+
// if (state.options && state.options.context) {
|
|
1060
|
+
// state.selectors.locators[0].text = state.options.context;
|
|
1061
|
+
// }
|
|
1062
|
+
await state.element.waitFor({ timeout: timeout });
|
|
1063
|
+
found = true;
|
|
1064
|
+
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1065
|
+
}
|
|
1066
|
+
catch (e) {
|
|
1067
|
+
console.error("Error on waitForElement", e);
|
|
1068
|
+
// await _commandError(state, e, this);
|
|
1069
|
+
}
|
|
1070
|
+
finally {
|
|
1071
|
+
await _commandFinally(state, this);
|
|
1072
|
+
}
|
|
1073
|
+
return found;
|
|
1074
|
+
}
|
|
970
1075
|
async setCheck(selectors, checked = true, _params, options = {}, world = null) {
|
|
971
1076
|
const state = {
|
|
972
1077
|
selectors,
|
|
@@ -987,7 +1092,7 @@ class StableBrowser {
|
|
|
987
1092
|
try {
|
|
988
1093
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
989
1094
|
// console.log(`Highlighting while running from recorder`);
|
|
990
|
-
await this._highlightElements(element);
|
|
1095
|
+
await this._highlightElements(state.element);
|
|
991
1096
|
await state.element.setChecked(checked);
|
|
992
1097
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
993
1098
|
// await this._unHighlightElements(element);
|
|
@@ -1014,7 +1119,7 @@ class StableBrowser {
|
|
|
1014
1119
|
await _commandError(state, e, this);
|
|
1015
1120
|
}
|
|
1016
1121
|
finally {
|
|
1017
|
-
_commandFinally(state, this);
|
|
1122
|
+
await _commandFinally(state, this);
|
|
1018
1123
|
}
|
|
1019
1124
|
}
|
|
1020
1125
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1031,19 +1136,7 @@ class StableBrowser {
|
|
|
1031
1136
|
};
|
|
1032
1137
|
try {
|
|
1033
1138
|
await _preCommand(state, this);
|
|
1034
|
-
|
|
1035
|
-
await state.element.hover();
|
|
1036
|
-
// await _screenshot(state, this);
|
|
1037
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1038
|
-
}
|
|
1039
|
-
catch (e) {
|
|
1040
|
-
//await this.closeUnexpectedPopups();
|
|
1041
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1042
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1043
|
-
await state.element.hover({ timeout: 10000 });
|
|
1044
|
-
// await _screenshot(state, this);
|
|
1045
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1046
|
-
}
|
|
1139
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1047
1140
|
await _screenshot(state, this);
|
|
1048
1141
|
await this.waitForPageLoad();
|
|
1049
1142
|
return state.info;
|
|
@@ -1052,7 +1145,7 @@ class StableBrowser {
|
|
|
1052
1145
|
await _commandError(state, e, this);
|
|
1053
1146
|
}
|
|
1054
1147
|
finally {
|
|
1055
|
-
_commandFinally(state, this);
|
|
1148
|
+
await _commandFinally(state, this);
|
|
1056
1149
|
}
|
|
1057
1150
|
}
|
|
1058
1151
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1088,7 +1181,7 @@ class StableBrowser {
|
|
|
1088
1181
|
await _commandError(state, e, this);
|
|
1089
1182
|
}
|
|
1090
1183
|
finally {
|
|
1091
|
-
_commandFinally(state, this);
|
|
1184
|
+
await _commandFinally(state, this);
|
|
1092
1185
|
}
|
|
1093
1186
|
}
|
|
1094
1187
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1134,7 +1227,7 @@ class StableBrowser {
|
|
|
1134
1227
|
await _commandError(state, e, this);
|
|
1135
1228
|
}
|
|
1136
1229
|
finally {
|
|
1137
|
-
_commandFinally(state, this);
|
|
1230
|
+
await _commandFinally(state, this);
|
|
1138
1231
|
}
|
|
1139
1232
|
}
|
|
1140
1233
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1170,7 +1263,7 @@ class StableBrowser {
|
|
|
1170
1263
|
await _commandError(state, e, this);
|
|
1171
1264
|
}
|
|
1172
1265
|
finally {
|
|
1173
|
-
_commandFinally(state, this);
|
|
1266
|
+
await _commandFinally(state, this);
|
|
1174
1267
|
}
|
|
1175
1268
|
}
|
|
1176
1269
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1190,7 +1283,7 @@ class StableBrowser {
|
|
|
1190
1283
|
try {
|
|
1191
1284
|
await _preCommand(state, this);
|
|
1192
1285
|
try {
|
|
1193
|
-
await state.element
|
|
1286
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1194
1287
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1195
1288
|
if (format) {
|
|
1196
1289
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1239,7 +1332,7 @@ class StableBrowser {
|
|
|
1239
1332
|
await _commandError(state, e, this);
|
|
1240
1333
|
}
|
|
1241
1334
|
finally {
|
|
1242
|
-
_commandFinally(state, this);
|
|
1335
|
+
await _commandFinally(state, this);
|
|
1243
1336
|
}
|
|
1244
1337
|
}
|
|
1245
1338
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1258,6 +1351,9 @@ class StableBrowser {
|
|
|
1258
1351
|
operation: "clickType",
|
|
1259
1352
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1260
1353
|
};
|
|
1354
|
+
if (!options) {
|
|
1355
|
+
options = {};
|
|
1356
|
+
}
|
|
1261
1357
|
if (newValue !== _value) {
|
|
1262
1358
|
//this.logger.info(_value + "=" + newValue);
|
|
1263
1359
|
_value = newValue;
|
|
@@ -1265,7 +1361,7 @@ class StableBrowser {
|
|
|
1265
1361
|
try {
|
|
1266
1362
|
await _preCommand(state, this);
|
|
1267
1363
|
state.info.value = _value;
|
|
1268
|
-
if (
|
|
1364
|
+
if (!options.press) {
|
|
1269
1365
|
try {
|
|
1270
1366
|
let currentValue = await state.element.inputValue();
|
|
1271
1367
|
if (currentValue) {
|
|
@@ -1276,13 +1372,9 @@ class StableBrowser {
|
|
|
1276
1372
|
this.logger.info("unable to clear input value");
|
|
1277
1373
|
}
|
|
1278
1374
|
}
|
|
1279
|
-
if (options
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
}
|
|
1283
|
-
catch (e) {
|
|
1284
|
-
await state.element.dispatchEvent("click");
|
|
1285
|
-
}
|
|
1375
|
+
if (options.press) {
|
|
1376
|
+
options.timeout = 5000;
|
|
1377
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1286
1378
|
}
|
|
1287
1379
|
else {
|
|
1288
1380
|
try {
|
|
@@ -1340,7 +1432,7 @@ class StableBrowser {
|
|
|
1340
1432
|
await _commandError(state, e, this);
|
|
1341
1433
|
}
|
|
1342
1434
|
finally {
|
|
1343
|
-
_commandFinally(state, this);
|
|
1435
|
+
await _commandFinally(state, this);
|
|
1344
1436
|
}
|
|
1345
1437
|
}
|
|
1346
1438
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1370,7 +1462,42 @@ class StableBrowser {
|
|
|
1370
1462
|
await _commandError(state, e, this);
|
|
1371
1463
|
}
|
|
1372
1464
|
finally {
|
|
1373
|
-
_commandFinally(state, this);
|
|
1465
|
+
await _commandFinally(state, this);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1469
|
+
const state = {
|
|
1470
|
+
selectors,
|
|
1471
|
+
_params,
|
|
1472
|
+
files,
|
|
1473
|
+
value: '"' + files.join('", "') + '"',
|
|
1474
|
+
options,
|
|
1475
|
+
world,
|
|
1476
|
+
type: Types.SET_INPUT_FILES,
|
|
1477
|
+
text: `Set input files`,
|
|
1478
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1479
|
+
operation: "setInputFiles",
|
|
1480
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1481
|
+
};
|
|
1482
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1483
|
+
try {
|
|
1484
|
+
await _preCommand(state, this);
|
|
1485
|
+
for (let i = 0; i < files.length; i++) {
|
|
1486
|
+
const file = files[i];
|
|
1487
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1488
|
+
if (!fs.existsSync(filePath)) {
|
|
1489
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1490
|
+
}
|
|
1491
|
+
state.files[i] = filePath;
|
|
1492
|
+
}
|
|
1493
|
+
await state.element.setInputFiles(files);
|
|
1494
|
+
return state.info;
|
|
1495
|
+
}
|
|
1496
|
+
catch (e) {
|
|
1497
|
+
await _commandError(state, e, this);
|
|
1498
|
+
}
|
|
1499
|
+
finally {
|
|
1500
|
+
await _commandFinally(state, this);
|
|
1374
1501
|
}
|
|
1375
1502
|
}
|
|
1376
1503
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1486,7 +1613,7 @@ class StableBrowser {
|
|
|
1486
1613
|
await _commandError(state, e, this);
|
|
1487
1614
|
}
|
|
1488
1615
|
finally {
|
|
1489
|
-
_commandFinally(state, this);
|
|
1616
|
+
await _commandFinally(state, this);
|
|
1490
1617
|
}
|
|
1491
1618
|
}
|
|
1492
1619
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1521,7 +1648,7 @@ class StableBrowser {
|
|
|
1521
1648
|
while (Date.now() - startTime < timeout) {
|
|
1522
1649
|
try {
|
|
1523
1650
|
await _preCommand(state, this);
|
|
1524
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1651
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1525
1652
|
if (foundObj && foundObj.element) {
|
|
1526
1653
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1527
1654
|
}
|
|
@@ -1563,7 +1690,79 @@ class StableBrowser {
|
|
|
1563
1690
|
throw e;
|
|
1564
1691
|
}
|
|
1565
1692
|
finally {
|
|
1566
|
-
_commandFinally(state, this);
|
|
1693
|
+
await _commandFinally(state, this);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1697
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1698
|
+
const startTime = Date.now();
|
|
1699
|
+
const state = {
|
|
1700
|
+
_params,
|
|
1701
|
+
value: referanceSnapshot,
|
|
1702
|
+
options,
|
|
1703
|
+
world,
|
|
1704
|
+
locate: false,
|
|
1705
|
+
scroll: false,
|
|
1706
|
+
screenshot: true,
|
|
1707
|
+
highlight: false,
|
|
1708
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1709
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1710
|
+
operation: "snapshotValidation",
|
|
1711
|
+
log: "***** verify snapshot *****\n",
|
|
1712
|
+
};
|
|
1713
|
+
if (!referanceSnapshot) {
|
|
1714
|
+
throw new Error("referanceSnapshot is null");
|
|
1715
|
+
}
|
|
1716
|
+
let text = null;
|
|
1717
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1718
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1719
|
+
}
|
|
1720
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1721
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1722
|
+
}
|
|
1723
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1724
|
+
text = referanceSnapshot.substring(5);
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1728
|
+
}
|
|
1729
|
+
state.text = text;
|
|
1730
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1731
|
+
await _preCommand(state, this);
|
|
1732
|
+
let foundObj = null;
|
|
1733
|
+
try {
|
|
1734
|
+
let matchResult = null;
|
|
1735
|
+
while (Date.now() - startTime < timeout) {
|
|
1736
|
+
try {
|
|
1737
|
+
let scope = null;
|
|
1738
|
+
if (!frameSelectors) {
|
|
1739
|
+
scope = this.page;
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
scope = await this._findFrameScope({ nestFrmLoc: frameSelectors }, timeout, state.info);
|
|
1743
|
+
}
|
|
1744
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1745
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1746
|
+
if (matchResult.errorLine !== -1) {
|
|
1747
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1748
|
+
}
|
|
1749
|
+
// highlight and screenshot
|
|
1750
|
+
return state.info;
|
|
1751
|
+
}
|
|
1752
|
+
catch (e) {
|
|
1753
|
+
// Log error but continue retrying until timeout is reached
|
|
1754
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1755
|
+
}
|
|
1756
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1757
|
+
}
|
|
1758
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1759
|
+
}
|
|
1760
|
+
catch (e) {
|
|
1761
|
+
await _commandError(state, e, this);
|
|
1762
|
+
throw e;
|
|
1763
|
+
}
|
|
1764
|
+
finally {
|
|
1765
|
+
await _commandFinally(state, this);
|
|
1567
1766
|
}
|
|
1568
1767
|
}
|
|
1569
1768
|
async waitForUserInput(message, world = null) {
|
|
@@ -1601,6 +1800,15 @@ class StableBrowser {
|
|
|
1601
1800
|
// save the data to the file
|
|
1602
1801
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1603
1802
|
}
|
|
1803
|
+
overwriteTestData(testData, world = null) {
|
|
1804
|
+
if (!testData) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
// if data file exists, load it
|
|
1808
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1809
|
+
// save the data to the file
|
|
1810
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1811
|
+
}
|
|
1604
1812
|
_getDataFilePath(fileName) {
|
|
1605
1813
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1606
1814
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1853,7 +2061,7 @@ class StableBrowser {
|
|
|
1853
2061
|
await _commandError(state, e, this);
|
|
1854
2062
|
}
|
|
1855
2063
|
finally {
|
|
1856
|
-
_commandFinally(state, this);
|
|
2064
|
+
await _commandFinally(state, this);
|
|
1857
2065
|
}
|
|
1858
2066
|
}
|
|
1859
2067
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1884,10 +2092,31 @@ class StableBrowser {
|
|
|
1884
2092
|
case "value":
|
|
1885
2093
|
state.value = await state.element.inputValue();
|
|
1886
2094
|
break;
|
|
2095
|
+
case "text":
|
|
2096
|
+
state.value = await state.element.textContent();
|
|
2097
|
+
break;
|
|
1887
2098
|
default:
|
|
1888
2099
|
state.value = await state.element.getAttribute(attribute);
|
|
1889
2100
|
break;
|
|
1890
2101
|
}
|
|
2102
|
+
if (options !== null) {
|
|
2103
|
+
if (options.regex && options.regex !== "") {
|
|
2104
|
+
// Construct a regex pattern from the provided string
|
|
2105
|
+
const regex = options.regex.slice(1, -1);
|
|
2106
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2107
|
+
const matches = state.value.match(regexPattern);
|
|
2108
|
+
if (matches) {
|
|
2109
|
+
let newValue = "";
|
|
2110
|
+
for (const match of matches) {
|
|
2111
|
+
newValue += match;
|
|
2112
|
+
}
|
|
2113
|
+
state.value = newValue;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2117
|
+
state.value = state.value.trim();
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
1891
2120
|
state.info.value = state.value;
|
|
1892
2121
|
this.setTestData({ [variable]: state.value }, world);
|
|
1893
2122
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1898,7 +2127,7 @@ class StableBrowser {
|
|
|
1898
2127
|
await _commandError(state, e, this);
|
|
1899
2128
|
}
|
|
1900
2129
|
finally {
|
|
1901
|
-
_commandFinally(state, this);
|
|
2130
|
+
await _commandFinally(state, this);
|
|
1902
2131
|
}
|
|
1903
2132
|
}
|
|
1904
2133
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1923,12 +2152,15 @@ class StableBrowser {
|
|
|
1923
2152
|
let expectedValue;
|
|
1924
2153
|
try {
|
|
1925
2154
|
await _preCommand(state, this);
|
|
1926
|
-
expectedValue = state.value;
|
|
2155
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1927
2156
|
state.info.expectedValue = expectedValue;
|
|
1928
2157
|
switch (attribute) {
|
|
1929
2158
|
case "innerText":
|
|
1930
2159
|
val = String(await state.element.innerText());
|
|
1931
2160
|
break;
|
|
2161
|
+
case "text":
|
|
2162
|
+
val = String(await state.element.textContent());
|
|
2163
|
+
break;
|
|
1932
2164
|
case "value":
|
|
1933
2165
|
val = String(await state.element.inputValue());
|
|
1934
2166
|
break;
|
|
@@ -1950,17 +2182,42 @@ class StableBrowser {
|
|
|
1950
2182
|
let regex;
|
|
1951
2183
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1952
2184
|
const patternBody = expectedValue.slice(1, -1);
|
|
1953
|
-
|
|
2185
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2186
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2187
|
+
state.info.regex = true;
|
|
1954
2188
|
}
|
|
1955
2189
|
else {
|
|
1956
2190
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1957
2191
|
regex = new RegExp(escapedPattern, "g");
|
|
1958
2192
|
}
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2193
|
+
if (attribute === "innerText") {
|
|
2194
|
+
if (state.info.regex) {
|
|
2195
|
+
if (!regex.test(val)) {
|
|
2196
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2197
|
+
state.info.failCause.assertionFailed = true;
|
|
2198
|
+
state.info.failCause.lastError = errorMessage;
|
|
2199
|
+
throw new Error(errorMessage);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
else {
|
|
2203
|
+
const valLines = val.split("\n");
|
|
2204
|
+
const expectedLines = expectedValue.split("\n");
|
|
2205
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2206
|
+
if (!isPart) {
|
|
2207
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2208
|
+
state.info.failCause.assertionFailed = true;
|
|
2209
|
+
state.info.failCause.lastError = errorMessage;
|
|
2210
|
+
throw new Error(errorMessage);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
else {
|
|
2215
|
+
if (!val.match(regex)) {
|
|
2216
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2217
|
+
state.info.failCause.assertionFailed = true;
|
|
2218
|
+
state.info.failCause.lastError = errorMessage;
|
|
2219
|
+
throw new Error(errorMessage);
|
|
2220
|
+
}
|
|
1964
2221
|
}
|
|
1965
2222
|
return state.info;
|
|
1966
2223
|
}
|
|
@@ -1968,7 +2225,7 @@ class StableBrowser {
|
|
|
1968
2225
|
await _commandError(state, e, this);
|
|
1969
2226
|
}
|
|
1970
2227
|
finally {
|
|
1971
|
-
_commandFinally(state, this);
|
|
2228
|
+
await _commandFinally(state, this);
|
|
1972
2229
|
}
|
|
1973
2230
|
}
|
|
1974
2231
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2213,7 +2470,7 @@ class StableBrowser {
|
|
|
2213
2470
|
Object.assign(e, { info: info });
|
|
2214
2471
|
error = e;
|
|
2215
2472
|
// throw e;
|
|
2216
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2473
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
|
|
2217
2474
|
}
|
|
2218
2475
|
finally {
|
|
2219
2476
|
const endTime = Date.now();
|
|
@@ -2238,27 +2495,89 @@ class StableBrowser {
|
|
|
2238
2495
|
});
|
|
2239
2496
|
}
|
|
2240
2497
|
}
|
|
2241
|
-
async
|
|
2498
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2499
|
+
const startTime = Date.now();
|
|
2500
|
+
let error = null;
|
|
2501
|
+
let screenshotId = null;
|
|
2502
|
+
let screenshotPath = null;
|
|
2503
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2504
|
+
const info = {};
|
|
2505
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2506
|
+
info.operation = "verifyPageTitle";
|
|
2507
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2508
|
+
if (newValue !== title) {
|
|
2509
|
+
this.logger.info(title + "=" + newValue);
|
|
2510
|
+
title = newValue;
|
|
2511
|
+
}
|
|
2512
|
+
info.title = title;
|
|
2513
|
+
try {
|
|
2514
|
+
for (let i = 0; i < 30; i++) {
|
|
2515
|
+
const foundTitle = await this.page.title();
|
|
2516
|
+
if (!foundTitle.includes(title)) {
|
|
2517
|
+
if (i === 29) {
|
|
2518
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2519
|
+
}
|
|
2520
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2524
|
+
return info;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
catch (e) {
|
|
2528
|
+
//await this.closeUnexpectedPopups();
|
|
2529
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2530
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2531
|
+
info.screenshotPath = screenshotPath;
|
|
2532
|
+
Object.assign(e, { info: info });
|
|
2533
|
+
error = e;
|
|
2534
|
+
// throw e;
|
|
2535
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2536
|
+
}
|
|
2537
|
+
finally {
|
|
2538
|
+
const endTime = Date.now();
|
|
2539
|
+
_reportToWorld(world, {
|
|
2540
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2541
|
+
text: "Verify page title",
|
|
2542
|
+
_text: "Verify the page title contains " + title,
|
|
2543
|
+
screenshotId,
|
|
2544
|
+
result: error
|
|
2545
|
+
? {
|
|
2546
|
+
status: "FAILED",
|
|
2547
|
+
startTime,
|
|
2548
|
+
endTime,
|
|
2549
|
+
message: error?.message,
|
|
2550
|
+
}
|
|
2551
|
+
: {
|
|
2552
|
+
status: "PASSED",
|
|
2553
|
+
startTime,
|
|
2554
|
+
endTime,
|
|
2555
|
+
},
|
|
2556
|
+
info: info,
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2242
2561
|
const frames = this.page.frames();
|
|
2243
2562
|
let results = [];
|
|
2244
|
-
let ignoreCase = false;
|
|
2563
|
+
// let ignoreCase = false;
|
|
2245
2564
|
for (let i = 0; i < frames.length; i++) {
|
|
2246
2565
|
if (dateAlternatives.date) {
|
|
2247
2566
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2248
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2567
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2249
2568
|
result.frame = frames[i];
|
|
2250
2569
|
results.push(result);
|
|
2251
2570
|
}
|
|
2252
2571
|
}
|
|
2253
2572
|
else if (numberAlternatives.number) {
|
|
2254
2573
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2255
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2574
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2256
2575
|
result.frame = frames[i];
|
|
2257
2576
|
results.push(result);
|
|
2258
2577
|
}
|
|
2259
2578
|
}
|
|
2260
2579
|
else {
|
|
2261
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2580
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2262
2581
|
result.frame = frames[i];
|
|
2263
2582
|
results.push(result);
|
|
2264
2583
|
}
|
|
@@ -2277,11 +2596,14 @@ class StableBrowser {
|
|
|
2277
2596
|
scroll: false,
|
|
2278
2597
|
highlight: false,
|
|
2279
2598
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2280
|
-
text: `Verify text exists in page`,
|
|
2599
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2281
2600
|
_text: `Verify the text '${text}' exists in page`,
|
|
2282
2601
|
operation: "verifyTextExistInPage",
|
|
2283
2602
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2284
2603
|
};
|
|
2604
|
+
if (testForRegex(text)) {
|
|
2605
|
+
text = text.replace(/\\"/g, '"');
|
|
2606
|
+
}
|
|
2285
2607
|
const timeout = this._getFindElementTimeout(options);
|
|
2286
2608
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2287
2609
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2352,7 +2674,7 @@ class StableBrowser {
|
|
|
2352
2674
|
await _commandError(state, e, this);
|
|
2353
2675
|
}
|
|
2354
2676
|
finally {
|
|
2355
|
-
_commandFinally(state, this);
|
|
2677
|
+
await _commandFinally(state, this);
|
|
2356
2678
|
}
|
|
2357
2679
|
}
|
|
2358
2680
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2365,11 +2687,14 @@ class StableBrowser {
|
|
|
2365
2687
|
scroll: false,
|
|
2366
2688
|
highlight: false,
|
|
2367
2689
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2368
|
-
text: `Verify text does not exist in page`,
|
|
2690
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2369
2691
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2370
2692
|
operation: "verifyTextNotExistInPage",
|
|
2371
2693
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2372
2694
|
};
|
|
2695
|
+
if (testForRegex(text)) {
|
|
2696
|
+
text = text.replace(/\\"/g, '"');
|
|
2697
|
+
}
|
|
2373
2698
|
const timeout = this._getFindElementTimeout(options);
|
|
2374
2699
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2375
2700
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2406,7 +2731,7 @@ class StableBrowser {
|
|
|
2406
2731
|
await _commandError(state, e, this);
|
|
2407
2732
|
}
|
|
2408
2733
|
finally {
|
|
2409
|
-
_commandFinally(state, this);
|
|
2734
|
+
await _commandFinally(state, this);
|
|
2410
2735
|
}
|
|
2411
2736
|
}
|
|
2412
2737
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2448,7 +2773,7 @@ class StableBrowser {
|
|
|
2448
2773
|
};
|
|
2449
2774
|
while (true) {
|
|
2450
2775
|
try {
|
|
2451
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2776
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2452
2777
|
}
|
|
2453
2778
|
catch (error) {
|
|
2454
2779
|
// ignore
|
|
@@ -2476,7 +2801,7 @@ class StableBrowser {
|
|
|
2476
2801
|
const count = await frame.locator(css).count();
|
|
2477
2802
|
for (let j = 0; j < count; j++) {
|
|
2478
2803
|
const continer = await frame.locator(css).nth(j);
|
|
2479
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2804
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2480
2805
|
if (result.elementCount > 0) {
|
|
2481
2806
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2482
2807
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2517,9 +2842,33 @@ class StableBrowser {
|
|
|
2517
2842
|
await _commandError(state, e, this);
|
|
2518
2843
|
}
|
|
2519
2844
|
finally {
|
|
2520
|
-
_commandFinally(state, this);
|
|
2845
|
+
await _commandFinally(state, this);
|
|
2521
2846
|
}
|
|
2522
2847
|
}
|
|
2848
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2849
|
+
const frames = this.page.frames();
|
|
2850
|
+
let results = [];
|
|
2851
|
+
let ignoreCase = false;
|
|
2852
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2853
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2854
|
+
result.frame = frames[i];
|
|
2855
|
+
const climbArray = [];
|
|
2856
|
+
for (let i = 0; i < climb; i++) {
|
|
2857
|
+
climbArray.push("..");
|
|
2858
|
+
}
|
|
2859
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2860
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2861
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2862
|
+
if (count > 0) {
|
|
2863
|
+
result.elementCount = count;
|
|
2864
|
+
result.locator = newLocator;
|
|
2865
|
+
results.push(result);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
// state.info.results = results;
|
|
2869
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2870
|
+
return resultWithElementsFound;
|
|
2871
|
+
}
|
|
2523
2872
|
async visualVerification(text, options = {}, world = null) {
|
|
2524
2873
|
const startTime = Date.now();
|
|
2525
2874
|
let error = null;
|
|
@@ -2836,7 +3185,13 @@ class StableBrowser {
|
|
|
2836
3185
|
}
|
|
2837
3186
|
}
|
|
2838
3187
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2839
|
-
|
|
3188
|
+
try {
|
|
3189
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3190
|
+
}
|
|
3191
|
+
catch (error) {
|
|
3192
|
+
this.logger.debug(error);
|
|
3193
|
+
throw error;
|
|
3194
|
+
}
|
|
2840
3195
|
}
|
|
2841
3196
|
_getLoadTimeout(options) {
|
|
2842
3197
|
let timeout = 15000;
|
|
@@ -2873,6 +3228,9 @@ class StableBrowser {
|
|
|
2873
3228
|
this.registerEventListeners(this.context);
|
|
2874
3229
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2875
3230
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3231
|
+
if (this.onRestoreSaveState) {
|
|
3232
|
+
this.onRestoreSaveState(path);
|
|
3233
|
+
}
|
|
2876
3234
|
}
|
|
2877
3235
|
async waitForPageLoad(options = {}, world = null) {
|
|
2878
3236
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2955,11 +3313,98 @@ class StableBrowser {
|
|
|
2955
3313
|
await _commandError(state, e, this);
|
|
2956
3314
|
}
|
|
2957
3315
|
finally {
|
|
2958
|
-
_commandFinally(state, this);
|
|
3316
|
+
await _commandFinally(state, this);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3320
|
+
let operation = null;
|
|
3321
|
+
if (!options || !options.operation) {
|
|
3322
|
+
throw new Error("operation is not defined");
|
|
3323
|
+
}
|
|
3324
|
+
operation = options.operation;
|
|
3325
|
+
// validate operation is one of the supported operations
|
|
3326
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3327
|
+
throw new Error("operation is not supported");
|
|
3328
|
+
}
|
|
3329
|
+
const state = {
|
|
3330
|
+
options,
|
|
3331
|
+
world,
|
|
3332
|
+
locate: false,
|
|
3333
|
+
scroll: false,
|
|
3334
|
+
highlight: false,
|
|
3335
|
+
type: Types.TABLE_OPERATION,
|
|
3336
|
+
text: `Table operation`,
|
|
3337
|
+
_text: `Table ${operation} operation`,
|
|
3338
|
+
operation: operation,
|
|
3339
|
+
log: "***** Table operation *****\n",
|
|
3340
|
+
};
|
|
3341
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3342
|
+
try {
|
|
3343
|
+
await _preCommand(state, this);
|
|
3344
|
+
const start = Date.now();
|
|
3345
|
+
let cellArea = null;
|
|
3346
|
+
while (true) {
|
|
3347
|
+
try {
|
|
3348
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3349
|
+
if (cellArea) {
|
|
3350
|
+
break;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
catch (e) {
|
|
3354
|
+
// ignore
|
|
3355
|
+
}
|
|
3356
|
+
if (Date.now() - start > timeout) {
|
|
3357
|
+
throw new Error(`Cell not found in table`);
|
|
3358
|
+
}
|
|
3359
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3360
|
+
}
|
|
3361
|
+
switch (operation) {
|
|
3362
|
+
case "click":
|
|
3363
|
+
if (!options.css) {
|
|
3364
|
+
// will click in the center of the cell
|
|
3365
|
+
let xOffset = 0;
|
|
3366
|
+
let yOffset = 0;
|
|
3367
|
+
if (options.xOffset) {
|
|
3368
|
+
xOffset = options.xOffset;
|
|
3369
|
+
}
|
|
3370
|
+
if (options.yOffset) {
|
|
3371
|
+
yOffset = options.yOffset;
|
|
3372
|
+
}
|
|
3373
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3374
|
+
}
|
|
3375
|
+
else {
|
|
3376
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3377
|
+
if (results.length === 0) {
|
|
3378
|
+
throw new Error(`Element not found in cell area`);
|
|
3379
|
+
}
|
|
3380
|
+
state.element = results[0];
|
|
3381
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3382
|
+
}
|
|
3383
|
+
break;
|
|
3384
|
+
case "hover+click":
|
|
3385
|
+
if (!options.css) {
|
|
3386
|
+
throw new Error("css is not defined");
|
|
3387
|
+
}
|
|
3388
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3389
|
+
if (results.length === 0) {
|
|
3390
|
+
throw new Error(`Element not found in cell area`);
|
|
3391
|
+
}
|
|
3392
|
+
state.element = results[0];
|
|
3393
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3394
|
+
break;
|
|
3395
|
+
default:
|
|
3396
|
+
throw new Error("operation is not supported");
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
catch (e) {
|
|
3400
|
+
await _commandError(state, e, this);
|
|
3401
|
+
}
|
|
3402
|
+
finally {
|
|
3403
|
+
await _commandFinally(state, this);
|
|
2959
3404
|
}
|
|
2960
3405
|
}
|
|
2961
3406
|
saveTestDataAsGlobal(options, world) {
|
|
2962
|
-
const dataFile =
|
|
3407
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2963
3408
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2964
3409
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2965
3410
|
}
|
|
@@ -3060,7 +3505,39 @@ class StableBrowser {
|
|
|
3060
3505
|
console.log("#-#");
|
|
3061
3506
|
}
|
|
3062
3507
|
}
|
|
3508
|
+
async beforeScenario(world, scenario) {
|
|
3509
|
+
this.beforeScenarioCalled = true;
|
|
3510
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3511
|
+
this.scenarioName = scenario.pickle.name;
|
|
3512
|
+
}
|
|
3513
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3514
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3515
|
+
}
|
|
3516
|
+
if (this.context) {
|
|
3517
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3518
|
+
}
|
|
3519
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3520
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3521
|
+
// check if @global_test_data tag is present
|
|
3522
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3523
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
// update test data based on feature/scenario
|
|
3527
|
+
let envName = null;
|
|
3528
|
+
if (this.context && this.context.environment) {
|
|
3529
|
+
envName = this.context.environment.name;
|
|
3530
|
+
}
|
|
3531
|
+
if (!process.env.TEMP_RUN) {
|
|
3532
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3533
|
+
}
|
|
3534
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3535
|
+
}
|
|
3536
|
+
async afterScenario(world, scenario) { }
|
|
3063
3537
|
async beforeStep(world, step) {
|
|
3538
|
+
if (!this.beforeScenarioCalled) {
|
|
3539
|
+
this.beforeScenario(world, step);
|
|
3540
|
+
}
|
|
3064
3541
|
if (this.stepIndex === undefined) {
|
|
3065
3542
|
this.stepIndex = 0;
|
|
3066
3543
|
}
|
|
@@ -3077,21 +3554,11 @@ class StableBrowser {
|
|
|
3077
3554
|
else {
|
|
3078
3555
|
this.stepName = "step " + this.stepIndex;
|
|
3079
3556
|
}
|
|
3080
|
-
if (this.context) {
|
|
3081
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3082
|
-
}
|
|
3083
3557
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3084
3558
|
if (this.context.browserObject.context) {
|
|
3085
3559
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3086
3560
|
}
|
|
3087
3561
|
}
|
|
3088
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3089
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3090
|
-
// check if @global_test_data tag is present
|
|
3091
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3092
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
3562
|
if (this.initSnapshotTaken === false) {
|
|
3096
3563
|
this.initSnapshotTaken = true;
|
|
3097
3564
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
@@ -3116,15 +3583,22 @@ class StableBrowser {
|
|
|
3116
3583
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3117
3584
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3118
3585
|
for (let i = 0; i < frames.length; i++) {
|
|
3119
|
-
content.push(`- frame: ${i}`);
|
|
3120
3586
|
const frame = frames[i];
|
|
3121
|
-
|
|
3122
|
-
|
|
3587
|
+
try {
|
|
3588
|
+
// Ensure frame is attached and has body
|
|
3589
|
+
const body = frame.locator("body");
|
|
3590
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3591
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3592
|
+
content.push(`- frame: ${i}`);
|
|
3593
|
+
content.push(snapshot);
|
|
3594
|
+
}
|
|
3595
|
+
catch (innerErr) { }
|
|
3123
3596
|
}
|
|
3124
3597
|
return content.join("\n");
|
|
3125
3598
|
}
|
|
3126
3599
|
catch (e) {
|
|
3127
|
-
console.
|
|
3600
|
+
console.log("Error in getAriaSnapshot");
|
|
3601
|
+
//console.debug(e);
|
|
3128
3602
|
}
|
|
3129
3603
|
return null;
|
|
3130
3604
|
}
|
|
@@ -3135,6 +3609,13 @@ class StableBrowser {
|
|
|
3135
3609
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3136
3610
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3137
3611
|
});
|
|
3612
|
+
if (world && world.attach) {
|
|
3613
|
+
await world.attach(JSON.stringify({
|
|
3614
|
+
type: "trace",
|
|
3615
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3616
|
+
}), "application/json+trace");
|
|
3617
|
+
}
|
|
3618
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3138
3619
|
}
|
|
3139
3620
|
}
|
|
3140
3621
|
if (this.context) {
|