automation_model 1.0.630-dev → 1.0.630-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 +16 -16
- 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 +32 -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 +357 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +20 -3
- package/lib/stable_browser.js +568 -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 +6 -5
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 { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
27
|
+
import { loadBrunoParams } from "./bruno.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,85 @@ 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
|
+
try {
|
|
1750
|
+
// highlight and screenshot
|
|
1751
|
+
await highlightSnapshot(newValue, scope);
|
|
1752
|
+
// take screenshot
|
|
1753
|
+
await _screenshot(state, this);
|
|
1754
|
+
}
|
|
1755
|
+
catch (e) { }
|
|
1756
|
+
return state.info;
|
|
1757
|
+
}
|
|
1758
|
+
catch (e) {
|
|
1759
|
+
// Log error but continue retrying until timeout is reached
|
|
1760
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1761
|
+
}
|
|
1762
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1763
|
+
}
|
|
1764
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1765
|
+
}
|
|
1766
|
+
catch (e) {
|
|
1767
|
+
await _commandError(state, e, this);
|
|
1768
|
+
throw e;
|
|
1769
|
+
}
|
|
1770
|
+
finally {
|
|
1771
|
+
await _commandFinally(state, this);
|
|
1600
1772
|
}
|
|
1601
1773
|
}
|
|
1602
1774
|
async waitForUserInput(message, world = null) {
|
|
@@ -1634,6 +1806,15 @@ class StableBrowser {
|
|
|
1634
1806
|
// save the data to the file
|
|
1635
1807
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1636
1808
|
}
|
|
1809
|
+
overwriteTestData(testData, world = null) {
|
|
1810
|
+
if (!testData) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
// if data file exists, load it
|
|
1814
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1815
|
+
// save the data to the file
|
|
1816
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1817
|
+
}
|
|
1637
1818
|
_getDataFilePath(fileName) {
|
|
1638
1819
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1639
1820
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1886,7 +2067,7 @@ class StableBrowser {
|
|
|
1886
2067
|
await _commandError(state, e, this);
|
|
1887
2068
|
}
|
|
1888
2069
|
finally {
|
|
1889
|
-
_commandFinally(state, this);
|
|
2070
|
+
await _commandFinally(state, this);
|
|
1890
2071
|
}
|
|
1891
2072
|
}
|
|
1892
2073
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1917,10 +2098,31 @@ class StableBrowser {
|
|
|
1917
2098
|
case "value":
|
|
1918
2099
|
state.value = await state.element.inputValue();
|
|
1919
2100
|
break;
|
|
2101
|
+
case "text":
|
|
2102
|
+
state.value = await state.element.textContent();
|
|
2103
|
+
break;
|
|
1920
2104
|
default:
|
|
1921
2105
|
state.value = await state.element.getAttribute(attribute);
|
|
1922
2106
|
break;
|
|
1923
2107
|
}
|
|
2108
|
+
if (options !== null) {
|
|
2109
|
+
if (options.regex && options.regex !== "") {
|
|
2110
|
+
// Construct a regex pattern from the provided string
|
|
2111
|
+
const regex = options.regex.slice(1, -1);
|
|
2112
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2113
|
+
const matches = state.value.match(regexPattern);
|
|
2114
|
+
if (matches) {
|
|
2115
|
+
let newValue = "";
|
|
2116
|
+
for (const match of matches) {
|
|
2117
|
+
newValue += match;
|
|
2118
|
+
}
|
|
2119
|
+
state.value = newValue;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2123
|
+
state.value = state.value.trim();
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1924
2126
|
state.info.value = state.value;
|
|
1925
2127
|
this.setTestData({ [variable]: state.value }, world);
|
|
1926
2128
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1931,7 +2133,7 @@ class StableBrowser {
|
|
|
1931
2133
|
await _commandError(state, e, this);
|
|
1932
2134
|
}
|
|
1933
2135
|
finally {
|
|
1934
|
-
_commandFinally(state, this);
|
|
2136
|
+
await _commandFinally(state, this);
|
|
1935
2137
|
}
|
|
1936
2138
|
}
|
|
1937
2139
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1956,12 +2158,15 @@ class StableBrowser {
|
|
|
1956
2158
|
let expectedValue;
|
|
1957
2159
|
try {
|
|
1958
2160
|
await _preCommand(state, this);
|
|
1959
|
-
expectedValue = state.value;
|
|
2161
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1960
2162
|
state.info.expectedValue = expectedValue;
|
|
1961
2163
|
switch (attribute) {
|
|
1962
2164
|
case "innerText":
|
|
1963
2165
|
val = String(await state.element.innerText());
|
|
1964
2166
|
break;
|
|
2167
|
+
case "text":
|
|
2168
|
+
val = String(await state.element.textContent());
|
|
2169
|
+
break;
|
|
1965
2170
|
case "value":
|
|
1966
2171
|
val = String(await state.element.inputValue());
|
|
1967
2172
|
break;
|
|
@@ -1983,17 +2188,42 @@ class StableBrowser {
|
|
|
1983
2188
|
let regex;
|
|
1984
2189
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1985
2190
|
const patternBody = expectedValue.slice(1, -1);
|
|
1986
|
-
|
|
2191
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2192
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2193
|
+
state.info.regex = true;
|
|
1987
2194
|
}
|
|
1988
2195
|
else {
|
|
1989
2196
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1990
2197
|
regex = new RegExp(escapedPattern, "g");
|
|
1991
2198
|
}
|
|
1992
|
-
if (
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2199
|
+
if (attribute === "innerText") {
|
|
2200
|
+
if (state.info.regex) {
|
|
2201
|
+
if (!regex.test(val)) {
|
|
2202
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2203
|
+
state.info.failCause.assertionFailed = true;
|
|
2204
|
+
state.info.failCause.lastError = errorMessage;
|
|
2205
|
+
throw new Error(errorMessage);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
else {
|
|
2209
|
+
const valLines = val.split("\n");
|
|
2210
|
+
const expectedLines = expectedValue.split("\n");
|
|
2211
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2212
|
+
if (!isPart) {
|
|
2213
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2214
|
+
state.info.failCause.assertionFailed = true;
|
|
2215
|
+
state.info.failCause.lastError = errorMessage;
|
|
2216
|
+
throw new Error(errorMessage);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
else {
|
|
2221
|
+
if (!val.match(regex)) {
|
|
2222
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2223
|
+
state.info.failCause.assertionFailed = true;
|
|
2224
|
+
state.info.failCause.lastError = errorMessage;
|
|
2225
|
+
throw new Error(errorMessage);
|
|
2226
|
+
}
|
|
1997
2227
|
}
|
|
1998
2228
|
return state.info;
|
|
1999
2229
|
}
|
|
@@ -2001,7 +2231,7 @@ class StableBrowser {
|
|
|
2001
2231
|
await _commandError(state, e, this);
|
|
2002
2232
|
}
|
|
2003
2233
|
finally {
|
|
2004
|
-
_commandFinally(state, this);
|
|
2234
|
+
await _commandFinally(state, this);
|
|
2005
2235
|
}
|
|
2006
2236
|
}
|
|
2007
2237
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2246,7 +2476,7 @@ class StableBrowser {
|
|
|
2246
2476
|
Object.assign(e, { info: info });
|
|
2247
2477
|
error = e;
|
|
2248
2478
|
// throw e;
|
|
2249
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2479
|
+
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
|
|
2250
2480
|
}
|
|
2251
2481
|
finally {
|
|
2252
2482
|
const endTime = Date.now();
|
|
@@ -2271,27 +2501,89 @@ class StableBrowser {
|
|
|
2271
2501
|
});
|
|
2272
2502
|
}
|
|
2273
2503
|
}
|
|
2274
|
-
async
|
|
2504
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2505
|
+
const startTime = Date.now();
|
|
2506
|
+
let error = null;
|
|
2507
|
+
let screenshotId = null;
|
|
2508
|
+
let screenshotPath = null;
|
|
2509
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2510
|
+
const info = {};
|
|
2511
|
+
info.log = "***** verify page title " + title + " *****\n";
|
|
2512
|
+
info.operation = "verifyPageTitle";
|
|
2513
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2514
|
+
if (newValue !== title) {
|
|
2515
|
+
this.logger.info(title + "=" + newValue);
|
|
2516
|
+
title = newValue;
|
|
2517
|
+
}
|
|
2518
|
+
info.title = title;
|
|
2519
|
+
try {
|
|
2520
|
+
for (let i = 0; i < 30; i++) {
|
|
2521
|
+
const foundTitle = await this.page.title();
|
|
2522
|
+
if (!foundTitle.includes(title)) {
|
|
2523
|
+
if (i === 29) {
|
|
2524
|
+
throw new Error(`url ${foundTitle} doesn't contain ${title}`);
|
|
2525
|
+
}
|
|
2526
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2530
|
+
return info;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
catch (e) {
|
|
2534
|
+
//await this.closeUnexpectedPopups();
|
|
2535
|
+
this.logger.error("verify page title failed " + info.log);
|
|
2536
|
+
({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
|
|
2537
|
+
info.screenshotPath = screenshotPath;
|
|
2538
|
+
Object.assign(e, { info: info });
|
|
2539
|
+
error = e;
|
|
2540
|
+
// throw e;
|
|
2541
|
+
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
2542
|
+
}
|
|
2543
|
+
finally {
|
|
2544
|
+
const endTime = Date.now();
|
|
2545
|
+
_reportToWorld(world, {
|
|
2546
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2547
|
+
text: "Verify page title",
|
|
2548
|
+
_text: "Verify the page title contains " + title,
|
|
2549
|
+
screenshotId,
|
|
2550
|
+
result: error
|
|
2551
|
+
? {
|
|
2552
|
+
status: "FAILED",
|
|
2553
|
+
startTime,
|
|
2554
|
+
endTime,
|
|
2555
|
+
message: error?.message,
|
|
2556
|
+
}
|
|
2557
|
+
: {
|
|
2558
|
+
status: "PASSED",
|
|
2559
|
+
startTime,
|
|
2560
|
+
endTime,
|
|
2561
|
+
},
|
|
2562
|
+
info: info,
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2275
2567
|
const frames = this.page.frames();
|
|
2276
2568
|
let results = [];
|
|
2277
|
-
let ignoreCase = false;
|
|
2569
|
+
// let ignoreCase = false;
|
|
2278
2570
|
for (let i = 0; i < frames.length; i++) {
|
|
2279
2571
|
if (dateAlternatives.date) {
|
|
2280
2572
|
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,
|
|
2573
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2282
2574
|
result.frame = frames[i];
|
|
2283
2575
|
results.push(result);
|
|
2284
2576
|
}
|
|
2285
2577
|
}
|
|
2286
2578
|
else if (numberAlternatives.number) {
|
|
2287
2579
|
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,
|
|
2580
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2289
2581
|
result.frame = frames[i];
|
|
2290
2582
|
results.push(result);
|
|
2291
2583
|
}
|
|
2292
2584
|
}
|
|
2293
2585
|
else {
|
|
2294
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2586
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2295
2587
|
result.frame = frames[i];
|
|
2296
2588
|
results.push(result);
|
|
2297
2589
|
}
|
|
@@ -2310,11 +2602,14 @@ class StableBrowser {
|
|
|
2310
2602
|
scroll: false,
|
|
2311
2603
|
highlight: false,
|
|
2312
2604
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2313
|
-
text: `Verify text exists in page`,
|
|
2605
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2314
2606
|
_text: `Verify the text '${text}' exists in page`,
|
|
2315
2607
|
operation: "verifyTextExistInPage",
|
|
2316
2608
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
2317
2609
|
};
|
|
2610
|
+
if (testForRegex(text)) {
|
|
2611
|
+
text = text.replace(/\\"/g, '"');
|
|
2612
|
+
}
|
|
2318
2613
|
const timeout = this._getFindElementTimeout(options);
|
|
2319
2614
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2320
2615
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2385,7 +2680,7 @@ class StableBrowser {
|
|
|
2385
2680
|
await _commandError(state, e, this);
|
|
2386
2681
|
}
|
|
2387
2682
|
finally {
|
|
2388
|
-
_commandFinally(state, this);
|
|
2683
|
+
await _commandFinally(state, this);
|
|
2389
2684
|
}
|
|
2390
2685
|
}
|
|
2391
2686
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2398,11 +2693,14 @@ class StableBrowser {
|
|
|
2398
2693
|
scroll: false,
|
|
2399
2694
|
highlight: false,
|
|
2400
2695
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2401
|
-
text: `Verify text does not exist in page`,
|
|
2696
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2402
2697
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2403
2698
|
operation: "verifyTextNotExistInPage",
|
|
2404
2699
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
2405
2700
|
};
|
|
2701
|
+
if (testForRegex(text)) {
|
|
2702
|
+
text = text.replace(/\\"/g, '"');
|
|
2703
|
+
}
|
|
2406
2704
|
const timeout = this._getFindElementTimeout(options);
|
|
2407
2705
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2408
2706
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -2439,7 +2737,7 @@ class StableBrowser {
|
|
|
2439
2737
|
await _commandError(state, e, this);
|
|
2440
2738
|
}
|
|
2441
2739
|
finally {
|
|
2442
|
-
_commandFinally(state, this);
|
|
2740
|
+
await _commandFinally(state, this);
|
|
2443
2741
|
}
|
|
2444
2742
|
}
|
|
2445
2743
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2481,7 +2779,7 @@ class StableBrowser {
|
|
|
2481
2779
|
};
|
|
2482
2780
|
while (true) {
|
|
2483
2781
|
try {
|
|
2484
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2782
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2485
2783
|
}
|
|
2486
2784
|
catch (error) {
|
|
2487
2785
|
// ignore
|
|
@@ -2509,7 +2807,7 @@ class StableBrowser {
|
|
|
2509
2807
|
const count = await frame.locator(css).count();
|
|
2510
2808
|
for (let j = 0; j < count; j++) {
|
|
2511
2809
|
const continer = await frame.locator(css).nth(j);
|
|
2512
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2810
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2513
2811
|
if (result.elementCount > 0) {
|
|
2514
2812
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2515
2813
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2550,9 +2848,33 @@ class StableBrowser {
|
|
|
2550
2848
|
await _commandError(state, e, this);
|
|
2551
2849
|
}
|
|
2552
2850
|
finally {
|
|
2553
|
-
_commandFinally(state, this);
|
|
2851
|
+
await _commandFinally(state, this);
|
|
2554
2852
|
}
|
|
2555
2853
|
}
|
|
2854
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2855
|
+
const frames = this.page.frames();
|
|
2856
|
+
let results = [];
|
|
2857
|
+
let ignoreCase = false;
|
|
2858
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2859
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2860
|
+
result.frame = frames[i];
|
|
2861
|
+
const climbArray = [];
|
|
2862
|
+
for (let i = 0; i < climb; i++) {
|
|
2863
|
+
climbArray.push("..");
|
|
2864
|
+
}
|
|
2865
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2866
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2867
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2868
|
+
if (count > 0) {
|
|
2869
|
+
result.elementCount = count;
|
|
2870
|
+
result.locator = newLocator;
|
|
2871
|
+
results.push(result);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
// state.info.results = results;
|
|
2875
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2876
|
+
return resultWithElementsFound;
|
|
2877
|
+
}
|
|
2556
2878
|
async visualVerification(text, options = {}, world = null) {
|
|
2557
2879
|
const startTime = Date.now();
|
|
2558
2880
|
let error = null;
|
|
@@ -2869,7 +3191,13 @@ class StableBrowser {
|
|
|
2869
3191
|
}
|
|
2870
3192
|
}
|
|
2871
3193
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2872
|
-
|
|
3194
|
+
try {
|
|
3195
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3196
|
+
}
|
|
3197
|
+
catch (error) {
|
|
3198
|
+
this.logger.debug(error);
|
|
3199
|
+
throw error;
|
|
3200
|
+
}
|
|
2873
3201
|
}
|
|
2874
3202
|
_getLoadTimeout(options) {
|
|
2875
3203
|
let timeout = 15000;
|
|
@@ -2906,6 +3234,9 @@ class StableBrowser {
|
|
|
2906
3234
|
this.registerEventListeners(this.context);
|
|
2907
3235
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2908
3236
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3237
|
+
if (this.onRestoreSaveState) {
|
|
3238
|
+
this.onRestoreSaveState(path);
|
|
3239
|
+
}
|
|
2909
3240
|
}
|
|
2910
3241
|
async waitForPageLoad(options = {}, world = null) {
|
|
2911
3242
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2988,11 +3319,98 @@ class StableBrowser {
|
|
|
2988
3319
|
await _commandError(state, e, this);
|
|
2989
3320
|
}
|
|
2990
3321
|
finally {
|
|
2991
|
-
_commandFinally(state, this);
|
|
3322
|
+
await _commandFinally(state, this);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3326
|
+
let operation = null;
|
|
3327
|
+
if (!options || !options.operation) {
|
|
3328
|
+
throw new Error("operation is not defined");
|
|
3329
|
+
}
|
|
3330
|
+
operation = options.operation;
|
|
3331
|
+
// validate operation is one of the supported operations
|
|
3332
|
+
if (operation != "click" && operation != "hover+click") {
|
|
3333
|
+
throw new Error("operation is not supported");
|
|
3334
|
+
}
|
|
3335
|
+
const state = {
|
|
3336
|
+
options,
|
|
3337
|
+
world,
|
|
3338
|
+
locate: false,
|
|
3339
|
+
scroll: false,
|
|
3340
|
+
highlight: false,
|
|
3341
|
+
type: Types.TABLE_OPERATION,
|
|
3342
|
+
text: `Table operation`,
|
|
3343
|
+
_text: `Table ${operation} operation`,
|
|
3344
|
+
operation: operation,
|
|
3345
|
+
log: "***** Table operation *****\n",
|
|
3346
|
+
};
|
|
3347
|
+
const timeout = this._getFindElementTimeout(options);
|
|
3348
|
+
try {
|
|
3349
|
+
await _preCommand(state, this);
|
|
3350
|
+
const start = Date.now();
|
|
3351
|
+
let cellArea = null;
|
|
3352
|
+
while (true) {
|
|
3353
|
+
try {
|
|
3354
|
+
cellArea = await _findCellArea(headerText, rowText, this, state);
|
|
3355
|
+
if (cellArea) {
|
|
3356
|
+
break;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
catch (e) {
|
|
3360
|
+
// ignore
|
|
3361
|
+
}
|
|
3362
|
+
if (Date.now() - start > timeout) {
|
|
3363
|
+
throw new Error(`Cell not found in table`);
|
|
3364
|
+
}
|
|
3365
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3366
|
+
}
|
|
3367
|
+
switch (operation) {
|
|
3368
|
+
case "click":
|
|
3369
|
+
if (!options.css) {
|
|
3370
|
+
// will click in the center of the cell
|
|
3371
|
+
let xOffset = 0;
|
|
3372
|
+
let yOffset = 0;
|
|
3373
|
+
if (options.xOffset) {
|
|
3374
|
+
xOffset = options.xOffset;
|
|
3375
|
+
}
|
|
3376
|
+
if (options.yOffset) {
|
|
3377
|
+
yOffset = options.yOffset;
|
|
3378
|
+
}
|
|
3379
|
+
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3380
|
+
}
|
|
3381
|
+
else {
|
|
3382
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3383
|
+
if (results.length === 0) {
|
|
3384
|
+
throw new Error(`Element not found in cell area`);
|
|
3385
|
+
}
|
|
3386
|
+
state.element = results[0];
|
|
3387
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3388
|
+
}
|
|
3389
|
+
break;
|
|
3390
|
+
case "hover+click":
|
|
3391
|
+
if (!options.css) {
|
|
3392
|
+
throw new Error("css is not defined");
|
|
3393
|
+
}
|
|
3394
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3395
|
+
if (results.length === 0) {
|
|
3396
|
+
throw new Error(`Element not found in cell area`);
|
|
3397
|
+
}
|
|
3398
|
+
state.element = results[0];
|
|
3399
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3400
|
+
break;
|
|
3401
|
+
default:
|
|
3402
|
+
throw new Error("operation is not supported");
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
catch (e) {
|
|
3406
|
+
await _commandError(state, e, this);
|
|
3407
|
+
}
|
|
3408
|
+
finally {
|
|
3409
|
+
await _commandFinally(state, this);
|
|
2992
3410
|
}
|
|
2993
3411
|
}
|
|
2994
3412
|
saveTestDataAsGlobal(options, world) {
|
|
2995
|
-
const dataFile =
|
|
3413
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
2996
3414
|
process.env.GLOBAL_TEST_DATA_FILE = dataFile;
|
|
2997
3415
|
this.logger.info("Save the scenario test data as global for the following scenarios.");
|
|
2998
3416
|
}
|
|
@@ -3093,7 +3511,39 @@ class StableBrowser {
|
|
|
3093
3511
|
console.log("#-#");
|
|
3094
3512
|
}
|
|
3095
3513
|
}
|
|
3514
|
+
async beforeScenario(world, scenario) {
|
|
3515
|
+
this.beforeScenarioCalled = true;
|
|
3516
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3517
|
+
this.scenarioName = scenario.pickle.name;
|
|
3518
|
+
}
|
|
3519
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3520
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3521
|
+
}
|
|
3522
|
+
if (this.context) {
|
|
3523
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3524
|
+
}
|
|
3525
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3526
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3527
|
+
// check if @global_test_data tag is present
|
|
3528
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3529
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
// update test data based on feature/scenario
|
|
3533
|
+
let envName = null;
|
|
3534
|
+
if (this.context && this.context.environment) {
|
|
3535
|
+
envName = this.context.environment.name;
|
|
3536
|
+
}
|
|
3537
|
+
if (!process.env.TEMP_RUN) {
|
|
3538
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3539
|
+
}
|
|
3540
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3541
|
+
}
|
|
3542
|
+
async afterScenario(world, scenario) { }
|
|
3096
3543
|
async beforeStep(world, step) {
|
|
3544
|
+
if (!this.beforeScenarioCalled) {
|
|
3545
|
+
this.beforeScenario(world, step);
|
|
3546
|
+
}
|
|
3097
3547
|
if (this.stepIndex === undefined) {
|
|
3098
3548
|
this.stepIndex = 0;
|
|
3099
3549
|
}
|
|
@@ -3110,21 +3560,11 @@ class StableBrowser {
|
|
|
3110
3560
|
else {
|
|
3111
3561
|
this.stepName = "step " + this.stepIndex;
|
|
3112
3562
|
}
|
|
3113
|
-
if (this.context) {
|
|
3114
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3115
|
-
}
|
|
3116
3563
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3117
3564
|
if (this.context.browserObject.context) {
|
|
3118
3565
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3119
3566
|
}
|
|
3120
3567
|
}
|
|
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
3568
|
if (this.initSnapshotTaken === false) {
|
|
3129
3569
|
this.initSnapshotTaken = true;
|
|
3130
3570
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
@@ -3149,15 +3589,22 @@ class StableBrowser {
|
|
|
3149
3589
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3150
3590
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3151
3591
|
for (let i = 0; i < frames.length; i++) {
|
|
3152
|
-
content.push(`- frame: ${i}`);
|
|
3153
3592
|
const frame = frames[i];
|
|
3154
|
-
|
|
3155
|
-
|
|
3593
|
+
try {
|
|
3594
|
+
// Ensure frame is attached and has body
|
|
3595
|
+
const body = frame.locator("body");
|
|
3596
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3597
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3598
|
+
content.push(`- frame: ${i}`);
|
|
3599
|
+
content.push(snapshot);
|
|
3600
|
+
}
|
|
3601
|
+
catch (innerErr) { }
|
|
3156
3602
|
}
|
|
3157
3603
|
return content.join("\n");
|
|
3158
3604
|
}
|
|
3159
3605
|
catch (e) {
|
|
3160
|
-
console.
|
|
3606
|
+
console.log("Error in getAriaSnapshot");
|
|
3607
|
+
//console.debug(e);
|
|
3161
3608
|
}
|
|
3162
3609
|
return null;
|
|
3163
3610
|
}
|
|
@@ -3168,6 +3615,13 @@ class StableBrowser {
|
|
|
3168
3615
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3169
3616
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3170
3617
|
});
|
|
3618
|
+
if (world && world.attach) {
|
|
3619
|
+
await world.attach(JSON.stringify({
|
|
3620
|
+
type: "trace",
|
|
3621
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3622
|
+
}), "application/json+trace");
|
|
3623
|
+
}
|
|
3624
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3171
3625
|
}
|
|
3172
3626
|
}
|
|
3173
3627
|
if (this.context) {
|