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