automation_model 1.0.654-dev → 1.0.654-stage
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/analyze_helper.js.map +1 -1
- package/lib/api.d.ts +0 -1
- package/lib/api.js +35 -21
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +143 -57
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +28 -8
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.d.ts +2 -0
- package/lib/bruno.js +381 -0
- package/lib/bruno.js.map +1 -0
- package/lib/command_common.d.ts +1 -1
- package/lib/command_common.js +24 -4
- package/lib/command_common.js.map +1 -1
- package/lib/date_time.js.map +1 -1
- package/lib/drawRect.js.map +1 -1
- package/lib/environment.d.ts +1 -0
- package/lib/environment.js +1 -0
- package/lib/environment.js.map +1 -1
- package/lib/error-messages.js.map +1 -1
- package/lib/file_checker.d.ts +1 -0
- package/lib/file_checker.js +61 -0
- package/lib/file_checker.js.map +1 -0
- package/lib/find_function.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.js +4 -4
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js.map +1 -1
- package/lib/locator.d.ts +1 -0
- package/lib/locator.js +10 -3
- package/lib/locator.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.js.map +1 -1
- package/lib/scripts/axe.mini.js +3 -3
- package/lib/snapshot_validation.d.ts +37 -0
- package/lib/snapshot_validation.js +357 -0
- package/lib/snapshot_validation.js.map +1 -0
- package/lib/stable_browser.d.ts +58 -24
- package/lib/stable_browser.js +905 -180
- 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,18 @@ 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 (this.configuration && this.configuration.fastMode === true) {
|
|
125
|
+
this.fastMode = true;
|
|
126
|
+
}
|
|
127
|
+
if (process.env.FAST_MODE === "true") {
|
|
128
|
+
this.fastMode = true;
|
|
129
|
+
}
|
|
130
|
+
if (process.env.FAST_MODE === "false") {
|
|
131
|
+
this.fastMode = false;
|
|
132
|
+
}
|
|
133
|
+
if (this.context) {
|
|
134
|
+
this.context.fastMode = this.fastMode;
|
|
135
|
+
}
|
|
111
136
|
this.registerEventListeners(this.context);
|
|
112
137
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
113
138
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
@@ -118,6 +143,9 @@ class StableBrowser {
|
|
|
118
143
|
if (!context.pageLoading) {
|
|
119
144
|
context.pageLoading = { status: false };
|
|
120
145
|
}
|
|
146
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
147
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
148
|
+
}
|
|
121
149
|
context.playContext.on("page", async function (page) {
|
|
122
150
|
if (this.configuration && this.configuration.closePopups === true) {
|
|
123
151
|
console.log("close unexpected popups");
|
|
@@ -126,6 +154,14 @@ class StableBrowser {
|
|
|
126
154
|
}
|
|
127
155
|
context.pageLoading.status = true;
|
|
128
156
|
this.page = page;
|
|
157
|
+
try {
|
|
158
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
159
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
console.error("Error on dialog accept registration", error);
|
|
164
|
+
}
|
|
129
165
|
context.page = page;
|
|
130
166
|
context.pages.push(page);
|
|
131
167
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
@@ -177,9 +213,35 @@ class StableBrowser {
|
|
|
177
213
|
if (newContextCreated) {
|
|
178
214
|
this.registerEventListeners(this.context);
|
|
179
215
|
await this.goto(this.context.environment.baseUrl);
|
|
180
|
-
|
|
216
|
+
if (!this.fastMode) {
|
|
217
|
+
await this.waitForPageLoad();
|
|
218
|
+
}
|
|
181
219
|
}
|
|
182
220
|
}
|
|
221
|
+
async switchTab(tabTitleOrIndex) {
|
|
222
|
+
// first check if the tabNameOrIndex is a number
|
|
223
|
+
let index = parseInt(tabTitleOrIndex);
|
|
224
|
+
if (!isNaN(index)) {
|
|
225
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
226
|
+
this.page = this.context.pages[index];
|
|
227
|
+
this.context.page = this.page;
|
|
228
|
+
await this.page.bringToFront();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
233
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
234
|
+
let page = this.context.pages[i];
|
|
235
|
+
let title = await page.title();
|
|
236
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
237
|
+
this.page = page;
|
|
238
|
+
this.context.page = this.page;
|
|
239
|
+
await this.page.bringToFront();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
244
|
+
}
|
|
183
245
|
registerConsoleLogListener(page, context) {
|
|
184
246
|
if (!this.context.webLogger) {
|
|
185
247
|
this.context.webLogger = [];
|
|
@@ -247,6 +309,7 @@ class StableBrowser {
|
|
|
247
309
|
if (!url) {
|
|
248
310
|
throw new Error("url is null, verify that the environment file is correct");
|
|
249
311
|
}
|
|
312
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
250
313
|
if (!url.startsWith("http")) {
|
|
251
314
|
url = "https://" + url;
|
|
252
315
|
}
|
|
@@ -275,7 +338,7 @@ class StableBrowser {
|
|
|
275
338
|
_commandError(state, error, this);
|
|
276
339
|
}
|
|
277
340
|
finally {
|
|
278
|
-
_commandFinally(state, this);
|
|
341
|
+
await _commandFinally(state, this);
|
|
279
342
|
}
|
|
280
343
|
}
|
|
281
344
|
async _getLocator(locator, scope, _params) {
|
|
@@ -391,7 +454,7 @@ class StableBrowser {
|
|
|
391
454
|
}
|
|
392
455
|
return { elementCount: tagCount, randomToken };
|
|
393
456
|
}
|
|
394
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
457
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
395
458
|
if (!info) {
|
|
396
459
|
info = {};
|
|
397
460
|
}
|
|
@@ -458,7 +521,7 @@ class StableBrowser {
|
|
|
458
521
|
}
|
|
459
522
|
return;
|
|
460
523
|
}
|
|
461
|
-
if (info.locatorLog && count === 0) {
|
|
524
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
462
525
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
463
526
|
}
|
|
464
527
|
for (let j = 0; j < count; j++) {
|
|
@@ -473,7 +536,7 @@ class StableBrowser {
|
|
|
473
536
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
474
537
|
}
|
|
475
538
|
}
|
|
476
|
-
else {
|
|
539
|
+
else if (logErrors) {
|
|
477
540
|
info.failCause.visible = visible;
|
|
478
541
|
info.failCause.enabled = enabled;
|
|
479
542
|
if (!info.printMessages) {
|
|
@@ -565,7 +628,7 @@ class StableBrowser {
|
|
|
565
628
|
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
566
629
|
if (!element.rerun) {
|
|
567
630
|
const randomToken = Math.random().toString(36).substring(7);
|
|
568
|
-
element.evaluate((el, randomToken) => {
|
|
631
|
+
await element.evaluate((el, randomToken) => {
|
|
569
632
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
570
633
|
}, randomToken);
|
|
571
634
|
// if (element._frame) {
|
|
@@ -579,7 +642,7 @@ class StableBrowser {
|
|
|
579
642
|
if (frameSelectorIndex !== -1) {
|
|
580
643
|
// remove everything after the >> internal:control=enter-frame
|
|
581
644
|
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
582
|
-
prefixSelector = frameSelector + " >> internal:control=enter-frame";
|
|
645
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
583
646
|
}
|
|
584
647
|
// if (element?._frame?._selector) {
|
|
585
648
|
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
@@ -737,14 +800,9 @@ class StableBrowser {
|
|
|
737
800
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
738
801
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
739
802
|
}
|
|
740
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
803
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
741
804
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
742
805
|
}
|
|
743
|
-
else {
|
|
744
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
745
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
806
|
let foundElements = result.foundElements;
|
|
749
807
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
750
808
|
info.box = foundElements[0].box;
|
|
@@ -799,6 +857,11 @@ class StableBrowser {
|
|
|
799
857
|
visibleOnly = false;
|
|
800
858
|
}
|
|
801
859
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
860
|
+
// sheck of more of half of the timeout has passed
|
|
861
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
862
|
+
highPriorityOnly = false;
|
|
863
|
+
visibleOnly = false;
|
|
864
|
+
}
|
|
802
865
|
}
|
|
803
866
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
804
867
|
// if (info.locatorLog) {
|
|
@@ -814,7 +877,7 @@ class StableBrowser {
|
|
|
814
877
|
}
|
|
815
878
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
816
879
|
}
|
|
817
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
880
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
818
881
|
let foundElements = [];
|
|
819
882
|
const result = {
|
|
820
883
|
foundElements: foundElements,
|
|
@@ -833,7 +896,9 @@ class StableBrowser {
|
|
|
833
896
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
834
897
|
}
|
|
835
898
|
catch (e) {
|
|
836
|
-
|
|
899
|
+
if (logErrors) {
|
|
900
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
901
|
+
}
|
|
837
902
|
}
|
|
838
903
|
}
|
|
839
904
|
if (foundLocators.length === 1) {
|
|
@@ -874,7 +939,7 @@ class StableBrowser {
|
|
|
874
939
|
});
|
|
875
940
|
result.locatorIndex = i;
|
|
876
941
|
}
|
|
877
|
-
else {
|
|
942
|
+
else if (logErrors) {
|
|
878
943
|
info.failCause.foundMultiple = true;
|
|
879
944
|
if (info.locatorLog) {
|
|
880
945
|
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
@@ -926,7 +991,7 @@ class StableBrowser {
|
|
|
926
991
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
927
992
|
}
|
|
928
993
|
finally {
|
|
929
|
-
_commandFinally(state, this);
|
|
994
|
+
await _commandFinally(state, this);
|
|
930
995
|
}
|
|
931
996
|
}
|
|
932
997
|
}
|
|
@@ -975,7 +1040,7 @@ class StableBrowser {
|
|
|
975
1040
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
976
1041
|
}
|
|
977
1042
|
finally {
|
|
978
|
-
_commandFinally(state, this);
|
|
1043
|
+
await _commandFinally(state, this);
|
|
979
1044
|
}
|
|
980
1045
|
}
|
|
981
1046
|
}
|
|
@@ -997,14 +1062,16 @@ class StableBrowser {
|
|
|
997
1062
|
try {
|
|
998
1063
|
await _preCommand(state, this);
|
|
999
1064
|
await performAction("click", state.element, options, this, state, _params);
|
|
1000
|
-
|
|
1065
|
+
if (!this.fastMode) {
|
|
1066
|
+
await this.waitForPageLoad();
|
|
1067
|
+
}
|
|
1001
1068
|
return state.info;
|
|
1002
1069
|
}
|
|
1003
1070
|
catch (e) {
|
|
1004
1071
|
await _commandError(state, e, this);
|
|
1005
1072
|
}
|
|
1006
1073
|
finally {
|
|
1007
|
-
_commandFinally(state, this);
|
|
1074
|
+
await _commandFinally(state, this);
|
|
1008
1075
|
}
|
|
1009
1076
|
}
|
|
1010
1077
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1035,7 +1102,7 @@ class StableBrowser {
|
|
|
1035
1102
|
// await _commandError(state, e, this);
|
|
1036
1103
|
}
|
|
1037
1104
|
finally {
|
|
1038
|
-
_commandFinally(state, this);
|
|
1105
|
+
await _commandFinally(state, this);
|
|
1039
1106
|
}
|
|
1040
1107
|
return found;
|
|
1041
1108
|
}
|
|
@@ -1060,7 +1127,7 @@ class StableBrowser {
|
|
|
1060
1127
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1061
1128
|
// console.log(`Highlighting while running from recorder`);
|
|
1062
1129
|
await this._highlightElements(state.element);
|
|
1063
|
-
await state.element.setChecked(checked);
|
|
1130
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
1064
1131
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1065
1132
|
// await this._unHighlightElements(element);
|
|
1066
1133
|
// }
|
|
@@ -1072,11 +1139,28 @@ class StableBrowser {
|
|
|
1072
1139
|
this.logger.info("element did not change its state, ignoring...");
|
|
1073
1140
|
}
|
|
1074
1141
|
else {
|
|
1142
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1075
1143
|
//await this.closeUnexpectedPopups();
|
|
1076
1144
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1077
|
-
state.
|
|
1078
|
-
|
|
1079
|
-
|
|
1145
|
+
state.element_found = false;
|
|
1146
|
+
try {
|
|
1147
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1148
|
+
state.element_found = true;
|
|
1149
|
+
// check the check state
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
// element dismissed
|
|
1153
|
+
}
|
|
1154
|
+
if (state.element_found) {
|
|
1155
|
+
const isChecked = await state.element.isChecked();
|
|
1156
|
+
if (isChecked !== checked) {
|
|
1157
|
+
// perform click
|
|
1158
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1080
1164
|
}
|
|
1081
1165
|
}
|
|
1082
1166
|
await this.waitForPageLoad();
|
|
@@ -1086,7 +1170,7 @@ class StableBrowser {
|
|
|
1086
1170
|
await _commandError(state, e, this);
|
|
1087
1171
|
}
|
|
1088
1172
|
finally {
|
|
1089
|
-
_commandFinally(state, this);
|
|
1173
|
+
await _commandFinally(state, this);
|
|
1090
1174
|
}
|
|
1091
1175
|
}
|
|
1092
1176
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1112,7 +1196,7 @@ class StableBrowser {
|
|
|
1112
1196
|
await _commandError(state, e, this);
|
|
1113
1197
|
}
|
|
1114
1198
|
finally {
|
|
1115
|
-
_commandFinally(state, this);
|
|
1199
|
+
await _commandFinally(state, this);
|
|
1116
1200
|
}
|
|
1117
1201
|
}
|
|
1118
1202
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1148,7 +1232,7 @@ class StableBrowser {
|
|
|
1148
1232
|
await _commandError(state, e, this);
|
|
1149
1233
|
}
|
|
1150
1234
|
finally {
|
|
1151
|
-
_commandFinally(state, this);
|
|
1235
|
+
await _commandFinally(state, this);
|
|
1152
1236
|
}
|
|
1153
1237
|
}
|
|
1154
1238
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1194,7 +1278,7 @@ class StableBrowser {
|
|
|
1194
1278
|
await _commandError(state, e, this);
|
|
1195
1279
|
}
|
|
1196
1280
|
finally {
|
|
1197
|
-
_commandFinally(state, this);
|
|
1281
|
+
await _commandFinally(state, this);
|
|
1198
1282
|
}
|
|
1199
1283
|
}
|
|
1200
1284
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1230,7 +1314,7 @@ class StableBrowser {
|
|
|
1230
1314
|
await _commandError(state, e, this);
|
|
1231
1315
|
}
|
|
1232
1316
|
finally {
|
|
1233
|
-
_commandFinally(state, this);
|
|
1317
|
+
await _commandFinally(state, this);
|
|
1234
1318
|
}
|
|
1235
1319
|
}
|
|
1236
1320
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1299,7 +1383,7 @@ class StableBrowser {
|
|
|
1299
1383
|
await _commandError(state, e, this);
|
|
1300
1384
|
}
|
|
1301
1385
|
finally {
|
|
1302
|
-
_commandFinally(state, this);
|
|
1386
|
+
await _commandFinally(state, this);
|
|
1303
1387
|
}
|
|
1304
1388
|
}
|
|
1305
1389
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1372,7 +1456,9 @@ class StableBrowser {
|
|
|
1372
1456
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1373
1457
|
}
|
|
1374
1458
|
}
|
|
1459
|
+
//if (!this.fastMode) {
|
|
1375
1460
|
await _screenshot(state, this);
|
|
1461
|
+
//}
|
|
1376
1462
|
if (enter === true) {
|
|
1377
1463
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1378
1464
|
await this.page.keyboard.press("Enter");
|
|
@@ -1399,7 +1485,7 @@ class StableBrowser {
|
|
|
1399
1485
|
await _commandError(state, e, this);
|
|
1400
1486
|
}
|
|
1401
1487
|
finally {
|
|
1402
|
-
_commandFinally(state, this);
|
|
1488
|
+
await _commandFinally(state, this);
|
|
1403
1489
|
}
|
|
1404
1490
|
}
|
|
1405
1491
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1429,7 +1515,42 @@ class StableBrowser {
|
|
|
1429
1515
|
await _commandError(state, e, this);
|
|
1430
1516
|
}
|
|
1431
1517
|
finally {
|
|
1432
|
-
_commandFinally(state, this);
|
|
1518
|
+
await _commandFinally(state, this);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1522
|
+
const state = {
|
|
1523
|
+
selectors,
|
|
1524
|
+
_params,
|
|
1525
|
+
files,
|
|
1526
|
+
value: '"' + files.join('", "') + '"',
|
|
1527
|
+
options,
|
|
1528
|
+
world,
|
|
1529
|
+
type: Types.SET_INPUT_FILES,
|
|
1530
|
+
text: `Set input files`,
|
|
1531
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1532
|
+
operation: "setInputFiles",
|
|
1533
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1534
|
+
};
|
|
1535
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1536
|
+
try {
|
|
1537
|
+
await _preCommand(state, this);
|
|
1538
|
+
for (let i = 0; i < files.length; i++) {
|
|
1539
|
+
const file = files[i];
|
|
1540
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1541
|
+
if (!fs.existsSync(filePath)) {
|
|
1542
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1543
|
+
}
|
|
1544
|
+
state.files[i] = filePath;
|
|
1545
|
+
}
|
|
1546
|
+
await state.element.setInputFiles(files);
|
|
1547
|
+
return state.info;
|
|
1548
|
+
}
|
|
1549
|
+
catch (e) {
|
|
1550
|
+
await _commandError(state, e, this);
|
|
1551
|
+
}
|
|
1552
|
+
finally {
|
|
1553
|
+
await _commandFinally(state, this);
|
|
1433
1554
|
}
|
|
1434
1555
|
}
|
|
1435
1556
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1545,7 +1666,7 @@ class StableBrowser {
|
|
|
1545
1666
|
await _commandError(state, e, this);
|
|
1546
1667
|
}
|
|
1547
1668
|
finally {
|
|
1548
|
-
_commandFinally(state, this);
|
|
1669
|
+
await _commandFinally(state, this);
|
|
1549
1670
|
}
|
|
1550
1671
|
}
|
|
1551
1672
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1580,7 +1701,7 @@ class StableBrowser {
|
|
|
1580
1701
|
while (Date.now() - startTime < timeout) {
|
|
1581
1702
|
try {
|
|
1582
1703
|
await _preCommand(state, this);
|
|
1583
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1704
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1584
1705
|
if (foundObj && foundObj.element) {
|
|
1585
1706
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1586
1707
|
}
|
|
@@ -1622,7 +1743,84 @@ class StableBrowser {
|
|
|
1622
1743
|
throw e;
|
|
1623
1744
|
}
|
|
1624
1745
|
finally {
|
|
1625
|
-
_commandFinally(state, this);
|
|
1746
|
+
await _commandFinally(state, this);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1750
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1751
|
+
const startTime = Date.now();
|
|
1752
|
+
const state = {
|
|
1753
|
+
_params,
|
|
1754
|
+
value: referanceSnapshot,
|
|
1755
|
+
options,
|
|
1756
|
+
world,
|
|
1757
|
+
locate: false,
|
|
1758
|
+
scroll: false,
|
|
1759
|
+
screenshot: true,
|
|
1760
|
+
highlight: false,
|
|
1761
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1762
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1763
|
+
operation: "snapshotValidation",
|
|
1764
|
+
log: "***** verify snapshot *****\n",
|
|
1765
|
+
};
|
|
1766
|
+
if (!referanceSnapshot) {
|
|
1767
|
+
throw new Error("referanceSnapshot is null");
|
|
1768
|
+
}
|
|
1769
|
+
let text = null;
|
|
1770
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1771
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1772
|
+
}
|
|
1773
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1774
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1775
|
+
}
|
|
1776
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1777
|
+
text = referanceSnapshot.substring(5);
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1781
|
+
}
|
|
1782
|
+
state.text = text;
|
|
1783
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1784
|
+
await _preCommand(state, this);
|
|
1785
|
+
let foundObj = null;
|
|
1786
|
+
try {
|
|
1787
|
+
let matchResult = null;
|
|
1788
|
+
while (Date.now() - startTime < timeout) {
|
|
1789
|
+
try {
|
|
1790
|
+
let scope = null;
|
|
1791
|
+
if (!frameSelectors) {
|
|
1792
|
+
scope = this.page;
|
|
1793
|
+
}
|
|
1794
|
+
else {
|
|
1795
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1796
|
+
}
|
|
1797
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1798
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1799
|
+
if (matchResult.errorLine !== -1) {
|
|
1800
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1801
|
+
}
|
|
1802
|
+
// highlight and screenshot
|
|
1803
|
+
try {
|
|
1804
|
+
await await highlightSnapshot(newValue, scope);
|
|
1805
|
+
await _screenshot(state, this);
|
|
1806
|
+
}
|
|
1807
|
+
catch (e) { }
|
|
1808
|
+
return state.info;
|
|
1809
|
+
}
|
|
1810
|
+
catch (e) {
|
|
1811
|
+
// Log error but continue retrying until timeout is reached
|
|
1812
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1813
|
+
}
|
|
1814
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1815
|
+
}
|
|
1816
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1817
|
+
}
|
|
1818
|
+
catch (e) {
|
|
1819
|
+
await _commandError(state, e, this);
|
|
1820
|
+
throw e;
|
|
1821
|
+
}
|
|
1822
|
+
finally {
|
|
1823
|
+
await _commandFinally(state, this);
|
|
1626
1824
|
}
|
|
1627
1825
|
}
|
|
1628
1826
|
async waitForUserInput(message, world = null) {
|
|
@@ -1921,7 +2119,7 @@ class StableBrowser {
|
|
|
1921
2119
|
await _commandError(state, e, this);
|
|
1922
2120
|
}
|
|
1923
2121
|
finally {
|
|
1924
|
-
_commandFinally(state, this);
|
|
2122
|
+
await _commandFinally(state, this);
|
|
1925
2123
|
}
|
|
1926
2124
|
}
|
|
1927
2125
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1952,10 +2150,102 @@ class StableBrowser {
|
|
|
1952
2150
|
case "value":
|
|
1953
2151
|
state.value = await state.element.inputValue();
|
|
1954
2152
|
break;
|
|
2153
|
+
case "text":
|
|
2154
|
+
state.value = await state.element.textContent();
|
|
2155
|
+
break;
|
|
1955
2156
|
default:
|
|
1956
2157
|
state.value = await state.element.getAttribute(attribute);
|
|
1957
2158
|
break;
|
|
1958
2159
|
}
|
|
2160
|
+
if (options !== null) {
|
|
2161
|
+
if (options.regex && options.regex !== "") {
|
|
2162
|
+
// Construct a regex pattern from the provided string
|
|
2163
|
+
const regex = options.regex.slice(1, -1);
|
|
2164
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2165
|
+
const matches = state.value.match(regexPattern);
|
|
2166
|
+
if (matches) {
|
|
2167
|
+
let newValue = "";
|
|
2168
|
+
for (const match of matches) {
|
|
2169
|
+
newValue += match;
|
|
2170
|
+
}
|
|
2171
|
+
state.value = newValue;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2175
|
+
state.value = state.value.trim();
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
state.info.value = state.value;
|
|
2179
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2180
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2181
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2182
|
+
return state.info;
|
|
2183
|
+
}
|
|
2184
|
+
catch (e) {
|
|
2185
|
+
await _commandError(state, e, this);
|
|
2186
|
+
}
|
|
2187
|
+
finally {
|
|
2188
|
+
await _commandFinally(state, this);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2192
|
+
const state = {
|
|
2193
|
+
selectors,
|
|
2194
|
+
_params,
|
|
2195
|
+
property,
|
|
2196
|
+
variable,
|
|
2197
|
+
options,
|
|
2198
|
+
world,
|
|
2199
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2200
|
+
text: `Extract property from element`,
|
|
2201
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2202
|
+
operation: "extractProperty",
|
|
2203
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2204
|
+
allowDisabled: true,
|
|
2205
|
+
};
|
|
2206
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2207
|
+
try {
|
|
2208
|
+
await _preCommand(state, this);
|
|
2209
|
+
switch (property) {
|
|
2210
|
+
case "inner_text":
|
|
2211
|
+
state.value = await state.element.innerText();
|
|
2212
|
+
break;
|
|
2213
|
+
case "href":
|
|
2214
|
+
state.value = await state.element.getAttribute("href");
|
|
2215
|
+
break;
|
|
2216
|
+
case "value":
|
|
2217
|
+
state.value = await state.element.inputValue();
|
|
2218
|
+
break;
|
|
2219
|
+
case "text":
|
|
2220
|
+
state.value = await state.element.textContent();
|
|
2221
|
+
break;
|
|
2222
|
+
default:
|
|
2223
|
+
if (property.startsWith("dataset.")) {
|
|
2224
|
+
const dataAttribute = property.substring(8);
|
|
2225
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2226
|
+
}
|
|
2227
|
+
else {
|
|
2228
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (options !== null) {
|
|
2232
|
+
if (options.regex && options.regex !== "") {
|
|
2233
|
+
// Construct a regex pattern from the provided string
|
|
2234
|
+
const regex = options.regex.slice(1, -1);
|
|
2235
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2236
|
+
const matches = state.value.match(regexPattern);
|
|
2237
|
+
if (matches) {
|
|
2238
|
+
let newValue = "";
|
|
2239
|
+
for (const match of matches) {
|
|
2240
|
+
newValue += match;
|
|
2241
|
+
}
|
|
2242
|
+
state.value = newValue;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2246
|
+
state.value = state.value.trim();
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
1959
2249
|
state.info.value = state.value;
|
|
1960
2250
|
this.setTestData({ [variable]: state.value }, world);
|
|
1961
2251
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1966,7 +2256,7 @@ class StableBrowser {
|
|
|
1966
2256
|
await _commandError(state, e, this);
|
|
1967
2257
|
}
|
|
1968
2258
|
finally {
|
|
1969
|
-
_commandFinally(state, this);
|
|
2259
|
+
await _commandFinally(state, this);
|
|
1970
2260
|
}
|
|
1971
2261
|
}
|
|
1972
2262
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1991,12 +2281,15 @@ class StableBrowser {
|
|
|
1991
2281
|
let expectedValue;
|
|
1992
2282
|
try {
|
|
1993
2283
|
await _preCommand(state, this);
|
|
1994
|
-
expectedValue = state.value;
|
|
2284
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1995
2285
|
state.info.expectedValue = expectedValue;
|
|
1996
2286
|
switch (attribute) {
|
|
1997
2287
|
case "innerText":
|
|
1998
2288
|
val = String(await state.element.innerText());
|
|
1999
2289
|
break;
|
|
2290
|
+
case "text":
|
|
2291
|
+
val = String(await state.element.textContent());
|
|
2292
|
+
break;
|
|
2000
2293
|
case "value":
|
|
2001
2294
|
val = String(await state.element.inputValue());
|
|
2002
2295
|
break;
|
|
@@ -2018,17 +2311,163 @@ class StableBrowser {
|
|
|
2018
2311
|
let regex;
|
|
2019
2312
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2020
2313
|
const patternBody = expectedValue.slice(1, -1);
|
|
2021
|
-
|
|
2314
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2315
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2316
|
+
state.info.regex = true;
|
|
2317
|
+
}
|
|
2318
|
+
else {
|
|
2319
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2320
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2321
|
+
}
|
|
2322
|
+
if (attribute === "innerText") {
|
|
2323
|
+
if (state.info.regex) {
|
|
2324
|
+
if (!regex.test(val)) {
|
|
2325
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2326
|
+
state.info.failCause.assertionFailed = true;
|
|
2327
|
+
state.info.failCause.lastError = errorMessage;
|
|
2328
|
+
throw new Error(errorMessage);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
else {
|
|
2332
|
+
const valLines = val.split("\n");
|
|
2333
|
+
const expectedLines = expectedValue.split("\n");
|
|
2334
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2335
|
+
if (!isPart) {
|
|
2336
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2337
|
+
state.info.failCause.assertionFailed = true;
|
|
2338
|
+
state.info.failCause.lastError = errorMessage;
|
|
2339
|
+
throw new Error(errorMessage);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
if (!val.match(regex)) {
|
|
2345
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2346
|
+
state.info.failCause.assertionFailed = true;
|
|
2347
|
+
state.info.failCause.lastError = errorMessage;
|
|
2348
|
+
throw new Error(errorMessage);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return state.info;
|
|
2352
|
+
}
|
|
2353
|
+
catch (e) {
|
|
2354
|
+
await _commandError(state, e, this);
|
|
2355
|
+
}
|
|
2356
|
+
finally {
|
|
2357
|
+
await _commandFinally(state, this);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2361
|
+
const state = {
|
|
2362
|
+
selectors,
|
|
2363
|
+
_params,
|
|
2364
|
+
property,
|
|
2365
|
+
value,
|
|
2366
|
+
options,
|
|
2367
|
+
world,
|
|
2368
|
+
type: Types.VERIFY_PROPERTY,
|
|
2369
|
+
highlight: true,
|
|
2370
|
+
screenshot: true,
|
|
2371
|
+
text: `Verify element property`,
|
|
2372
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2373
|
+
operation: "verifyProperty",
|
|
2374
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2375
|
+
allowDisabled: true,
|
|
2376
|
+
};
|
|
2377
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2378
|
+
let val;
|
|
2379
|
+
let expectedValue;
|
|
2380
|
+
try {
|
|
2381
|
+
await _preCommand(state, this);
|
|
2382
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2383
|
+
state.info.expectedValue = expectedValue;
|
|
2384
|
+
switch (property) {
|
|
2385
|
+
case "innerText":
|
|
2386
|
+
val = String(await state.element.innerText());
|
|
2387
|
+
break;
|
|
2388
|
+
case "text":
|
|
2389
|
+
val = String(await state.element.textContent());
|
|
2390
|
+
break;
|
|
2391
|
+
case "value":
|
|
2392
|
+
val = String(await state.element.inputValue());
|
|
2393
|
+
break;
|
|
2394
|
+
case "checked":
|
|
2395
|
+
val = String(await state.element.isChecked());
|
|
2396
|
+
break;
|
|
2397
|
+
case "disabled":
|
|
2398
|
+
val = String(await state.element.isDisabled());
|
|
2399
|
+
break;
|
|
2400
|
+
case "readOnly":
|
|
2401
|
+
const isEditable = await state.element.isEditable();
|
|
2402
|
+
val = String(!isEditable);
|
|
2403
|
+
break;
|
|
2404
|
+
case "innerHTML":
|
|
2405
|
+
val = String(await state.element.innerHTML());
|
|
2406
|
+
break;
|
|
2407
|
+
case "outerHTML":
|
|
2408
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2409
|
+
break;
|
|
2410
|
+
default:
|
|
2411
|
+
if (property.startsWith("dataset.")) {
|
|
2412
|
+
const dataAttribute = property.substring(8);
|
|
2413
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2414
|
+
}
|
|
2415
|
+
else {
|
|
2416
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
// Helper function to remove all style="" attributes
|
|
2420
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2421
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2422
|
+
};
|
|
2423
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2424
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2425
|
+
val = removeStyleAttributes(val);
|
|
2426
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2427
|
+
}
|
|
2428
|
+
state.info.value = val;
|
|
2429
|
+
let regex;
|
|
2430
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2431
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2432
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2433
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2434
|
+
state.info.regex = true;
|
|
2022
2435
|
}
|
|
2023
2436
|
else {
|
|
2024
2437
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2025
2438
|
regex = new RegExp(escapedPattern, "g");
|
|
2026
2439
|
}
|
|
2027
|
-
if (
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2440
|
+
if (property === "innerText") {
|
|
2441
|
+
if (state.info.regex) {
|
|
2442
|
+
if (!regex.test(val)) {
|
|
2443
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2444
|
+
state.info.failCause.assertionFailed = true;
|
|
2445
|
+
state.info.failCause.lastError = errorMessage;
|
|
2446
|
+
throw new Error(errorMessage);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
else {
|
|
2450
|
+
// Fix: Replace escaped newlines with actual newlines before splitting
|
|
2451
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2452
|
+
const valLines = val.split("\n");
|
|
2453
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2454
|
+
// Check if all expected lines are present in the actual lines
|
|
2455
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2456
|
+
if (!isPart) {
|
|
2457
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2458
|
+
state.info.failCause.assertionFailed = true;
|
|
2459
|
+
state.info.failCause.lastError = errorMessage;
|
|
2460
|
+
throw new Error(errorMessage);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
else {
|
|
2465
|
+
if (!val.match(regex)) {
|
|
2466
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2467
|
+
state.info.failCause.assertionFailed = true;
|
|
2468
|
+
state.info.failCause.lastError = errorMessage;
|
|
2469
|
+
throw new Error(errorMessage);
|
|
2470
|
+
}
|
|
2032
2471
|
}
|
|
2033
2472
|
return state.info;
|
|
2034
2473
|
}
|
|
@@ -2036,7 +2475,7 @@ class StableBrowser {
|
|
|
2036
2475
|
await _commandError(state, e, this);
|
|
2037
2476
|
}
|
|
2038
2477
|
finally {
|
|
2039
|
-
_commandFinally(state, this);
|
|
2478
|
+
await _commandFinally(state, this);
|
|
2040
2479
|
}
|
|
2041
2480
|
}
|
|
2042
2481
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2196,56 +2635,49 @@ class StableBrowser {
|
|
|
2196
2635
|
console.debug(error);
|
|
2197
2636
|
}
|
|
2198
2637
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
// });
|
|
2242
|
-
// }
|
|
2243
|
-
// } catch (error) {
|
|
2244
|
-
// // console.debug(error);
|
|
2245
|
-
// }
|
|
2246
|
-
// }
|
|
2638
|
+
_matcher(text) {
|
|
2639
|
+
if (!text) {
|
|
2640
|
+
return { matcher: "contains", queryText: "" };
|
|
2641
|
+
}
|
|
2642
|
+
if (text.length < 2) {
|
|
2643
|
+
return { matcher: "contains", queryText: text };
|
|
2644
|
+
}
|
|
2645
|
+
const split = text.split(":");
|
|
2646
|
+
const matcher = split[0].toLowerCase();
|
|
2647
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2648
|
+
return { matcher, queryText };
|
|
2649
|
+
}
|
|
2650
|
+
_getDomain(url) {
|
|
2651
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2652
|
+
return "";
|
|
2653
|
+
}
|
|
2654
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2655
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2656
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2657
|
+
}
|
|
2658
|
+
let n = hostnameFragments.length;
|
|
2659
|
+
let fragments = [...hostnameFragments];
|
|
2660
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2661
|
+
hostnameFragments.pop();
|
|
2662
|
+
n = hostnameFragments.length;
|
|
2663
|
+
}
|
|
2664
|
+
if (n == 0) {
|
|
2665
|
+
if (fragments[0] === "www")
|
|
2666
|
+
fragments = fragments.slice(1);
|
|
2667
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2668
|
+
}
|
|
2669
|
+
if (hostnameFragments[0] === "www")
|
|
2670
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2671
|
+
return hostnameFragments.join(".");
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Verify the page path matches the given path.
|
|
2675
|
+
* @param {string} pathPart - The path to verify.
|
|
2676
|
+
* @param {object} options - Options for verification.
|
|
2677
|
+
* @param {object} world - The world context.
|
|
2678
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2679
|
+
*/
|
|
2247
2680
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2248
|
-
const startTime = Date.now();
|
|
2249
2681
|
let error = null;
|
|
2250
2682
|
let screenshotId = null;
|
|
2251
2683
|
let screenshotPath = null;
|
|
@@ -2259,51 +2691,212 @@ class StableBrowser {
|
|
|
2259
2691
|
pathPart = newValue;
|
|
2260
2692
|
}
|
|
2261
2693
|
info.pathPart = pathPart;
|
|
2694
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2695
|
+
const state = {
|
|
2696
|
+
text_search: queryText,
|
|
2697
|
+
options,
|
|
2698
|
+
world,
|
|
2699
|
+
locate: false,
|
|
2700
|
+
scroll: false,
|
|
2701
|
+
highlight: false,
|
|
2702
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2703
|
+
text: `Verify the page url is ${queryText}`,
|
|
2704
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2705
|
+
operation: "verifyPagePath",
|
|
2706
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2707
|
+
};
|
|
2262
2708
|
try {
|
|
2709
|
+
await _preCommand(state, this);
|
|
2710
|
+
state.info.text = queryText;
|
|
2263
2711
|
for (let i = 0; i < 30; i++) {
|
|
2264
2712
|
const url = await this.page.url();
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2713
|
+
switch (matcher) {
|
|
2714
|
+
case "exact":
|
|
2715
|
+
if (url !== queryText) {
|
|
2716
|
+
if (i === 29) {
|
|
2717
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2718
|
+
}
|
|
2719
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
break;
|
|
2723
|
+
case "contains":
|
|
2724
|
+
if (!url.includes(queryText)) {
|
|
2725
|
+
if (i === 29) {
|
|
2726
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2727
|
+
}
|
|
2728
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
break;
|
|
2732
|
+
case "starts-with":
|
|
2733
|
+
{
|
|
2734
|
+
const domain = this._getDomain(url);
|
|
2735
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2736
|
+
if (i === 29) {
|
|
2737
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2738
|
+
}
|
|
2739
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2740
|
+
continue;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
break;
|
|
2744
|
+
case "ends-with":
|
|
2745
|
+
{
|
|
2746
|
+
const urlObj = new URL(url);
|
|
2747
|
+
let route = "/";
|
|
2748
|
+
if (urlObj.pathname !== "/") {
|
|
2749
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2750
|
+
}
|
|
2751
|
+
else {
|
|
2752
|
+
route = "/";
|
|
2753
|
+
}
|
|
2754
|
+
if (route !== queryText) {
|
|
2755
|
+
if (i === 29) {
|
|
2756
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2757
|
+
}
|
|
2758
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
break;
|
|
2763
|
+
case "regex":
|
|
2764
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2765
|
+
if (!regex.test(url)) {
|
|
2766
|
+
if (i === 29) {
|
|
2767
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2768
|
+
}
|
|
2769
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
break;
|
|
2773
|
+
default:
|
|
2774
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2775
|
+
if (!url.includes(pathPart)) {
|
|
2776
|
+
if (i === 29) {
|
|
2777
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2778
|
+
}
|
|
2779
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2271
2782
|
}
|
|
2272
|
-
|
|
2273
|
-
return info;
|
|
2783
|
+
await _screenshot(state, this);
|
|
2784
|
+
return state.info;
|
|
2274
2785
|
}
|
|
2275
2786
|
}
|
|
2276
2787
|
catch (e) {
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
info.screenshotPath = screenshotPath;
|
|
2281
|
-
Object.assign(e, { info: info });
|
|
2282
|
-
error = e;
|
|
2283
|
-
// throw e;
|
|
2284
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2788
|
+
state.info.failCause.lastError = e.message;
|
|
2789
|
+
state.info.failCause.assertionFailed = true;
|
|
2790
|
+
await _commandError(state, e, this);
|
|
2285
2791
|
}
|
|
2286
2792
|
finally {
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2793
|
+
await _commandFinally(state, this);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Verify the page title matches the given title.
|
|
2798
|
+
* @param {string} title - The title to verify.
|
|
2799
|
+
* @param {object} options - Options for verification.
|
|
2800
|
+
* @param {object} world - The world context.
|
|
2801
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2802
|
+
*/
|
|
2803
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2804
|
+
let error = null;
|
|
2805
|
+
let screenshotId = null;
|
|
2806
|
+
let screenshotPath = null;
|
|
2807
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2808
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2809
|
+
if (newValue !== title) {
|
|
2810
|
+
this.logger.info(title + "=" + newValue);
|
|
2811
|
+
title = newValue;
|
|
2812
|
+
}
|
|
2813
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2814
|
+
const state = {
|
|
2815
|
+
text_search: queryText,
|
|
2816
|
+
options,
|
|
2817
|
+
world,
|
|
2818
|
+
locate: false,
|
|
2819
|
+
scroll: false,
|
|
2820
|
+
highlight: false,
|
|
2821
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2822
|
+
text: `Verify the page title is ${queryText}`,
|
|
2823
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2824
|
+
operation: "verifyPageTitle",
|
|
2825
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2826
|
+
};
|
|
2827
|
+
try {
|
|
2828
|
+
await _preCommand(state, this);
|
|
2829
|
+
state.info.text = queryText;
|
|
2830
|
+
for (let i = 0; i < 30; i++) {
|
|
2831
|
+
const foundTitle = await this.page.title();
|
|
2832
|
+
switch (matcher) {
|
|
2833
|
+
case "exact":
|
|
2834
|
+
if (foundTitle !== queryText) {
|
|
2835
|
+
if (i === 29) {
|
|
2836
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2837
|
+
}
|
|
2838
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2841
|
+
break;
|
|
2842
|
+
case "contains":
|
|
2843
|
+
if (!foundTitle.includes(queryText)) {
|
|
2844
|
+
if (i === 29) {
|
|
2845
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2846
|
+
}
|
|
2847
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
break;
|
|
2851
|
+
case "starts-with":
|
|
2852
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2853
|
+
if (i === 29) {
|
|
2854
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2855
|
+
}
|
|
2856
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
break;
|
|
2860
|
+
case "ends-with":
|
|
2861
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2862
|
+
if (i === 29) {
|
|
2863
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2864
|
+
}
|
|
2865
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
break;
|
|
2869
|
+
case "regex":
|
|
2870
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2871
|
+
if (!regex.test(foundTitle)) {
|
|
2872
|
+
if (i === 29) {
|
|
2873
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2874
|
+
}
|
|
2875
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
break;
|
|
2879
|
+
default:
|
|
2880
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2881
|
+
if (!foundTitle.includes(title)) {
|
|
2882
|
+
if (i === 29) {
|
|
2883
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2884
|
+
}
|
|
2885
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
await _screenshot(state, this);
|
|
2890
|
+
return state.info;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
catch (e) {
|
|
2894
|
+
state.info.failCause.lastError = e.message;
|
|
2895
|
+
state.info.failCause.assertionFailed = true;
|
|
2896
|
+
await _commandError(state, e, this);
|
|
2897
|
+
}
|
|
2898
|
+
finally {
|
|
2899
|
+
await _commandFinally(state, this);
|
|
2307
2900
|
}
|
|
2308
2901
|
}
|
|
2309
2902
|
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
@@ -2345,7 +2938,7 @@ class StableBrowser {
|
|
|
2345
2938
|
scroll: false,
|
|
2346
2939
|
highlight: false,
|
|
2347
2940
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2348
|
-
text: `Verify the text '${text}' exists in page`,
|
|
2941
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2349
2942
|
_text: `Verify the text '${text}' exists in page`,
|
|
2350
2943
|
operation: "verifyTextExistInPage",
|
|
2351
2944
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2387,27 +2980,10 @@ class StableBrowser {
|
|
|
2387
2980
|
const frame = resultWithElementsFound[0].frame;
|
|
2388
2981
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2389
2982
|
await this._highlightElements(frame, dataAttribute);
|
|
2390
|
-
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2391
|
-
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2392
|
-
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2393
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2394
|
-
// this._unhighlightElements(frame, dataAttribute)
|
|
2395
|
-
// .then(async () => {
|
|
2396
|
-
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2397
|
-
// })
|
|
2398
|
-
// .catch(
|
|
2399
|
-
// (e) => {}
|
|
2400
|
-
// console.error(e)
|
|
2401
|
-
// );
|
|
2402
|
-
// });
|
|
2403
|
-
// }
|
|
2404
2983
|
const element = await frame.locator(dataAttribute).first();
|
|
2405
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2406
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2407
2984
|
if (element) {
|
|
2408
2985
|
await this.scrollIfNeeded(element, state.info);
|
|
2409
2986
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2410
|
-
// await _screenshot(state, this, element);
|
|
2411
2987
|
}
|
|
2412
2988
|
}
|
|
2413
2989
|
await _screenshot(state, this);
|
|
@@ -2417,13 +2993,12 @@ class StableBrowser {
|
|
|
2417
2993
|
console.error(error);
|
|
2418
2994
|
}
|
|
2419
2995
|
}
|
|
2420
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2421
2996
|
}
|
|
2422
2997
|
catch (e) {
|
|
2423
2998
|
await _commandError(state, e, this);
|
|
2424
2999
|
}
|
|
2425
3000
|
finally {
|
|
2426
|
-
_commandFinally(state, this);
|
|
3001
|
+
await _commandFinally(state, this);
|
|
2427
3002
|
}
|
|
2428
3003
|
}
|
|
2429
3004
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2436,7 +3011,7 @@ class StableBrowser {
|
|
|
2436
3011
|
scroll: false,
|
|
2437
3012
|
highlight: false,
|
|
2438
3013
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2439
|
-
text: `Verify the text '${text}' does not exist in page`,
|
|
3014
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2440
3015
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2441
3016
|
operation: "verifyTextNotExistInPage",
|
|
2442
3017
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2480,7 +3055,7 @@ class StableBrowser {
|
|
|
2480
3055
|
await _commandError(state, e, this);
|
|
2481
3056
|
}
|
|
2482
3057
|
finally {
|
|
2483
|
-
_commandFinally(state, this);
|
|
3058
|
+
await _commandFinally(state, this);
|
|
2484
3059
|
}
|
|
2485
3060
|
}
|
|
2486
3061
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2550,7 +3125,7 @@ class StableBrowser {
|
|
|
2550
3125
|
const count = await frame.locator(css).count();
|
|
2551
3126
|
for (let j = 0; j < count; j++) {
|
|
2552
3127
|
const continer = await frame.locator(css).nth(j);
|
|
2553
|
-
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false,
|
|
3128
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2554
3129
|
if (result.elementCount > 0) {
|
|
2555
3130
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2556
3131
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2591,7 +3166,7 @@ class StableBrowser {
|
|
|
2591
3166
|
await _commandError(state, e, this);
|
|
2592
3167
|
}
|
|
2593
3168
|
finally {
|
|
2594
|
-
_commandFinally(state, this);
|
|
3169
|
+
await _commandFinally(state, this);
|
|
2595
3170
|
}
|
|
2596
3171
|
}
|
|
2597
3172
|
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
@@ -2933,8 +3508,51 @@ class StableBrowser {
|
|
|
2933
3508
|
});
|
|
2934
3509
|
}
|
|
2935
3510
|
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3513
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3514
|
+
* @param options - Optional configuration object
|
|
3515
|
+
* @param world - Optional world context
|
|
3516
|
+
* @returns Promise that resolves after the specified duration
|
|
3517
|
+
*/
|
|
3518
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3519
|
+
const state = {
|
|
3520
|
+
duration,
|
|
3521
|
+
options,
|
|
3522
|
+
world,
|
|
3523
|
+
locate: false,
|
|
3524
|
+
scroll: false,
|
|
3525
|
+
screenshot: false,
|
|
3526
|
+
highlight: false,
|
|
3527
|
+
type: Types.SLEEP,
|
|
3528
|
+
text: `Sleep for ${duration} ms`,
|
|
3529
|
+
_text: `Sleep for ${duration} ms`,
|
|
3530
|
+
operation: "sleep",
|
|
3531
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3532
|
+
};
|
|
3533
|
+
try {
|
|
3534
|
+
await _preCommand(state, this);
|
|
3535
|
+
if (duration < 0) {
|
|
3536
|
+
throw new Error("Sleep duration cannot be negative");
|
|
3537
|
+
}
|
|
3538
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3539
|
+
return state.info;
|
|
3540
|
+
}
|
|
3541
|
+
catch (e) {
|
|
3542
|
+
await _commandError(state, e, this);
|
|
3543
|
+
}
|
|
3544
|
+
finally {
|
|
3545
|
+
await _commandFinally(state, this);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
2936
3548
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2937
|
-
|
|
3549
|
+
try {
|
|
3550
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3551
|
+
}
|
|
3552
|
+
catch (error) {
|
|
3553
|
+
this.logger.debug(error);
|
|
3554
|
+
throw error;
|
|
3555
|
+
}
|
|
2938
3556
|
}
|
|
2939
3557
|
_getLoadTimeout(options) {
|
|
2940
3558
|
let timeout = 15000;
|
|
@@ -2957,6 +3575,7 @@ class StableBrowser {
|
|
|
2957
3575
|
}
|
|
2958
3576
|
async saveStoreState(path = null, world = null) {
|
|
2959
3577
|
const storageState = await this.page.context().storageState();
|
|
3578
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2960
3579
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2961
3580
|
if (path) {
|
|
2962
3581
|
// save { storageState: storageState } into the path
|
|
@@ -2967,10 +3586,14 @@ class StableBrowser {
|
|
|
2967
3586
|
}
|
|
2968
3587
|
}
|
|
2969
3588
|
async restoreSaveState(path = null, world = null) {
|
|
3589
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2970
3590
|
await refreshBrowser(this, path, world);
|
|
2971
3591
|
this.registerEventListeners(this.context);
|
|
2972
3592
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2973
3593
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3594
|
+
if (this.onRestoreSaveState) {
|
|
3595
|
+
this.onRestoreSaveState(path);
|
|
3596
|
+
}
|
|
2974
3597
|
}
|
|
2975
3598
|
async waitForPageLoad(options = {}, world = null) {
|
|
2976
3599
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -3053,7 +3676,7 @@ class StableBrowser {
|
|
|
3053
3676
|
await _commandError(state, e, this);
|
|
3054
3677
|
}
|
|
3055
3678
|
finally {
|
|
3056
|
-
_commandFinally(state, this);
|
|
3679
|
+
await _commandFinally(state, this);
|
|
3057
3680
|
}
|
|
3058
3681
|
}
|
|
3059
3682
|
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
@@ -3140,7 +3763,7 @@ class StableBrowser {
|
|
|
3140
3763
|
await _commandError(state, e, this);
|
|
3141
3764
|
}
|
|
3142
3765
|
finally {
|
|
3143
|
-
_commandFinally(state, this);
|
|
3766
|
+
await _commandFinally(state, this);
|
|
3144
3767
|
}
|
|
3145
3768
|
}
|
|
3146
3769
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3245,7 +3868,39 @@ class StableBrowser {
|
|
|
3245
3868
|
console.log("#-#");
|
|
3246
3869
|
}
|
|
3247
3870
|
}
|
|
3871
|
+
async beforeScenario(world, scenario) {
|
|
3872
|
+
this.beforeScenarioCalled = true;
|
|
3873
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3874
|
+
this.scenarioName = scenario.pickle.name;
|
|
3875
|
+
}
|
|
3876
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3877
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3878
|
+
}
|
|
3879
|
+
if (this.context) {
|
|
3880
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3881
|
+
}
|
|
3882
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3883
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3884
|
+
// check if @global_test_data tag is present
|
|
3885
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3886
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
// update test data based on feature/scenario
|
|
3890
|
+
let envName = null;
|
|
3891
|
+
if (this.context && this.context.environment) {
|
|
3892
|
+
envName = this.context.environment.name;
|
|
3893
|
+
}
|
|
3894
|
+
if (!process.env.TEMP_RUN) {
|
|
3895
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
3896
|
+
}
|
|
3897
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3898
|
+
}
|
|
3899
|
+
async afterScenario(world, scenario) { }
|
|
3248
3900
|
async beforeStep(world, step) {
|
|
3901
|
+
if (!this.beforeScenarioCalled) {
|
|
3902
|
+
this.beforeScenario(world, step);
|
|
3903
|
+
}
|
|
3249
3904
|
if (this.stepIndex === undefined) {
|
|
3250
3905
|
this.stepIndex = 0;
|
|
3251
3906
|
}
|
|
@@ -3262,24 +3917,14 @@ class StableBrowser {
|
|
|
3262
3917
|
else {
|
|
3263
3918
|
this.stepName = "step " + this.stepIndex;
|
|
3264
3919
|
}
|
|
3265
|
-
if (this.context) {
|
|
3266
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3267
|
-
}
|
|
3268
3920
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3269
3921
|
if (this.context.browserObject.context) {
|
|
3270
3922
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3271
3923
|
}
|
|
3272
3924
|
}
|
|
3273
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3274
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3275
|
-
// check if @global_test_data tag is present
|
|
3276
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3277
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
3925
|
if (this.initSnapshotTaken === false) {
|
|
3281
3926
|
this.initSnapshotTaken = true;
|
|
3282
|
-
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3927
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3283
3928
|
const snapshot = await this.getAriaSnapshot();
|
|
3284
3929
|
if (snapshot) {
|
|
3285
3930
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -3301,18 +3946,68 @@ class StableBrowser {
|
|
|
3301
3946
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3302
3947
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3303
3948
|
for (let i = 0; i < frames.length; i++) {
|
|
3304
|
-
content.push(`- frame: ${i}`);
|
|
3305
3949
|
const frame = frames[i];
|
|
3306
|
-
|
|
3307
|
-
|
|
3950
|
+
try {
|
|
3951
|
+
// Ensure frame is attached and has body
|
|
3952
|
+
const body = frame.locator("body");
|
|
3953
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3954
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3955
|
+
content.push(`- frame: ${i}`);
|
|
3956
|
+
content.push(snapshot);
|
|
3957
|
+
}
|
|
3958
|
+
catch (innerErr) { }
|
|
3308
3959
|
}
|
|
3309
3960
|
return content.join("\n");
|
|
3310
3961
|
}
|
|
3311
3962
|
catch (e) {
|
|
3312
|
-
console.
|
|
3963
|
+
console.log("Error in getAriaSnapshot");
|
|
3964
|
+
//console.debug(e);
|
|
3313
3965
|
}
|
|
3314
3966
|
return null;
|
|
3315
3967
|
}
|
|
3968
|
+
/**
|
|
3969
|
+
* Sends command with custom payload to report.
|
|
3970
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3971
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3972
|
+
* @param content - Content of the command to be shown in the report.
|
|
3973
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3974
|
+
* @param world - Optional world context.
|
|
3975
|
+
* @public
|
|
3976
|
+
*/
|
|
3977
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3978
|
+
const state = {
|
|
3979
|
+
options,
|
|
3980
|
+
world,
|
|
3981
|
+
locate: false,
|
|
3982
|
+
scroll: false,
|
|
3983
|
+
screenshot: options.screenshot ?? false,
|
|
3984
|
+
highlight: options.highlight ?? false,
|
|
3985
|
+
type: Types.REPORT_COMMAND,
|
|
3986
|
+
text: commandText,
|
|
3987
|
+
_text: commandText,
|
|
3988
|
+
operation: "report_command",
|
|
3989
|
+
log: "***** " + commandText + " *****\n",
|
|
3990
|
+
};
|
|
3991
|
+
try {
|
|
3992
|
+
await _preCommand(state, this);
|
|
3993
|
+
const payload = {
|
|
3994
|
+
type: options.type ?? "text",
|
|
3995
|
+
content: content,
|
|
3996
|
+
screenshotId: null,
|
|
3997
|
+
};
|
|
3998
|
+
state.payload = payload;
|
|
3999
|
+
if (commandStatus === "FAILED") {
|
|
4000
|
+
state.throwError = true;
|
|
4001
|
+
throw new Error("Command failed");
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
catch (e) {
|
|
4005
|
+
await _commandError(state, e, this);
|
|
4006
|
+
}
|
|
4007
|
+
finally {
|
|
4008
|
+
await _commandFinally(state, this);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
3316
4011
|
async afterStep(world, step) {
|
|
3317
4012
|
this.stepName = null;
|
|
3318
4013
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3320,6 +4015,13 @@ class StableBrowser {
|
|
|
3320
4015
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3321
4016
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3322
4017
|
});
|
|
4018
|
+
if (world && world.attach) {
|
|
4019
|
+
await world.attach(JSON.stringify({
|
|
4020
|
+
type: "trace",
|
|
4021
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4022
|
+
}), "application/json+trace");
|
|
4023
|
+
}
|
|
4024
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3323
4025
|
}
|
|
3324
4026
|
}
|
|
3325
4027
|
if (this.context) {
|
|
@@ -3332,6 +4034,29 @@ class StableBrowser {
|
|
|
3332
4034
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3333
4035
|
}
|
|
3334
4036
|
}
|
|
4037
|
+
if (!process.env.TEMP_RUN) {
|
|
4038
|
+
const state = {
|
|
4039
|
+
world,
|
|
4040
|
+
locate: false,
|
|
4041
|
+
scroll: false,
|
|
4042
|
+
screenshot: true,
|
|
4043
|
+
highlight: true,
|
|
4044
|
+
type: Types.STEP_COMPLETE,
|
|
4045
|
+
text: "end of scenario",
|
|
4046
|
+
_text: "end of scenario",
|
|
4047
|
+
operation: "step_complete",
|
|
4048
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4049
|
+
};
|
|
4050
|
+
try {
|
|
4051
|
+
await _preCommand(state, this);
|
|
4052
|
+
}
|
|
4053
|
+
catch (e) {
|
|
4054
|
+
await _commandError(state, e, this);
|
|
4055
|
+
}
|
|
4056
|
+
finally {
|
|
4057
|
+
await _commandFinally(state, this);
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
3335
4060
|
}
|
|
3336
4061
|
}
|
|
3337
4062
|
function createTimedPromise(promise, label) {
|