automation_model 1.0.648-dev → 1.0.648-stage
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/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 +143 -57
- 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 +1 -1
- package/lib/command_common.js +24 -4
- 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.js +4 -4
- package/lib/init_browser.js.map +1 -1
- 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.js.map +1 -1
- package/lib/scripts/axe.mini.js +3 -3
- 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 +59 -24
- package/lib/stable_browser.js +908 -186
- 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 +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +5 -3
- package/lib/utils.js +154 -65
- 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 { highlightSnapshot, 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,13 @@ 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",
|
|
68
|
+
SLEEP: "sleep",
|
|
58
69
|
};
|
|
59
70
|
export const apps = {};
|
|
60
71
|
const formatElementName = (elementName) => {
|
|
@@ -66,6 +77,7 @@ class StableBrowser {
|
|
|
66
77
|
logger;
|
|
67
78
|
context;
|
|
68
79
|
world;
|
|
80
|
+
fastMode;
|
|
69
81
|
project_path = null;
|
|
70
82
|
webLogFile = null;
|
|
71
83
|
networkLogger = null;
|
|
@@ -74,12 +86,13 @@ class StableBrowser {
|
|
|
74
86
|
tags = null;
|
|
75
87
|
isRecording = false;
|
|
76
88
|
initSnapshotTaken = false;
|
|
77
|
-
constructor(browser, page, logger = null, context = null, world = null) {
|
|
89
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
|
|
78
90
|
this.browser = browser;
|
|
79
91
|
this.page = page;
|
|
80
92
|
this.logger = logger;
|
|
81
93
|
this.context = context;
|
|
82
94
|
this.world = world;
|
|
95
|
+
this.fastMode = fastMode;
|
|
83
96
|
if (!this.logger) {
|
|
84
97
|
this.logger = console;
|
|
85
98
|
}
|
|
@@ -108,6 +121,12 @@ class StableBrowser {
|
|
|
108
121
|
context.pages = [this.page];
|
|
109
122
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
110
123
|
this.world = world;
|
|
124
|
+
if (process.env.FAST_MODE === "true") {
|
|
125
|
+
this.fastMode = true;
|
|
126
|
+
}
|
|
127
|
+
if (this.context) {
|
|
128
|
+
this.context.fastMode = this.fastMode;
|
|
129
|
+
}
|
|
111
130
|
this.registerEventListeners(this.context);
|
|
112
131
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
113
132
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
@@ -118,6 +137,9 @@ class StableBrowser {
|
|
|
118
137
|
if (!context.pageLoading) {
|
|
119
138
|
context.pageLoading = { status: false };
|
|
120
139
|
}
|
|
140
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
141
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
142
|
+
}
|
|
121
143
|
context.playContext.on("page", async function (page) {
|
|
122
144
|
if (this.configuration && this.configuration.closePopups === true) {
|
|
123
145
|
console.log("close unexpected popups");
|
|
@@ -126,6 +148,14 @@ class StableBrowser {
|
|
|
126
148
|
}
|
|
127
149
|
context.pageLoading.status = true;
|
|
128
150
|
this.page = page;
|
|
151
|
+
try {
|
|
152
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
153
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error("Error on dialog accept registration", error);
|
|
158
|
+
}
|
|
129
159
|
context.page = page;
|
|
130
160
|
context.pages.push(page);
|
|
131
161
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
@@ -177,8 +207,34 @@ class StableBrowser {
|
|
|
177
207
|
if (newContextCreated) {
|
|
178
208
|
this.registerEventListeners(this.context);
|
|
179
209
|
await this.goto(this.context.environment.baseUrl);
|
|
180
|
-
|
|
210
|
+
if (!this.fastMode) {
|
|
211
|
+
await this.waitForPageLoad();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async switchTab(tabTitleOrIndex) {
|
|
216
|
+
// first check if the tabNameOrIndex is a number
|
|
217
|
+
let index = parseInt(tabTitleOrIndex);
|
|
218
|
+
if (!isNaN(index)) {
|
|
219
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
220
|
+
this.page = this.context.pages[index];
|
|
221
|
+
this.context.page = this.page;
|
|
222
|
+
await this.page.bringToFront();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
181
225
|
}
|
|
226
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
227
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
228
|
+
let page = this.context.pages[i];
|
|
229
|
+
let title = await page.title();
|
|
230
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
231
|
+
this.page = page;
|
|
232
|
+
this.context.page = this.page;
|
|
233
|
+
await this.page.bringToFront();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
182
238
|
}
|
|
183
239
|
registerConsoleLogListener(page, context) {
|
|
184
240
|
if (!this.context.webLogger) {
|
|
@@ -247,6 +303,7 @@ class StableBrowser {
|
|
|
247
303
|
if (!url) {
|
|
248
304
|
throw new Error("url is null, verify that the environment file is correct");
|
|
249
305
|
}
|
|
306
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
250
307
|
if (!url.startsWith("http")) {
|
|
251
308
|
url = "https://" + url;
|
|
252
309
|
}
|
|
@@ -275,7 +332,7 @@ class StableBrowser {
|
|
|
275
332
|
_commandError(state, error, this);
|
|
276
333
|
}
|
|
277
334
|
finally {
|
|
278
|
-
_commandFinally(state, this);
|
|
335
|
+
await _commandFinally(state, this);
|
|
279
336
|
}
|
|
280
337
|
}
|
|
281
338
|
async _getLocator(locator, scope, _params) {
|
|
@@ -391,7 +448,7 @@ class StableBrowser {
|
|
|
391
448
|
}
|
|
392
449
|
return { elementCount: tagCount, randomToken };
|
|
393
450
|
}
|
|
394
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
451
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
395
452
|
if (!info) {
|
|
396
453
|
info = {};
|
|
397
454
|
}
|
|
@@ -458,7 +515,7 @@ class StableBrowser {
|
|
|
458
515
|
}
|
|
459
516
|
return;
|
|
460
517
|
}
|
|
461
|
-
if (info.locatorLog && count === 0) {
|
|
518
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
462
519
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
463
520
|
}
|
|
464
521
|
for (let j = 0; j < count; j++) {
|
|
@@ -473,7 +530,7 @@ class StableBrowser {
|
|
|
473
530
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
474
531
|
}
|
|
475
532
|
}
|
|
476
|
-
else {
|
|
533
|
+
else if (logErrors) {
|
|
477
534
|
info.failCause.visible = visible;
|
|
478
535
|
info.failCause.enabled = enabled;
|
|
479
536
|
if (!info.printMessages) {
|
|
@@ -565,15 +622,27 @@ class StableBrowser {
|
|
|
565
622
|
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
566
623
|
if (!element.rerun) {
|
|
567
624
|
const randomToken = Math.random().toString(36).substring(7);
|
|
568
|
-
element.evaluate((el, randomToken) => {
|
|
625
|
+
await element.evaluate((el, randomToken) => {
|
|
569
626
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
570
627
|
}, randomToken);
|
|
571
|
-
if (element._frame) {
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
const scope = element.page();
|
|
575
|
-
|
|
576
|
-
|
|
628
|
+
// if (element._frame) {
|
|
629
|
+
// return element;
|
|
630
|
+
// }
|
|
631
|
+
const scope = element._frame ?? element.page();
|
|
632
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
633
|
+
let prefixSelector = "";
|
|
634
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
635
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
636
|
+
if (frameSelectorIndex !== -1) {
|
|
637
|
+
// remove everything after the >> internal:control=enter-frame
|
|
638
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
639
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
640
|
+
}
|
|
641
|
+
// if (element?._frame?._selector) {
|
|
642
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
643
|
+
// }
|
|
644
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
645
|
+
return scope.locator(newSelector);
|
|
577
646
|
}
|
|
578
647
|
}
|
|
579
648
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -725,14 +794,9 @@ class StableBrowser {
|
|
|
725
794
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
726
795
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
727
796
|
}
|
|
728
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
797
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
729
798
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
730
799
|
}
|
|
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
800
|
let foundElements = result.foundElements;
|
|
737
801
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
738
802
|
info.box = foundElements[0].box;
|
|
@@ -787,6 +851,11 @@ class StableBrowser {
|
|
|
787
851
|
visibleOnly = false;
|
|
788
852
|
}
|
|
789
853
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
854
|
+
// sheck of more of half of the timeout has passed
|
|
855
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
856
|
+
highPriorityOnly = false;
|
|
857
|
+
visibleOnly = false;
|
|
858
|
+
}
|
|
790
859
|
}
|
|
791
860
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
792
861
|
// if (info.locatorLog) {
|
|
@@ -802,7 +871,7 @@ class StableBrowser {
|
|
|
802
871
|
}
|
|
803
872
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
804
873
|
}
|
|
805
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
874
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
806
875
|
let foundElements = [];
|
|
807
876
|
const result = {
|
|
808
877
|
foundElements: foundElements,
|
|
@@ -821,7 +890,9 @@ class StableBrowser {
|
|
|
821
890
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
822
891
|
}
|
|
823
892
|
catch (e) {
|
|
824
|
-
|
|
893
|
+
if (logErrors) {
|
|
894
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
895
|
+
}
|
|
825
896
|
}
|
|
826
897
|
}
|
|
827
898
|
if (foundLocators.length === 1) {
|
|
@@ -862,7 +933,7 @@ class StableBrowser {
|
|
|
862
933
|
});
|
|
863
934
|
result.locatorIndex = i;
|
|
864
935
|
}
|
|
865
|
-
else {
|
|
936
|
+
else if (logErrors) {
|
|
866
937
|
info.failCause.foundMultiple = true;
|
|
867
938
|
if (info.locatorLog) {
|
|
868
939
|
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
@@ -914,7 +985,7 @@ class StableBrowser {
|
|
|
914
985
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
915
986
|
}
|
|
916
987
|
finally {
|
|
917
|
-
_commandFinally(state, this);
|
|
988
|
+
await _commandFinally(state, this);
|
|
918
989
|
}
|
|
919
990
|
}
|
|
920
991
|
}
|
|
@@ -963,7 +1034,7 @@ class StableBrowser {
|
|
|
963
1034
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
964
1035
|
}
|
|
965
1036
|
finally {
|
|
966
|
-
_commandFinally(state, this);
|
|
1037
|
+
await _commandFinally(state, this);
|
|
967
1038
|
}
|
|
968
1039
|
}
|
|
969
1040
|
}
|
|
@@ -985,14 +1056,16 @@ class StableBrowser {
|
|
|
985
1056
|
try {
|
|
986
1057
|
await _preCommand(state, this);
|
|
987
1058
|
await performAction("click", state.element, options, this, state, _params);
|
|
988
|
-
|
|
1059
|
+
if (!this.fastMode) {
|
|
1060
|
+
await this.waitForPageLoad();
|
|
1061
|
+
}
|
|
989
1062
|
return state.info;
|
|
990
1063
|
}
|
|
991
1064
|
catch (e) {
|
|
992
1065
|
await _commandError(state, e, this);
|
|
993
1066
|
}
|
|
994
1067
|
finally {
|
|
995
|
-
_commandFinally(state, this);
|
|
1068
|
+
await _commandFinally(state, this);
|
|
996
1069
|
}
|
|
997
1070
|
}
|
|
998
1071
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1023,7 +1096,7 @@ class StableBrowser {
|
|
|
1023
1096
|
// await _commandError(state, e, this);
|
|
1024
1097
|
}
|
|
1025
1098
|
finally {
|
|
1026
|
-
_commandFinally(state, this);
|
|
1099
|
+
await _commandFinally(state, this);
|
|
1027
1100
|
}
|
|
1028
1101
|
return found;
|
|
1029
1102
|
}
|
|
@@ -1047,8 +1120,8 @@ class StableBrowser {
|
|
|
1047
1120
|
try {
|
|
1048
1121
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1049
1122
|
// console.log(`Highlighting while running from recorder`);
|
|
1050
|
-
await this._highlightElements(element);
|
|
1051
|
-
await state.element.setChecked(checked);
|
|
1123
|
+
await this._highlightElements(state.element);
|
|
1124
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
1052
1125
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1053
1126
|
// await this._unHighlightElements(element);
|
|
1054
1127
|
// }
|
|
@@ -1060,11 +1133,28 @@ class StableBrowser {
|
|
|
1060
1133
|
this.logger.info("element did not change its state, ignoring...");
|
|
1061
1134
|
}
|
|
1062
1135
|
else {
|
|
1136
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1063
1137
|
//await this.closeUnexpectedPopups();
|
|
1064
1138
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1065
|
-
state.
|
|
1066
|
-
|
|
1067
|
-
|
|
1139
|
+
state.element_found = false;
|
|
1140
|
+
try {
|
|
1141
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1142
|
+
state.element_found = true;
|
|
1143
|
+
// check the check state
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
// element dismissed
|
|
1147
|
+
}
|
|
1148
|
+
if (state.element_found) {
|
|
1149
|
+
const isChecked = await state.element.isChecked();
|
|
1150
|
+
if (isChecked !== checked) {
|
|
1151
|
+
// perform click
|
|
1152
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1068
1158
|
}
|
|
1069
1159
|
}
|
|
1070
1160
|
await this.waitForPageLoad();
|
|
@@ -1074,7 +1164,7 @@ class StableBrowser {
|
|
|
1074
1164
|
await _commandError(state, e, this);
|
|
1075
1165
|
}
|
|
1076
1166
|
finally {
|
|
1077
|
-
_commandFinally(state, this);
|
|
1167
|
+
await _commandFinally(state, this);
|
|
1078
1168
|
}
|
|
1079
1169
|
}
|
|
1080
1170
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1100,7 +1190,7 @@ class StableBrowser {
|
|
|
1100
1190
|
await _commandError(state, e, this);
|
|
1101
1191
|
}
|
|
1102
1192
|
finally {
|
|
1103
|
-
_commandFinally(state, this);
|
|
1193
|
+
await _commandFinally(state, this);
|
|
1104
1194
|
}
|
|
1105
1195
|
}
|
|
1106
1196
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1136,7 +1226,7 @@ class StableBrowser {
|
|
|
1136
1226
|
await _commandError(state, e, this);
|
|
1137
1227
|
}
|
|
1138
1228
|
finally {
|
|
1139
|
-
_commandFinally(state, this);
|
|
1229
|
+
await _commandFinally(state, this);
|
|
1140
1230
|
}
|
|
1141
1231
|
}
|
|
1142
1232
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1182,7 +1272,7 @@ class StableBrowser {
|
|
|
1182
1272
|
await _commandError(state, e, this);
|
|
1183
1273
|
}
|
|
1184
1274
|
finally {
|
|
1185
|
-
_commandFinally(state, this);
|
|
1275
|
+
await _commandFinally(state, this);
|
|
1186
1276
|
}
|
|
1187
1277
|
}
|
|
1188
1278
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1218,7 +1308,7 @@ class StableBrowser {
|
|
|
1218
1308
|
await _commandError(state, e, this);
|
|
1219
1309
|
}
|
|
1220
1310
|
finally {
|
|
1221
|
-
_commandFinally(state, this);
|
|
1311
|
+
await _commandFinally(state, this);
|
|
1222
1312
|
}
|
|
1223
1313
|
}
|
|
1224
1314
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1287,7 +1377,7 @@ class StableBrowser {
|
|
|
1287
1377
|
await _commandError(state, e, this);
|
|
1288
1378
|
}
|
|
1289
1379
|
finally {
|
|
1290
|
-
_commandFinally(state, this);
|
|
1380
|
+
await _commandFinally(state, this);
|
|
1291
1381
|
}
|
|
1292
1382
|
}
|
|
1293
1383
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1360,7 +1450,9 @@ class StableBrowser {
|
|
|
1360
1450
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1361
1451
|
}
|
|
1362
1452
|
}
|
|
1453
|
+
//if (!this.fastMode) {
|
|
1363
1454
|
await _screenshot(state, this);
|
|
1455
|
+
//}
|
|
1364
1456
|
if (enter === true) {
|
|
1365
1457
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1366
1458
|
await this.page.keyboard.press("Enter");
|
|
@@ -1387,7 +1479,7 @@ class StableBrowser {
|
|
|
1387
1479
|
await _commandError(state, e, this);
|
|
1388
1480
|
}
|
|
1389
1481
|
finally {
|
|
1390
|
-
_commandFinally(state, this);
|
|
1482
|
+
await _commandFinally(state, this);
|
|
1391
1483
|
}
|
|
1392
1484
|
}
|
|
1393
1485
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1417,7 +1509,42 @@ class StableBrowser {
|
|
|
1417
1509
|
await _commandError(state, e, this);
|
|
1418
1510
|
}
|
|
1419
1511
|
finally {
|
|
1420
|
-
_commandFinally(state, this);
|
|
1512
|
+
await _commandFinally(state, this);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1516
|
+
const state = {
|
|
1517
|
+
selectors,
|
|
1518
|
+
_params,
|
|
1519
|
+
files,
|
|
1520
|
+
value: '"' + files.join('", "') + '"',
|
|
1521
|
+
options,
|
|
1522
|
+
world,
|
|
1523
|
+
type: Types.SET_INPUT_FILES,
|
|
1524
|
+
text: `Set input files`,
|
|
1525
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1526
|
+
operation: "setInputFiles",
|
|
1527
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1528
|
+
};
|
|
1529
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1530
|
+
try {
|
|
1531
|
+
await _preCommand(state, this);
|
|
1532
|
+
for (let i = 0; i < files.length; i++) {
|
|
1533
|
+
const file = files[i];
|
|
1534
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1535
|
+
if (!fs.existsSync(filePath)) {
|
|
1536
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1537
|
+
}
|
|
1538
|
+
state.files[i] = filePath;
|
|
1539
|
+
}
|
|
1540
|
+
await state.element.setInputFiles(files);
|
|
1541
|
+
return state.info;
|
|
1542
|
+
}
|
|
1543
|
+
catch (e) {
|
|
1544
|
+
await _commandError(state, e, this);
|
|
1545
|
+
}
|
|
1546
|
+
finally {
|
|
1547
|
+
await _commandFinally(state, this);
|
|
1421
1548
|
}
|
|
1422
1549
|
}
|
|
1423
1550
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1533,7 +1660,7 @@ class StableBrowser {
|
|
|
1533
1660
|
await _commandError(state, e, this);
|
|
1534
1661
|
}
|
|
1535
1662
|
finally {
|
|
1536
|
-
_commandFinally(state, this);
|
|
1663
|
+
await _commandFinally(state, this);
|
|
1537
1664
|
}
|
|
1538
1665
|
}
|
|
1539
1666
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1568,7 +1695,7 @@ class StableBrowser {
|
|
|
1568
1695
|
while (Date.now() - startTime < timeout) {
|
|
1569
1696
|
try {
|
|
1570
1697
|
await _preCommand(state, this);
|
|
1571
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1698
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1572
1699
|
if (foundObj && foundObj.element) {
|
|
1573
1700
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1574
1701
|
}
|
|
@@ -1610,7 +1737,84 @@ class StableBrowser {
|
|
|
1610
1737
|
throw e;
|
|
1611
1738
|
}
|
|
1612
1739
|
finally {
|
|
1613
|
-
_commandFinally(state, this);
|
|
1740
|
+
await _commandFinally(state, this);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1744
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1745
|
+
const startTime = Date.now();
|
|
1746
|
+
const state = {
|
|
1747
|
+
_params,
|
|
1748
|
+
value: referanceSnapshot,
|
|
1749
|
+
options,
|
|
1750
|
+
world,
|
|
1751
|
+
locate: false,
|
|
1752
|
+
scroll: false,
|
|
1753
|
+
screenshot: true,
|
|
1754
|
+
highlight: false,
|
|
1755
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1756
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1757
|
+
operation: "snapshotValidation",
|
|
1758
|
+
log: "***** verify snapshot *****\n",
|
|
1759
|
+
};
|
|
1760
|
+
if (!referanceSnapshot) {
|
|
1761
|
+
throw new Error("referanceSnapshot is null");
|
|
1762
|
+
}
|
|
1763
|
+
let text = null;
|
|
1764
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1765
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1766
|
+
}
|
|
1767
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1768
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1769
|
+
}
|
|
1770
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1771
|
+
text = referanceSnapshot.substring(5);
|
|
1772
|
+
}
|
|
1773
|
+
else {
|
|
1774
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1775
|
+
}
|
|
1776
|
+
state.text = text;
|
|
1777
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1778
|
+
await _preCommand(state, this);
|
|
1779
|
+
let foundObj = null;
|
|
1780
|
+
try {
|
|
1781
|
+
let matchResult = null;
|
|
1782
|
+
while (Date.now() - startTime < timeout) {
|
|
1783
|
+
try {
|
|
1784
|
+
let scope = null;
|
|
1785
|
+
if (!frameSelectors) {
|
|
1786
|
+
scope = this.page;
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1790
|
+
}
|
|
1791
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1792
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1793
|
+
if (matchResult.errorLine !== -1) {
|
|
1794
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1795
|
+
}
|
|
1796
|
+
// highlight and screenshot
|
|
1797
|
+
try {
|
|
1798
|
+
await await highlightSnapshot(newValue, scope);
|
|
1799
|
+
await _screenshot(state, this);
|
|
1800
|
+
}
|
|
1801
|
+
catch (e) { }
|
|
1802
|
+
return state.info;
|
|
1803
|
+
}
|
|
1804
|
+
catch (e) {
|
|
1805
|
+
// Log error but continue retrying until timeout is reached
|
|
1806
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1807
|
+
}
|
|
1808
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1809
|
+
}
|
|
1810
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1811
|
+
}
|
|
1812
|
+
catch (e) {
|
|
1813
|
+
await _commandError(state, e, this);
|
|
1814
|
+
throw e;
|
|
1815
|
+
}
|
|
1816
|
+
finally {
|
|
1817
|
+
await _commandFinally(state, this);
|
|
1614
1818
|
}
|
|
1615
1819
|
}
|
|
1616
1820
|
async waitForUserInput(message, world = null) {
|
|
@@ -1648,6 +1852,15 @@ class StableBrowser {
|
|
|
1648
1852
|
// save the data to the file
|
|
1649
1853
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1650
1854
|
}
|
|
1855
|
+
overwriteTestData(testData, world = null) {
|
|
1856
|
+
if (!testData) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
// if data file exists, load it
|
|
1860
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1861
|
+
// save the data to the file
|
|
1862
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1863
|
+
}
|
|
1651
1864
|
_getDataFilePath(fileName) {
|
|
1652
1865
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1653
1866
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1900,7 +2113,7 @@ class StableBrowser {
|
|
|
1900
2113
|
await _commandError(state, e, this);
|
|
1901
2114
|
}
|
|
1902
2115
|
finally {
|
|
1903
|
-
_commandFinally(state, this);
|
|
2116
|
+
await _commandFinally(state, this);
|
|
1904
2117
|
}
|
|
1905
2118
|
}
|
|
1906
2119
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1931,10 +2144,102 @@ class StableBrowser {
|
|
|
1931
2144
|
case "value":
|
|
1932
2145
|
state.value = await state.element.inputValue();
|
|
1933
2146
|
break;
|
|
2147
|
+
case "text":
|
|
2148
|
+
state.value = await state.element.textContent();
|
|
2149
|
+
break;
|
|
1934
2150
|
default:
|
|
1935
2151
|
state.value = await state.element.getAttribute(attribute);
|
|
1936
2152
|
break;
|
|
1937
2153
|
}
|
|
2154
|
+
if (options !== null) {
|
|
2155
|
+
if (options.regex && options.regex !== "") {
|
|
2156
|
+
// Construct a regex pattern from the provided string
|
|
2157
|
+
const regex = options.regex.slice(1, -1);
|
|
2158
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2159
|
+
const matches = state.value.match(regexPattern);
|
|
2160
|
+
if (matches) {
|
|
2161
|
+
let newValue = "";
|
|
2162
|
+
for (const match of matches) {
|
|
2163
|
+
newValue += match;
|
|
2164
|
+
}
|
|
2165
|
+
state.value = newValue;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2169
|
+
state.value = state.value.trim();
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
state.info.value = state.value;
|
|
2173
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2174
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2175
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2176
|
+
return state.info;
|
|
2177
|
+
}
|
|
2178
|
+
catch (e) {
|
|
2179
|
+
await _commandError(state, e, this);
|
|
2180
|
+
}
|
|
2181
|
+
finally {
|
|
2182
|
+
await _commandFinally(state, this);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2186
|
+
const state = {
|
|
2187
|
+
selectors,
|
|
2188
|
+
_params,
|
|
2189
|
+
property,
|
|
2190
|
+
variable,
|
|
2191
|
+
options,
|
|
2192
|
+
world,
|
|
2193
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2194
|
+
text: `Extract property from element`,
|
|
2195
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2196
|
+
operation: "extractProperty",
|
|
2197
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2198
|
+
allowDisabled: true,
|
|
2199
|
+
};
|
|
2200
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2201
|
+
try {
|
|
2202
|
+
await _preCommand(state, this);
|
|
2203
|
+
switch (property) {
|
|
2204
|
+
case "inner_text":
|
|
2205
|
+
state.value = await state.element.innerText();
|
|
2206
|
+
break;
|
|
2207
|
+
case "href":
|
|
2208
|
+
state.value = await state.element.getAttribute("href");
|
|
2209
|
+
break;
|
|
2210
|
+
case "value":
|
|
2211
|
+
state.value = await state.element.inputValue();
|
|
2212
|
+
break;
|
|
2213
|
+
case "text":
|
|
2214
|
+
state.value = await state.element.textContent();
|
|
2215
|
+
break;
|
|
2216
|
+
default:
|
|
2217
|
+
if (property.startsWith("dataset.")) {
|
|
2218
|
+
const dataAttribute = property.substring(8);
|
|
2219
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2220
|
+
}
|
|
2221
|
+
else {
|
|
2222
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
if (options !== null) {
|
|
2226
|
+
if (options.regex && options.regex !== "") {
|
|
2227
|
+
// Construct a regex pattern from the provided string
|
|
2228
|
+
const regex = options.regex.slice(1, -1);
|
|
2229
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2230
|
+
const matches = state.value.match(regexPattern);
|
|
2231
|
+
if (matches) {
|
|
2232
|
+
let newValue = "";
|
|
2233
|
+
for (const match of matches) {
|
|
2234
|
+
newValue += match;
|
|
2235
|
+
}
|
|
2236
|
+
state.value = newValue;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2240
|
+
state.value = state.value.trim();
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
1938
2243
|
state.info.value = state.value;
|
|
1939
2244
|
this.setTestData({ [variable]: state.value }, world);
|
|
1940
2245
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1945,7 +2250,7 @@ class StableBrowser {
|
|
|
1945
2250
|
await _commandError(state, e, this);
|
|
1946
2251
|
}
|
|
1947
2252
|
finally {
|
|
1948
|
-
_commandFinally(state, this);
|
|
2253
|
+
await _commandFinally(state, this);
|
|
1949
2254
|
}
|
|
1950
2255
|
}
|
|
1951
2256
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1970,12 +2275,15 @@ class StableBrowser {
|
|
|
1970
2275
|
let expectedValue;
|
|
1971
2276
|
try {
|
|
1972
2277
|
await _preCommand(state, this);
|
|
1973
|
-
expectedValue = state.value;
|
|
2278
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1974
2279
|
state.info.expectedValue = expectedValue;
|
|
1975
2280
|
switch (attribute) {
|
|
1976
2281
|
case "innerText":
|
|
1977
2282
|
val = String(await state.element.innerText());
|
|
1978
2283
|
break;
|
|
2284
|
+
case "text":
|
|
2285
|
+
val = String(await state.element.textContent());
|
|
2286
|
+
break;
|
|
1979
2287
|
case "value":
|
|
1980
2288
|
val = String(await state.element.inputValue());
|
|
1981
2289
|
break;
|
|
@@ -1997,17 +2305,145 @@ class StableBrowser {
|
|
|
1997
2305
|
let regex;
|
|
1998
2306
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1999
2307
|
const patternBody = expectedValue.slice(1, -1);
|
|
2000
|
-
|
|
2308
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2309
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2310
|
+
state.info.regex = true;
|
|
2311
|
+
}
|
|
2312
|
+
else {
|
|
2313
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2314
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2315
|
+
}
|
|
2316
|
+
if (attribute === "innerText") {
|
|
2317
|
+
if (state.info.regex) {
|
|
2318
|
+
if (!regex.test(val)) {
|
|
2319
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2320
|
+
state.info.failCause.assertionFailed = true;
|
|
2321
|
+
state.info.failCause.lastError = errorMessage;
|
|
2322
|
+
throw new Error(errorMessage);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
else {
|
|
2326
|
+
const valLines = val.split("\n");
|
|
2327
|
+
const expectedLines = expectedValue.split("\n");
|
|
2328
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2329
|
+
if (!isPart) {
|
|
2330
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2331
|
+
state.info.failCause.assertionFailed = true;
|
|
2332
|
+
state.info.failCause.lastError = errorMessage;
|
|
2333
|
+
throw new Error(errorMessage);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
else {
|
|
2338
|
+
if (!val.match(regex)) {
|
|
2339
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2340
|
+
state.info.failCause.assertionFailed = true;
|
|
2341
|
+
state.info.failCause.lastError = errorMessage;
|
|
2342
|
+
throw new Error(errorMessage);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return state.info;
|
|
2346
|
+
}
|
|
2347
|
+
catch (e) {
|
|
2348
|
+
await _commandError(state, e, this);
|
|
2349
|
+
}
|
|
2350
|
+
finally {
|
|
2351
|
+
await _commandFinally(state, this);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2355
|
+
const state = {
|
|
2356
|
+
selectors,
|
|
2357
|
+
_params,
|
|
2358
|
+
property,
|
|
2359
|
+
value,
|
|
2360
|
+
options,
|
|
2361
|
+
world,
|
|
2362
|
+
type: Types.VERIFY_PROPERTY,
|
|
2363
|
+
highlight: true,
|
|
2364
|
+
screenshot: true,
|
|
2365
|
+
text: `Verify element property`,
|
|
2366
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2367
|
+
operation: "verifyProperty",
|
|
2368
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2369
|
+
allowDisabled: true,
|
|
2370
|
+
};
|
|
2371
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2372
|
+
let val;
|
|
2373
|
+
let expectedValue;
|
|
2374
|
+
try {
|
|
2375
|
+
await _preCommand(state, this);
|
|
2376
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2377
|
+
state.info.expectedValue = expectedValue;
|
|
2378
|
+
switch (property) {
|
|
2379
|
+
case "innerText":
|
|
2380
|
+
val = String(await state.element.innerText());
|
|
2381
|
+
break;
|
|
2382
|
+
case "text":
|
|
2383
|
+
val = String(await state.element.textContent());
|
|
2384
|
+
break;
|
|
2385
|
+
case "value":
|
|
2386
|
+
val = String(await state.element.inputValue());
|
|
2387
|
+
break;
|
|
2388
|
+
case "checked":
|
|
2389
|
+
val = String(await state.element.isChecked());
|
|
2390
|
+
break;
|
|
2391
|
+
case "disabled":
|
|
2392
|
+
val = String(await state.element.isDisabled());
|
|
2393
|
+
break;
|
|
2394
|
+
case "readOnly":
|
|
2395
|
+
const isEditable = await state.element.isEditable();
|
|
2396
|
+
val = String(!isEditable);
|
|
2397
|
+
break;
|
|
2398
|
+
default:
|
|
2399
|
+
if (property.startsWith("dataset.")) {
|
|
2400
|
+
const dataAttribute = property.substring(8);
|
|
2401
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2402
|
+
}
|
|
2403
|
+
else {
|
|
2404
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
state.info.value = val;
|
|
2408
|
+
let regex;
|
|
2409
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2410
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2411
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2412
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2413
|
+
state.info.regex = true;
|
|
2001
2414
|
}
|
|
2002
2415
|
else {
|
|
2003
2416
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2004
2417
|
regex = new RegExp(escapedPattern, "g");
|
|
2005
2418
|
}
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2419
|
+
if (property === "innerText") {
|
|
2420
|
+
if (state.info.regex) {
|
|
2421
|
+
if (!regex.test(val)) {
|
|
2422
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2423
|
+
state.info.failCause.assertionFailed = true;
|
|
2424
|
+
state.info.failCause.lastError = errorMessage;
|
|
2425
|
+
throw new Error(errorMessage);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
const valLines = val.split("\n");
|
|
2430
|
+
const expectedLines = expectedValue.split("\n");
|
|
2431
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2432
|
+
if (!isPart) {
|
|
2433
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2434
|
+
state.info.failCause.assertionFailed = true;
|
|
2435
|
+
state.info.failCause.lastError = errorMessage;
|
|
2436
|
+
throw new Error(errorMessage);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
else {
|
|
2441
|
+
if (!val.match(regex)) {
|
|
2442
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2443
|
+
state.info.failCause.assertionFailed = true;
|
|
2444
|
+
state.info.failCause.lastError = errorMessage;
|
|
2445
|
+
throw new Error(errorMessage);
|
|
2446
|
+
}
|
|
2011
2447
|
}
|
|
2012
2448
|
return state.info;
|
|
2013
2449
|
}
|
|
@@ -2015,7 +2451,7 @@ class StableBrowser {
|
|
|
2015
2451
|
await _commandError(state, e, this);
|
|
2016
2452
|
}
|
|
2017
2453
|
finally {
|
|
2018
|
-
_commandFinally(state, this);
|
|
2454
|
+
await _commandFinally(state, this);
|
|
2019
2455
|
}
|
|
2020
2456
|
}
|
|
2021
2457
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2175,56 +2611,49 @@ class StableBrowser {
|
|
|
2175
2611
|
console.debug(error);
|
|
2176
2612
|
}
|
|
2177
2613
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
// });
|
|
2221
|
-
// }
|
|
2222
|
-
// } catch (error) {
|
|
2223
|
-
// // console.debug(error);
|
|
2224
|
-
// }
|
|
2225
|
-
// }
|
|
2614
|
+
_matcher(text) {
|
|
2615
|
+
if (!text) {
|
|
2616
|
+
return { matcher: "contains", queryText: "" };
|
|
2617
|
+
}
|
|
2618
|
+
if (text.length < 2) {
|
|
2619
|
+
return { matcher: "contains", queryText: text };
|
|
2620
|
+
}
|
|
2621
|
+
const split = text.split(":");
|
|
2622
|
+
const matcher = split[0].toLowerCase();
|
|
2623
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2624
|
+
return { matcher, queryText };
|
|
2625
|
+
}
|
|
2626
|
+
_getDomain(url) {
|
|
2627
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2628
|
+
return "";
|
|
2629
|
+
}
|
|
2630
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2631
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2632
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2633
|
+
}
|
|
2634
|
+
let n = hostnameFragments.length;
|
|
2635
|
+
let fragments = [...hostnameFragments];
|
|
2636
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2637
|
+
hostnameFragments.pop();
|
|
2638
|
+
n = hostnameFragments.length;
|
|
2639
|
+
}
|
|
2640
|
+
if (n == 0) {
|
|
2641
|
+
if (fragments[0] === "www")
|
|
2642
|
+
fragments = fragments.slice(1);
|
|
2643
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2644
|
+
}
|
|
2645
|
+
if (hostnameFragments[0] === "www")
|
|
2646
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2647
|
+
return hostnameFragments.join(".");
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Verify the page path matches the given path.
|
|
2651
|
+
* @param {string} pathPart - The path to verify.
|
|
2652
|
+
* @param {object} options - Options for verification.
|
|
2653
|
+
* @param {object} world - The world context.
|
|
2654
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2655
|
+
*/
|
|
2226
2656
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2227
|
-
const startTime = Date.now();
|
|
2228
2657
|
let error = null;
|
|
2229
2658
|
let screenshotId = null;
|
|
2230
2659
|
let screenshotPath = null;
|
|
@@ -2238,51 +2667,212 @@ class StableBrowser {
|
|
|
2238
2667
|
pathPart = newValue;
|
|
2239
2668
|
}
|
|
2240
2669
|
info.pathPart = pathPart;
|
|
2670
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2671
|
+
const state = {
|
|
2672
|
+
text_search: queryText,
|
|
2673
|
+
options,
|
|
2674
|
+
world,
|
|
2675
|
+
locate: false,
|
|
2676
|
+
scroll: false,
|
|
2677
|
+
highlight: false,
|
|
2678
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2679
|
+
text: `Verify the page url is ${queryText}`,
|
|
2680
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2681
|
+
operation: "verifyPagePath",
|
|
2682
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2683
|
+
};
|
|
2241
2684
|
try {
|
|
2685
|
+
await _preCommand(state, this);
|
|
2686
|
+
state.info.text = queryText;
|
|
2242
2687
|
for (let i = 0; i < 30; i++) {
|
|
2243
2688
|
const url = await this.page.url();
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2689
|
+
switch (matcher) {
|
|
2690
|
+
case "exact":
|
|
2691
|
+
if (url !== queryText) {
|
|
2692
|
+
if (i === 29) {
|
|
2693
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2694
|
+
}
|
|
2695
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2698
|
+
break;
|
|
2699
|
+
case "contains":
|
|
2700
|
+
if (!url.includes(queryText)) {
|
|
2701
|
+
if (i === 29) {
|
|
2702
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2703
|
+
}
|
|
2704
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
break;
|
|
2708
|
+
case "starts-with":
|
|
2709
|
+
{
|
|
2710
|
+
const domain = this._getDomain(url);
|
|
2711
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2712
|
+
if (i === 29) {
|
|
2713
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2714
|
+
}
|
|
2715
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
break;
|
|
2720
|
+
case "ends-with":
|
|
2721
|
+
{
|
|
2722
|
+
const urlObj = new URL(url);
|
|
2723
|
+
let route = "/";
|
|
2724
|
+
if (urlObj.pathname !== "/") {
|
|
2725
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2726
|
+
}
|
|
2727
|
+
else {
|
|
2728
|
+
route = "/";
|
|
2729
|
+
}
|
|
2730
|
+
if (route !== queryText) {
|
|
2731
|
+
if (i === 29) {
|
|
2732
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2733
|
+
}
|
|
2734
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
break;
|
|
2739
|
+
case "regex":
|
|
2740
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2741
|
+
if (!regex.test(url)) {
|
|
2742
|
+
if (i === 29) {
|
|
2743
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2744
|
+
}
|
|
2745
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
break;
|
|
2749
|
+
default:
|
|
2750
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2751
|
+
if (!url.includes(pathPart)) {
|
|
2752
|
+
if (i === 29) {
|
|
2753
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2754
|
+
}
|
|
2755
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2250
2758
|
}
|
|
2251
|
-
|
|
2252
|
-
return info;
|
|
2759
|
+
await _screenshot(state, this);
|
|
2760
|
+
return state.info;
|
|
2253
2761
|
}
|
|
2254
2762
|
}
|
|
2255
2763
|
catch (e) {
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
info.screenshotPath = screenshotPath;
|
|
2260
|
-
Object.assign(e, { info: info });
|
|
2261
|
-
error = e;
|
|
2262
|
-
// throw e;
|
|
2263
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2764
|
+
state.info.failCause.lastError = e.message;
|
|
2765
|
+
state.info.failCause.assertionFailed = true;
|
|
2766
|
+
await _commandError(state, e, this);
|
|
2264
2767
|
}
|
|
2265
2768
|
finally {
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2769
|
+
await _commandFinally(state, this);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Verify the page title matches the given title.
|
|
2774
|
+
* @param {string} title - The title to verify.
|
|
2775
|
+
* @param {object} options - Options for verification.
|
|
2776
|
+
* @param {object} world - The world context.
|
|
2777
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2778
|
+
*/
|
|
2779
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2780
|
+
let error = null;
|
|
2781
|
+
let screenshotId = null;
|
|
2782
|
+
let screenshotPath = null;
|
|
2783
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2784
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2785
|
+
if (newValue !== title) {
|
|
2786
|
+
this.logger.info(title + "=" + newValue);
|
|
2787
|
+
title = newValue;
|
|
2788
|
+
}
|
|
2789
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2790
|
+
const state = {
|
|
2791
|
+
text_search: queryText,
|
|
2792
|
+
options,
|
|
2793
|
+
world,
|
|
2794
|
+
locate: false,
|
|
2795
|
+
scroll: false,
|
|
2796
|
+
highlight: false,
|
|
2797
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2798
|
+
text: `Verify the page title is ${queryText}`,
|
|
2799
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2800
|
+
operation: "verifyPageTitle",
|
|
2801
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2802
|
+
};
|
|
2803
|
+
try {
|
|
2804
|
+
await _preCommand(state, this);
|
|
2805
|
+
state.info.text = queryText;
|
|
2806
|
+
for (let i = 0; i < 30; i++) {
|
|
2807
|
+
const foundTitle = await this.page.title();
|
|
2808
|
+
switch (matcher) {
|
|
2809
|
+
case "exact":
|
|
2810
|
+
if (foundTitle !== queryText) {
|
|
2811
|
+
if (i === 29) {
|
|
2812
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2813
|
+
}
|
|
2814
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2817
|
+
break;
|
|
2818
|
+
case "contains":
|
|
2819
|
+
if (!foundTitle.includes(queryText)) {
|
|
2820
|
+
if (i === 29) {
|
|
2821
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2822
|
+
}
|
|
2823
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2824
|
+
continue;
|
|
2825
|
+
}
|
|
2826
|
+
break;
|
|
2827
|
+
case "starts-with":
|
|
2828
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2829
|
+
if (i === 29) {
|
|
2830
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2831
|
+
}
|
|
2832
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
break;
|
|
2836
|
+
case "ends-with":
|
|
2837
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2838
|
+
if (i === 29) {
|
|
2839
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2840
|
+
}
|
|
2841
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
break;
|
|
2845
|
+
case "regex":
|
|
2846
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2847
|
+
if (!regex.test(foundTitle)) {
|
|
2848
|
+
if (i === 29) {
|
|
2849
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2850
|
+
}
|
|
2851
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
break;
|
|
2855
|
+
default:
|
|
2856
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2857
|
+
if (!foundTitle.includes(title)) {
|
|
2858
|
+
if (i === 29) {
|
|
2859
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2860
|
+
}
|
|
2861
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
await _screenshot(state, this);
|
|
2866
|
+
return state.info;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
catch (e) {
|
|
2870
|
+
state.info.failCause.lastError = e.message;
|
|
2871
|
+
state.info.failCause.assertionFailed = true;
|
|
2872
|
+
await _commandError(state, e, this);
|
|
2873
|
+
}
|
|
2874
|
+
finally {
|
|
2875
|
+
await _commandFinally(state, this);
|
|
2286
2876
|
}
|
|
2287
2877
|
}
|
|
2288
2878
|
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
@@ -2324,7 +2914,7 @@ class StableBrowser {
|
|
|
2324
2914
|
scroll: false,
|
|
2325
2915
|
highlight: false,
|
|
2326
2916
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2327
|
-
text: `Verify the text '${text}' exists in page`,
|
|
2917
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2328
2918
|
_text: `Verify the text '${text}' exists in page`,
|
|
2329
2919
|
operation: "verifyTextExistInPage",
|
|
2330
2920
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2366,27 +2956,10 @@ class StableBrowser {
|
|
|
2366
2956
|
const frame = resultWithElementsFound[0].frame;
|
|
2367
2957
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2368
2958
|
await this._highlightElements(frame, dataAttribute);
|
|
2369
|
-
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2370
|
-
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2371
|
-
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2372
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2373
|
-
// this._unhighlightElements(frame, dataAttribute)
|
|
2374
|
-
// .then(async () => {
|
|
2375
|
-
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2376
|
-
// })
|
|
2377
|
-
// .catch(
|
|
2378
|
-
// (e) => {}
|
|
2379
|
-
// console.error(e)
|
|
2380
|
-
// );
|
|
2381
|
-
// });
|
|
2382
|
-
// }
|
|
2383
2959
|
const element = await frame.locator(dataAttribute).first();
|
|
2384
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2385
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2386
2960
|
if (element) {
|
|
2387
2961
|
await this.scrollIfNeeded(element, state.info);
|
|
2388
2962
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2389
|
-
// await _screenshot(state, this, element);
|
|
2390
2963
|
}
|
|
2391
2964
|
}
|
|
2392
2965
|
await _screenshot(state, this);
|
|
@@ -2396,13 +2969,12 @@ class StableBrowser {
|
|
|
2396
2969
|
console.error(error);
|
|
2397
2970
|
}
|
|
2398
2971
|
}
|
|
2399
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2400
2972
|
}
|
|
2401
2973
|
catch (e) {
|
|
2402
2974
|
await _commandError(state, e, this);
|
|
2403
2975
|
}
|
|
2404
2976
|
finally {
|
|
2405
|
-
_commandFinally(state, this);
|
|
2977
|
+
await _commandFinally(state, this);
|
|
2406
2978
|
}
|
|
2407
2979
|
}
|
|
2408
2980
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2415,7 +2987,7 @@ class StableBrowser {
|
|
|
2415
2987
|
scroll: false,
|
|
2416
2988
|
highlight: false,
|
|
2417
2989
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2418
|
-
text: `Verify text does not exist in page`,
|
|
2990
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2419
2991
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2420
2992
|
operation: "verifyTextNotExistInPage",
|
|
2421
2993
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2459,7 +3031,7 @@ class StableBrowser {
|
|
|
2459
3031
|
await _commandError(state, e, this);
|
|
2460
3032
|
}
|
|
2461
3033
|
finally {
|
|
2462
|
-
_commandFinally(state, this);
|
|
3034
|
+
await _commandFinally(state, this);
|
|
2463
3035
|
}
|
|
2464
3036
|
}
|
|
2465
3037
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2529,7 +3101,7 @@ class StableBrowser {
|
|
|
2529
3101
|
const count = await frame.locator(css).count();
|
|
2530
3102
|
for (let j = 0; j < count; j++) {
|
|
2531
3103
|
const continer = await frame.locator(css).nth(j);
|
|
2532
|
-
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false,
|
|
3104
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2533
3105
|
if (result.elementCount > 0) {
|
|
2534
3106
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2535
3107
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2570,7 +3142,7 @@ class StableBrowser {
|
|
|
2570
3142
|
await _commandError(state, e, this);
|
|
2571
3143
|
}
|
|
2572
3144
|
finally {
|
|
2573
|
-
_commandFinally(state, this);
|
|
3145
|
+
await _commandFinally(state, this);
|
|
2574
3146
|
}
|
|
2575
3147
|
}
|
|
2576
3148
|
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
@@ -2912,8 +3484,51 @@ class StableBrowser {
|
|
|
2912
3484
|
});
|
|
2913
3485
|
}
|
|
2914
3486
|
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3489
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3490
|
+
* @param options - Optional configuration object
|
|
3491
|
+
* @param world - Optional world context
|
|
3492
|
+
* @returns Promise that resolves after the specified duration
|
|
3493
|
+
*/
|
|
3494
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3495
|
+
const state = {
|
|
3496
|
+
duration,
|
|
3497
|
+
options,
|
|
3498
|
+
world,
|
|
3499
|
+
locate: false,
|
|
3500
|
+
scroll: false,
|
|
3501
|
+
screenshot: false,
|
|
3502
|
+
highlight: false,
|
|
3503
|
+
type: Types.SLEEP,
|
|
3504
|
+
text: `Sleep for ${duration} ms`,
|
|
3505
|
+
_text: `Sleep for ${duration} ms`,
|
|
3506
|
+
operation: "sleep",
|
|
3507
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3508
|
+
};
|
|
3509
|
+
try {
|
|
3510
|
+
await _preCommand(state, this);
|
|
3511
|
+
if (duration < 0) {
|
|
3512
|
+
throw new Error("Sleep duration cannot be negative");
|
|
3513
|
+
}
|
|
3514
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3515
|
+
return state.info;
|
|
3516
|
+
}
|
|
3517
|
+
catch (e) {
|
|
3518
|
+
await _commandError(state, e, this);
|
|
3519
|
+
}
|
|
3520
|
+
finally {
|
|
3521
|
+
await _commandFinally(state, this);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
2915
3524
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2916
|
-
|
|
3525
|
+
try {
|
|
3526
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3527
|
+
}
|
|
3528
|
+
catch (error) {
|
|
3529
|
+
this.logger.debug(error);
|
|
3530
|
+
throw error;
|
|
3531
|
+
}
|
|
2917
3532
|
}
|
|
2918
3533
|
_getLoadTimeout(options) {
|
|
2919
3534
|
let timeout = 15000;
|
|
@@ -2936,6 +3551,7 @@ class StableBrowser {
|
|
|
2936
3551
|
}
|
|
2937
3552
|
async saveStoreState(path = null, world = null) {
|
|
2938
3553
|
const storageState = await this.page.context().storageState();
|
|
3554
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2939
3555
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2940
3556
|
if (path) {
|
|
2941
3557
|
// save { storageState: storageState } into the path
|
|
@@ -2946,10 +3562,14 @@ class StableBrowser {
|
|
|
2946
3562
|
}
|
|
2947
3563
|
}
|
|
2948
3564
|
async restoreSaveState(path = null, world = null) {
|
|
3565
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2949
3566
|
await refreshBrowser(this, path, world);
|
|
2950
3567
|
this.registerEventListeners(this.context);
|
|
2951
3568
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2952
3569
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3570
|
+
if (this.onRestoreSaveState) {
|
|
3571
|
+
this.onRestoreSaveState(path);
|
|
3572
|
+
}
|
|
2953
3573
|
}
|
|
2954
3574
|
async waitForPageLoad(options = {}, world = null) {
|
|
2955
3575
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -3032,7 +3652,7 @@ class StableBrowser {
|
|
|
3032
3652
|
await _commandError(state, e, this);
|
|
3033
3653
|
}
|
|
3034
3654
|
finally {
|
|
3035
|
-
_commandFinally(state, this);
|
|
3655
|
+
await _commandFinally(state, this);
|
|
3036
3656
|
}
|
|
3037
3657
|
}
|
|
3038
3658
|
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
@@ -3119,7 +3739,7 @@ class StableBrowser {
|
|
|
3119
3739
|
await _commandError(state, e, this);
|
|
3120
3740
|
}
|
|
3121
3741
|
finally {
|
|
3122
|
-
_commandFinally(state, this);
|
|
3742
|
+
await _commandFinally(state, this);
|
|
3123
3743
|
}
|
|
3124
3744
|
}
|
|
3125
3745
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3224,7 +3844,39 @@ class StableBrowser {
|
|
|
3224
3844
|
console.log("#-#");
|
|
3225
3845
|
}
|
|
3226
3846
|
}
|
|
3847
|
+
async beforeScenario(world, scenario) {
|
|
3848
|
+
this.beforeScenarioCalled = true;
|
|
3849
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3850
|
+
this.scenarioName = scenario.pickle.name;
|
|
3851
|
+
}
|
|
3852
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3853
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3854
|
+
}
|
|
3855
|
+
if (this.context) {
|
|
3856
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3857
|
+
}
|
|
3858
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3859
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3860
|
+
// check if @global_test_data tag is present
|
|
3861
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3862
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
// update test data based on feature/scenario
|
|
3866
|
+
let envName = null;
|
|
3867
|
+
if (this.context && this.context.environment) {
|
|
3868
|
+
envName = this.context.environment.name;
|
|
3869
|
+
}
|
|
3870
|
+
if (!process.env.TEMP_RUN) {
|
|
3871
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
3872
|
+
}
|
|
3873
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3874
|
+
}
|
|
3875
|
+
async afterScenario(world, scenario) { }
|
|
3227
3876
|
async beforeStep(world, step) {
|
|
3877
|
+
if (!this.beforeScenarioCalled) {
|
|
3878
|
+
this.beforeScenario(world, step);
|
|
3879
|
+
}
|
|
3228
3880
|
if (this.stepIndex === undefined) {
|
|
3229
3881
|
this.stepIndex = 0;
|
|
3230
3882
|
}
|
|
@@ -3241,24 +3893,14 @@ class StableBrowser {
|
|
|
3241
3893
|
else {
|
|
3242
3894
|
this.stepName = "step " + this.stepIndex;
|
|
3243
3895
|
}
|
|
3244
|
-
if (this.context) {
|
|
3245
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3246
|
-
}
|
|
3247
3896
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3248
3897
|
if (this.context.browserObject.context) {
|
|
3249
3898
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3250
3899
|
}
|
|
3251
3900
|
}
|
|
3252
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3253
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3254
|
-
// check if @global_test_data tag is present
|
|
3255
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3256
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
3259
3901
|
if (this.initSnapshotTaken === false) {
|
|
3260
3902
|
this.initSnapshotTaken = true;
|
|
3261
|
-
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3903
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3262
3904
|
const snapshot = await this.getAriaSnapshot();
|
|
3263
3905
|
if (snapshot) {
|
|
3264
3906
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -3280,18 +3922,68 @@ class StableBrowser {
|
|
|
3280
3922
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3281
3923
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3282
3924
|
for (let i = 0; i < frames.length; i++) {
|
|
3283
|
-
content.push(`- frame: ${i}`);
|
|
3284
3925
|
const frame = frames[i];
|
|
3285
|
-
|
|
3286
|
-
|
|
3926
|
+
try {
|
|
3927
|
+
// Ensure frame is attached and has body
|
|
3928
|
+
const body = frame.locator("body");
|
|
3929
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3930
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3931
|
+
content.push(`- frame: ${i}`);
|
|
3932
|
+
content.push(snapshot);
|
|
3933
|
+
}
|
|
3934
|
+
catch (innerErr) { }
|
|
3287
3935
|
}
|
|
3288
3936
|
return content.join("\n");
|
|
3289
3937
|
}
|
|
3290
3938
|
catch (e) {
|
|
3291
|
-
console.
|
|
3939
|
+
console.log("Error in getAriaSnapshot");
|
|
3940
|
+
//console.debug(e);
|
|
3292
3941
|
}
|
|
3293
3942
|
return null;
|
|
3294
3943
|
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Sends command with custom payload to report.
|
|
3946
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3947
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3948
|
+
* @param content - Content of the command to be shown in the report.
|
|
3949
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3950
|
+
* @param world - Optional world context.
|
|
3951
|
+
* @public
|
|
3952
|
+
*/
|
|
3953
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3954
|
+
const state = {
|
|
3955
|
+
options,
|
|
3956
|
+
world,
|
|
3957
|
+
locate: false,
|
|
3958
|
+
scroll: false,
|
|
3959
|
+
screenshot: options.screenshot ?? false,
|
|
3960
|
+
highlight: options.highlight ?? false,
|
|
3961
|
+
type: Types.REPORT_COMMAND,
|
|
3962
|
+
text: commandText,
|
|
3963
|
+
_text: commandText,
|
|
3964
|
+
operation: "report_command",
|
|
3965
|
+
log: "***** " + commandText + " *****\n",
|
|
3966
|
+
};
|
|
3967
|
+
try {
|
|
3968
|
+
await _preCommand(state, this);
|
|
3969
|
+
const payload = {
|
|
3970
|
+
type: options.type ?? "text",
|
|
3971
|
+
content: content,
|
|
3972
|
+
screenshotId: null,
|
|
3973
|
+
};
|
|
3974
|
+
state.payload = payload;
|
|
3975
|
+
if (commandStatus === "FAILED") {
|
|
3976
|
+
state.throwError = true;
|
|
3977
|
+
throw new Error("Command failed");
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
catch (e) {
|
|
3981
|
+
await _commandError(state, e, this);
|
|
3982
|
+
}
|
|
3983
|
+
finally {
|
|
3984
|
+
await _commandFinally(state, this);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3295
3987
|
async afterStep(world, step) {
|
|
3296
3988
|
this.stepName = null;
|
|
3297
3989
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3299,6 +3991,13 @@ class StableBrowser {
|
|
|
3299
3991
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3300
3992
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3301
3993
|
});
|
|
3994
|
+
if (world && world.attach) {
|
|
3995
|
+
await world.attach(JSON.stringify({
|
|
3996
|
+
type: "trace",
|
|
3997
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3998
|
+
}), "application/json+trace");
|
|
3999
|
+
}
|
|
4000
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3302
4001
|
}
|
|
3303
4002
|
}
|
|
3304
4003
|
if (this.context) {
|
|
@@ -3311,6 +4010,29 @@ class StableBrowser {
|
|
|
3311
4010
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3312
4011
|
}
|
|
3313
4012
|
}
|
|
4013
|
+
if (!process.env.TEMP_RUN) {
|
|
4014
|
+
const state = {
|
|
4015
|
+
world,
|
|
4016
|
+
locate: false,
|
|
4017
|
+
scroll: false,
|
|
4018
|
+
screenshot: true,
|
|
4019
|
+
highlight: true,
|
|
4020
|
+
type: Types.STEP_COMPLETE,
|
|
4021
|
+
text: "end of scenario",
|
|
4022
|
+
_text: "end of scenario",
|
|
4023
|
+
operation: "step_complete",
|
|
4024
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4025
|
+
};
|
|
4026
|
+
try {
|
|
4027
|
+
await _preCommand(state, this);
|
|
4028
|
+
}
|
|
4029
|
+
catch (e) {
|
|
4030
|
+
await _commandError(state, e, this);
|
|
4031
|
+
}
|
|
4032
|
+
finally {
|
|
4033
|
+
await _commandFinally(state, this);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
3314
4036
|
}
|
|
3315
4037
|
}
|
|
3316
4038
|
function createTimedPromise(promise, label) {
|