automation_model 1.0.653-dev → 1.0.653-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 +3 -1
- 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.d.ts +1 -0
- package/lib/locator.js +10 -3
- 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 +58 -24
- package/lib/stable_browser.js +916 -185
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +9 -7
- package/lib/table.js +82 -12
- 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 +132 -68
- package/lib/utils.js.map +1 -1
- package/package.json +12 -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
|
}
|
|
@@ -1048,7 +1121,7 @@ class StableBrowser {
|
|
|
1048
1121
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1049
1122
|
// console.log(`Highlighting while running from recorder`);
|
|
1050
1123
|
await this._highlightElements(state.element);
|
|
1051
|
-
await state.element.setChecked(checked);
|
|
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) {
|
|
@@ -1909,7 +2113,7 @@ class StableBrowser {
|
|
|
1909
2113
|
await _commandError(state, e, this);
|
|
1910
2114
|
}
|
|
1911
2115
|
finally {
|
|
1912
|
-
_commandFinally(state, this);
|
|
2116
|
+
await _commandFinally(state, this);
|
|
1913
2117
|
}
|
|
1914
2118
|
}
|
|
1915
2119
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1940,10 +2144,102 @@ class StableBrowser {
|
|
|
1940
2144
|
case "value":
|
|
1941
2145
|
state.value = await state.element.inputValue();
|
|
1942
2146
|
break;
|
|
2147
|
+
case "text":
|
|
2148
|
+
state.value = await state.element.textContent();
|
|
2149
|
+
break;
|
|
1943
2150
|
default:
|
|
1944
2151
|
state.value = await state.element.getAttribute(attribute);
|
|
1945
2152
|
break;
|
|
1946
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
|
+
}
|
|
1947
2243
|
state.info.value = state.value;
|
|
1948
2244
|
this.setTestData({ [variable]: state.value }, world);
|
|
1949
2245
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1954,7 +2250,7 @@ class StableBrowser {
|
|
|
1954
2250
|
await _commandError(state, e, this);
|
|
1955
2251
|
}
|
|
1956
2252
|
finally {
|
|
1957
|
-
_commandFinally(state, this);
|
|
2253
|
+
await _commandFinally(state, this);
|
|
1958
2254
|
}
|
|
1959
2255
|
}
|
|
1960
2256
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1979,12 +2275,15 @@ class StableBrowser {
|
|
|
1979
2275
|
let expectedValue;
|
|
1980
2276
|
try {
|
|
1981
2277
|
await _preCommand(state, this);
|
|
1982
|
-
expectedValue = state.value;
|
|
2278
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1983
2279
|
state.info.expectedValue = expectedValue;
|
|
1984
2280
|
switch (attribute) {
|
|
1985
2281
|
case "innerText":
|
|
1986
2282
|
val = String(await state.element.innerText());
|
|
1987
2283
|
break;
|
|
2284
|
+
case "text":
|
|
2285
|
+
val = String(await state.element.textContent());
|
|
2286
|
+
break;
|
|
1988
2287
|
case "value":
|
|
1989
2288
|
val = String(await state.element.inputValue());
|
|
1990
2289
|
break;
|
|
@@ -2006,17 +2305,42 @@ class StableBrowser {
|
|
|
2006
2305
|
let regex;
|
|
2007
2306
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2008
2307
|
const patternBody = expectedValue.slice(1, -1);
|
|
2009
|
-
|
|
2308
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2309
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2310
|
+
state.info.regex = true;
|
|
2010
2311
|
}
|
|
2011
2312
|
else {
|
|
2012
2313
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2013
2314
|
regex = new RegExp(escapedPattern, "g");
|
|
2014
2315
|
}
|
|
2015
|
-
if (
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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
|
+
}
|
|
2020
2344
|
}
|
|
2021
2345
|
return state.info;
|
|
2022
2346
|
}
|
|
@@ -2024,7 +2348,128 @@ class StableBrowser {
|
|
|
2024
2348
|
await _commandError(state, e, this);
|
|
2025
2349
|
}
|
|
2026
2350
|
finally {
|
|
2027
|
-
_commandFinally(state, this);
|
|
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
|
+
case "innerHTML":
|
|
2399
|
+
val = String(await state.element.innerHTML());
|
|
2400
|
+
break;
|
|
2401
|
+
case "outerHTML":
|
|
2402
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2403
|
+
break;
|
|
2404
|
+
default:
|
|
2405
|
+
if (property.startsWith("dataset.")) {
|
|
2406
|
+
const dataAttribute = property.substring(8);
|
|
2407
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2408
|
+
}
|
|
2409
|
+
else {
|
|
2410
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
// Helper function to remove all style="" attributes
|
|
2414
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2415
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, '');
|
|
2416
|
+
};
|
|
2417
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2418
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2419
|
+
val = removeStyleAttributes(val);
|
|
2420
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2421
|
+
}
|
|
2422
|
+
state.info.value = val;
|
|
2423
|
+
let regex;
|
|
2424
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2425
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2426
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2427
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2428
|
+
state.info.regex = true;
|
|
2429
|
+
}
|
|
2430
|
+
else {
|
|
2431
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2432
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2433
|
+
}
|
|
2434
|
+
if (property === "innerText") {
|
|
2435
|
+
if (state.info.regex) {
|
|
2436
|
+
if (!regex.test(val)) {
|
|
2437
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2438
|
+
state.info.failCause.assertionFailed = true;
|
|
2439
|
+
state.info.failCause.lastError = errorMessage;
|
|
2440
|
+
throw new Error(errorMessage);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
else {
|
|
2444
|
+
// Fix: Replace escaped newlines with actual newlines before splitting
|
|
2445
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, '\n');
|
|
2446
|
+
const valLines = val.split("\n");
|
|
2447
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2448
|
+
// Check if all expected lines are present in the actual lines
|
|
2449
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2450
|
+
if (!isPart) {
|
|
2451
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2452
|
+
state.info.failCause.assertionFailed = true;
|
|
2453
|
+
state.info.failCause.lastError = errorMessage;
|
|
2454
|
+
throw new Error(errorMessage);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
else {
|
|
2459
|
+
if (!val.match(regex)) {
|
|
2460
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2461
|
+
state.info.failCause.assertionFailed = true;
|
|
2462
|
+
state.info.failCause.lastError = errorMessage;
|
|
2463
|
+
throw new Error(errorMessage);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return state.info;
|
|
2467
|
+
}
|
|
2468
|
+
catch (e) {
|
|
2469
|
+
await _commandError(state, e, this);
|
|
2470
|
+
}
|
|
2471
|
+
finally {
|
|
2472
|
+
await _commandFinally(state, this);
|
|
2028
2473
|
}
|
|
2029
2474
|
}
|
|
2030
2475
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2184,56 +2629,49 @@ class StableBrowser {
|
|
|
2184
2629
|
console.debug(error);
|
|
2185
2630
|
}
|
|
2186
2631
|
}
|
|
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
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
// });
|
|
2230
|
-
// }
|
|
2231
|
-
// } catch (error) {
|
|
2232
|
-
// // console.debug(error);
|
|
2233
|
-
// }
|
|
2234
|
-
// }
|
|
2632
|
+
_matcher(text) {
|
|
2633
|
+
if (!text) {
|
|
2634
|
+
return { matcher: "contains", queryText: "" };
|
|
2635
|
+
}
|
|
2636
|
+
if (text.length < 2) {
|
|
2637
|
+
return { matcher: "contains", queryText: text };
|
|
2638
|
+
}
|
|
2639
|
+
const split = text.split(":");
|
|
2640
|
+
const matcher = split[0].toLowerCase();
|
|
2641
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2642
|
+
return { matcher, queryText };
|
|
2643
|
+
}
|
|
2644
|
+
_getDomain(url) {
|
|
2645
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2646
|
+
return "";
|
|
2647
|
+
}
|
|
2648
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2649
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2650
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2651
|
+
}
|
|
2652
|
+
let n = hostnameFragments.length;
|
|
2653
|
+
let fragments = [...hostnameFragments];
|
|
2654
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2655
|
+
hostnameFragments.pop();
|
|
2656
|
+
n = hostnameFragments.length;
|
|
2657
|
+
}
|
|
2658
|
+
if (n == 0) {
|
|
2659
|
+
if (fragments[0] === "www")
|
|
2660
|
+
fragments = fragments.slice(1);
|
|
2661
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2662
|
+
}
|
|
2663
|
+
if (hostnameFragments[0] === "www")
|
|
2664
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2665
|
+
return hostnameFragments.join(".");
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Verify the page path matches the given path.
|
|
2669
|
+
* @param {string} pathPart - The path to verify.
|
|
2670
|
+
* @param {object} options - Options for verification.
|
|
2671
|
+
* @param {object} world - The world context.
|
|
2672
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2673
|
+
*/
|
|
2235
2674
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2236
|
-
const startTime = Date.now();
|
|
2237
2675
|
let error = null;
|
|
2238
2676
|
let screenshotId = null;
|
|
2239
2677
|
let screenshotPath = null;
|
|
@@ -2247,51 +2685,212 @@ class StableBrowser {
|
|
|
2247
2685
|
pathPart = newValue;
|
|
2248
2686
|
}
|
|
2249
2687
|
info.pathPart = pathPart;
|
|
2688
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2689
|
+
const state = {
|
|
2690
|
+
text_search: queryText,
|
|
2691
|
+
options,
|
|
2692
|
+
world,
|
|
2693
|
+
locate: false,
|
|
2694
|
+
scroll: false,
|
|
2695
|
+
highlight: false,
|
|
2696
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2697
|
+
text: `Verify the page url is ${queryText}`,
|
|
2698
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2699
|
+
operation: "verifyPagePath",
|
|
2700
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2701
|
+
};
|
|
2250
2702
|
try {
|
|
2703
|
+
await _preCommand(state, this);
|
|
2704
|
+
state.info.text = queryText;
|
|
2251
2705
|
for (let i = 0; i < 30; i++) {
|
|
2252
2706
|
const url = await this.page.url();
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2707
|
+
switch (matcher) {
|
|
2708
|
+
case "exact":
|
|
2709
|
+
if (url !== queryText) {
|
|
2710
|
+
if (i === 29) {
|
|
2711
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2712
|
+
}
|
|
2713
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
break;
|
|
2717
|
+
case "contains":
|
|
2718
|
+
if (!url.includes(queryText)) {
|
|
2719
|
+
if (i === 29) {
|
|
2720
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2721
|
+
}
|
|
2722
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
break;
|
|
2726
|
+
case "starts-with":
|
|
2727
|
+
{
|
|
2728
|
+
const domain = this._getDomain(url);
|
|
2729
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2730
|
+
if (i === 29) {
|
|
2731
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2732
|
+
}
|
|
2733
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
break;
|
|
2738
|
+
case "ends-with":
|
|
2739
|
+
{
|
|
2740
|
+
const urlObj = new URL(url);
|
|
2741
|
+
let route = "/";
|
|
2742
|
+
if (urlObj.pathname !== "/") {
|
|
2743
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2744
|
+
}
|
|
2745
|
+
else {
|
|
2746
|
+
route = "/";
|
|
2747
|
+
}
|
|
2748
|
+
if (route !== queryText) {
|
|
2749
|
+
if (i === 29) {
|
|
2750
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2751
|
+
}
|
|
2752
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
break;
|
|
2757
|
+
case "regex":
|
|
2758
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2759
|
+
if (!regex.test(url)) {
|
|
2760
|
+
if (i === 29) {
|
|
2761
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2762
|
+
}
|
|
2763
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
break;
|
|
2767
|
+
default:
|
|
2768
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2769
|
+
if (!url.includes(pathPart)) {
|
|
2770
|
+
if (i === 29) {
|
|
2771
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2772
|
+
}
|
|
2773
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2259
2776
|
}
|
|
2260
|
-
|
|
2261
|
-
return info;
|
|
2777
|
+
await _screenshot(state, this);
|
|
2778
|
+
return state.info;
|
|
2262
2779
|
}
|
|
2263
2780
|
}
|
|
2264
2781
|
catch (e) {
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
info.screenshotPath = screenshotPath;
|
|
2269
|
-
Object.assign(e, { info: info });
|
|
2270
|
-
error = e;
|
|
2271
|
-
// throw e;
|
|
2272
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2782
|
+
state.info.failCause.lastError = e.message;
|
|
2783
|
+
state.info.failCause.assertionFailed = true;
|
|
2784
|
+
await _commandError(state, e, this);
|
|
2273
2785
|
}
|
|
2274
2786
|
finally {
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2787
|
+
await _commandFinally(state, this);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Verify the page title matches the given title.
|
|
2792
|
+
* @param {string} title - The title to verify.
|
|
2793
|
+
* @param {object} options - Options for verification.
|
|
2794
|
+
* @param {object} world - The world context.
|
|
2795
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2796
|
+
*/
|
|
2797
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2798
|
+
let error = null;
|
|
2799
|
+
let screenshotId = null;
|
|
2800
|
+
let screenshotPath = null;
|
|
2801
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2802
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2803
|
+
if (newValue !== title) {
|
|
2804
|
+
this.logger.info(title + "=" + newValue);
|
|
2805
|
+
title = newValue;
|
|
2806
|
+
}
|
|
2807
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2808
|
+
const state = {
|
|
2809
|
+
text_search: queryText,
|
|
2810
|
+
options,
|
|
2811
|
+
world,
|
|
2812
|
+
locate: false,
|
|
2813
|
+
scroll: false,
|
|
2814
|
+
highlight: false,
|
|
2815
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2816
|
+
text: `Verify the page title is ${queryText}`,
|
|
2817
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2818
|
+
operation: "verifyPageTitle",
|
|
2819
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2820
|
+
};
|
|
2821
|
+
try {
|
|
2822
|
+
await _preCommand(state, this);
|
|
2823
|
+
state.info.text = queryText;
|
|
2824
|
+
for (let i = 0; i < 30; i++) {
|
|
2825
|
+
const foundTitle = await this.page.title();
|
|
2826
|
+
switch (matcher) {
|
|
2827
|
+
case "exact":
|
|
2828
|
+
if (foundTitle !== queryText) {
|
|
2829
|
+
if (i === 29) {
|
|
2830
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2831
|
+
}
|
|
2832
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
break;
|
|
2836
|
+
case "contains":
|
|
2837
|
+
if (!foundTitle.includes(queryText)) {
|
|
2838
|
+
if (i === 29) {
|
|
2839
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2840
|
+
}
|
|
2841
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
break;
|
|
2845
|
+
case "starts-with":
|
|
2846
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2847
|
+
if (i === 29) {
|
|
2848
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2849
|
+
}
|
|
2850
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
break;
|
|
2854
|
+
case "ends-with":
|
|
2855
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2856
|
+
if (i === 29) {
|
|
2857
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2858
|
+
}
|
|
2859
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
break;
|
|
2863
|
+
case "regex":
|
|
2864
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2865
|
+
if (!regex.test(foundTitle)) {
|
|
2866
|
+
if (i === 29) {
|
|
2867
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2868
|
+
}
|
|
2869
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
break;
|
|
2873
|
+
default:
|
|
2874
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2875
|
+
if (!foundTitle.includes(title)) {
|
|
2876
|
+
if (i === 29) {
|
|
2877
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2878
|
+
}
|
|
2879
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
await _screenshot(state, this);
|
|
2884
|
+
return state.info;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
catch (e) {
|
|
2888
|
+
state.info.failCause.lastError = e.message;
|
|
2889
|
+
state.info.failCause.assertionFailed = true;
|
|
2890
|
+
await _commandError(state, e, this);
|
|
2891
|
+
}
|
|
2892
|
+
finally {
|
|
2893
|
+
await _commandFinally(state, this);
|
|
2295
2894
|
}
|
|
2296
2895
|
}
|
|
2297
2896
|
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
@@ -2333,7 +2932,7 @@ class StableBrowser {
|
|
|
2333
2932
|
scroll: false,
|
|
2334
2933
|
highlight: false,
|
|
2335
2934
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2336
|
-
text: `Verify the text '${text}' exists in page`,
|
|
2935
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2337
2936
|
_text: `Verify the text '${text}' exists in page`,
|
|
2338
2937
|
operation: "verifyTextExistInPage",
|
|
2339
2938
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2375,27 +2974,10 @@ class StableBrowser {
|
|
|
2375
2974
|
const frame = resultWithElementsFound[0].frame;
|
|
2376
2975
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2377
2976
|
await this._highlightElements(frame, dataAttribute);
|
|
2378
|
-
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2379
|
-
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2380
|
-
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2381
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2382
|
-
// this._unhighlightElements(frame, dataAttribute)
|
|
2383
|
-
// .then(async () => {
|
|
2384
|
-
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2385
|
-
// })
|
|
2386
|
-
// .catch(
|
|
2387
|
-
// (e) => {}
|
|
2388
|
-
// console.error(e)
|
|
2389
|
-
// );
|
|
2390
|
-
// });
|
|
2391
|
-
// }
|
|
2392
2977
|
const element = await frame.locator(dataAttribute).first();
|
|
2393
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2394
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2395
2978
|
if (element) {
|
|
2396
2979
|
await this.scrollIfNeeded(element, state.info);
|
|
2397
2980
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2398
|
-
// await _screenshot(state, this, element);
|
|
2399
2981
|
}
|
|
2400
2982
|
}
|
|
2401
2983
|
await _screenshot(state, this);
|
|
@@ -2405,13 +2987,12 @@ class StableBrowser {
|
|
|
2405
2987
|
console.error(error);
|
|
2406
2988
|
}
|
|
2407
2989
|
}
|
|
2408
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2409
2990
|
}
|
|
2410
2991
|
catch (e) {
|
|
2411
2992
|
await _commandError(state, e, this);
|
|
2412
2993
|
}
|
|
2413
2994
|
finally {
|
|
2414
|
-
_commandFinally(state, this);
|
|
2995
|
+
await _commandFinally(state, this);
|
|
2415
2996
|
}
|
|
2416
2997
|
}
|
|
2417
2998
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2424,7 +3005,7 @@ class StableBrowser {
|
|
|
2424
3005
|
scroll: false,
|
|
2425
3006
|
highlight: false,
|
|
2426
3007
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2427
|
-
text: `Verify the text '${text}' does not exist in page`,
|
|
3008
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2428
3009
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2429
3010
|
operation: "verifyTextNotExistInPage",
|
|
2430
3011
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2468,7 +3049,7 @@ class StableBrowser {
|
|
|
2468
3049
|
await _commandError(state, e, this);
|
|
2469
3050
|
}
|
|
2470
3051
|
finally {
|
|
2471
|
-
_commandFinally(state, this);
|
|
3052
|
+
await _commandFinally(state, this);
|
|
2472
3053
|
}
|
|
2473
3054
|
}
|
|
2474
3055
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2538,7 +3119,7 @@ class StableBrowser {
|
|
|
2538
3119
|
const count = await frame.locator(css).count();
|
|
2539
3120
|
for (let j = 0; j < count; j++) {
|
|
2540
3121
|
const continer = await frame.locator(css).nth(j);
|
|
2541
|
-
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false,
|
|
3122
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2542
3123
|
if (result.elementCount > 0) {
|
|
2543
3124
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2544
3125
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2579,7 +3160,7 @@ class StableBrowser {
|
|
|
2579
3160
|
await _commandError(state, e, this);
|
|
2580
3161
|
}
|
|
2581
3162
|
finally {
|
|
2582
|
-
_commandFinally(state, this);
|
|
3163
|
+
await _commandFinally(state, this);
|
|
2583
3164
|
}
|
|
2584
3165
|
}
|
|
2585
3166
|
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
@@ -2921,8 +3502,51 @@ class StableBrowser {
|
|
|
2921
3502
|
});
|
|
2922
3503
|
}
|
|
2923
3504
|
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3507
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3508
|
+
* @param options - Optional configuration object
|
|
3509
|
+
* @param world - Optional world context
|
|
3510
|
+
* @returns Promise that resolves after the specified duration
|
|
3511
|
+
*/
|
|
3512
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3513
|
+
const state = {
|
|
3514
|
+
duration,
|
|
3515
|
+
options,
|
|
3516
|
+
world,
|
|
3517
|
+
locate: false,
|
|
3518
|
+
scroll: false,
|
|
3519
|
+
screenshot: false,
|
|
3520
|
+
highlight: false,
|
|
3521
|
+
type: Types.SLEEP,
|
|
3522
|
+
text: `Sleep for ${duration} ms`,
|
|
3523
|
+
_text: `Sleep for ${duration} ms`,
|
|
3524
|
+
operation: "sleep",
|
|
3525
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3526
|
+
};
|
|
3527
|
+
try {
|
|
3528
|
+
await _preCommand(state, this);
|
|
3529
|
+
if (duration < 0) {
|
|
3530
|
+
throw new Error("Sleep duration cannot be negative");
|
|
3531
|
+
}
|
|
3532
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3533
|
+
return state.info;
|
|
3534
|
+
}
|
|
3535
|
+
catch (e) {
|
|
3536
|
+
await _commandError(state, e, this);
|
|
3537
|
+
}
|
|
3538
|
+
finally {
|
|
3539
|
+
await _commandFinally(state, this);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
2924
3542
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2925
|
-
|
|
3543
|
+
try {
|
|
3544
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3545
|
+
}
|
|
3546
|
+
catch (error) {
|
|
3547
|
+
this.logger.debug(error);
|
|
3548
|
+
throw error;
|
|
3549
|
+
}
|
|
2926
3550
|
}
|
|
2927
3551
|
_getLoadTimeout(options) {
|
|
2928
3552
|
let timeout = 15000;
|
|
@@ -2945,6 +3569,7 @@ class StableBrowser {
|
|
|
2945
3569
|
}
|
|
2946
3570
|
async saveStoreState(path = null, world = null) {
|
|
2947
3571
|
const storageState = await this.page.context().storageState();
|
|
3572
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2948
3573
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2949
3574
|
if (path) {
|
|
2950
3575
|
// save { storageState: storageState } into the path
|
|
@@ -2955,10 +3580,14 @@ class StableBrowser {
|
|
|
2955
3580
|
}
|
|
2956
3581
|
}
|
|
2957
3582
|
async restoreSaveState(path = null, world = null) {
|
|
3583
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2958
3584
|
await refreshBrowser(this, path, world);
|
|
2959
3585
|
this.registerEventListeners(this.context);
|
|
2960
3586
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2961
3587
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3588
|
+
if (this.onRestoreSaveState) {
|
|
3589
|
+
this.onRestoreSaveState(path);
|
|
3590
|
+
}
|
|
2962
3591
|
}
|
|
2963
3592
|
async waitForPageLoad(options = {}, world = null) {
|
|
2964
3593
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -3041,7 +3670,7 @@ class StableBrowser {
|
|
|
3041
3670
|
await _commandError(state, e, this);
|
|
3042
3671
|
}
|
|
3043
3672
|
finally {
|
|
3044
|
-
_commandFinally(state, this);
|
|
3673
|
+
await _commandFinally(state, this);
|
|
3045
3674
|
}
|
|
3046
3675
|
}
|
|
3047
3676
|
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
@@ -3128,7 +3757,7 @@ class StableBrowser {
|
|
|
3128
3757
|
await _commandError(state, e, this);
|
|
3129
3758
|
}
|
|
3130
3759
|
finally {
|
|
3131
|
-
_commandFinally(state, this);
|
|
3760
|
+
await _commandFinally(state, this);
|
|
3132
3761
|
}
|
|
3133
3762
|
}
|
|
3134
3763
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3233,7 +3862,39 @@ class StableBrowser {
|
|
|
3233
3862
|
console.log("#-#");
|
|
3234
3863
|
}
|
|
3235
3864
|
}
|
|
3865
|
+
async beforeScenario(world, scenario) {
|
|
3866
|
+
this.beforeScenarioCalled = true;
|
|
3867
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3868
|
+
this.scenarioName = scenario.pickle.name;
|
|
3869
|
+
}
|
|
3870
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3871
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3872
|
+
}
|
|
3873
|
+
if (this.context) {
|
|
3874
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3875
|
+
}
|
|
3876
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3877
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3878
|
+
// check if @global_test_data tag is present
|
|
3879
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3880
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
// update test data based on feature/scenario
|
|
3884
|
+
let envName = null;
|
|
3885
|
+
if (this.context && this.context.environment) {
|
|
3886
|
+
envName = this.context.environment.name;
|
|
3887
|
+
}
|
|
3888
|
+
if (!process.env.TEMP_RUN) {
|
|
3889
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
3890
|
+
}
|
|
3891
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3892
|
+
}
|
|
3893
|
+
async afterScenario(world, scenario) { }
|
|
3236
3894
|
async beforeStep(world, step) {
|
|
3895
|
+
if (!this.beforeScenarioCalled) {
|
|
3896
|
+
this.beforeScenario(world, step);
|
|
3897
|
+
}
|
|
3237
3898
|
if (this.stepIndex === undefined) {
|
|
3238
3899
|
this.stepIndex = 0;
|
|
3239
3900
|
}
|
|
@@ -3250,24 +3911,14 @@ class StableBrowser {
|
|
|
3250
3911
|
else {
|
|
3251
3912
|
this.stepName = "step " + this.stepIndex;
|
|
3252
3913
|
}
|
|
3253
|
-
if (this.context) {
|
|
3254
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3255
|
-
}
|
|
3256
3914
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3257
3915
|
if (this.context.browserObject.context) {
|
|
3258
3916
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3259
3917
|
}
|
|
3260
3918
|
}
|
|
3261
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3262
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3263
|
-
// check if @global_test_data tag is present
|
|
3264
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3265
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
3919
|
if (this.initSnapshotTaken === false) {
|
|
3269
3920
|
this.initSnapshotTaken = true;
|
|
3270
|
-
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3921
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3271
3922
|
const snapshot = await this.getAriaSnapshot();
|
|
3272
3923
|
if (snapshot) {
|
|
3273
3924
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -3289,18 +3940,68 @@ class StableBrowser {
|
|
|
3289
3940
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3290
3941
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3291
3942
|
for (let i = 0; i < frames.length; i++) {
|
|
3292
|
-
content.push(`- frame: ${i}`);
|
|
3293
3943
|
const frame = frames[i];
|
|
3294
|
-
|
|
3295
|
-
|
|
3944
|
+
try {
|
|
3945
|
+
// Ensure frame is attached and has body
|
|
3946
|
+
const body = frame.locator("body");
|
|
3947
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3948
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3949
|
+
content.push(`- frame: ${i}`);
|
|
3950
|
+
content.push(snapshot);
|
|
3951
|
+
}
|
|
3952
|
+
catch (innerErr) { }
|
|
3296
3953
|
}
|
|
3297
3954
|
return content.join("\n");
|
|
3298
3955
|
}
|
|
3299
3956
|
catch (e) {
|
|
3300
|
-
console.
|
|
3957
|
+
console.log("Error in getAriaSnapshot");
|
|
3958
|
+
//console.debug(e);
|
|
3301
3959
|
}
|
|
3302
3960
|
return null;
|
|
3303
3961
|
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Sends command with custom payload to report.
|
|
3964
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3965
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3966
|
+
* @param content - Content of the command to be shown in the report.
|
|
3967
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3968
|
+
* @param world - Optional world context.
|
|
3969
|
+
* @public
|
|
3970
|
+
*/
|
|
3971
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3972
|
+
const state = {
|
|
3973
|
+
options,
|
|
3974
|
+
world,
|
|
3975
|
+
locate: false,
|
|
3976
|
+
scroll: false,
|
|
3977
|
+
screenshot: options.screenshot ?? false,
|
|
3978
|
+
highlight: options.highlight ?? false,
|
|
3979
|
+
type: Types.REPORT_COMMAND,
|
|
3980
|
+
text: commandText,
|
|
3981
|
+
_text: commandText,
|
|
3982
|
+
operation: "report_command",
|
|
3983
|
+
log: "***** " + commandText + " *****\n",
|
|
3984
|
+
};
|
|
3985
|
+
try {
|
|
3986
|
+
await _preCommand(state, this);
|
|
3987
|
+
const payload = {
|
|
3988
|
+
type: options.type ?? "text",
|
|
3989
|
+
content: content,
|
|
3990
|
+
screenshotId: null,
|
|
3991
|
+
};
|
|
3992
|
+
state.payload = payload;
|
|
3993
|
+
if (commandStatus === "FAILED") {
|
|
3994
|
+
state.throwError = true;
|
|
3995
|
+
throw new Error("Command failed");
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
catch (e) {
|
|
3999
|
+
await _commandError(state, e, this);
|
|
4000
|
+
}
|
|
4001
|
+
finally {
|
|
4002
|
+
await _commandFinally(state, this);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
3304
4005
|
async afterStep(world, step) {
|
|
3305
4006
|
this.stepName = null;
|
|
3306
4007
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3308,6 +4009,13 @@ class StableBrowser {
|
|
|
3308
4009
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3309
4010
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3310
4011
|
});
|
|
4012
|
+
if (world && world.attach) {
|
|
4013
|
+
await world.attach(JSON.stringify({
|
|
4014
|
+
type: "trace",
|
|
4015
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4016
|
+
}), "application/json+trace");
|
|
4017
|
+
}
|
|
4018
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3311
4019
|
}
|
|
3312
4020
|
}
|
|
3313
4021
|
if (this.context) {
|
|
@@ -3320,6 +4028,29 @@ class StableBrowser {
|
|
|
3320
4028
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3321
4029
|
}
|
|
3322
4030
|
}
|
|
4031
|
+
if (!process.env.TEMP_RUN) {
|
|
4032
|
+
const state = {
|
|
4033
|
+
world,
|
|
4034
|
+
locate: false,
|
|
4035
|
+
scroll: false,
|
|
4036
|
+
screenshot: true,
|
|
4037
|
+
highlight: true,
|
|
4038
|
+
type: Types.STEP_COMPLETE,
|
|
4039
|
+
text: "end of scenario",
|
|
4040
|
+
_text: "end of scenario",
|
|
4041
|
+
operation: "step_complete",
|
|
4042
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4043
|
+
};
|
|
4044
|
+
try {
|
|
4045
|
+
await _preCommand(state, this);
|
|
4046
|
+
}
|
|
4047
|
+
catch (e) {
|
|
4048
|
+
await _commandError(state, e, this);
|
|
4049
|
+
}
|
|
4050
|
+
finally {
|
|
4051
|
+
await _commandFinally(state, this);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
3323
4054
|
}
|
|
3324
4055
|
}
|
|
3325
4056
|
function createTimedPromise(promise, label) {
|