automation_model 1.0.629-dev → 1.0.629-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 +38 -9
- 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 +20 -3
- package/lib/stable_browser.js +562 -114
- 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,17 +10,21 @@ 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",
|
|
26
30
|
WAIT_ELEMENT: "wait_element",
|
|
@@ -45,6 +49,7 @@ export const Types = {
|
|
|
45
49
|
UNCHECK: "uncheck_element",
|
|
46
50
|
EXTRACT: "extract_attribute",
|
|
47
51
|
CLOSE_PAGE: "close_page",
|
|
52
|
+
TABLE_OPERATION: "table_operation",
|
|
48
53
|
SET_DATE_TIME: "set_date_time",
|
|
49
54
|
SET_VIEWPORT: "set_viewport",
|
|
50
55
|
VERIFY_VISUAL: "verify_visual",
|
|
@@ -53,6 +58,10 @@ export const Types = {
|
|
|
53
58
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
54
59
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
55
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",
|
|
56
65
|
};
|
|
57
66
|
export const apps = {};
|
|
58
67
|
const formatElementName = (elementName) => {
|
|
@@ -178,6 +187,30 @@ class StableBrowser {
|
|
|
178
187
|
await this.waitForPageLoad();
|
|
179
188
|
}
|
|
180
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
|
+
}
|
|
181
214
|
registerConsoleLogListener(page, context) {
|
|
182
215
|
if (!this.context.webLogger) {
|
|
183
216
|
this.context.webLogger = [];
|
|
@@ -273,7 +306,7 @@ class StableBrowser {
|
|
|
273
306
|
_commandError(state, error, this);
|
|
274
307
|
}
|
|
275
308
|
finally {
|
|
276
|
-
_commandFinally(state, this);
|
|
309
|
+
await _commandFinally(state, this);
|
|
277
310
|
}
|
|
278
311
|
}
|
|
279
312
|
async _getLocator(locator, scope, _params) {
|
|
@@ -354,7 +387,7 @@ class StableBrowser {
|
|
|
354
387
|
return resultCss;
|
|
355
388
|
}
|
|
356
389
|
async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
|
|
357
|
-
const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase)
|
|
390
|
+
const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
|
|
358
391
|
const locator = scope.locator(query);
|
|
359
392
|
const count = await locator.count();
|
|
360
393
|
if (!tag1) {
|
|
@@ -374,6 +407,12 @@ class StableBrowser {
|
|
|
374
407
|
if (!el.setAttribute) {
|
|
375
408
|
el = el.parentElement;
|
|
376
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
|
+
// }
|
|
377
416
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
378
417
|
return true;
|
|
379
418
|
}, [tag1, randomToken]))) {
|
|
@@ -383,7 +422,7 @@ class StableBrowser {
|
|
|
383
422
|
}
|
|
384
423
|
return { elementCount: tagCount, randomToken };
|
|
385
424
|
}
|
|
386
|
-
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) {
|
|
387
426
|
if (!info) {
|
|
388
427
|
info = {};
|
|
389
428
|
}
|
|
@@ -450,7 +489,7 @@ class StableBrowser {
|
|
|
450
489
|
}
|
|
451
490
|
return;
|
|
452
491
|
}
|
|
453
|
-
if (info.locatorLog && count === 0) {
|
|
492
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
454
493
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
455
494
|
}
|
|
456
495
|
for (let j = 0; j < count; j++) {
|
|
@@ -465,7 +504,7 @@ class StableBrowser {
|
|
|
465
504
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
466
505
|
}
|
|
467
506
|
}
|
|
468
|
-
else {
|
|
507
|
+
else if (logErrors) {
|
|
469
508
|
info.failCause.visible = visible;
|
|
470
509
|
info.failCause.enabled = enabled;
|
|
471
510
|
if (!info.printMessages) {
|
|
@@ -560,12 +599,24 @@ class StableBrowser {
|
|
|
560
599
|
element.evaluate((el, randomToken) => {
|
|
561
600
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
562
601
|
}, randomToken);
|
|
563
|
-
if (element._frame) {
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
const scope = element.page();
|
|
567
|
-
|
|
568
|
-
|
|
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);
|
|
569
620
|
}
|
|
570
621
|
}
|
|
571
622
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -717,14 +768,9 @@ class StableBrowser {
|
|
|
717
768
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
718
769
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
719
770
|
}
|
|
720
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
771
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
721
772
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
722
773
|
}
|
|
723
|
-
else {
|
|
724
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
725
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
774
|
let foundElements = result.foundElements;
|
|
729
775
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
730
776
|
info.box = foundElements[0].box;
|
|
@@ -779,6 +825,11 @@ class StableBrowser {
|
|
|
779
825
|
visibleOnly = false;
|
|
780
826
|
}
|
|
781
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
|
+
}
|
|
782
833
|
}
|
|
783
834
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
784
835
|
// if (info.locatorLog) {
|
|
@@ -794,7 +845,7 @@ class StableBrowser {
|
|
|
794
845
|
}
|
|
795
846
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
796
847
|
}
|
|
797
|
-
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) {
|
|
798
849
|
let foundElements = [];
|
|
799
850
|
const result = {
|
|
800
851
|
foundElements: foundElements,
|
|
@@ -813,7 +864,9 @@ class StableBrowser {
|
|
|
813
864
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
814
865
|
}
|
|
815
866
|
catch (e) {
|
|
816
|
-
|
|
867
|
+
if (logErrors) {
|
|
868
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
869
|
+
}
|
|
817
870
|
}
|
|
818
871
|
}
|
|
819
872
|
if (foundLocators.length === 1) {
|
|
@@ -825,9 +878,40 @@ class StableBrowser {
|
|
|
825
878
|
result.locatorIndex = i;
|
|
826
879
|
}
|
|
827
880
|
if (foundLocators.length > 1) {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
+
}
|
|
831
915
|
}
|
|
832
916
|
}
|
|
833
917
|
}
|
|
@@ -875,7 +959,7 @@ class StableBrowser {
|
|
|
875
959
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
876
960
|
}
|
|
877
961
|
finally {
|
|
878
|
-
_commandFinally(state, this);
|
|
962
|
+
await _commandFinally(state, this);
|
|
879
963
|
}
|
|
880
964
|
}
|
|
881
965
|
}
|
|
@@ -924,7 +1008,7 @@ class StableBrowser {
|
|
|
924
1008
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
925
1009
|
}
|
|
926
1010
|
finally {
|
|
927
|
-
_commandFinally(state, this);
|
|
1011
|
+
await _commandFinally(state, this);
|
|
928
1012
|
}
|
|
929
1013
|
}
|
|
930
1014
|
}
|
|
@@ -945,19 +1029,7 @@ class StableBrowser {
|
|
|
945
1029
|
};
|
|
946
1030
|
try {
|
|
947
1031
|
await _preCommand(state, this);
|
|
948
|
-
|
|
949
|
-
// state.selectors.locators[0].text = state.options.context;
|
|
950
|
-
// }
|
|
951
|
-
try {
|
|
952
|
-
await state.element.click();
|
|
953
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
954
|
-
}
|
|
955
|
-
catch (e) {
|
|
956
|
-
// await this.closeUnexpectedPopups();
|
|
957
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
958
|
-
await state.element.dispatchEvent("click");
|
|
959
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
960
|
-
}
|
|
1032
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
961
1033
|
await this.waitForPageLoad();
|
|
962
1034
|
return state.info;
|
|
963
1035
|
}
|
|
@@ -965,7 +1037,7 @@ class StableBrowser {
|
|
|
965
1037
|
await _commandError(state, e, this);
|
|
966
1038
|
}
|
|
967
1039
|
finally {
|
|
968
|
-
_commandFinally(state, this);
|
|
1040
|
+
await _commandFinally(state, this);
|
|
969
1041
|
}
|
|
970
1042
|
}
|
|
971
1043
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -996,7 +1068,7 @@ class StableBrowser {
|
|
|
996
1068
|
// await _commandError(state, e, this);
|
|
997
1069
|
}
|
|
998
1070
|
finally {
|
|
999
|
-
_commandFinally(state, this);
|
|
1071
|
+
await _commandFinally(state, this);
|
|
1000
1072
|
}
|
|
1001
1073
|
return found;
|
|
1002
1074
|
}
|
|
@@ -1020,7 +1092,7 @@ class StableBrowser {
|
|
|
1020
1092
|
try {
|
|
1021
1093
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1022
1094
|
// console.log(`Highlighting while running from recorder`);
|
|
1023
|
-
await this._highlightElements(element);
|
|
1095
|
+
await this._highlightElements(state.element);
|
|
1024
1096
|
await state.element.setChecked(checked);
|
|
1025
1097
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1026
1098
|
// await this._unHighlightElements(element);
|
|
@@ -1047,7 +1119,7 @@ class StableBrowser {
|
|
|
1047
1119
|
await _commandError(state, e, this);
|
|
1048
1120
|
}
|
|
1049
1121
|
finally {
|
|
1050
|
-
_commandFinally(state, this);
|
|
1122
|
+
await _commandFinally(state, this);
|
|
1051
1123
|
}
|
|
1052
1124
|
}
|
|
1053
1125
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1064,19 +1136,7 @@ class StableBrowser {
|
|
|
1064
1136
|
};
|
|
1065
1137
|
try {
|
|
1066
1138
|
await _preCommand(state, this);
|
|
1067
|
-
|
|
1068
|
-
await state.element.hover();
|
|
1069
|
-
// await _screenshot(state, this);
|
|
1070
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1071
|
-
}
|
|
1072
|
-
catch (e) {
|
|
1073
|
-
//await this.closeUnexpectedPopups();
|
|
1074
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1075
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1076
|
-
await state.element.hover({ timeout: 10000 });
|
|
1077
|
-
// await _screenshot(state, this);
|
|
1078
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1079
|
-
}
|
|
1139
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1080
1140
|
await _screenshot(state, this);
|
|
1081
1141
|
await this.waitForPageLoad();
|
|
1082
1142
|
return state.info;
|
|
@@ -1085,7 +1145,7 @@ class StableBrowser {
|
|
|
1085
1145
|
await _commandError(state, e, this);
|
|
1086
1146
|
}
|
|
1087
1147
|
finally {
|
|
1088
|
-
_commandFinally(state, this);
|
|
1148
|
+
await _commandFinally(state, this);
|
|
1089
1149
|
}
|
|
1090
1150
|
}
|
|
1091
1151
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1121,7 +1181,7 @@ class StableBrowser {
|
|
|
1121
1181
|
await _commandError(state, e, this);
|
|
1122
1182
|
}
|
|
1123
1183
|
finally {
|
|
1124
|
-
_commandFinally(state, this);
|
|
1184
|
+
await _commandFinally(state, this);
|
|
1125
1185
|
}
|
|
1126
1186
|
}
|
|
1127
1187
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1167,7 +1227,7 @@ class StableBrowser {
|
|
|
1167
1227
|
await _commandError(state, e, this);
|
|
1168
1228
|
}
|
|
1169
1229
|
finally {
|
|
1170
|
-
_commandFinally(state, this);
|
|
1230
|
+
await _commandFinally(state, this);
|
|
1171
1231
|
}
|
|
1172
1232
|
}
|
|
1173
1233
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1203,7 +1263,7 @@ class StableBrowser {
|
|
|
1203
1263
|
await _commandError(state, e, this);
|
|
1204
1264
|
}
|
|
1205
1265
|
finally {
|
|
1206
|
-
_commandFinally(state, this);
|
|
1266
|
+
await _commandFinally(state, this);
|
|
1207
1267
|
}
|
|
1208
1268
|
}
|
|
1209
1269
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1223,7 +1283,7 @@ class StableBrowser {
|
|
|
1223
1283
|
try {
|
|
1224
1284
|
await _preCommand(state, this);
|
|
1225
1285
|
try {
|
|
1226
|
-
await state.element
|
|
1286
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1227
1287
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1228
1288
|
if (format) {
|
|
1229
1289
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1272,7 +1332,7 @@ class StableBrowser {
|
|
|
1272
1332
|
await _commandError(state, e, this);
|
|
1273
1333
|
}
|
|
1274
1334
|
finally {
|
|
1275
|
-
_commandFinally(state, this);
|
|
1335
|
+
await _commandFinally(state, this);
|
|
1276
1336
|
}
|
|
1277
1337
|
}
|
|
1278
1338
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1291,6 +1351,9 @@ class StableBrowser {
|
|
|
1291
1351
|
operation: "clickType",
|
|
1292
1352
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1293
1353
|
};
|
|
1354
|
+
if (!options) {
|
|
1355
|
+
options = {};
|
|
1356
|
+
}
|
|
1294
1357
|
if (newValue !== _value) {
|
|
1295
1358
|
//this.logger.info(_value + "=" + newValue);
|
|
1296
1359
|
_value = newValue;
|
|
@@ -1298,7 +1361,7 @@ class StableBrowser {
|
|
|
1298
1361
|
try {
|
|
1299
1362
|
await _preCommand(state, this);
|
|
1300
1363
|
state.info.value = _value;
|
|
1301
|
-
if (
|
|
1364
|
+
if (!options.press) {
|
|
1302
1365
|
try {
|
|
1303
1366
|
let currentValue = await state.element.inputValue();
|
|
1304
1367
|
if (currentValue) {
|
|
@@ -1309,13 +1372,9 @@ class StableBrowser {
|
|
|
1309
1372
|
this.logger.info("unable to clear input value");
|
|
1310
1373
|
}
|
|
1311
1374
|
}
|
|
1312
|
-
if (options
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
catch (e) {
|
|
1317
|
-
await state.element.dispatchEvent("click");
|
|
1318
|
-
}
|
|
1375
|
+
if (options.press) {
|
|
1376
|
+
options.timeout = 5000;
|
|
1377
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1319
1378
|
}
|
|
1320
1379
|
else {
|
|
1321
1380
|
try {
|
|
@@ -1373,7 +1432,7 @@ class StableBrowser {
|
|
|
1373
1432
|
await _commandError(state, e, this);
|
|
1374
1433
|
}
|
|
1375
1434
|
finally {
|
|
1376
|
-
_commandFinally(state, this);
|
|
1435
|
+
await _commandFinally(state, this);
|
|
1377
1436
|
}
|
|
1378
1437
|
}
|
|
1379
1438
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1403,7 +1462,42 @@ class StableBrowser {
|
|
|
1403
1462
|
await _commandError(state, e, this);
|
|
1404
1463
|
}
|
|
1405
1464
|
finally {
|
|
1406
|
-
_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);
|
|
1407
1501
|
}
|
|
1408
1502
|
}
|
|
1409
1503
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1519,7 +1613,7 @@ class StableBrowser {
|
|
|
1519
1613
|
await _commandError(state, e, this);
|
|
1520
1614
|
}
|
|
1521
1615
|
finally {
|
|
1522
|
-
_commandFinally(state, this);
|
|
1616
|
+
await _commandFinally(state, this);
|
|
1523
1617
|
}
|
|
1524
1618
|
}
|
|
1525
1619
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1554,7 +1648,7 @@ class StableBrowser {
|
|
|
1554
1648
|
while (Date.now() - startTime < timeout) {
|
|
1555
1649
|
try {
|
|
1556
1650
|
await _preCommand(state, this);
|
|
1557
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1651
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1558
1652
|
if (foundObj && foundObj.element) {
|
|
1559
1653
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1560
1654
|
}
|
|
@@ -1596,7 +1690,79 @@ class StableBrowser {
|
|
|
1596
1690
|
throw e;
|
|
1597
1691
|
}
|
|
1598
1692
|
finally {
|
|
1599
|
-
_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(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);
|
|
1600
1766
|
}
|
|
1601
1767
|
}
|
|
1602
1768
|
async waitForUserInput(message, world = null) {
|
|
@@ -1634,6 +1800,15 @@ class StableBrowser {
|
|
|
1634
1800
|
// save the data to the file
|
|
1635
1801
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1636
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
|
+
}
|
|
1637
1812
|
_getDataFilePath(fileName) {
|
|
1638
1813
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1639
1814
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1886,7 +2061,7 @@ class StableBrowser {
|
|
|
1886
2061
|
await _commandError(state, e, this);
|
|
1887
2062
|
}
|
|
1888
2063
|
finally {
|
|
1889
|
-
_commandFinally(state, this);
|
|
2064
|
+
await _commandFinally(state, this);
|
|
1890
2065
|
}
|
|
1891
2066
|
}
|
|
1892
2067
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1917,10 +2092,31 @@ class StableBrowser {
|
|
|
1917
2092
|
case "value":
|
|
1918
2093
|
state.value = await state.element.inputValue();
|
|
1919
2094
|
break;
|
|
2095
|
+
case "text":
|
|
2096
|
+
state.value = await state.element.textContent();
|
|
2097
|
+
break;
|
|
1920
2098
|
default:
|
|
1921
2099
|
state.value = await state.element.getAttribute(attribute);
|
|
1922
2100
|
break;
|
|
1923
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
|
+
}
|
|
1924
2120
|
state.info.value = state.value;
|
|
1925
2121
|
this.setTestData({ [variable]: state.value }, world);
|
|
1926
2122
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1931,7 +2127,7 @@ class StableBrowser {
|
|
|
1931
2127
|
await _commandError(state, e, this);
|
|
1932
2128
|
}
|
|
1933
2129
|
finally {
|
|
1934
|
-
_commandFinally(state, this);
|
|
2130
|
+
await _commandFinally(state, this);
|
|
1935
2131
|
}
|
|
1936
2132
|
}
|
|
1937
2133
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1956,12 +2152,15 @@ class StableBrowser {
|
|
|
1956
2152
|
let expectedValue;
|
|
1957
2153
|
try {
|
|
1958
2154
|
await _preCommand(state, this);
|
|
1959
|
-
expectedValue = state.value;
|
|
2155
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1960
2156
|
state.info.expectedValue = expectedValue;
|
|
1961
2157
|
switch (attribute) {
|
|
1962
2158
|
case "innerText":
|
|
1963
2159
|
val = String(await state.element.innerText());
|
|
1964
2160
|
break;
|
|
2161
|
+
case "text":
|
|
2162
|
+
val = String(await state.element.textContent());
|
|
2163
|
+
break;
|
|
1965
2164
|
case "value":
|
|
1966
2165
|
val = String(await state.element.inputValue());
|
|
1967
2166
|
break;
|
|
@@ -1983,17 +2182,42 @@ class StableBrowser {
|
|
|
1983
2182
|
let regex;
|
|
1984
2183
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1985
2184
|
const patternBody = expectedValue.slice(1, -1);
|
|
1986
|
-
|
|
2185
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2186
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2187
|
+
state.info.regex = true;
|
|
1987
2188
|
}
|
|
1988
2189
|
else {
|
|
1989
2190
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1990
2191
|
regex = new RegExp(escapedPattern, "g");
|
|
1991
2192
|
}
|
|
1992
|
-
if (
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
+
}
|
|
1997
2221
|
}
|
|
1998
2222
|
return state.info;
|
|
1999
2223
|
}
|
|
@@ -2001,7 +2225,7 @@ class StableBrowser {
|
|
|
2001
2225
|
await _commandError(state, e, this);
|
|
2002
2226
|
}
|
|
2003
2227
|
finally {
|
|
2004
|
-
_commandFinally(state, this);
|
|
2228
|
+
await _commandFinally(state, this);
|
|
2005
2229
|
}
|
|
2006
2230
|
}
|
|
2007
2231
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2246,7 +2470,7 @@ class StableBrowser {
|
|
|
2246
2470
|
Object.assign(e, { info: info });
|
|
2247
2471
|
error = e;
|
|
2248
2472
|
// throw e;
|
|
2249
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2473
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
|
|
2250
2474
|
}
|
|
2251
2475
|
finally {
|
|
2252
2476
|
const endTime = Date.now();
|
|
@@ -2271,27 +2495,89 @@ class StableBrowser {
|
|
|
2271
2495
|
});
|
|
2272
2496
|
}
|
|
2273
2497
|
}
|
|
2274
|
-
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) {
|
|
2275
2561
|
const frames = this.page.frames();
|
|
2276
2562
|
let results = [];
|
|
2277
|
-
let ignoreCase = false;
|
|
2563
|
+
// let ignoreCase = false;
|
|
2278
2564
|
for (let i = 0; i < frames.length; i++) {
|
|
2279
2565
|
if (dateAlternatives.date) {
|
|
2280
2566
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2281
|
-
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, {});
|
|
2282
2568
|
result.frame = frames[i];
|
|
2283
2569
|
results.push(result);
|
|
2284
2570
|
}
|
|
2285
2571
|
}
|
|
2286
2572
|
else if (numberAlternatives.number) {
|
|
2287
2573
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2288
|
-
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, {});
|
|
2289
2575
|
result.frame = frames[i];
|
|
2290
2576
|
results.push(result);
|
|
2291
2577
|
}
|
|
2292
2578
|
}
|
|
2293
2579
|
else {
|
|
2294
|
-
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, {});
|
|
2295
2581
|
result.frame = frames[i];
|
|
2296
2582
|
results.push(result);
|
|
2297
2583
|
}
|
|
@@ -2310,11 +2596,14 @@ class StableBrowser {
|
|
|
2310
2596
|
scroll: false,
|
|
2311
2597
|
highlight: false,
|
|
2312
2598
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2313
|
-
text: `Verify text exists in page`,
|
|
2599
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2314
2600
|
_text: `Verify the text '${text}' exists in page`,
|
|
2315
2601
|
operation: "verifyTextExistInPage",
|
|
2316
2602
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2317
2603
|
};
|
|
2604
|
+
if (testForRegex(text)) {
|
|
2605
|
+
text = text.replace(/\\"/g, '"');
|
|
2606
|
+
}
|
|
2318
2607
|
const timeout = this._getFindElementTimeout(options);
|
|
2319
2608
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2320
2609
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2385,7 +2674,7 @@ class StableBrowser {
|
|
|
2385
2674
|
await _commandError(state, e, this);
|
|
2386
2675
|
}
|
|
2387
2676
|
finally {
|
|
2388
|
-
_commandFinally(state, this);
|
|
2677
|
+
await _commandFinally(state, this);
|
|
2389
2678
|
}
|
|
2390
2679
|
}
|
|
2391
2680
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2398,11 +2687,14 @@ class StableBrowser {
|
|
|
2398
2687
|
scroll: false,
|
|
2399
2688
|
highlight: false,
|
|
2400
2689
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2401
|
-
text: `Verify text does not exist in page`,
|
|
2690
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2402
2691
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2403
2692
|
operation: "verifyTextNotExistInPage",
|
|
2404
2693
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2405
2694
|
};
|
|
2695
|
+
if (testForRegex(text)) {
|
|
2696
|
+
text = text.replace(/\\"/g, '"');
|
|
2697
|
+
}
|
|
2406
2698
|
const timeout = this._getFindElementTimeout(options);
|
|
2407
2699
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2408
2700
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2439,7 +2731,7 @@ class StableBrowser {
|
|
|
2439
2731
|
await _commandError(state, e, this);
|
|
2440
2732
|
}
|
|
2441
2733
|
finally {
|
|
2442
|
-
_commandFinally(state, this);
|
|
2734
|
+
await _commandFinally(state, this);
|
|
2443
2735
|
}
|
|
2444
2736
|
}
|
|
2445
2737
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2481,7 +2773,7 @@ class StableBrowser {
|
|
|
2481
2773
|
};
|
|
2482
2774
|
while (true) {
|
|
2483
2775
|
try {
|
|
2484
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2776
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2485
2777
|
}
|
|
2486
2778
|
catch (error) {
|
|
2487
2779
|
// ignore
|
|
@@ -2509,7 +2801,7 @@ class StableBrowser {
|
|
|
2509
2801
|
const count = await frame.locator(css).count();
|
|
2510
2802
|
for (let j = 0; j < count; j++) {
|
|
2511
2803
|
const continer = await frame.locator(css).nth(j);
|
|
2512
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2804
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2513
2805
|
if (result.elementCount > 0) {
|
|
2514
2806
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2515
2807
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2550,9 +2842,33 @@ class StableBrowser {
|
|
|
2550
2842
|
await _commandError(state, e, this);
|
|
2551
2843
|
}
|
|
2552
2844
|
finally {
|
|
2553
|
-
_commandFinally(state, this);
|
|
2845
|
+
await _commandFinally(state, this);
|
|
2554
2846
|
}
|
|
2555
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
|
+
}
|
|
2556
2872
|
async visualVerification(text, options = {}, world = null) {
|
|
2557
2873
|
const startTime = Date.now();
|
|
2558
2874
|
let error = null;
|
|
@@ -2869,7 +3185,13 @@ class StableBrowser {
|
|
|
2869
3185
|
}
|
|
2870
3186
|
}
|
|
2871
3187
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2872
|
-
|
|
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
|
+
}
|
|
2873
3195
|
}
|
|
2874
3196
|
_getLoadTimeout(options) {
|
|
2875
3197
|
let timeout = 15000;
|
|
@@ -2906,6 +3228,9 @@ class StableBrowser {
|
|
|
2906
3228
|
this.registerEventListeners(this.context);
|
|
2907
3229
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2908
3230
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3231
|
+
if (this.onRestoreSaveState) {
|
|
3232
|
+
this.onRestoreSaveState(path);
|
|
3233
|
+
}
|
|
2909
3234
|
}
|
|
2910
3235
|
async waitForPageLoad(options = {}, world = null) {
|
|
2911
3236
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2988,11 +3313,98 @@ class StableBrowser {
|
|
|
2988
3313
|
await _commandError(state, e, this);
|
|
2989
3314
|
}
|
|
2990
3315
|
finally {
|
|
2991
|
-
_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);
|
|
2992
3404
|
}
|
|
2993
3405
|
}
|
|
2994
3406
|
saveTestDataAsGlobal(options, world) {
|
|
2995
|
-
const dataFile =
|
|
3407
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2996
3408
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2997
3409
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2998
3410
|
}
|
|
@@ -3093,7 +3505,39 @@ class StableBrowser {
|
|
|
3093
3505
|
console.log("#-#");
|
|
3094
3506
|
}
|
|
3095
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) { }
|
|
3096
3537
|
async beforeStep(world, step) {
|
|
3538
|
+
if (!this.beforeScenarioCalled) {
|
|
3539
|
+
this.beforeScenario(world, step);
|
|
3540
|
+
}
|
|
3097
3541
|
if (this.stepIndex === undefined) {
|
|
3098
3542
|
this.stepIndex = 0;
|
|
3099
3543
|
}
|
|
@@ -3110,21 +3554,11 @@ class StableBrowser {
|
|
|
3110
3554
|
else {
|
|
3111
3555
|
this.stepName = "step " + this.stepIndex;
|
|
3112
3556
|
}
|
|
3113
|
-
if (this.context) {
|
|
3114
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3115
|
-
}
|
|
3116
3557
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3117
3558
|
if (this.context.browserObject.context) {
|
|
3118
3559
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3119
3560
|
}
|
|
3120
3561
|
}
|
|
3121
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3122
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3123
|
-
// check if @global_test_data tag is present
|
|
3124
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3125
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
3562
|
if (this.initSnapshotTaken === false) {
|
|
3129
3563
|
this.initSnapshotTaken = true;
|
|
3130
3564
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
@@ -3149,15 +3583,22 @@ class StableBrowser {
|
|
|
3149
3583
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3150
3584
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3151
3585
|
for (let i = 0; i < frames.length; i++) {
|
|
3152
|
-
content.push(`- frame: ${i}`);
|
|
3153
3586
|
const frame = frames[i];
|
|
3154
|
-
|
|
3155
|
-
|
|
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) { }
|
|
3156
3596
|
}
|
|
3157
3597
|
return content.join("\n");
|
|
3158
3598
|
}
|
|
3159
3599
|
catch (e) {
|
|
3160
|
-
console.
|
|
3600
|
+
console.log("Error in getAriaSnapshot");
|
|
3601
|
+
//console.debug(e);
|
|
3161
3602
|
}
|
|
3162
3603
|
return null;
|
|
3163
3604
|
}
|
|
@@ -3168,6 +3609,13 @@ class StableBrowser {
|
|
|
3168
3609
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3169
3610
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3170
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`);
|
|
3171
3619
|
}
|
|
3172
3620
|
}
|
|
3173
3621
|
if (this.context) {
|