automation_model 1.0.652-dev → 1.0.652-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.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 +59 -24
- package/lib/stable_browser.js +925 -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,9 +207,35 @@ 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
|
+
}
|
|
181
213
|
}
|
|
182
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
|
+
}
|
|
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);
|
|
238
|
+
}
|
|
183
239
|
registerConsoleLogListener(page, context) {
|
|
184
240
|
if (!this.context.webLogger) {
|
|
185
241
|
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) {
|
|
@@ -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,31 @@ 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
|
+
}
|
|
1938
2172
|
state.info.value = state.value;
|
|
1939
2173
|
this.setTestData({ [variable]: state.value }, world);
|
|
1940
2174
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1945,7 +2179,78 @@ class StableBrowser {
|
|
|
1945
2179
|
await _commandError(state, e, this);
|
|
1946
2180
|
}
|
|
1947
2181
|
finally {
|
|
1948
|
-
_commandFinally(state, this);
|
|
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
|
+
}
|
|
2243
|
+
state.info.value = state.value;
|
|
2244
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2245
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2246
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2247
|
+
return state.info;
|
|
2248
|
+
}
|
|
2249
|
+
catch (e) {
|
|
2250
|
+
await _commandError(state, e, this);
|
|
2251
|
+
}
|
|
2252
|
+
finally {
|
|
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,163 @@ 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
|
+
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;
|
|
2001
2429
|
}
|
|
2002
2430
|
else {
|
|
2003
2431
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2004
2432
|
regex = new RegExp(escapedPattern, "g");
|
|
2005
2433
|
}
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
+
}
|
|
2011
2465
|
}
|
|
2012
2466
|
return state.info;
|
|
2013
2467
|
}
|
|
@@ -2015,7 +2469,7 @@ class StableBrowser {
|
|
|
2015
2469
|
await _commandError(state, e, this);
|
|
2016
2470
|
}
|
|
2017
2471
|
finally {
|
|
2018
|
-
_commandFinally(state, this);
|
|
2472
|
+
await _commandFinally(state, this);
|
|
2019
2473
|
}
|
|
2020
2474
|
}
|
|
2021
2475
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2175,56 +2629,49 @@ class StableBrowser {
|
|
|
2175
2629
|
console.debug(error);
|
|
2176
2630
|
}
|
|
2177
2631
|
}
|
|
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
|
-
// }
|
|
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
|
+
*/
|
|
2226
2674
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2227
|
-
const startTime = Date.now();
|
|
2228
2675
|
let error = null;
|
|
2229
2676
|
let screenshotId = null;
|
|
2230
2677
|
let screenshotPath = null;
|
|
@@ -2238,51 +2685,212 @@ class StableBrowser {
|
|
|
2238
2685
|
pathPart = newValue;
|
|
2239
2686
|
}
|
|
2240
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
|
+
};
|
|
2241
2702
|
try {
|
|
2703
|
+
await _preCommand(state, this);
|
|
2704
|
+
state.info.text = queryText;
|
|
2242
2705
|
for (let i = 0; i < 30; i++) {
|
|
2243
2706
|
const url = await this.page.url();
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
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
|
+
}
|
|
2250
2776
|
}
|
|
2251
|
-
|
|
2252
|
-
return info;
|
|
2777
|
+
await _screenshot(state, this);
|
|
2778
|
+
return state.info;
|
|
2253
2779
|
}
|
|
2254
2780
|
}
|
|
2255
2781
|
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);
|
|
2782
|
+
state.info.failCause.lastError = e.message;
|
|
2783
|
+
state.info.failCause.assertionFailed = true;
|
|
2784
|
+
await _commandError(state, e, this);
|
|
2264
2785
|
}
|
|
2265
2786
|
finally {
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
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);
|
|
2286
2894
|
}
|
|
2287
2895
|
}
|
|
2288
2896
|
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
@@ -2324,7 +2932,7 @@ class StableBrowser {
|
|
|
2324
2932
|
scroll: false,
|
|
2325
2933
|
highlight: false,
|
|
2326
2934
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2327
|
-
text: `Verify the text '${text}' exists in page`,
|
|
2935
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2328
2936
|
_text: `Verify the text '${text}' exists in page`,
|
|
2329
2937
|
operation: "verifyTextExistInPage",
|
|
2330
2938
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2366,27 +2974,10 @@ class StableBrowser {
|
|
|
2366
2974
|
const frame = resultWithElementsFound[0].frame;
|
|
2367
2975
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2368
2976
|
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
2977
|
const element = await frame.locator(dataAttribute).first();
|
|
2384
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2385
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2386
2978
|
if (element) {
|
|
2387
2979
|
await this.scrollIfNeeded(element, state.info);
|
|
2388
2980
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2389
|
-
// await _screenshot(state, this, element);
|
|
2390
2981
|
}
|
|
2391
2982
|
}
|
|
2392
2983
|
await _screenshot(state, this);
|
|
@@ -2396,13 +2987,12 @@ class StableBrowser {
|
|
|
2396
2987
|
console.error(error);
|
|
2397
2988
|
}
|
|
2398
2989
|
}
|
|
2399
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2400
2990
|
}
|
|
2401
2991
|
catch (e) {
|
|
2402
2992
|
await _commandError(state, e, this);
|
|
2403
2993
|
}
|
|
2404
2994
|
finally {
|
|
2405
|
-
_commandFinally(state, this);
|
|
2995
|
+
await _commandFinally(state, this);
|
|
2406
2996
|
}
|
|
2407
2997
|
}
|
|
2408
2998
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2415,7 +3005,7 @@ class StableBrowser {
|
|
|
2415
3005
|
scroll: false,
|
|
2416
3006
|
highlight: false,
|
|
2417
3007
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2418
|
-
text: `Verify the text '${text}' does not exist in page`,
|
|
3008
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2419
3009
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2420
3010
|
operation: "verifyTextNotExistInPage",
|
|
2421
3011
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2459,7 +3049,7 @@ class StableBrowser {
|
|
|
2459
3049
|
await _commandError(state, e, this);
|
|
2460
3050
|
}
|
|
2461
3051
|
finally {
|
|
2462
|
-
_commandFinally(state, this);
|
|
3052
|
+
await _commandFinally(state, this);
|
|
2463
3053
|
}
|
|
2464
3054
|
}
|
|
2465
3055
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2529,7 +3119,7 @@ class StableBrowser {
|
|
|
2529
3119
|
const count = await frame.locator(css).count();
|
|
2530
3120
|
for (let j = 0; j < count; j++) {
|
|
2531
3121
|
const continer = await frame.locator(css).nth(j);
|
|
2532
|
-
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, {});
|
|
2533
3123
|
if (result.elementCount > 0) {
|
|
2534
3124
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2535
3125
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2570,7 +3160,7 @@ class StableBrowser {
|
|
|
2570
3160
|
await _commandError(state, e, this);
|
|
2571
3161
|
}
|
|
2572
3162
|
finally {
|
|
2573
|
-
_commandFinally(state, this);
|
|
3163
|
+
await _commandFinally(state, this);
|
|
2574
3164
|
}
|
|
2575
3165
|
}
|
|
2576
3166
|
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
@@ -2912,8 +3502,51 @@ class StableBrowser {
|
|
|
2912
3502
|
});
|
|
2913
3503
|
}
|
|
2914
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
|
+
}
|
|
2915
3542
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2916
|
-
|
|
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
|
+
}
|
|
2917
3550
|
}
|
|
2918
3551
|
_getLoadTimeout(options) {
|
|
2919
3552
|
let timeout = 15000;
|
|
@@ -2936,6 +3569,7 @@ class StableBrowser {
|
|
|
2936
3569
|
}
|
|
2937
3570
|
async saveStoreState(path = null, world = null) {
|
|
2938
3571
|
const storageState = await this.page.context().storageState();
|
|
3572
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2939
3573
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2940
3574
|
if (path) {
|
|
2941
3575
|
// save { storageState: storageState } into the path
|
|
@@ -2946,10 +3580,14 @@ class StableBrowser {
|
|
|
2946
3580
|
}
|
|
2947
3581
|
}
|
|
2948
3582
|
async restoreSaveState(path = null, world = null) {
|
|
3583
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2949
3584
|
await refreshBrowser(this, path, world);
|
|
2950
3585
|
this.registerEventListeners(this.context);
|
|
2951
3586
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2952
3587
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3588
|
+
if (this.onRestoreSaveState) {
|
|
3589
|
+
this.onRestoreSaveState(path);
|
|
3590
|
+
}
|
|
2953
3591
|
}
|
|
2954
3592
|
async waitForPageLoad(options = {}, world = null) {
|
|
2955
3593
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -3032,7 +3670,7 @@ class StableBrowser {
|
|
|
3032
3670
|
await _commandError(state, e, this);
|
|
3033
3671
|
}
|
|
3034
3672
|
finally {
|
|
3035
|
-
_commandFinally(state, this);
|
|
3673
|
+
await _commandFinally(state, this);
|
|
3036
3674
|
}
|
|
3037
3675
|
}
|
|
3038
3676
|
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
@@ -3119,7 +3757,7 @@ class StableBrowser {
|
|
|
3119
3757
|
await _commandError(state, e, this);
|
|
3120
3758
|
}
|
|
3121
3759
|
finally {
|
|
3122
|
-
_commandFinally(state, this);
|
|
3760
|
+
await _commandFinally(state, this);
|
|
3123
3761
|
}
|
|
3124
3762
|
}
|
|
3125
3763
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3224,7 +3862,39 @@ class StableBrowser {
|
|
|
3224
3862
|
console.log("#-#");
|
|
3225
3863
|
}
|
|
3226
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) { }
|
|
3227
3894
|
async beforeStep(world, step) {
|
|
3895
|
+
if (!this.beforeScenarioCalled) {
|
|
3896
|
+
this.beforeScenario(world, step);
|
|
3897
|
+
}
|
|
3228
3898
|
if (this.stepIndex === undefined) {
|
|
3229
3899
|
this.stepIndex = 0;
|
|
3230
3900
|
}
|
|
@@ -3241,24 +3911,14 @@ class StableBrowser {
|
|
|
3241
3911
|
else {
|
|
3242
3912
|
this.stepName = "step " + this.stepIndex;
|
|
3243
3913
|
}
|
|
3244
|
-
if (this.context) {
|
|
3245
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3246
|
-
}
|
|
3247
3914
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3248
3915
|
if (this.context.browserObject.context) {
|
|
3249
3916
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3250
3917
|
}
|
|
3251
3918
|
}
|
|
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
3919
|
if (this.initSnapshotTaken === false) {
|
|
3260
3920
|
this.initSnapshotTaken = true;
|
|
3261
|
-
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3921
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3262
3922
|
const snapshot = await this.getAriaSnapshot();
|
|
3263
3923
|
if (snapshot) {
|
|
3264
3924
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -3280,18 +3940,68 @@ class StableBrowser {
|
|
|
3280
3940
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3281
3941
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3282
3942
|
for (let i = 0; i < frames.length; i++) {
|
|
3283
|
-
content.push(`- frame: ${i}`);
|
|
3284
3943
|
const frame = frames[i];
|
|
3285
|
-
|
|
3286
|
-
|
|
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) { }
|
|
3287
3953
|
}
|
|
3288
3954
|
return content.join("\n");
|
|
3289
3955
|
}
|
|
3290
3956
|
catch (e) {
|
|
3291
|
-
console.
|
|
3957
|
+
console.log("Error in getAriaSnapshot");
|
|
3958
|
+
//console.debug(e);
|
|
3292
3959
|
}
|
|
3293
3960
|
return null;
|
|
3294
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
|
+
}
|
|
3295
4005
|
async afterStep(world, step) {
|
|
3296
4006
|
this.stepName = null;
|
|
3297
4007
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3299,6 +4009,13 @@ class StableBrowser {
|
|
|
3299
4009
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3300
4010
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3301
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`);
|
|
3302
4019
|
}
|
|
3303
4020
|
}
|
|
3304
4021
|
if (this.context) {
|
|
@@ -3311,6 +4028,29 @@ class StableBrowser {
|
|
|
3311
4028
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3312
4029
|
}
|
|
3313
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
|
+
}
|
|
3314
4054
|
}
|
|
3315
4055
|
}
|
|
3316
4056
|
function createTimedPromise(promise, label) {
|