automation_model 1.0.655-dev → 1.0.655-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 +145 -58
- 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 +907 -187
- 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 +18 -3
- package/lib/utils.js +135 -69
- package/lib/utils.js.map +1 -1
- package/package.json +12 -6
package/lib/stable_browser.js
CHANGED
|
@@ -10,11 +10,12 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
10
|
import drawRectangle from "./drawRect.js";
|
|
11
11
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
14
14
|
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) {
|
|
@@ -1765,12 +1963,7 @@ class StableBrowser {
|
|
|
1765
1963
|
}
|
|
1766
1964
|
}
|
|
1767
1965
|
getTestData(world = null) {
|
|
1768
|
-
|
|
1769
|
-
let data = {};
|
|
1770
|
-
if (fs.existsSync(dataFile)) {
|
|
1771
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1772
|
-
}
|
|
1773
|
-
return data;
|
|
1966
|
+
return _getTestData(world, this.context, this);
|
|
1774
1967
|
}
|
|
1775
1968
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1776
1969
|
// collect url/path/title
|
|
@@ -1921,7 +2114,7 @@ class StableBrowser {
|
|
|
1921
2114
|
await _commandError(state, e, this);
|
|
1922
2115
|
}
|
|
1923
2116
|
finally {
|
|
1924
|
-
_commandFinally(state, this);
|
|
2117
|
+
await _commandFinally(state, this);
|
|
1925
2118
|
}
|
|
1926
2119
|
}
|
|
1927
2120
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1952,10 +2145,102 @@ class StableBrowser {
|
|
|
1952
2145
|
case "value":
|
|
1953
2146
|
state.value = await state.element.inputValue();
|
|
1954
2147
|
break;
|
|
2148
|
+
case "text":
|
|
2149
|
+
state.value = await state.element.textContent();
|
|
2150
|
+
break;
|
|
1955
2151
|
default:
|
|
1956
2152
|
state.value = await state.element.getAttribute(attribute);
|
|
1957
2153
|
break;
|
|
1958
2154
|
}
|
|
2155
|
+
if (options !== null) {
|
|
2156
|
+
if (options.regex && options.regex !== "") {
|
|
2157
|
+
// Construct a regex pattern from the provided string
|
|
2158
|
+
const regex = options.regex.slice(1, -1);
|
|
2159
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2160
|
+
const matches = state.value.match(regexPattern);
|
|
2161
|
+
if (matches) {
|
|
2162
|
+
let newValue = "";
|
|
2163
|
+
for (const match of matches) {
|
|
2164
|
+
newValue += match;
|
|
2165
|
+
}
|
|
2166
|
+
state.value = newValue;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2170
|
+
state.value = state.value.trim();
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
state.info.value = state.value;
|
|
2174
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2175
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2176
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2177
|
+
return state.info;
|
|
2178
|
+
}
|
|
2179
|
+
catch (e) {
|
|
2180
|
+
await _commandError(state, e, this);
|
|
2181
|
+
}
|
|
2182
|
+
finally {
|
|
2183
|
+
await _commandFinally(state, this);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2187
|
+
const state = {
|
|
2188
|
+
selectors,
|
|
2189
|
+
_params,
|
|
2190
|
+
property,
|
|
2191
|
+
variable,
|
|
2192
|
+
options,
|
|
2193
|
+
world,
|
|
2194
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2195
|
+
text: `Extract property from element`,
|
|
2196
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2197
|
+
operation: "extractProperty",
|
|
2198
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2199
|
+
allowDisabled: true,
|
|
2200
|
+
};
|
|
2201
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2202
|
+
try {
|
|
2203
|
+
await _preCommand(state, this);
|
|
2204
|
+
switch (property) {
|
|
2205
|
+
case "inner_text":
|
|
2206
|
+
state.value = await state.element.innerText();
|
|
2207
|
+
break;
|
|
2208
|
+
case "href":
|
|
2209
|
+
state.value = await state.element.getAttribute("href");
|
|
2210
|
+
break;
|
|
2211
|
+
case "value":
|
|
2212
|
+
state.value = await state.element.inputValue();
|
|
2213
|
+
break;
|
|
2214
|
+
case "text":
|
|
2215
|
+
state.value = await state.element.textContent();
|
|
2216
|
+
break;
|
|
2217
|
+
default:
|
|
2218
|
+
if (property.startsWith("dataset.")) {
|
|
2219
|
+
const dataAttribute = property.substring(8);
|
|
2220
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2221
|
+
}
|
|
2222
|
+
else {
|
|
2223
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (options !== null) {
|
|
2227
|
+
if (options.regex && options.regex !== "") {
|
|
2228
|
+
// Construct a regex pattern from the provided string
|
|
2229
|
+
const regex = options.regex.slice(1, -1);
|
|
2230
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2231
|
+
const matches = state.value.match(regexPattern);
|
|
2232
|
+
if (matches) {
|
|
2233
|
+
let newValue = "";
|
|
2234
|
+
for (const match of matches) {
|
|
2235
|
+
newValue += match;
|
|
2236
|
+
}
|
|
2237
|
+
state.value = newValue;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2241
|
+
state.value = state.value.trim();
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
1959
2244
|
state.info.value = state.value;
|
|
1960
2245
|
this.setTestData({ [variable]: state.value }, world);
|
|
1961
2246
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1966,7 +2251,7 @@ class StableBrowser {
|
|
|
1966
2251
|
await _commandError(state, e, this);
|
|
1967
2252
|
}
|
|
1968
2253
|
finally {
|
|
1969
|
-
_commandFinally(state, this);
|
|
2254
|
+
await _commandFinally(state, this);
|
|
1970
2255
|
}
|
|
1971
2256
|
}
|
|
1972
2257
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1991,12 +2276,15 @@ class StableBrowser {
|
|
|
1991
2276
|
let expectedValue;
|
|
1992
2277
|
try {
|
|
1993
2278
|
await _preCommand(state, this);
|
|
1994
|
-
expectedValue = state.value;
|
|
2279
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1995
2280
|
state.info.expectedValue = expectedValue;
|
|
1996
2281
|
switch (attribute) {
|
|
1997
2282
|
case "innerText":
|
|
1998
2283
|
val = String(await state.element.innerText());
|
|
1999
2284
|
break;
|
|
2285
|
+
case "text":
|
|
2286
|
+
val = String(await state.element.textContent());
|
|
2287
|
+
break;
|
|
2000
2288
|
case "value":
|
|
2001
2289
|
val = String(await state.element.inputValue());
|
|
2002
2290
|
break;
|
|
@@ -2018,17 +2306,42 @@ class StableBrowser {
|
|
|
2018
2306
|
let regex;
|
|
2019
2307
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2020
2308
|
const patternBody = expectedValue.slice(1, -1);
|
|
2021
|
-
|
|
2309
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2310
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2311
|
+
state.info.regex = true;
|
|
2022
2312
|
}
|
|
2023
2313
|
else {
|
|
2024
2314
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2025
2315
|
regex = new RegExp(escapedPattern, "g");
|
|
2026
2316
|
}
|
|
2027
|
-
if (
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2317
|
+
if (attribute === "innerText") {
|
|
2318
|
+
if (state.info.regex) {
|
|
2319
|
+
if (!regex.test(val)) {
|
|
2320
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2321
|
+
state.info.failCause.assertionFailed = true;
|
|
2322
|
+
state.info.failCause.lastError = errorMessage;
|
|
2323
|
+
throw new Error(errorMessage);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
else {
|
|
2327
|
+
const valLines = val.split("\n");
|
|
2328
|
+
const expectedLines = expectedValue.split("\n");
|
|
2329
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2330
|
+
if (!isPart) {
|
|
2331
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2332
|
+
state.info.failCause.assertionFailed = true;
|
|
2333
|
+
state.info.failCause.lastError = errorMessage;
|
|
2334
|
+
throw new Error(errorMessage);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
else {
|
|
2339
|
+
if (!val.match(regex)) {
|
|
2340
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2341
|
+
state.info.failCause.assertionFailed = true;
|
|
2342
|
+
state.info.failCause.lastError = errorMessage;
|
|
2343
|
+
throw new Error(errorMessage);
|
|
2344
|
+
}
|
|
2032
2345
|
}
|
|
2033
2346
|
return state.info;
|
|
2034
2347
|
}
|
|
@@ -2036,7 +2349,128 @@ class StableBrowser {
|
|
|
2036
2349
|
await _commandError(state, e, this);
|
|
2037
2350
|
}
|
|
2038
2351
|
finally {
|
|
2039
|
-
_commandFinally(state, this);
|
|
2352
|
+
await _commandFinally(state, this);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2356
|
+
const state = {
|
|
2357
|
+
selectors,
|
|
2358
|
+
_params,
|
|
2359
|
+
property,
|
|
2360
|
+
value,
|
|
2361
|
+
options,
|
|
2362
|
+
world,
|
|
2363
|
+
type: Types.VERIFY_PROPERTY,
|
|
2364
|
+
highlight: true,
|
|
2365
|
+
screenshot: true,
|
|
2366
|
+
text: `Verify element property`,
|
|
2367
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2368
|
+
operation: "verifyProperty",
|
|
2369
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2370
|
+
allowDisabled: true,
|
|
2371
|
+
};
|
|
2372
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2373
|
+
let val;
|
|
2374
|
+
let expectedValue;
|
|
2375
|
+
try {
|
|
2376
|
+
await _preCommand(state, this);
|
|
2377
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2378
|
+
state.info.expectedValue = expectedValue;
|
|
2379
|
+
switch (property) {
|
|
2380
|
+
case "innerText":
|
|
2381
|
+
val = String(await state.element.innerText());
|
|
2382
|
+
break;
|
|
2383
|
+
case "text":
|
|
2384
|
+
val = String(await state.element.textContent());
|
|
2385
|
+
break;
|
|
2386
|
+
case "value":
|
|
2387
|
+
val = String(await state.element.inputValue());
|
|
2388
|
+
break;
|
|
2389
|
+
case "checked":
|
|
2390
|
+
val = String(await state.element.isChecked());
|
|
2391
|
+
break;
|
|
2392
|
+
case "disabled":
|
|
2393
|
+
val = String(await state.element.isDisabled());
|
|
2394
|
+
break;
|
|
2395
|
+
case "readOnly":
|
|
2396
|
+
const isEditable = await state.element.isEditable();
|
|
2397
|
+
val = String(!isEditable);
|
|
2398
|
+
break;
|
|
2399
|
+
case "innerHTML":
|
|
2400
|
+
val = String(await state.element.innerHTML());
|
|
2401
|
+
break;
|
|
2402
|
+
case "outerHTML":
|
|
2403
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2404
|
+
break;
|
|
2405
|
+
default:
|
|
2406
|
+
if (property.startsWith("dataset.")) {
|
|
2407
|
+
const dataAttribute = property.substring(8);
|
|
2408
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2409
|
+
}
|
|
2410
|
+
else {
|
|
2411
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
// Helper function to remove all style="" attributes
|
|
2415
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2416
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2417
|
+
};
|
|
2418
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2419
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2420
|
+
val = removeStyleAttributes(val);
|
|
2421
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2422
|
+
}
|
|
2423
|
+
state.info.value = val;
|
|
2424
|
+
let regex;
|
|
2425
|
+
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2426
|
+
const patternBody = expectedValue.slice(1, -1);
|
|
2427
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2428
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2429
|
+
state.info.regex = true;
|
|
2430
|
+
}
|
|
2431
|
+
else {
|
|
2432
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2433
|
+
regex = new RegExp(escapedPattern, "g");
|
|
2434
|
+
}
|
|
2435
|
+
if (property === "innerText") {
|
|
2436
|
+
if (state.info.regex) {
|
|
2437
|
+
if (!regex.test(val)) {
|
|
2438
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2439
|
+
state.info.failCause.assertionFailed = true;
|
|
2440
|
+
state.info.failCause.lastError = errorMessage;
|
|
2441
|
+
throw new Error(errorMessage);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
else {
|
|
2445
|
+
// Fix: Replace escaped newlines with actual newlines before splitting
|
|
2446
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2447
|
+
const valLines = val.split("\n");
|
|
2448
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2449
|
+
// Check if all expected lines are present in the actual lines
|
|
2450
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2451
|
+
if (!isPart) {
|
|
2452
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2453
|
+
state.info.failCause.assertionFailed = true;
|
|
2454
|
+
state.info.failCause.lastError = errorMessage;
|
|
2455
|
+
throw new Error(errorMessage);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
else {
|
|
2460
|
+
if (!val.match(regex)) {
|
|
2461
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2462
|
+
state.info.failCause.assertionFailed = true;
|
|
2463
|
+
state.info.failCause.lastError = errorMessage;
|
|
2464
|
+
throw new Error(errorMessage);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return state.info;
|
|
2468
|
+
}
|
|
2469
|
+
catch (e) {
|
|
2470
|
+
await _commandError(state, e, this);
|
|
2471
|
+
}
|
|
2472
|
+
finally {
|
|
2473
|
+
await _commandFinally(state, this);
|
|
2040
2474
|
}
|
|
2041
2475
|
}
|
|
2042
2476
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2196,56 +2630,49 @@ class StableBrowser {
|
|
|
2196
2630
|
console.debug(error);
|
|
2197
2631
|
}
|
|
2198
2632
|
}
|
|
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
|
-
// }
|
|
2633
|
+
_matcher(text) {
|
|
2634
|
+
if (!text) {
|
|
2635
|
+
return { matcher: "contains", queryText: "" };
|
|
2636
|
+
}
|
|
2637
|
+
if (text.length < 2) {
|
|
2638
|
+
return { matcher: "contains", queryText: text };
|
|
2639
|
+
}
|
|
2640
|
+
const split = text.split(":");
|
|
2641
|
+
const matcher = split[0].toLowerCase();
|
|
2642
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2643
|
+
return { matcher, queryText };
|
|
2644
|
+
}
|
|
2645
|
+
_getDomain(url) {
|
|
2646
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2647
|
+
return "";
|
|
2648
|
+
}
|
|
2649
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2650
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2651
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2652
|
+
}
|
|
2653
|
+
let n = hostnameFragments.length;
|
|
2654
|
+
let fragments = [...hostnameFragments];
|
|
2655
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2656
|
+
hostnameFragments.pop();
|
|
2657
|
+
n = hostnameFragments.length;
|
|
2658
|
+
}
|
|
2659
|
+
if (n == 0) {
|
|
2660
|
+
if (fragments[0] === "www")
|
|
2661
|
+
fragments = fragments.slice(1);
|
|
2662
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2663
|
+
}
|
|
2664
|
+
if (hostnameFragments[0] === "www")
|
|
2665
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2666
|
+
return hostnameFragments.join(".");
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Verify the page path matches the given path.
|
|
2670
|
+
* @param {string} pathPart - The path to verify.
|
|
2671
|
+
* @param {object} options - Options for verification.
|
|
2672
|
+
* @param {object} world - The world context.
|
|
2673
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2674
|
+
*/
|
|
2247
2675
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2248
|
-
const startTime = Date.now();
|
|
2249
2676
|
let error = null;
|
|
2250
2677
|
let screenshotId = null;
|
|
2251
2678
|
let screenshotPath = null;
|
|
@@ -2259,51 +2686,212 @@ class StableBrowser {
|
|
|
2259
2686
|
pathPart = newValue;
|
|
2260
2687
|
}
|
|
2261
2688
|
info.pathPart = pathPart;
|
|
2689
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2690
|
+
const state = {
|
|
2691
|
+
text_search: queryText,
|
|
2692
|
+
options,
|
|
2693
|
+
world,
|
|
2694
|
+
locate: false,
|
|
2695
|
+
scroll: false,
|
|
2696
|
+
highlight: false,
|
|
2697
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2698
|
+
text: `Verify the page url is ${queryText}`,
|
|
2699
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2700
|
+
operation: "verifyPagePath",
|
|
2701
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2702
|
+
};
|
|
2262
2703
|
try {
|
|
2704
|
+
await _preCommand(state, this);
|
|
2705
|
+
state.info.text = queryText;
|
|
2263
2706
|
for (let i = 0; i < 30; i++) {
|
|
2264
2707
|
const url = await this.page.url();
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2708
|
+
switch (matcher) {
|
|
2709
|
+
case "exact":
|
|
2710
|
+
if (url !== queryText) {
|
|
2711
|
+
if (i === 29) {
|
|
2712
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2713
|
+
}
|
|
2714
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
break;
|
|
2718
|
+
case "contains":
|
|
2719
|
+
if (!url.includes(queryText)) {
|
|
2720
|
+
if (i === 29) {
|
|
2721
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2722
|
+
}
|
|
2723
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2724
|
+
continue;
|
|
2725
|
+
}
|
|
2726
|
+
break;
|
|
2727
|
+
case "starts-with":
|
|
2728
|
+
{
|
|
2729
|
+
const domain = this._getDomain(url);
|
|
2730
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2731
|
+
if (i === 29) {
|
|
2732
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2733
|
+
}
|
|
2734
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
break;
|
|
2739
|
+
case "ends-with":
|
|
2740
|
+
{
|
|
2741
|
+
const urlObj = new URL(url);
|
|
2742
|
+
let route = "/";
|
|
2743
|
+
if (urlObj.pathname !== "/") {
|
|
2744
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2745
|
+
}
|
|
2746
|
+
else {
|
|
2747
|
+
route = "/";
|
|
2748
|
+
}
|
|
2749
|
+
if (route !== queryText) {
|
|
2750
|
+
if (i === 29) {
|
|
2751
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2752
|
+
}
|
|
2753
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
break;
|
|
2758
|
+
case "regex":
|
|
2759
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2760
|
+
if (!regex.test(url)) {
|
|
2761
|
+
if (i === 29) {
|
|
2762
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2763
|
+
}
|
|
2764
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
break;
|
|
2768
|
+
default:
|
|
2769
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2770
|
+
if (!url.includes(pathPart)) {
|
|
2771
|
+
if (i === 29) {
|
|
2772
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2773
|
+
}
|
|
2774
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2271
2777
|
}
|
|
2272
|
-
|
|
2273
|
-
return info;
|
|
2778
|
+
await _screenshot(state, this);
|
|
2779
|
+
return state.info;
|
|
2274
2780
|
}
|
|
2275
2781
|
}
|
|
2276
2782
|
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);
|
|
2783
|
+
state.info.failCause.lastError = e.message;
|
|
2784
|
+
state.info.failCause.assertionFailed = true;
|
|
2785
|
+
await _commandError(state, e, this);
|
|
2285
2786
|
}
|
|
2286
2787
|
finally {
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2788
|
+
await _commandFinally(state, this);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Verify the page title matches the given title.
|
|
2793
|
+
* @param {string} title - The title to verify.
|
|
2794
|
+
* @param {object} options - Options for verification.
|
|
2795
|
+
* @param {object} world - The world context.
|
|
2796
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2797
|
+
*/
|
|
2798
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2799
|
+
let error = null;
|
|
2800
|
+
let screenshotId = null;
|
|
2801
|
+
let screenshotPath = null;
|
|
2802
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2803
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2804
|
+
if (newValue !== title) {
|
|
2805
|
+
this.logger.info(title + "=" + newValue);
|
|
2806
|
+
title = newValue;
|
|
2807
|
+
}
|
|
2808
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2809
|
+
const state = {
|
|
2810
|
+
text_search: queryText,
|
|
2811
|
+
options,
|
|
2812
|
+
world,
|
|
2813
|
+
locate: false,
|
|
2814
|
+
scroll: false,
|
|
2815
|
+
highlight: false,
|
|
2816
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2817
|
+
text: `Verify the page title is ${queryText}`,
|
|
2818
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2819
|
+
operation: "verifyPageTitle",
|
|
2820
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2821
|
+
};
|
|
2822
|
+
try {
|
|
2823
|
+
await _preCommand(state, this);
|
|
2824
|
+
state.info.text = queryText;
|
|
2825
|
+
for (let i = 0; i < 30; i++) {
|
|
2826
|
+
const foundTitle = await this.page.title();
|
|
2827
|
+
switch (matcher) {
|
|
2828
|
+
case "exact":
|
|
2829
|
+
if (foundTitle !== queryText) {
|
|
2830
|
+
if (i === 29) {
|
|
2831
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2832
|
+
}
|
|
2833
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
break;
|
|
2837
|
+
case "contains":
|
|
2838
|
+
if (!foundTitle.includes(queryText)) {
|
|
2839
|
+
if (i === 29) {
|
|
2840
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2841
|
+
}
|
|
2842
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
break;
|
|
2846
|
+
case "starts-with":
|
|
2847
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2848
|
+
if (i === 29) {
|
|
2849
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2850
|
+
}
|
|
2851
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
break;
|
|
2855
|
+
case "ends-with":
|
|
2856
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2857
|
+
if (i === 29) {
|
|
2858
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2859
|
+
}
|
|
2860
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
break;
|
|
2864
|
+
case "regex":
|
|
2865
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2866
|
+
if (!regex.test(foundTitle)) {
|
|
2867
|
+
if (i === 29) {
|
|
2868
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2869
|
+
}
|
|
2870
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2871
|
+
continue;
|
|
2872
|
+
}
|
|
2873
|
+
break;
|
|
2874
|
+
default:
|
|
2875
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2876
|
+
if (!foundTitle.includes(title)) {
|
|
2877
|
+
if (i === 29) {
|
|
2878
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2879
|
+
}
|
|
2880
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
await _screenshot(state, this);
|
|
2885
|
+
return state.info;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
catch (e) {
|
|
2889
|
+
state.info.failCause.lastError = e.message;
|
|
2890
|
+
state.info.failCause.assertionFailed = true;
|
|
2891
|
+
await _commandError(state, e, this);
|
|
2892
|
+
}
|
|
2893
|
+
finally {
|
|
2894
|
+
await _commandFinally(state, this);
|
|
2307
2895
|
}
|
|
2308
2896
|
}
|
|
2309
2897
|
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
@@ -2345,7 +2933,7 @@ class StableBrowser {
|
|
|
2345
2933
|
scroll: false,
|
|
2346
2934
|
highlight: false,
|
|
2347
2935
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2348
|
-
text: `Verify the text '${text}' exists in page`,
|
|
2936
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2349
2937
|
_text: `Verify the text '${text}' exists in page`,
|
|
2350
2938
|
operation: "verifyTextExistInPage",
|
|
2351
2939
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2387,27 +2975,10 @@ class StableBrowser {
|
|
|
2387
2975
|
const frame = resultWithElementsFound[0].frame;
|
|
2388
2976
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2389
2977
|
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
2978
|
const element = await frame.locator(dataAttribute).first();
|
|
2405
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2406
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2407
2979
|
if (element) {
|
|
2408
2980
|
await this.scrollIfNeeded(element, state.info);
|
|
2409
2981
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2410
|
-
// await _screenshot(state, this, element);
|
|
2411
2982
|
}
|
|
2412
2983
|
}
|
|
2413
2984
|
await _screenshot(state, this);
|
|
@@ -2417,13 +2988,12 @@ class StableBrowser {
|
|
|
2417
2988
|
console.error(error);
|
|
2418
2989
|
}
|
|
2419
2990
|
}
|
|
2420
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2421
2991
|
}
|
|
2422
2992
|
catch (e) {
|
|
2423
2993
|
await _commandError(state, e, this);
|
|
2424
2994
|
}
|
|
2425
2995
|
finally {
|
|
2426
|
-
_commandFinally(state, this);
|
|
2996
|
+
await _commandFinally(state, this);
|
|
2427
2997
|
}
|
|
2428
2998
|
}
|
|
2429
2999
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2436,7 +3006,7 @@ class StableBrowser {
|
|
|
2436
3006
|
scroll: false,
|
|
2437
3007
|
highlight: false,
|
|
2438
3008
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2439
|
-
text: `Verify the text '${text}' does not exist in page`,
|
|
3009
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2440
3010
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2441
3011
|
operation: "verifyTextNotExistInPage",
|
|
2442
3012
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2480,7 +3050,7 @@ class StableBrowser {
|
|
|
2480
3050
|
await _commandError(state, e, this);
|
|
2481
3051
|
}
|
|
2482
3052
|
finally {
|
|
2483
|
-
_commandFinally(state, this);
|
|
3053
|
+
await _commandFinally(state, this);
|
|
2484
3054
|
}
|
|
2485
3055
|
}
|
|
2486
3056
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2550,7 +3120,7 @@ class StableBrowser {
|
|
|
2550
3120
|
const count = await frame.locator(css).count();
|
|
2551
3121
|
for (let j = 0; j < count; j++) {
|
|
2552
3122
|
const continer = await frame.locator(css).nth(j);
|
|
2553
|
-
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false,
|
|
3123
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2554
3124
|
if (result.elementCount > 0) {
|
|
2555
3125
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2556
3126
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2591,7 +3161,7 @@ class StableBrowser {
|
|
|
2591
3161
|
await _commandError(state, e, this);
|
|
2592
3162
|
}
|
|
2593
3163
|
finally {
|
|
2594
|
-
_commandFinally(state, this);
|
|
3164
|
+
await _commandFinally(state, this);
|
|
2595
3165
|
}
|
|
2596
3166
|
}
|
|
2597
3167
|
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
@@ -2933,8 +3503,51 @@ class StableBrowser {
|
|
|
2933
3503
|
});
|
|
2934
3504
|
}
|
|
2935
3505
|
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3508
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3509
|
+
* @param options - Optional configuration object
|
|
3510
|
+
* @param world - Optional world context
|
|
3511
|
+
* @returns Promise that resolves after the specified duration
|
|
3512
|
+
*/
|
|
3513
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3514
|
+
const state = {
|
|
3515
|
+
duration,
|
|
3516
|
+
options,
|
|
3517
|
+
world,
|
|
3518
|
+
locate: false,
|
|
3519
|
+
scroll: false,
|
|
3520
|
+
screenshot: false,
|
|
3521
|
+
highlight: false,
|
|
3522
|
+
type: Types.SLEEP,
|
|
3523
|
+
text: `Sleep for ${duration} ms`,
|
|
3524
|
+
_text: `Sleep for ${duration} ms`,
|
|
3525
|
+
operation: "sleep",
|
|
3526
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3527
|
+
};
|
|
3528
|
+
try {
|
|
3529
|
+
await _preCommand(state, this);
|
|
3530
|
+
if (duration < 0) {
|
|
3531
|
+
throw new Error("Sleep duration cannot be negative");
|
|
3532
|
+
}
|
|
3533
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3534
|
+
return state.info;
|
|
3535
|
+
}
|
|
3536
|
+
catch (e) {
|
|
3537
|
+
await _commandError(state, e, this);
|
|
3538
|
+
}
|
|
3539
|
+
finally {
|
|
3540
|
+
await _commandFinally(state, this);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
2936
3543
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2937
|
-
|
|
3544
|
+
try {
|
|
3545
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3546
|
+
}
|
|
3547
|
+
catch (error) {
|
|
3548
|
+
this.logger.debug(error);
|
|
3549
|
+
throw error;
|
|
3550
|
+
}
|
|
2938
3551
|
}
|
|
2939
3552
|
_getLoadTimeout(options) {
|
|
2940
3553
|
let timeout = 15000;
|
|
@@ -2957,6 +3570,7 @@ class StableBrowser {
|
|
|
2957
3570
|
}
|
|
2958
3571
|
async saveStoreState(path = null, world = null) {
|
|
2959
3572
|
const storageState = await this.page.context().storageState();
|
|
3573
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2960
3574
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2961
3575
|
if (path) {
|
|
2962
3576
|
// save { storageState: storageState } into the path
|
|
@@ -2967,10 +3581,14 @@ class StableBrowser {
|
|
|
2967
3581
|
}
|
|
2968
3582
|
}
|
|
2969
3583
|
async restoreSaveState(path = null, world = null) {
|
|
3584
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2970
3585
|
await refreshBrowser(this, path, world);
|
|
2971
3586
|
this.registerEventListeners(this.context);
|
|
2972
3587
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2973
3588
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3589
|
+
if (this.onRestoreSaveState) {
|
|
3590
|
+
this.onRestoreSaveState(path);
|
|
3591
|
+
}
|
|
2974
3592
|
}
|
|
2975
3593
|
async waitForPageLoad(options = {}, world = null) {
|
|
2976
3594
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -3053,7 +3671,7 @@ class StableBrowser {
|
|
|
3053
3671
|
await _commandError(state, e, this);
|
|
3054
3672
|
}
|
|
3055
3673
|
finally {
|
|
3056
|
-
_commandFinally(state, this);
|
|
3674
|
+
await _commandFinally(state, this);
|
|
3057
3675
|
}
|
|
3058
3676
|
}
|
|
3059
3677
|
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
@@ -3140,7 +3758,7 @@ class StableBrowser {
|
|
|
3140
3758
|
await _commandError(state, e, this);
|
|
3141
3759
|
}
|
|
3142
3760
|
finally {
|
|
3143
|
-
_commandFinally(state, this);
|
|
3761
|
+
await _commandFinally(state, this);
|
|
3144
3762
|
}
|
|
3145
3763
|
}
|
|
3146
3764
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3245,7 +3863,39 @@ class StableBrowser {
|
|
|
3245
3863
|
console.log("#-#");
|
|
3246
3864
|
}
|
|
3247
3865
|
}
|
|
3866
|
+
async beforeScenario(world, scenario) {
|
|
3867
|
+
this.beforeScenarioCalled = true;
|
|
3868
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3869
|
+
this.scenarioName = scenario.pickle.name;
|
|
3870
|
+
}
|
|
3871
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3872
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3873
|
+
}
|
|
3874
|
+
if (this.context) {
|
|
3875
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3876
|
+
}
|
|
3877
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3878
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3879
|
+
// check if @global_test_data tag is present
|
|
3880
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3881
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
// update test data based on feature/scenario
|
|
3885
|
+
let envName = null;
|
|
3886
|
+
if (this.context && this.context.environment) {
|
|
3887
|
+
envName = this.context.environment.name;
|
|
3888
|
+
}
|
|
3889
|
+
if (!process.env.TEMP_RUN) {
|
|
3890
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
3891
|
+
}
|
|
3892
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3893
|
+
}
|
|
3894
|
+
async afterScenario(world, scenario) { }
|
|
3248
3895
|
async beforeStep(world, step) {
|
|
3896
|
+
if (!this.beforeScenarioCalled) {
|
|
3897
|
+
this.beforeScenario(world, step);
|
|
3898
|
+
}
|
|
3249
3899
|
if (this.stepIndex === undefined) {
|
|
3250
3900
|
this.stepIndex = 0;
|
|
3251
3901
|
}
|
|
@@ -3262,24 +3912,14 @@ class StableBrowser {
|
|
|
3262
3912
|
else {
|
|
3263
3913
|
this.stepName = "step " + this.stepIndex;
|
|
3264
3914
|
}
|
|
3265
|
-
if (this.context) {
|
|
3266
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3267
|
-
}
|
|
3268
3915
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3269
3916
|
if (this.context.browserObject.context) {
|
|
3270
3917
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3271
3918
|
}
|
|
3272
3919
|
}
|
|
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
3920
|
if (this.initSnapshotTaken === false) {
|
|
3281
3921
|
this.initSnapshotTaken = true;
|
|
3282
|
-
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
3922
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3283
3923
|
const snapshot = await this.getAriaSnapshot();
|
|
3284
3924
|
if (snapshot) {
|
|
3285
3925
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
@@ -3301,18 +3941,68 @@ class StableBrowser {
|
|
|
3301
3941
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3302
3942
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3303
3943
|
for (let i = 0; i < frames.length; i++) {
|
|
3304
|
-
content.push(`- frame: ${i}`);
|
|
3305
3944
|
const frame = frames[i];
|
|
3306
|
-
|
|
3307
|
-
|
|
3945
|
+
try {
|
|
3946
|
+
// Ensure frame is attached and has body
|
|
3947
|
+
const body = frame.locator("body");
|
|
3948
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3949
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3950
|
+
content.push(`- frame: ${i}`);
|
|
3951
|
+
content.push(snapshot);
|
|
3952
|
+
}
|
|
3953
|
+
catch (innerErr) { }
|
|
3308
3954
|
}
|
|
3309
3955
|
return content.join("\n");
|
|
3310
3956
|
}
|
|
3311
3957
|
catch (e) {
|
|
3312
|
-
console.
|
|
3958
|
+
console.log("Error in getAriaSnapshot");
|
|
3959
|
+
//console.debug(e);
|
|
3313
3960
|
}
|
|
3314
3961
|
return null;
|
|
3315
3962
|
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Sends command with custom payload to report.
|
|
3965
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3966
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3967
|
+
* @param content - Content of the command to be shown in the report.
|
|
3968
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3969
|
+
* @param world - Optional world context.
|
|
3970
|
+
* @public
|
|
3971
|
+
*/
|
|
3972
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3973
|
+
const state = {
|
|
3974
|
+
options,
|
|
3975
|
+
world,
|
|
3976
|
+
locate: false,
|
|
3977
|
+
scroll: false,
|
|
3978
|
+
screenshot: options.screenshot ?? false,
|
|
3979
|
+
highlight: options.highlight ?? false,
|
|
3980
|
+
type: Types.REPORT_COMMAND,
|
|
3981
|
+
text: commandText,
|
|
3982
|
+
_text: commandText,
|
|
3983
|
+
operation: "report_command",
|
|
3984
|
+
log: "***** " + commandText + " *****\n",
|
|
3985
|
+
};
|
|
3986
|
+
try {
|
|
3987
|
+
await _preCommand(state, this);
|
|
3988
|
+
const payload = {
|
|
3989
|
+
type: options.type ?? "text",
|
|
3990
|
+
content: content,
|
|
3991
|
+
screenshotId: null,
|
|
3992
|
+
};
|
|
3993
|
+
state.payload = payload;
|
|
3994
|
+
if (commandStatus === "FAILED") {
|
|
3995
|
+
state.throwError = true;
|
|
3996
|
+
throw new Error("Command failed");
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
catch (e) {
|
|
4000
|
+
await _commandError(state, e, this);
|
|
4001
|
+
}
|
|
4002
|
+
finally {
|
|
4003
|
+
await _commandFinally(state, this);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
3316
4006
|
async afterStep(world, step) {
|
|
3317
4007
|
this.stepName = null;
|
|
3318
4008
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3320,6 +4010,13 @@ class StableBrowser {
|
|
|
3320
4010
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3321
4011
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3322
4012
|
});
|
|
4013
|
+
if (world && world.attach) {
|
|
4014
|
+
await world.attach(JSON.stringify({
|
|
4015
|
+
type: "trace",
|
|
4016
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4017
|
+
}), "application/json+trace");
|
|
4018
|
+
}
|
|
4019
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3323
4020
|
}
|
|
3324
4021
|
}
|
|
3325
4022
|
if (this.context) {
|
|
@@ -3332,6 +4029,29 @@ class StableBrowser {
|
|
|
3332
4029
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3333
4030
|
}
|
|
3334
4031
|
}
|
|
4032
|
+
if (!process.env.TEMP_RUN) {
|
|
4033
|
+
const state = {
|
|
4034
|
+
world,
|
|
4035
|
+
locate: false,
|
|
4036
|
+
scroll: false,
|
|
4037
|
+
screenshot: true,
|
|
4038
|
+
highlight: true,
|
|
4039
|
+
type: Types.STEP_COMPLETE,
|
|
4040
|
+
text: "end of scenario",
|
|
4041
|
+
_text: "end of scenario",
|
|
4042
|
+
operation: "step_complete",
|
|
4043
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4044
|
+
};
|
|
4045
|
+
try {
|
|
4046
|
+
await _preCommand(state, this);
|
|
4047
|
+
}
|
|
4048
|
+
catch (e) {
|
|
4049
|
+
await _commandError(state, e, this);
|
|
4050
|
+
}
|
|
4051
|
+
finally {
|
|
4052
|
+
await _commandFinally(state, this);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
3335
4055
|
}
|
|
3336
4056
|
}
|
|
3337
4057
|
function createTimedPromise(promise, label) {
|