automation_model 1.0.639-dev → 1.0.639-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/analyze_helper.js.map +1 -1
- package/lib/api.d.ts +0 -1
- 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 +96 -26
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +28 -8
- 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 +36 -16
- package/lib/command_common.js.map +1 -1
- package/lib/date_time.js.map +1 -1
- package/lib/drawRect.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/error-messages.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/find_function.js.map +1 -1
- 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.js +1 -1
- package/lib/locator.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 +51 -24
- package/lib/stable_browser.js +738 -195
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.js.map +1 -1
- package/lib/table_analyze.js.map +1 -1
- package/lib/table_helper.js +15 -0
- package/lib/table_helper.js.map +1 -1
- 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 +6 -4
- package/lib/utils.js +131 -20
- package/lib/utils.js.map +1 -1
- package/package.json +11 -6
package/lib/stable_browser.js
CHANGED
|
@@ -15,6 +15,7 @@ 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";
|
|
@@ -22,23 +23,26 @@ import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
|
22
23
|
import { LocatorLog } from "./locator_log.js";
|
|
23
24
|
import axios from "axios";
|
|
24
25
|
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
|
+
import { snapshotValidation } from "./snapshot_validation.js";
|
|
27
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
25
28
|
export const Types = {
|
|
26
29
|
CLICK: "click_element",
|
|
27
30
|
WAIT_ELEMENT: "wait_element",
|
|
28
|
-
NAVIGATE: "navigate",
|
|
31
|
+
NAVIGATE: "navigate", ///
|
|
29
32
|
FILL: "fill_element",
|
|
30
|
-
EXECUTE: "execute_page_method",
|
|
31
|
-
OPEN: "open_environment",
|
|
33
|
+
EXECUTE: "execute_page_method", //
|
|
34
|
+
OPEN: "open_environment", //
|
|
32
35
|
COMPLETE: "step_complete",
|
|
33
36
|
ASK: "information_needed",
|
|
34
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
35
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
37
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
38
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
36
39
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
37
40
|
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
38
41
|
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
39
42
|
ANALYZE_TABLE: "analyze_table",
|
|
40
|
-
SELECT: "select_combobox",
|
|
43
|
+
SELECT: "select_combobox", //
|
|
41
44
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
45
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
42
46
|
TYPE_PRESS: "type_press",
|
|
43
47
|
PRESS: "press_key",
|
|
44
48
|
HOVER: "hover_element",
|
|
@@ -55,6 +59,12 @@ export const Types = {
|
|
|
55
59
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
56
60
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
57
61
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
62
|
+
BRUNO: "bruno",
|
|
63
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
64
|
+
SET_INPUT_FILES: "set_input_files",
|
|
65
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
66
|
+
REPORT_COMMAND: "report_command",
|
|
67
|
+
STEP_COMPLETE: "step_complete",
|
|
58
68
|
};
|
|
59
69
|
export const apps = {};
|
|
60
70
|
const formatElementName = (elementName) => {
|
|
@@ -126,6 +136,14 @@ class StableBrowser {
|
|
|
126
136
|
}
|
|
127
137
|
context.pageLoading.status = true;
|
|
128
138
|
this.page = page;
|
|
139
|
+
try {
|
|
140
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
141
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error("Error on dialog accept registration", error);
|
|
146
|
+
}
|
|
129
147
|
context.page = page;
|
|
130
148
|
context.pages.push(page);
|
|
131
149
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
@@ -180,6 +198,30 @@ class StableBrowser {
|
|
|
180
198
|
await this.waitForPageLoad();
|
|
181
199
|
}
|
|
182
200
|
}
|
|
201
|
+
async switchTab(tabTitleOrIndex) {
|
|
202
|
+
// first check if the tabNameOrIndex is a number
|
|
203
|
+
let index = parseInt(tabTitleOrIndex);
|
|
204
|
+
if (!isNaN(index)) {
|
|
205
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
206
|
+
this.page = this.context.pages[index];
|
|
207
|
+
this.context.page = this.page;
|
|
208
|
+
await this.page.bringToFront();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
213
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
214
|
+
let page = this.context.pages[i];
|
|
215
|
+
let title = await page.title();
|
|
216
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
217
|
+
this.page = page;
|
|
218
|
+
this.context.page = this.page;
|
|
219
|
+
await this.page.bringToFront();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
224
|
+
}
|
|
183
225
|
registerConsoleLogListener(page, context) {
|
|
184
226
|
if (!this.context.webLogger) {
|
|
185
227
|
this.context.webLogger = [];
|
|
@@ -247,6 +289,7 @@ class StableBrowser {
|
|
|
247
289
|
if (!url) {
|
|
248
290
|
throw new Error("url is null, verify that the environment file is correct");
|
|
249
291
|
}
|
|
292
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
250
293
|
if (!url.startsWith("http")) {
|
|
251
294
|
url = "https://" + url;
|
|
252
295
|
}
|
|
@@ -275,7 +318,7 @@ class StableBrowser {
|
|
|
275
318
|
_commandError(state, error, this);
|
|
276
319
|
}
|
|
277
320
|
finally {
|
|
278
|
-
_commandFinally(state, this);
|
|
321
|
+
await _commandFinally(state, this);
|
|
279
322
|
}
|
|
280
323
|
}
|
|
281
324
|
async _getLocator(locator, scope, _params) {
|
|
@@ -391,7 +434,7 @@ class StableBrowser {
|
|
|
391
434
|
}
|
|
392
435
|
return { elementCount: tagCount, randomToken };
|
|
393
436
|
}
|
|
394
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
437
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
395
438
|
if (!info) {
|
|
396
439
|
info = {};
|
|
397
440
|
}
|
|
@@ -458,7 +501,7 @@ class StableBrowser {
|
|
|
458
501
|
}
|
|
459
502
|
return;
|
|
460
503
|
}
|
|
461
|
-
if (info.locatorLog && count === 0) {
|
|
504
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
462
505
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
463
506
|
}
|
|
464
507
|
for (let j = 0; j < count; j++) {
|
|
@@ -473,7 +516,7 @@ class StableBrowser {
|
|
|
473
516
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
474
517
|
}
|
|
475
518
|
}
|
|
476
|
-
else {
|
|
519
|
+
else if (logErrors) {
|
|
477
520
|
info.failCause.visible = visible;
|
|
478
521
|
info.failCause.enabled = enabled;
|
|
479
522
|
if (!info.printMessages) {
|
|
@@ -565,15 +608,27 @@ class StableBrowser {
|
|
|
565
608
|
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
566
609
|
if (!element.rerun) {
|
|
567
610
|
const randomToken = Math.random().toString(36).substring(7);
|
|
568
|
-
element.evaluate((el, randomToken) => {
|
|
611
|
+
await element.evaluate((el, randomToken) => {
|
|
569
612
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
570
613
|
}, randomToken);
|
|
571
|
-
if (element._frame) {
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
const scope = element.page();
|
|
575
|
-
|
|
576
|
-
|
|
614
|
+
// if (element._frame) {
|
|
615
|
+
// return element;
|
|
616
|
+
// }
|
|
617
|
+
const scope = element._frame ?? element.page();
|
|
618
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
619
|
+
let prefixSelector = "";
|
|
620
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
621
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
622
|
+
if (frameSelectorIndex !== -1) {
|
|
623
|
+
// remove everything after the >> internal:control=enter-frame
|
|
624
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
625
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
626
|
+
}
|
|
627
|
+
// if (element?._frame?._selector) {
|
|
628
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
629
|
+
// }
|
|
630
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
631
|
+
return scope.locator(newSelector);
|
|
577
632
|
}
|
|
578
633
|
}
|
|
579
634
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -725,14 +780,9 @@ class StableBrowser {
|
|
|
725
780
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
726
781
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
727
782
|
}
|
|
728
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
783
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
729
784
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
730
785
|
}
|
|
731
|
-
else {
|
|
732
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
733
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
786
|
let foundElements = result.foundElements;
|
|
737
787
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
738
788
|
info.box = foundElements[0].box;
|
|
@@ -787,6 +837,11 @@ class StableBrowser {
|
|
|
787
837
|
visibleOnly = false;
|
|
788
838
|
}
|
|
789
839
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
840
|
+
// sheck of more of half of the timeout has passed
|
|
841
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
842
|
+
highPriorityOnly = false;
|
|
843
|
+
visibleOnly = false;
|
|
844
|
+
}
|
|
790
845
|
}
|
|
791
846
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
792
847
|
// if (info.locatorLog) {
|
|
@@ -802,7 +857,7 @@ class StableBrowser {
|
|
|
802
857
|
}
|
|
803
858
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
804
859
|
}
|
|
805
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
860
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
806
861
|
let foundElements = [];
|
|
807
862
|
const result = {
|
|
808
863
|
foundElements: foundElements,
|
|
@@ -821,7 +876,9 @@ class StableBrowser {
|
|
|
821
876
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
822
877
|
}
|
|
823
878
|
catch (e) {
|
|
824
|
-
|
|
879
|
+
if (logErrors) {
|
|
880
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
881
|
+
}
|
|
825
882
|
}
|
|
826
883
|
}
|
|
827
884
|
if (foundLocators.length === 1) {
|
|
@@ -833,9 +890,40 @@ class StableBrowser {
|
|
|
833
890
|
result.locatorIndex = i;
|
|
834
891
|
}
|
|
835
892
|
if (foundLocators.length > 1) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
893
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
894
|
+
const boxes = [];
|
|
895
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
896
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
897
|
+
}
|
|
898
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
899
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
900
|
+
if (j === k) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
904
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
905
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
906
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
907
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
908
|
+
// as the element is not unique, will remove it
|
|
909
|
+
boxes.splice(k, 1);
|
|
910
|
+
k--;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (boxes.length === 1) {
|
|
915
|
+
result.foundElements.push({
|
|
916
|
+
locator: boxes[0].locator.first(),
|
|
917
|
+
box: boxes[0].box,
|
|
918
|
+
unique: true,
|
|
919
|
+
});
|
|
920
|
+
result.locatorIndex = i;
|
|
921
|
+
}
|
|
922
|
+
else if (logErrors) {
|
|
923
|
+
info.failCause.foundMultiple = true;
|
|
924
|
+
if (info.locatorLog) {
|
|
925
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
926
|
+
}
|
|
839
927
|
}
|
|
840
928
|
}
|
|
841
929
|
}
|
|
@@ -883,7 +971,7 @@ class StableBrowser {
|
|
|
883
971
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
884
972
|
}
|
|
885
973
|
finally {
|
|
886
|
-
_commandFinally(state, this);
|
|
974
|
+
await _commandFinally(state, this);
|
|
887
975
|
}
|
|
888
976
|
}
|
|
889
977
|
}
|
|
@@ -932,7 +1020,7 @@ class StableBrowser {
|
|
|
932
1020
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
933
1021
|
}
|
|
934
1022
|
finally {
|
|
935
|
-
_commandFinally(state, this);
|
|
1023
|
+
await _commandFinally(state, this);
|
|
936
1024
|
}
|
|
937
1025
|
}
|
|
938
1026
|
}
|
|
@@ -961,7 +1049,7 @@ class StableBrowser {
|
|
|
961
1049
|
await _commandError(state, e, this);
|
|
962
1050
|
}
|
|
963
1051
|
finally {
|
|
964
|
-
_commandFinally(state, this);
|
|
1052
|
+
await _commandFinally(state, this);
|
|
965
1053
|
}
|
|
966
1054
|
}
|
|
967
1055
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -992,7 +1080,7 @@ class StableBrowser {
|
|
|
992
1080
|
// await _commandError(state, e, this);
|
|
993
1081
|
}
|
|
994
1082
|
finally {
|
|
995
|
-
_commandFinally(state, this);
|
|
1083
|
+
await _commandFinally(state, this);
|
|
996
1084
|
}
|
|
997
1085
|
return found;
|
|
998
1086
|
}
|
|
@@ -1016,8 +1104,8 @@ class StableBrowser {
|
|
|
1016
1104
|
try {
|
|
1017
1105
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1018
1106
|
// console.log(`Highlighting while running from recorder`);
|
|
1019
|
-
await this._highlightElements(element);
|
|
1020
|
-
await state.element.setChecked(checked);
|
|
1107
|
+
await this._highlightElements(state.element);
|
|
1108
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
1021
1109
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1022
1110
|
// await this._unHighlightElements(element);
|
|
1023
1111
|
// }
|
|
@@ -1029,11 +1117,28 @@ class StableBrowser {
|
|
|
1029
1117
|
this.logger.info("element did not change its state, ignoring...");
|
|
1030
1118
|
}
|
|
1031
1119
|
else {
|
|
1120
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1032
1121
|
//await this.closeUnexpectedPopups();
|
|
1033
1122
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1034
|
-
state.
|
|
1035
|
-
|
|
1036
|
-
|
|
1123
|
+
state.element_found = false;
|
|
1124
|
+
try {
|
|
1125
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1126
|
+
state.element_found = true;
|
|
1127
|
+
// check the check state
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
// element dismissed
|
|
1131
|
+
}
|
|
1132
|
+
if (state.element_found) {
|
|
1133
|
+
const isChecked = await state.element.isChecked();
|
|
1134
|
+
if (isChecked !== checked) {
|
|
1135
|
+
// perform click
|
|
1136
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1037
1142
|
}
|
|
1038
1143
|
}
|
|
1039
1144
|
await this.waitForPageLoad();
|
|
@@ -1043,7 +1148,7 @@ class StableBrowser {
|
|
|
1043
1148
|
await _commandError(state, e, this);
|
|
1044
1149
|
}
|
|
1045
1150
|
finally {
|
|
1046
|
-
_commandFinally(state, this);
|
|
1151
|
+
await _commandFinally(state, this);
|
|
1047
1152
|
}
|
|
1048
1153
|
}
|
|
1049
1154
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1069,7 +1174,7 @@ class StableBrowser {
|
|
|
1069
1174
|
await _commandError(state, e, this);
|
|
1070
1175
|
}
|
|
1071
1176
|
finally {
|
|
1072
|
-
_commandFinally(state, this);
|
|
1177
|
+
await _commandFinally(state, this);
|
|
1073
1178
|
}
|
|
1074
1179
|
}
|
|
1075
1180
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1105,7 +1210,7 @@ class StableBrowser {
|
|
|
1105
1210
|
await _commandError(state, e, this);
|
|
1106
1211
|
}
|
|
1107
1212
|
finally {
|
|
1108
|
-
_commandFinally(state, this);
|
|
1213
|
+
await _commandFinally(state, this);
|
|
1109
1214
|
}
|
|
1110
1215
|
}
|
|
1111
1216
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1151,7 +1256,7 @@ class StableBrowser {
|
|
|
1151
1256
|
await _commandError(state, e, this);
|
|
1152
1257
|
}
|
|
1153
1258
|
finally {
|
|
1154
|
-
_commandFinally(state, this);
|
|
1259
|
+
await _commandFinally(state, this);
|
|
1155
1260
|
}
|
|
1156
1261
|
}
|
|
1157
1262
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1187,7 +1292,7 @@ class StableBrowser {
|
|
|
1187
1292
|
await _commandError(state, e, this);
|
|
1188
1293
|
}
|
|
1189
1294
|
finally {
|
|
1190
|
-
_commandFinally(state, this);
|
|
1295
|
+
await _commandFinally(state, this);
|
|
1191
1296
|
}
|
|
1192
1297
|
}
|
|
1193
1298
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1256,7 +1361,7 @@ class StableBrowser {
|
|
|
1256
1361
|
await _commandError(state, e, this);
|
|
1257
1362
|
}
|
|
1258
1363
|
finally {
|
|
1259
|
-
_commandFinally(state, this);
|
|
1364
|
+
await _commandFinally(state, this);
|
|
1260
1365
|
}
|
|
1261
1366
|
}
|
|
1262
1367
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1275,6 +1380,9 @@ class StableBrowser {
|
|
|
1275
1380
|
operation: "clickType",
|
|
1276
1381
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1277
1382
|
};
|
|
1383
|
+
if (!options) {
|
|
1384
|
+
options = {};
|
|
1385
|
+
}
|
|
1278
1386
|
if (newValue !== _value) {
|
|
1279
1387
|
//this.logger.info(_value + "=" + newValue);
|
|
1280
1388
|
_value = newValue;
|
|
@@ -1282,7 +1390,7 @@ class StableBrowser {
|
|
|
1282
1390
|
try {
|
|
1283
1391
|
await _preCommand(state, this);
|
|
1284
1392
|
state.info.value = _value;
|
|
1285
|
-
if (
|
|
1393
|
+
if (!options.press) {
|
|
1286
1394
|
try {
|
|
1287
1395
|
let currentValue = await state.element.inputValue();
|
|
1288
1396
|
if (currentValue) {
|
|
@@ -1293,10 +1401,7 @@ class StableBrowser {
|
|
|
1293
1401
|
this.logger.info("unable to clear input value");
|
|
1294
1402
|
}
|
|
1295
1403
|
}
|
|
1296
|
-
if (options
|
|
1297
|
-
if (!options) {
|
|
1298
|
-
options = {};
|
|
1299
|
-
}
|
|
1404
|
+
if (options.press) {
|
|
1300
1405
|
options.timeout = 5000;
|
|
1301
1406
|
await performAction("click", state.element, options, this, state, _params);
|
|
1302
1407
|
}
|
|
@@ -1356,7 +1461,7 @@ class StableBrowser {
|
|
|
1356
1461
|
await _commandError(state, e, this);
|
|
1357
1462
|
}
|
|
1358
1463
|
finally {
|
|
1359
|
-
_commandFinally(state, this);
|
|
1464
|
+
await _commandFinally(state, this);
|
|
1360
1465
|
}
|
|
1361
1466
|
}
|
|
1362
1467
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1386,7 +1491,42 @@ class StableBrowser {
|
|
|
1386
1491
|
await _commandError(state, e, this);
|
|
1387
1492
|
}
|
|
1388
1493
|
finally {
|
|
1389
|
-
_commandFinally(state, this);
|
|
1494
|
+
await _commandFinally(state, this);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1498
|
+
const state = {
|
|
1499
|
+
selectors,
|
|
1500
|
+
_params,
|
|
1501
|
+
files,
|
|
1502
|
+
value: '"' + files.join('", "') + '"',
|
|
1503
|
+
options,
|
|
1504
|
+
world,
|
|
1505
|
+
type: Types.SET_INPUT_FILES,
|
|
1506
|
+
text: `Set input files`,
|
|
1507
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1508
|
+
operation: "setInputFiles",
|
|
1509
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1510
|
+
};
|
|
1511
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1512
|
+
try {
|
|
1513
|
+
await _preCommand(state, this);
|
|
1514
|
+
for (let i = 0; i < files.length; i++) {
|
|
1515
|
+
const file = files[i];
|
|
1516
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1517
|
+
if (!fs.existsSync(filePath)) {
|
|
1518
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1519
|
+
}
|
|
1520
|
+
state.files[i] = filePath;
|
|
1521
|
+
}
|
|
1522
|
+
await state.element.setInputFiles(files);
|
|
1523
|
+
return state.info;
|
|
1524
|
+
}
|
|
1525
|
+
catch (e) {
|
|
1526
|
+
await _commandError(state, e, this);
|
|
1527
|
+
}
|
|
1528
|
+
finally {
|
|
1529
|
+
await _commandFinally(state, this);
|
|
1390
1530
|
}
|
|
1391
1531
|
}
|
|
1392
1532
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1502,7 +1642,7 @@ class StableBrowser {
|
|
|
1502
1642
|
await _commandError(state, e, this);
|
|
1503
1643
|
}
|
|
1504
1644
|
finally {
|
|
1505
|
-
_commandFinally(state, this);
|
|
1645
|
+
await _commandFinally(state, this);
|
|
1506
1646
|
}
|
|
1507
1647
|
}
|
|
1508
1648
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1537,7 +1677,7 @@ class StableBrowser {
|
|
|
1537
1677
|
while (Date.now() - startTime < timeout) {
|
|
1538
1678
|
try {
|
|
1539
1679
|
await _preCommand(state, this);
|
|
1540
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1680
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1541
1681
|
if (foundObj && foundObj.element) {
|
|
1542
1682
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1543
1683
|
}
|
|
@@ -1579,7 +1719,79 @@ class StableBrowser {
|
|
|
1579
1719
|
throw e;
|
|
1580
1720
|
}
|
|
1581
1721
|
finally {
|
|
1582
|
-
_commandFinally(state, this);
|
|
1722
|
+
await _commandFinally(state, this);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1726
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1727
|
+
const startTime = Date.now();
|
|
1728
|
+
const state = {
|
|
1729
|
+
_params,
|
|
1730
|
+
value: referanceSnapshot,
|
|
1731
|
+
options,
|
|
1732
|
+
world,
|
|
1733
|
+
locate: false,
|
|
1734
|
+
scroll: false,
|
|
1735
|
+
screenshot: true,
|
|
1736
|
+
highlight: false,
|
|
1737
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1738
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1739
|
+
operation: "snapshotValidation",
|
|
1740
|
+
log: "***** verify snapshot *****\n",
|
|
1741
|
+
};
|
|
1742
|
+
if (!referanceSnapshot) {
|
|
1743
|
+
throw new Error("referanceSnapshot is null");
|
|
1744
|
+
}
|
|
1745
|
+
let text = null;
|
|
1746
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1747
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1748
|
+
}
|
|
1749
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1750
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1751
|
+
}
|
|
1752
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1753
|
+
text = referanceSnapshot.substring(5);
|
|
1754
|
+
}
|
|
1755
|
+
else {
|
|
1756
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1757
|
+
}
|
|
1758
|
+
state.text = text;
|
|
1759
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1760
|
+
await _preCommand(state, this);
|
|
1761
|
+
let foundObj = null;
|
|
1762
|
+
try {
|
|
1763
|
+
let matchResult = null;
|
|
1764
|
+
while (Date.now() - startTime < timeout) {
|
|
1765
|
+
try {
|
|
1766
|
+
let scope = null;
|
|
1767
|
+
if (!frameSelectors) {
|
|
1768
|
+
scope = this.page;
|
|
1769
|
+
}
|
|
1770
|
+
else {
|
|
1771
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1772
|
+
}
|
|
1773
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1774
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1775
|
+
if (matchResult.errorLine !== -1) {
|
|
1776
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1777
|
+
}
|
|
1778
|
+
// highlight and screenshot
|
|
1779
|
+
return state.info;
|
|
1780
|
+
}
|
|
1781
|
+
catch (e) {
|
|
1782
|
+
// Log error but continue retrying until timeout is reached
|
|
1783
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1784
|
+
}
|
|
1785
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1786
|
+
}
|
|
1787
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1788
|
+
}
|
|
1789
|
+
catch (e) {
|
|
1790
|
+
await _commandError(state, e, this);
|
|
1791
|
+
throw e;
|
|
1792
|
+
}
|
|
1793
|
+
finally {
|
|
1794
|
+
await _commandFinally(state, this);
|
|
1583
1795
|
}
|
|
1584
1796
|
}
|
|
1585
1797
|
async waitForUserInput(message, world = null) {
|
|
@@ -1617,6 +1829,15 @@ class StableBrowser {
|
|
|
1617
1829
|
// save the data to the file
|
|
1618
1830
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1619
1831
|
}
|
|
1832
|
+
overwriteTestData(testData, world = null) {
|
|
1833
|
+
if (!testData) {
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
// if data file exists, load it
|
|
1837
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1838
|
+
// save the data to the file
|
|
1839
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1840
|
+
}
|
|
1620
1841
|
_getDataFilePath(fileName) {
|
|
1621
1842
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1622
1843
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1869,7 +2090,7 @@ class StableBrowser {
|
|
|
1869
2090
|
await _commandError(state, e, this);
|
|
1870
2091
|
}
|
|
1871
2092
|
finally {
|
|
1872
|
-
_commandFinally(state, this);
|
|
2093
|
+
await _commandFinally(state, this);
|
|
1873
2094
|
}
|
|
1874
2095
|
}
|
|
1875
2096
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1900,10 +2121,31 @@ class StableBrowser {
|
|
|
1900
2121
|
case "value":
|
|
1901
2122
|
state.value = await state.element.inputValue();
|
|
1902
2123
|
break;
|
|
2124
|
+
case "text":
|
|
2125
|
+
state.value = await state.element.textContent();
|
|
2126
|
+
break;
|
|
1903
2127
|
default:
|
|
1904
2128
|
state.value = await state.element.getAttribute(attribute);
|
|
1905
2129
|
break;
|
|
1906
2130
|
}
|
|
2131
|
+
if (options !== null) {
|
|
2132
|
+
if (options.regex && options.regex !== "") {
|
|
2133
|
+
// Construct a regex pattern from the provided string
|
|
2134
|
+
const regex = options.regex.slice(1, -1);
|
|
2135
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2136
|
+
const matches = state.value.match(regexPattern);
|
|
2137
|
+
if (matches) {
|
|
2138
|
+
let newValue = "";
|
|
2139
|
+
for (const match of matches) {
|
|
2140
|
+
newValue += match;
|
|
2141
|
+
}
|
|
2142
|
+
state.value = newValue;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2146
|
+
state.value = state.value.trim();
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
1907
2149
|
state.info.value = state.value;
|
|
1908
2150
|
this.setTestData({ [variable]: state.value }, world);
|
|
1909
2151
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1914,7 +2156,7 @@ class StableBrowser {
|
|
|
1914
2156
|
await _commandError(state, e, this);
|
|
1915
2157
|
}
|
|
1916
2158
|
finally {
|
|
1917
|
-
_commandFinally(state, this);
|
|
2159
|
+
await _commandFinally(state, this);
|
|
1918
2160
|
}
|
|
1919
2161
|
}
|
|
1920
2162
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1939,12 +2181,15 @@ class StableBrowser {
|
|
|
1939
2181
|
let expectedValue;
|
|
1940
2182
|
try {
|
|
1941
2183
|
await _preCommand(state, this);
|
|
1942
|
-
expectedValue = state.value;
|
|
2184
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1943
2185
|
state.info.expectedValue = expectedValue;
|
|
1944
2186
|
switch (attribute) {
|
|
1945
2187
|
case "innerText":
|
|
1946
2188
|
val = String(await state.element.innerText());
|
|
1947
2189
|
break;
|
|
2190
|
+
case "text":
|
|
2191
|
+
val = String(await state.element.textContent());
|
|
2192
|
+
break;
|
|
1948
2193
|
case "value":
|
|
1949
2194
|
val = String(await state.element.inputValue());
|
|
1950
2195
|
break;
|
|
@@ -1966,17 +2211,42 @@ class StableBrowser {
|
|
|
1966
2211
|
let regex;
|
|
1967
2212
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1968
2213
|
const patternBody = expectedValue.slice(1, -1);
|
|
1969
|
-
|
|
2214
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2215
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2216
|
+
state.info.regex = true;
|
|
1970
2217
|
}
|
|
1971
2218
|
else {
|
|
1972
2219
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1973
2220
|
regex = new RegExp(escapedPattern, "g");
|
|
1974
2221
|
}
|
|
1975
|
-
if (
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2222
|
+
if (attribute === "innerText") {
|
|
2223
|
+
if (state.info.regex) {
|
|
2224
|
+
if (!regex.test(val)) {
|
|
2225
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2226
|
+
state.info.failCause.assertionFailed = true;
|
|
2227
|
+
state.info.failCause.lastError = errorMessage;
|
|
2228
|
+
throw new Error(errorMessage);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
const valLines = val.split("\n");
|
|
2233
|
+
const expectedLines = expectedValue.split("\n");
|
|
2234
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2235
|
+
if (!isPart) {
|
|
2236
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2237
|
+
state.info.failCause.assertionFailed = true;
|
|
2238
|
+
state.info.failCause.lastError = errorMessage;
|
|
2239
|
+
throw new Error(errorMessage);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
else {
|
|
2244
|
+
if (!val.match(regex)) {
|
|
2245
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2246
|
+
state.info.failCause.assertionFailed = true;
|
|
2247
|
+
state.info.failCause.lastError = errorMessage;
|
|
2248
|
+
throw new Error(errorMessage);
|
|
2249
|
+
}
|
|
1980
2250
|
}
|
|
1981
2251
|
return state.info;
|
|
1982
2252
|
}
|
|
@@ -1984,7 +2254,7 @@ class StableBrowser {
|
|
|
1984
2254
|
await _commandError(state, e, this);
|
|
1985
2255
|
}
|
|
1986
2256
|
finally {
|
|
1987
|
-
_commandFinally(state, this);
|
|
2257
|
+
await _commandFinally(state, this);
|
|
1988
2258
|
}
|
|
1989
2259
|
}
|
|
1990
2260
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2144,56 +2414,49 @@ class StableBrowser {
|
|
|
2144
2414
|
console.debug(error);
|
|
2145
2415
|
}
|
|
2146
2416
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
// });
|
|
2190
|
-
// }
|
|
2191
|
-
// } catch (error) {
|
|
2192
|
-
// // console.debug(error);
|
|
2193
|
-
// }
|
|
2194
|
-
// }
|
|
2417
|
+
_matcher(text) {
|
|
2418
|
+
if (!text) {
|
|
2419
|
+
return { matcher: "contains", queryText: "" };
|
|
2420
|
+
}
|
|
2421
|
+
if (text.length < 2) {
|
|
2422
|
+
return { matcher: "contains", queryText: text };
|
|
2423
|
+
}
|
|
2424
|
+
const split = text.split(":");
|
|
2425
|
+
const matcher = split[0].toLowerCase();
|
|
2426
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2427
|
+
return { matcher, queryText };
|
|
2428
|
+
}
|
|
2429
|
+
_getDomain(url) {
|
|
2430
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2431
|
+
return "";
|
|
2432
|
+
}
|
|
2433
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2434
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2435
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2436
|
+
}
|
|
2437
|
+
let n = hostnameFragments.length;
|
|
2438
|
+
let fragments = [...hostnameFragments];
|
|
2439
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2440
|
+
hostnameFragments.pop();
|
|
2441
|
+
n = hostnameFragments.length;
|
|
2442
|
+
}
|
|
2443
|
+
if (n == 0) {
|
|
2444
|
+
if (fragments[0] === "www")
|
|
2445
|
+
fragments = fragments.slice(1);
|
|
2446
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2447
|
+
}
|
|
2448
|
+
if (hostnameFragments[0] === "www")
|
|
2449
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2450
|
+
return hostnameFragments.join(".");
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Verify the page path matches the given path.
|
|
2454
|
+
* @param {string} pathPart - The path to verify.
|
|
2455
|
+
* @param {object} options - Options for verification.
|
|
2456
|
+
* @param {object} world - The world context.
|
|
2457
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2458
|
+
*/
|
|
2195
2459
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2196
|
-
const startTime = Date.now();
|
|
2197
2460
|
let error = null;
|
|
2198
2461
|
let screenshotId = null;
|
|
2199
2462
|
let screenshotPath = null;
|
|
@@ -2207,74 +2470,235 @@ class StableBrowser {
|
|
|
2207
2470
|
pathPart = newValue;
|
|
2208
2471
|
}
|
|
2209
2472
|
info.pathPart = pathPart;
|
|
2473
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2474
|
+
const state = {
|
|
2475
|
+
text_search: queryText,
|
|
2476
|
+
options,
|
|
2477
|
+
world,
|
|
2478
|
+
locate: false,
|
|
2479
|
+
scroll: false,
|
|
2480
|
+
highlight: false,
|
|
2481
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2482
|
+
text: `Verify the page url is ${queryText}`,
|
|
2483
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2484
|
+
operation: "verifyPagePath",
|
|
2485
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2486
|
+
};
|
|
2210
2487
|
try {
|
|
2488
|
+
await _preCommand(state, this);
|
|
2489
|
+
state.info.text = queryText;
|
|
2211
2490
|
for (let i = 0; i < 30; i++) {
|
|
2212
2491
|
const url = await this.page.url();
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2492
|
+
switch (matcher) {
|
|
2493
|
+
case "exact":
|
|
2494
|
+
if (url !== queryText) {
|
|
2495
|
+
if (i === 29) {
|
|
2496
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2497
|
+
}
|
|
2498
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
break;
|
|
2502
|
+
case "contains":
|
|
2503
|
+
if (!url.includes(queryText)) {
|
|
2504
|
+
if (i === 29) {
|
|
2505
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2506
|
+
}
|
|
2507
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
break;
|
|
2511
|
+
case "starts-with":
|
|
2512
|
+
{
|
|
2513
|
+
const domain = this._getDomain(url);
|
|
2514
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2515
|
+
if (i === 29) {
|
|
2516
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2517
|
+
}
|
|
2518
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
break;
|
|
2523
|
+
case "ends-with":
|
|
2524
|
+
{
|
|
2525
|
+
const urlObj = new URL(url);
|
|
2526
|
+
let route = "/";
|
|
2527
|
+
if (urlObj.pathname !== "/") {
|
|
2528
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2529
|
+
}
|
|
2530
|
+
else {
|
|
2531
|
+
route = "/";
|
|
2532
|
+
}
|
|
2533
|
+
if (route !== queryText) {
|
|
2534
|
+
if (i === 29) {
|
|
2535
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2536
|
+
}
|
|
2537
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
break;
|
|
2542
|
+
case "regex":
|
|
2543
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2544
|
+
if (!regex.test(url)) {
|
|
2545
|
+
if (i === 29) {
|
|
2546
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2547
|
+
}
|
|
2548
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
break;
|
|
2552
|
+
default:
|
|
2553
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2554
|
+
if (!url.includes(pathPart)) {
|
|
2555
|
+
if (i === 29) {
|
|
2556
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2557
|
+
}
|
|
2558
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2219
2561
|
}
|
|
2220
|
-
|
|
2221
|
-
return info;
|
|
2562
|
+
await _screenshot(state, this);
|
|
2563
|
+
return state.info;
|
|
2222
2564
|
}
|
|
2223
2565
|
}
|
|
2224
2566
|
catch (e) {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
info.screenshotPath = screenshotPath;
|
|
2229
|
-
Object.assign(e, { info: info });
|
|
2230
|
-
error = e;
|
|
2231
|
-
// throw e;
|
|
2232
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2567
|
+
state.info.failCause.lastError = e.message;
|
|
2568
|
+
state.info.failCause.assertionFailed = true;
|
|
2569
|
+
await _commandError(state, e, this);
|
|
2233
2570
|
}
|
|
2234
2571
|
finally {
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2572
|
+
await _commandFinally(state, this);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Verify the page title matches the given title.
|
|
2577
|
+
* @param {string} title - The title to verify.
|
|
2578
|
+
* @param {object} options - Options for verification.
|
|
2579
|
+
* @param {object} world - The world context.
|
|
2580
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2581
|
+
*/
|
|
2582
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2583
|
+
let error = null;
|
|
2584
|
+
let screenshotId = null;
|
|
2585
|
+
let screenshotPath = null;
|
|
2586
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2587
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2588
|
+
if (newValue !== title) {
|
|
2589
|
+
this.logger.info(title + "=" + newValue);
|
|
2590
|
+
title = newValue;
|
|
2591
|
+
}
|
|
2592
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2593
|
+
const state = {
|
|
2594
|
+
text_search: queryText,
|
|
2595
|
+
options,
|
|
2596
|
+
world,
|
|
2597
|
+
locate: false,
|
|
2598
|
+
scroll: false,
|
|
2599
|
+
highlight: false,
|
|
2600
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2601
|
+
text: `Verify the page title is ${queryText}`,
|
|
2602
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2603
|
+
operation: "verifyPageTitle",
|
|
2604
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2605
|
+
};
|
|
2606
|
+
try {
|
|
2607
|
+
await _preCommand(state, this);
|
|
2608
|
+
state.info.text = queryText;
|
|
2609
|
+
for (let i = 0; i < 30; i++) {
|
|
2610
|
+
const foundTitle = await this.page.title();
|
|
2611
|
+
switch (matcher) {
|
|
2612
|
+
case "exact":
|
|
2613
|
+
if (foundTitle !== queryText) {
|
|
2614
|
+
if (i === 29) {
|
|
2615
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2616
|
+
}
|
|
2617
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
break;
|
|
2621
|
+
case "contains":
|
|
2622
|
+
if (!foundTitle.includes(queryText)) {
|
|
2623
|
+
if (i === 29) {
|
|
2624
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2625
|
+
}
|
|
2626
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
break;
|
|
2630
|
+
case "starts-with":
|
|
2631
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2632
|
+
if (i === 29) {
|
|
2633
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2634
|
+
}
|
|
2635
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
break;
|
|
2639
|
+
case "ends-with":
|
|
2640
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2641
|
+
if (i === 29) {
|
|
2642
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2643
|
+
}
|
|
2644
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
break;
|
|
2648
|
+
case "regex":
|
|
2649
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2650
|
+
if (!regex.test(foundTitle)) {
|
|
2651
|
+
if (i === 29) {
|
|
2652
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2653
|
+
}
|
|
2654
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
break;
|
|
2658
|
+
default:
|
|
2659
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2660
|
+
if (!foundTitle.includes(title)) {
|
|
2661
|
+
if (i === 29) {
|
|
2662
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2663
|
+
}
|
|
2664
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
await _screenshot(state, this);
|
|
2669
|
+
return state.info;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
catch (e) {
|
|
2673
|
+
state.info.failCause.lastError = e.message;
|
|
2674
|
+
state.info.failCause.assertionFailed = true;
|
|
2675
|
+
await _commandError(state, e, this);
|
|
2676
|
+
}
|
|
2677
|
+
finally {
|
|
2678
|
+
await _commandFinally(state, this);
|
|
2255
2679
|
}
|
|
2256
2680
|
}
|
|
2257
|
-
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
|
|
2681
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2258
2682
|
const frames = this.page.frames();
|
|
2259
2683
|
let results = [];
|
|
2260
|
-
let ignoreCase = false;
|
|
2684
|
+
// let ignoreCase = false;
|
|
2261
2685
|
for (let i = 0; i < frames.length; i++) {
|
|
2262
2686
|
if (dateAlternatives.date) {
|
|
2263
2687
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2264
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2688
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2265
2689
|
result.frame = frames[i];
|
|
2266
2690
|
results.push(result);
|
|
2267
2691
|
}
|
|
2268
2692
|
}
|
|
2269
2693
|
else if (numberAlternatives.number) {
|
|
2270
2694
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2271
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2695
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2272
2696
|
result.frame = frames[i];
|
|
2273
2697
|
results.push(result);
|
|
2274
2698
|
}
|
|
2275
2699
|
}
|
|
2276
2700
|
else {
|
|
2277
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2701
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2278
2702
|
result.frame = frames[i];
|
|
2279
2703
|
results.push(result);
|
|
2280
2704
|
}
|
|
@@ -2293,7 +2717,7 @@ class StableBrowser {
|
|
|
2293
2717
|
scroll: false,
|
|
2294
2718
|
highlight: false,
|
|
2295
2719
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2296
|
-
text: `Verify text exists in page`,
|
|
2720
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2297
2721
|
_text: `Verify the text '${text}' exists in page`,
|
|
2298
2722
|
operation: "verifyTextExistInPage",
|
|
2299
2723
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2335,27 +2759,10 @@ class StableBrowser {
|
|
|
2335
2759
|
const frame = resultWithElementsFound[0].frame;
|
|
2336
2760
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2337
2761
|
await this._highlightElements(frame, dataAttribute);
|
|
2338
|
-
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2339
|
-
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2340
|
-
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2341
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2342
|
-
// this._unhighlightElements(frame, dataAttribute)
|
|
2343
|
-
// .then(async () => {
|
|
2344
|
-
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2345
|
-
// })
|
|
2346
|
-
// .catch(
|
|
2347
|
-
// (e) => {}
|
|
2348
|
-
// console.error(e)
|
|
2349
|
-
// );
|
|
2350
|
-
// });
|
|
2351
|
-
// }
|
|
2352
2762
|
const element = await frame.locator(dataAttribute).first();
|
|
2353
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2354
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2355
2763
|
if (element) {
|
|
2356
2764
|
await this.scrollIfNeeded(element, state.info);
|
|
2357
2765
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2358
|
-
// await _screenshot(state, this, element);
|
|
2359
2766
|
}
|
|
2360
2767
|
}
|
|
2361
2768
|
await _screenshot(state, this);
|
|
@@ -2365,13 +2772,12 @@ class StableBrowser {
|
|
|
2365
2772
|
console.error(error);
|
|
2366
2773
|
}
|
|
2367
2774
|
}
|
|
2368
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2369
2775
|
}
|
|
2370
2776
|
catch (e) {
|
|
2371
2777
|
await _commandError(state, e, this);
|
|
2372
2778
|
}
|
|
2373
2779
|
finally {
|
|
2374
|
-
_commandFinally(state, this);
|
|
2780
|
+
await _commandFinally(state, this);
|
|
2375
2781
|
}
|
|
2376
2782
|
}
|
|
2377
2783
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2384,7 +2790,7 @@ class StableBrowser {
|
|
|
2384
2790
|
scroll: false,
|
|
2385
2791
|
highlight: false,
|
|
2386
2792
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2387
|
-
text: `Verify text does not exist in page`,
|
|
2793
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2388
2794
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2389
2795
|
operation: "verifyTextNotExistInPage",
|
|
2390
2796
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2428,7 +2834,7 @@ class StableBrowser {
|
|
|
2428
2834
|
await _commandError(state, e, this);
|
|
2429
2835
|
}
|
|
2430
2836
|
finally {
|
|
2431
|
-
_commandFinally(state, this);
|
|
2837
|
+
await _commandFinally(state, this);
|
|
2432
2838
|
}
|
|
2433
2839
|
}
|
|
2434
2840
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2470,7 +2876,7 @@ class StableBrowser {
|
|
|
2470
2876
|
};
|
|
2471
2877
|
while (true) {
|
|
2472
2878
|
try {
|
|
2473
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2879
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2474
2880
|
}
|
|
2475
2881
|
catch (error) {
|
|
2476
2882
|
// ignore
|
|
@@ -2498,7 +2904,7 @@ class StableBrowser {
|
|
|
2498
2904
|
const count = await frame.locator(css).count();
|
|
2499
2905
|
for (let j = 0; j < count; j++) {
|
|
2500
2906
|
const continer = await frame.locator(css).nth(j);
|
|
2501
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2907
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2502
2908
|
if (result.elementCount > 0) {
|
|
2503
2909
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2504
2910
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2539,9 +2945,33 @@ class StableBrowser {
|
|
|
2539
2945
|
await _commandError(state, e, this);
|
|
2540
2946
|
}
|
|
2541
2947
|
finally {
|
|
2542
|
-
_commandFinally(state, this);
|
|
2948
|
+
await _commandFinally(state, this);
|
|
2543
2949
|
}
|
|
2544
2950
|
}
|
|
2951
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2952
|
+
const frames = this.page.frames();
|
|
2953
|
+
let results = [];
|
|
2954
|
+
let ignoreCase = false;
|
|
2955
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2956
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2957
|
+
result.frame = frames[i];
|
|
2958
|
+
const climbArray = [];
|
|
2959
|
+
for (let i = 0; i < climb; i++) {
|
|
2960
|
+
climbArray.push("..");
|
|
2961
|
+
}
|
|
2962
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2963
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2964
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2965
|
+
if (count > 0) {
|
|
2966
|
+
result.elementCount = count;
|
|
2967
|
+
result.locator = newLocator;
|
|
2968
|
+
results.push(result);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
// state.info.results = results;
|
|
2972
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2973
|
+
return resultWithElementsFound;
|
|
2974
|
+
}
|
|
2545
2975
|
async visualVerification(text, options = {}, world = null) {
|
|
2546
2976
|
const startTime = Date.now();
|
|
2547
2977
|
let error = null;
|
|
@@ -2858,7 +3288,13 @@ class StableBrowser {
|
|
|
2858
3288
|
}
|
|
2859
3289
|
}
|
|
2860
3290
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2861
|
-
|
|
3291
|
+
try {
|
|
3292
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3293
|
+
}
|
|
3294
|
+
catch (error) {
|
|
3295
|
+
this.logger.debug(error);
|
|
3296
|
+
throw error;
|
|
3297
|
+
}
|
|
2862
3298
|
}
|
|
2863
3299
|
_getLoadTimeout(options) {
|
|
2864
3300
|
let timeout = 15000;
|
|
@@ -2881,6 +3317,7 @@ class StableBrowser {
|
|
|
2881
3317
|
}
|
|
2882
3318
|
async saveStoreState(path = null, world = null) {
|
|
2883
3319
|
const storageState = await this.page.context().storageState();
|
|
3320
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2884
3321
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2885
3322
|
if (path) {
|
|
2886
3323
|
// save { storageState: storageState } into the path
|
|
@@ -2891,10 +3328,14 @@ class StableBrowser {
|
|
|
2891
3328
|
}
|
|
2892
3329
|
}
|
|
2893
3330
|
async restoreSaveState(path = null, world = null) {
|
|
3331
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2894
3332
|
await refreshBrowser(this, path, world);
|
|
2895
3333
|
this.registerEventListeners(this.context);
|
|
2896
3334
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2897
3335
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3336
|
+
if (this.onRestoreSaveState) {
|
|
3337
|
+
this.onRestoreSaveState(path);
|
|
3338
|
+
}
|
|
2898
3339
|
}
|
|
2899
3340
|
async waitForPageLoad(options = {}, world = null) {
|
|
2900
3341
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2977,7 +3418,7 @@ class StableBrowser {
|
|
|
2977
3418
|
await _commandError(state, e, this);
|
|
2978
3419
|
}
|
|
2979
3420
|
finally {
|
|
2980
|
-
_commandFinally(state, this);
|
|
3421
|
+
await _commandFinally(state, this);
|
|
2981
3422
|
}
|
|
2982
3423
|
}
|
|
2983
3424
|
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
@@ -3064,7 +3505,7 @@ class StableBrowser {
|
|
|
3064
3505
|
await _commandError(state, e, this);
|
|
3065
3506
|
}
|
|
3066
3507
|
finally {
|
|
3067
|
-
_commandFinally(state, this);
|
|
3508
|
+
await _commandFinally(state, this);
|
|
3068
3509
|
}
|
|
3069
3510
|
}
|
|
3070
3511
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3169,7 +3610,39 @@ class StableBrowser {
|
|
|
3169
3610
|
console.log("#-#");
|
|
3170
3611
|
}
|
|
3171
3612
|
}
|
|
3613
|
+
async beforeScenario(world, scenario) {
|
|
3614
|
+
this.beforeScenarioCalled = true;
|
|
3615
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3616
|
+
this.scenarioName = scenario.pickle.name;
|
|
3617
|
+
}
|
|
3618
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3619
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3620
|
+
}
|
|
3621
|
+
if (this.context) {
|
|
3622
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3623
|
+
}
|
|
3624
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3625
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3626
|
+
// check if @global_test_data tag is present
|
|
3627
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3628
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
// update test data based on feature/scenario
|
|
3632
|
+
let envName = null;
|
|
3633
|
+
if (this.context && this.context.environment) {
|
|
3634
|
+
envName = this.context.environment.name;
|
|
3635
|
+
}
|
|
3636
|
+
if (!process.env.TEMP_RUN) {
|
|
3637
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3638
|
+
}
|
|
3639
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3640
|
+
}
|
|
3641
|
+
async afterScenario(world, scenario) { }
|
|
3172
3642
|
async beforeStep(world, step) {
|
|
3643
|
+
if (!this.beforeScenarioCalled) {
|
|
3644
|
+
this.beforeScenario(world, step);
|
|
3645
|
+
}
|
|
3173
3646
|
if (this.stepIndex === undefined) {
|
|
3174
3647
|
this.stepIndex = 0;
|
|
3175
3648
|
}
|
|
@@ -3186,21 +3659,11 @@ class StableBrowser {
|
|
|
3186
3659
|
else {
|
|
3187
3660
|
this.stepName = "step " + this.stepIndex;
|
|
3188
3661
|
}
|
|
3189
|
-
if (this.context) {
|
|
3190
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3191
|
-
}
|
|
3192
3662
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3193
3663
|
if (this.context.browserObject.context) {
|
|
3194
3664
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3195
3665
|
}
|
|
3196
3666
|
}
|
|
3197
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3198
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3199
|
-
// check if @global_test_data tag is present
|
|
3200
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3201
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
3667
|
if (this.initSnapshotTaken === false) {
|
|
3205
3668
|
this.initSnapshotTaken = true;
|
|
3206
3669
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
@@ -3225,18 +3688,68 @@ class StableBrowser {
|
|
|
3225
3688
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3226
3689
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3227
3690
|
for (let i = 0; i < frames.length; i++) {
|
|
3228
|
-
content.push(`- frame: ${i}`);
|
|
3229
3691
|
const frame = frames[i];
|
|
3230
|
-
|
|
3231
|
-
|
|
3692
|
+
try {
|
|
3693
|
+
// Ensure frame is attached and has body
|
|
3694
|
+
const body = frame.locator("body");
|
|
3695
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3696
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3697
|
+
content.push(`- frame: ${i}`);
|
|
3698
|
+
content.push(snapshot);
|
|
3699
|
+
}
|
|
3700
|
+
catch (innerErr) { }
|
|
3232
3701
|
}
|
|
3233
3702
|
return content.join("\n");
|
|
3234
3703
|
}
|
|
3235
3704
|
catch (e) {
|
|
3236
|
-
console.
|
|
3705
|
+
console.log("Error in getAriaSnapshot");
|
|
3706
|
+
//console.debug(e);
|
|
3237
3707
|
}
|
|
3238
3708
|
return null;
|
|
3239
3709
|
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Sends command with custom payload to report.
|
|
3712
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3713
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3714
|
+
* @param content - Content of the command to be shown in the report.
|
|
3715
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3716
|
+
* @param world - Optional world context.
|
|
3717
|
+
* @public
|
|
3718
|
+
*/
|
|
3719
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3720
|
+
const state = {
|
|
3721
|
+
options,
|
|
3722
|
+
world,
|
|
3723
|
+
locate: false,
|
|
3724
|
+
scroll: false,
|
|
3725
|
+
screenshot: options.screenshot ?? false,
|
|
3726
|
+
highlight: options.highlight ?? false,
|
|
3727
|
+
type: Types.REPORT_COMMAND,
|
|
3728
|
+
text: commandText,
|
|
3729
|
+
_text: commandText,
|
|
3730
|
+
operation: "report_command",
|
|
3731
|
+
log: "***** " + commandText + " *****\n",
|
|
3732
|
+
};
|
|
3733
|
+
try {
|
|
3734
|
+
await _preCommand(state, this);
|
|
3735
|
+
const payload = {
|
|
3736
|
+
type: options.type ?? "text",
|
|
3737
|
+
content: content,
|
|
3738
|
+
screenshotId: null,
|
|
3739
|
+
};
|
|
3740
|
+
state.payload = payload;
|
|
3741
|
+
if (commandStatus === "FAILED") {
|
|
3742
|
+
state.throwError = true;
|
|
3743
|
+
throw new Error("Command failed");
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
catch (e) {
|
|
3747
|
+
await _commandError(state, e, this);
|
|
3748
|
+
}
|
|
3749
|
+
finally {
|
|
3750
|
+
await _commandFinally(state, this);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3240
3753
|
async afterStep(world, step) {
|
|
3241
3754
|
this.stepName = null;
|
|
3242
3755
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3244,6 +3757,13 @@ class StableBrowser {
|
|
|
3244
3757
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3245
3758
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3246
3759
|
});
|
|
3760
|
+
if (world && world.attach) {
|
|
3761
|
+
await world.attach(JSON.stringify({
|
|
3762
|
+
type: "trace",
|
|
3763
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3764
|
+
}), "application/json+trace");
|
|
3765
|
+
}
|
|
3766
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3247
3767
|
}
|
|
3248
3768
|
}
|
|
3249
3769
|
if (this.context) {
|
|
@@ -3256,6 +3776,29 @@ class StableBrowser {
|
|
|
3256
3776
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3257
3777
|
}
|
|
3258
3778
|
}
|
|
3779
|
+
if (!process.env.TEMP_RUN) {
|
|
3780
|
+
const state = {
|
|
3781
|
+
world,
|
|
3782
|
+
locate: false,
|
|
3783
|
+
scroll: false,
|
|
3784
|
+
screenshot: true,
|
|
3785
|
+
highlight: true,
|
|
3786
|
+
type: Types.STEP_COMPLETE,
|
|
3787
|
+
text: "end of scenario",
|
|
3788
|
+
_text: "end of scenario",
|
|
3789
|
+
operation: "step_complete",
|
|
3790
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
3791
|
+
};
|
|
3792
|
+
try {
|
|
3793
|
+
await _preCommand(state, this);
|
|
3794
|
+
}
|
|
3795
|
+
catch (e) {
|
|
3796
|
+
await _commandError(state, e, this);
|
|
3797
|
+
}
|
|
3798
|
+
finally {
|
|
3799
|
+
await _commandFinally(state, this);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3259
3802
|
}
|
|
3260
3803
|
}
|
|
3261
3804
|
function createTimedPromise(promise, label) {
|