automation_model 1.0.640-dev → 1.0.640-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 +16 -16
- 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 +96 -26
- 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 +4 -4
- package/lib/command_common.js +36 -16
- 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.d.ts +2 -2
- package/lib/init_browser.js +29 -27
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js +2 -2
- package/lib/locate_element.js.map +1 -1
- package/lib/locator.js +1 -1
- package/lib/locator.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +1 -1
- package/lib/network.js +5 -5
- package/lib/network.js.map +1 -1
- 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 +51 -24
- package/lib/stable_browser.js +741 -195
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.js.map +1 -1
- package/lib/table_analyze.js.map +1 -1
- package/lib/table_helper.js +15 -0
- package/lib/table_helper.js.map +1 -1
- package/lib/test_context.d.ts +2 -0
- package/lib/test_context.js +2 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +6 -4
- package/lib/utils.js +131 -20
- package/lib/utils.js.map +1 -1
- package/package.json +11 -6
package/lib/stable_browser.js
CHANGED
|
@@ -15,6 +15,7 @@ import csv from "csv-parser";
|
|
|
15
15
|
import { Readable } from "node:stream";
|
|
16
16
|
import readline from "readline";
|
|
17
17
|
import { getContext, refreshBrowser } from "./init_browser.js";
|
|
18
|
+
import { getTestData } from "./auto_page.js";
|
|
18
19
|
import { locate_element } from "./locate_element.js";
|
|
19
20
|
import { randomUUID } from "crypto";
|
|
20
21
|
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
@@ -22,23 +23,26 @@ import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
|
22
23
|
import { LocatorLog } from "./locator_log.js";
|
|
23
24
|
import axios from "axios";
|
|
24
25
|
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
|
+
import { snapshotValidation } from "./snapshot_validation.js";
|
|
27
|
+
import { loadBrunoParams } from "./bruno.js";
|
|
25
28
|
export const Types = {
|
|
26
29
|
CLICK: "click_element",
|
|
27
30
|
WAIT_ELEMENT: "wait_element",
|
|
28
|
-
NAVIGATE: "navigate",
|
|
31
|
+
NAVIGATE: "navigate", ///
|
|
29
32
|
FILL: "fill_element",
|
|
30
|
-
EXECUTE: "execute_page_method",
|
|
31
|
-
OPEN: "open_environment",
|
|
33
|
+
EXECUTE: "execute_page_method", //
|
|
34
|
+
OPEN: "open_environment", //
|
|
32
35
|
COMPLETE: "step_complete",
|
|
33
36
|
ASK: "information_needed",
|
|
34
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
35
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
37
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
38
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
36
39
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
37
40
|
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
38
41
|
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
39
42
|
ANALYZE_TABLE: "analyze_table",
|
|
40
|
-
SELECT: "select_combobox",
|
|
43
|
+
SELECT: "select_combobox", //
|
|
41
44
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
45
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
42
46
|
TYPE_PRESS: "type_press",
|
|
43
47
|
PRESS: "press_key",
|
|
44
48
|
HOVER: "hover_element",
|
|
@@ -55,6 +59,12 @@ export const Types = {
|
|
|
55
59
|
WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
|
|
56
60
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
57
61
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
62
|
+
BRUNO: "bruno",
|
|
63
|
+
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
64
|
+
SET_INPUT_FILES: "set_input_files",
|
|
65
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
66
|
+
REPORT_COMMAND: "report_command",
|
|
67
|
+
STEP_COMPLETE: "step_complete",
|
|
58
68
|
};
|
|
59
69
|
export const apps = {};
|
|
60
70
|
const formatElementName = (elementName) => {
|
|
@@ -118,6 +128,9 @@ class StableBrowser {
|
|
|
118
128
|
if (!context.pageLoading) {
|
|
119
129
|
context.pageLoading = { status: false };
|
|
120
130
|
}
|
|
131
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
132
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
133
|
+
}
|
|
121
134
|
context.playContext.on("page", async function (page) {
|
|
122
135
|
if (this.configuration && this.configuration.closePopups === true) {
|
|
123
136
|
console.log("close unexpected popups");
|
|
@@ -126,6 +139,14 @@ class StableBrowser {
|
|
|
126
139
|
}
|
|
127
140
|
context.pageLoading.status = true;
|
|
128
141
|
this.page = page;
|
|
142
|
+
try {
|
|
143
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
144
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error("Error on dialog accept registration", error);
|
|
149
|
+
}
|
|
129
150
|
context.page = page;
|
|
130
151
|
context.pages.push(page);
|
|
131
152
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
@@ -180,6 +201,30 @@ class StableBrowser {
|
|
|
180
201
|
await this.waitForPageLoad();
|
|
181
202
|
}
|
|
182
203
|
}
|
|
204
|
+
async switchTab(tabTitleOrIndex) {
|
|
205
|
+
// first check if the tabNameOrIndex is a number
|
|
206
|
+
let index = parseInt(tabTitleOrIndex);
|
|
207
|
+
if (!isNaN(index)) {
|
|
208
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
209
|
+
this.page = this.context.pages[index];
|
|
210
|
+
this.context.page = this.page;
|
|
211
|
+
await this.page.bringToFront();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
216
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
217
|
+
let page = this.context.pages[i];
|
|
218
|
+
let title = await page.title();
|
|
219
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
220
|
+
this.page = page;
|
|
221
|
+
this.context.page = this.page;
|
|
222
|
+
await this.page.bringToFront();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
227
|
+
}
|
|
183
228
|
registerConsoleLogListener(page, context) {
|
|
184
229
|
if (!this.context.webLogger) {
|
|
185
230
|
this.context.webLogger = [];
|
|
@@ -247,6 +292,7 @@ class StableBrowser {
|
|
|
247
292
|
if (!url) {
|
|
248
293
|
throw new Error("url is null, verify that the environment file is correct");
|
|
249
294
|
}
|
|
295
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
250
296
|
if (!url.startsWith("http")) {
|
|
251
297
|
url = "https://" + url;
|
|
252
298
|
}
|
|
@@ -275,7 +321,7 @@ class StableBrowser {
|
|
|
275
321
|
_commandError(state, error, this);
|
|
276
322
|
}
|
|
277
323
|
finally {
|
|
278
|
-
_commandFinally(state, this);
|
|
324
|
+
await _commandFinally(state, this);
|
|
279
325
|
}
|
|
280
326
|
}
|
|
281
327
|
async _getLocator(locator, scope, _params) {
|
|
@@ -391,7 +437,7 @@ class StableBrowser {
|
|
|
391
437
|
}
|
|
392
438
|
return { elementCount: tagCount, randomToken };
|
|
393
439
|
}
|
|
394
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
440
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
395
441
|
if (!info) {
|
|
396
442
|
info = {};
|
|
397
443
|
}
|
|
@@ -458,7 +504,7 @@ class StableBrowser {
|
|
|
458
504
|
}
|
|
459
505
|
return;
|
|
460
506
|
}
|
|
461
|
-
if (info.locatorLog && count === 0) {
|
|
507
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
462
508
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
463
509
|
}
|
|
464
510
|
for (let j = 0; j < count; j++) {
|
|
@@ -473,7 +519,7 @@ class StableBrowser {
|
|
|
473
519
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
474
520
|
}
|
|
475
521
|
}
|
|
476
|
-
else {
|
|
522
|
+
else if (logErrors) {
|
|
477
523
|
info.failCause.visible = visible;
|
|
478
524
|
info.failCause.enabled = enabled;
|
|
479
525
|
if (!info.printMessages) {
|
|
@@ -565,15 +611,27 @@ class StableBrowser {
|
|
|
565
611
|
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
566
612
|
if (!element.rerun) {
|
|
567
613
|
const randomToken = Math.random().toString(36).substring(7);
|
|
568
|
-
element.evaluate((el, randomToken) => {
|
|
614
|
+
await element.evaluate((el, randomToken) => {
|
|
569
615
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
570
616
|
}, randomToken);
|
|
571
|
-
if (element._frame) {
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
const scope = element.page();
|
|
575
|
-
|
|
576
|
-
|
|
617
|
+
// if (element._frame) {
|
|
618
|
+
// return element;
|
|
619
|
+
// }
|
|
620
|
+
const scope = element._frame ?? element.page();
|
|
621
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
622
|
+
let prefixSelector = "";
|
|
623
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
624
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
625
|
+
if (frameSelectorIndex !== -1) {
|
|
626
|
+
// remove everything after the >> internal:control=enter-frame
|
|
627
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
628
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
629
|
+
}
|
|
630
|
+
// if (element?._frame?._selector) {
|
|
631
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
632
|
+
// }
|
|
633
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
634
|
+
return scope.locator(newSelector);
|
|
577
635
|
}
|
|
578
636
|
}
|
|
579
637
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -725,14 +783,9 @@ class StableBrowser {
|
|
|
725
783
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
726
784
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
727
785
|
}
|
|
728
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
786
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
729
787
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
730
788
|
}
|
|
731
|
-
else {
|
|
732
|
-
if (result.foundElements.length === 0 && !highPriorityOnly) {
|
|
733
|
-
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
789
|
let foundElements = result.foundElements;
|
|
737
790
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
738
791
|
info.box = foundElements[0].box;
|
|
@@ -787,6 +840,11 @@ class StableBrowser {
|
|
|
787
840
|
visibleOnly = false;
|
|
788
841
|
}
|
|
789
842
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
843
|
+
// sheck of more of half of the timeout has passed
|
|
844
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
845
|
+
highPriorityOnly = false;
|
|
846
|
+
visibleOnly = false;
|
|
847
|
+
}
|
|
790
848
|
}
|
|
791
849
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
792
850
|
// if (info.locatorLog) {
|
|
@@ -802,7 +860,7 @@ class StableBrowser {
|
|
|
802
860
|
}
|
|
803
861
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
804
862
|
}
|
|
805
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
863
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
806
864
|
let foundElements = [];
|
|
807
865
|
const result = {
|
|
808
866
|
foundElements: foundElements,
|
|
@@ -821,7 +879,9 @@ class StableBrowser {
|
|
|
821
879
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
822
880
|
}
|
|
823
881
|
catch (e) {
|
|
824
|
-
|
|
882
|
+
if (logErrors) {
|
|
883
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
884
|
+
}
|
|
825
885
|
}
|
|
826
886
|
}
|
|
827
887
|
if (foundLocators.length === 1) {
|
|
@@ -833,9 +893,40 @@ class StableBrowser {
|
|
|
833
893
|
result.locatorIndex = i;
|
|
834
894
|
}
|
|
835
895
|
if (foundLocators.length > 1) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
896
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
897
|
+
const boxes = [];
|
|
898
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
899
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
900
|
+
}
|
|
901
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
902
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
903
|
+
if (j === k) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
907
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
908
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
909
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
910
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
911
|
+
// as the element is not unique, will remove it
|
|
912
|
+
boxes.splice(k, 1);
|
|
913
|
+
k--;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (boxes.length === 1) {
|
|
918
|
+
result.foundElements.push({
|
|
919
|
+
locator: boxes[0].locator.first(),
|
|
920
|
+
box: boxes[0].box,
|
|
921
|
+
unique: true,
|
|
922
|
+
});
|
|
923
|
+
result.locatorIndex = i;
|
|
924
|
+
}
|
|
925
|
+
else if (logErrors) {
|
|
926
|
+
info.failCause.foundMultiple = true;
|
|
927
|
+
if (info.locatorLog) {
|
|
928
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
929
|
+
}
|
|
839
930
|
}
|
|
840
931
|
}
|
|
841
932
|
}
|
|
@@ -883,7 +974,7 @@ class StableBrowser {
|
|
|
883
974
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
884
975
|
}
|
|
885
976
|
finally {
|
|
886
|
-
_commandFinally(state, this);
|
|
977
|
+
await _commandFinally(state, this);
|
|
887
978
|
}
|
|
888
979
|
}
|
|
889
980
|
}
|
|
@@ -932,7 +1023,7 @@ class StableBrowser {
|
|
|
932
1023
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
933
1024
|
}
|
|
934
1025
|
finally {
|
|
935
|
-
_commandFinally(state, this);
|
|
1026
|
+
await _commandFinally(state, this);
|
|
936
1027
|
}
|
|
937
1028
|
}
|
|
938
1029
|
}
|
|
@@ -961,7 +1052,7 @@ class StableBrowser {
|
|
|
961
1052
|
await _commandError(state, e, this);
|
|
962
1053
|
}
|
|
963
1054
|
finally {
|
|
964
|
-
_commandFinally(state, this);
|
|
1055
|
+
await _commandFinally(state, this);
|
|
965
1056
|
}
|
|
966
1057
|
}
|
|
967
1058
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -992,7 +1083,7 @@ class StableBrowser {
|
|
|
992
1083
|
// await _commandError(state, e, this);
|
|
993
1084
|
}
|
|
994
1085
|
finally {
|
|
995
|
-
_commandFinally(state, this);
|
|
1086
|
+
await _commandFinally(state, this);
|
|
996
1087
|
}
|
|
997
1088
|
return found;
|
|
998
1089
|
}
|
|
@@ -1016,8 +1107,8 @@ class StableBrowser {
|
|
|
1016
1107
|
try {
|
|
1017
1108
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1018
1109
|
// console.log(`Highlighting while running from recorder`);
|
|
1019
|
-
await this._highlightElements(element);
|
|
1020
|
-
await state.element.setChecked(checked);
|
|
1110
|
+
await this._highlightElements(state.element);
|
|
1111
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
1021
1112
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1022
1113
|
// await this._unHighlightElements(element);
|
|
1023
1114
|
// }
|
|
@@ -1029,11 +1120,28 @@ class StableBrowser {
|
|
|
1029
1120
|
this.logger.info("element did not change its state, ignoring...");
|
|
1030
1121
|
}
|
|
1031
1122
|
else {
|
|
1123
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1032
1124
|
//await this.closeUnexpectedPopups();
|
|
1033
1125
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1034
|
-
state.
|
|
1035
|
-
|
|
1036
|
-
|
|
1126
|
+
state.element_found = false;
|
|
1127
|
+
try {
|
|
1128
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1129
|
+
state.element_found = true;
|
|
1130
|
+
// check the check state
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
// element dismissed
|
|
1134
|
+
}
|
|
1135
|
+
if (state.element_found) {
|
|
1136
|
+
const isChecked = await state.element.isChecked();
|
|
1137
|
+
if (isChecked !== checked) {
|
|
1138
|
+
// perform click
|
|
1139
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1037
1145
|
}
|
|
1038
1146
|
}
|
|
1039
1147
|
await this.waitForPageLoad();
|
|
@@ -1043,7 +1151,7 @@ class StableBrowser {
|
|
|
1043
1151
|
await _commandError(state, e, this);
|
|
1044
1152
|
}
|
|
1045
1153
|
finally {
|
|
1046
|
-
_commandFinally(state, this);
|
|
1154
|
+
await _commandFinally(state, this);
|
|
1047
1155
|
}
|
|
1048
1156
|
}
|
|
1049
1157
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1069,7 +1177,7 @@ class StableBrowser {
|
|
|
1069
1177
|
await _commandError(state, e, this);
|
|
1070
1178
|
}
|
|
1071
1179
|
finally {
|
|
1072
|
-
_commandFinally(state, this);
|
|
1180
|
+
await _commandFinally(state, this);
|
|
1073
1181
|
}
|
|
1074
1182
|
}
|
|
1075
1183
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1105,7 +1213,7 @@ class StableBrowser {
|
|
|
1105
1213
|
await _commandError(state, e, this);
|
|
1106
1214
|
}
|
|
1107
1215
|
finally {
|
|
1108
|
-
_commandFinally(state, this);
|
|
1216
|
+
await _commandFinally(state, this);
|
|
1109
1217
|
}
|
|
1110
1218
|
}
|
|
1111
1219
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1151,7 +1259,7 @@ class StableBrowser {
|
|
|
1151
1259
|
await _commandError(state, e, this);
|
|
1152
1260
|
}
|
|
1153
1261
|
finally {
|
|
1154
|
-
_commandFinally(state, this);
|
|
1262
|
+
await _commandFinally(state, this);
|
|
1155
1263
|
}
|
|
1156
1264
|
}
|
|
1157
1265
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1187,7 +1295,7 @@ class StableBrowser {
|
|
|
1187
1295
|
await _commandError(state, e, this);
|
|
1188
1296
|
}
|
|
1189
1297
|
finally {
|
|
1190
|
-
_commandFinally(state, this);
|
|
1298
|
+
await _commandFinally(state, this);
|
|
1191
1299
|
}
|
|
1192
1300
|
}
|
|
1193
1301
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1256,7 +1364,7 @@ class StableBrowser {
|
|
|
1256
1364
|
await _commandError(state, e, this);
|
|
1257
1365
|
}
|
|
1258
1366
|
finally {
|
|
1259
|
-
_commandFinally(state, this);
|
|
1367
|
+
await _commandFinally(state, this);
|
|
1260
1368
|
}
|
|
1261
1369
|
}
|
|
1262
1370
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1275,6 +1383,9 @@ class StableBrowser {
|
|
|
1275
1383
|
operation: "clickType",
|
|
1276
1384
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1277
1385
|
};
|
|
1386
|
+
if (!options) {
|
|
1387
|
+
options = {};
|
|
1388
|
+
}
|
|
1278
1389
|
if (newValue !== _value) {
|
|
1279
1390
|
//this.logger.info(_value + "=" + newValue);
|
|
1280
1391
|
_value = newValue;
|
|
@@ -1282,7 +1393,7 @@ class StableBrowser {
|
|
|
1282
1393
|
try {
|
|
1283
1394
|
await _preCommand(state, this);
|
|
1284
1395
|
state.info.value = _value;
|
|
1285
|
-
if (
|
|
1396
|
+
if (!options.press) {
|
|
1286
1397
|
try {
|
|
1287
1398
|
let currentValue = await state.element.inputValue();
|
|
1288
1399
|
if (currentValue) {
|
|
@@ -1293,10 +1404,7 @@ class StableBrowser {
|
|
|
1293
1404
|
this.logger.info("unable to clear input value");
|
|
1294
1405
|
}
|
|
1295
1406
|
}
|
|
1296
|
-
if (options
|
|
1297
|
-
if (!options) {
|
|
1298
|
-
options = {};
|
|
1299
|
-
}
|
|
1407
|
+
if (options.press) {
|
|
1300
1408
|
options.timeout = 5000;
|
|
1301
1409
|
await performAction("click", state.element, options, this, state, _params);
|
|
1302
1410
|
}
|
|
@@ -1356,7 +1464,7 @@ class StableBrowser {
|
|
|
1356
1464
|
await _commandError(state, e, this);
|
|
1357
1465
|
}
|
|
1358
1466
|
finally {
|
|
1359
|
-
_commandFinally(state, this);
|
|
1467
|
+
await _commandFinally(state, this);
|
|
1360
1468
|
}
|
|
1361
1469
|
}
|
|
1362
1470
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1386,7 +1494,42 @@ class StableBrowser {
|
|
|
1386
1494
|
await _commandError(state, e, this);
|
|
1387
1495
|
}
|
|
1388
1496
|
finally {
|
|
1389
|
-
_commandFinally(state, this);
|
|
1497
|
+
await _commandFinally(state, this);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1501
|
+
const state = {
|
|
1502
|
+
selectors,
|
|
1503
|
+
_params,
|
|
1504
|
+
files,
|
|
1505
|
+
value: '"' + files.join('", "') + '"',
|
|
1506
|
+
options,
|
|
1507
|
+
world,
|
|
1508
|
+
type: Types.SET_INPUT_FILES,
|
|
1509
|
+
text: `Set input files`,
|
|
1510
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1511
|
+
operation: "setInputFiles",
|
|
1512
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1513
|
+
};
|
|
1514
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1515
|
+
try {
|
|
1516
|
+
await _preCommand(state, this);
|
|
1517
|
+
for (let i = 0; i < files.length; i++) {
|
|
1518
|
+
const file = files[i];
|
|
1519
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1520
|
+
if (!fs.existsSync(filePath)) {
|
|
1521
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1522
|
+
}
|
|
1523
|
+
state.files[i] = filePath;
|
|
1524
|
+
}
|
|
1525
|
+
await state.element.setInputFiles(files);
|
|
1526
|
+
return state.info;
|
|
1527
|
+
}
|
|
1528
|
+
catch (e) {
|
|
1529
|
+
await _commandError(state, e, this);
|
|
1530
|
+
}
|
|
1531
|
+
finally {
|
|
1532
|
+
await _commandFinally(state, this);
|
|
1390
1533
|
}
|
|
1391
1534
|
}
|
|
1392
1535
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1502,7 +1645,7 @@ class StableBrowser {
|
|
|
1502
1645
|
await _commandError(state, e, this);
|
|
1503
1646
|
}
|
|
1504
1647
|
finally {
|
|
1505
|
-
_commandFinally(state, this);
|
|
1648
|
+
await _commandFinally(state, this);
|
|
1506
1649
|
}
|
|
1507
1650
|
}
|
|
1508
1651
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1537,7 +1680,7 @@ class StableBrowser {
|
|
|
1537
1680
|
while (Date.now() - startTime < timeout) {
|
|
1538
1681
|
try {
|
|
1539
1682
|
await _preCommand(state, this);
|
|
1540
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1683
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1541
1684
|
if (foundObj && foundObj.element) {
|
|
1542
1685
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1543
1686
|
}
|
|
@@ -1579,7 +1722,79 @@ class StableBrowser {
|
|
|
1579
1722
|
throw e;
|
|
1580
1723
|
}
|
|
1581
1724
|
finally {
|
|
1582
|
-
_commandFinally(state, this);
|
|
1725
|
+
await _commandFinally(state, this);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1729
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1730
|
+
const startTime = Date.now();
|
|
1731
|
+
const state = {
|
|
1732
|
+
_params,
|
|
1733
|
+
value: referanceSnapshot,
|
|
1734
|
+
options,
|
|
1735
|
+
world,
|
|
1736
|
+
locate: false,
|
|
1737
|
+
scroll: false,
|
|
1738
|
+
screenshot: true,
|
|
1739
|
+
highlight: false,
|
|
1740
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1741
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1742
|
+
operation: "snapshotValidation",
|
|
1743
|
+
log: "***** verify snapshot *****\n",
|
|
1744
|
+
};
|
|
1745
|
+
if (!referanceSnapshot) {
|
|
1746
|
+
throw new Error("referanceSnapshot is null");
|
|
1747
|
+
}
|
|
1748
|
+
let text = null;
|
|
1749
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1750
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1751
|
+
}
|
|
1752
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1753
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1754
|
+
}
|
|
1755
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1756
|
+
text = referanceSnapshot.substring(5);
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1760
|
+
}
|
|
1761
|
+
state.text = text;
|
|
1762
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1763
|
+
await _preCommand(state, this);
|
|
1764
|
+
let foundObj = null;
|
|
1765
|
+
try {
|
|
1766
|
+
let matchResult = null;
|
|
1767
|
+
while (Date.now() - startTime < timeout) {
|
|
1768
|
+
try {
|
|
1769
|
+
let scope = null;
|
|
1770
|
+
if (!frameSelectors) {
|
|
1771
|
+
scope = this.page;
|
|
1772
|
+
}
|
|
1773
|
+
else {
|
|
1774
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1775
|
+
}
|
|
1776
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1777
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1778
|
+
if (matchResult.errorLine !== -1) {
|
|
1779
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1780
|
+
}
|
|
1781
|
+
// highlight and screenshot
|
|
1782
|
+
return state.info;
|
|
1783
|
+
}
|
|
1784
|
+
catch (e) {
|
|
1785
|
+
// Log error but continue retrying until timeout is reached
|
|
1786
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1787
|
+
}
|
|
1788
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1789
|
+
}
|
|
1790
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1791
|
+
}
|
|
1792
|
+
catch (e) {
|
|
1793
|
+
await _commandError(state, e, this);
|
|
1794
|
+
throw e;
|
|
1795
|
+
}
|
|
1796
|
+
finally {
|
|
1797
|
+
await _commandFinally(state, this);
|
|
1583
1798
|
}
|
|
1584
1799
|
}
|
|
1585
1800
|
async waitForUserInput(message, world = null) {
|
|
@@ -1617,6 +1832,15 @@ class StableBrowser {
|
|
|
1617
1832
|
// save the data to the file
|
|
1618
1833
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1619
1834
|
}
|
|
1835
|
+
overwriteTestData(testData, world = null) {
|
|
1836
|
+
if (!testData) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
// if data file exists, load it
|
|
1840
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1841
|
+
// save the data to the file
|
|
1842
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1843
|
+
}
|
|
1620
1844
|
_getDataFilePath(fileName) {
|
|
1621
1845
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1622
1846
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1869,7 +2093,7 @@ class StableBrowser {
|
|
|
1869
2093
|
await _commandError(state, e, this);
|
|
1870
2094
|
}
|
|
1871
2095
|
finally {
|
|
1872
|
-
_commandFinally(state, this);
|
|
2096
|
+
await _commandFinally(state, this);
|
|
1873
2097
|
}
|
|
1874
2098
|
}
|
|
1875
2099
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1900,10 +2124,31 @@ class StableBrowser {
|
|
|
1900
2124
|
case "value":
|
|
1901
2125
|
state.value = await state.element.inputValue();
|
|
1902
2126
|
break;
|
|
2127
|
+
case "text":
|
|
2128
|
+
state.value = await state.element.textContent();
|
|
2129
|
+
break;
|
|
1903
2130
|
default:
|
|
1904
2131
|
state.value = await state.element.getAttribute(attribute);
|
|
1905
2132
|
break;
|
|
1906
2133
|
}
|
|
2134
|
+
if (options !== null) {
|
|
2135
|
+
if (options.regex && options.regex !== "") {
|
|
2136
|
+
// Construct a regex pattern from the provided string
|
|
2137
|
+
const regex = options.regex.slice(1, -1);
|
|
2138
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2139
|
+
const matches = state.value.match(regexPattern);
|
|
2140
|
+
if (matches) {
|
|
2141
|
+
let newValue = "";
|
|
2142
|
+
for (const match of matches) {
|
|
2143
|
+
newValue += match;
|
|
2144
|
+
}
|
|
2145
|
+
state.value = newValue;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2149
|
+
state.value = state.value.trim();
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
1907
2152
|
state.info.value = state.value;
|
|
1908
2153
|
this.setTestData({ [variable]: state.value }, world);
|
|
1909
2154
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1914,7 +2159,7 @@ class StableBrowser {
|
|
|
1914
2159
|
await _commandError(state, e, this);
|
|
1915
2160
|
}
|
|
1916
2161
|
finally {
|
|
1917
|
-
_commandFinally(state, this);
|
|
2162
|
+
await _commandFinally(state, this);
|
|
1918
2163
|
}
|
|
1919
2164
|
}
|
|
1920
2165
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1939,12 +2184,15 @@ class StableBrowser {
|
|
|
1939
2184
|
let expectedValue;
|
|
1940
2185
|
try {
|
|
1941
2186
|
await _preCommand(state, this);
|
|
1942
|
-
expectedValue = state.value;
|
|
2187
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1943
2188
|
state.info.expectedValue = expectedValue;
|
|
1944
2189
|
switch (attribute) {
|
|
1945
2190
|
case "innerText":
|
|
1946
2191
|
val = String(await state.element.innerText());
|
|
1947
2192
|
break;
|
|
2193
|
+
case "text":
|
|
2194
|
+
val = String(await state.element.textContent());
|
|
2195
|
+
break;
|
|
1948
2196
|
case "value":
|
|
1949
2197
|
val = String(await state.element.inputValue());
|
|
1950
2198
|
break;
|
|
@@ -1966,17 +2214,42 @@ class StableBrowser {
|
|
|
1966
2214
|
let regex;
|
|
1967
2215
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1968
2216
|
const patternBody = expectedValue.slice(1, -1);
|
|
1969
|
-
|
|
2217
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2218
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2219
|
+
state.info.regex = true;
|
|
1970
2220
|
}
|
|
1971
2221
|
else {
|
|
1972
2222
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1973
2223
|
regex = new RegExp(escapedPattern, "g");
|
|
1974
2224
|
}
|
|
1975
|
-
if (
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2225
|
+
if (attribute === "innerText") {
|
|
2226
|
+
if (state.info.regex) {
|
|
2227
|
+
if (!regex.test(val)) {
|
|
2228
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2229
|
+
state.info.failCause.assertionFailed = true;
|
|
2230
|
+
state.info.failCause.lastError = errorMessage;
|
|
2231
|
+
throw new Error(errorMessage);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
else {
|
|
2235
|
+
const valLines = val.split("\n");
|
|
2236
|
+
const expectedLines = expectedValue.split("\n");
|
|
2237
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2238
|
+
if (!isPart) {
|
|
2239
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2240
|
+
state.info.failCause.assertionFailed = true;
|
|
2241
|
+
state.info.failCause.lastError = errorMessage;
|
|
2242
|
+
throw new Error(errorMessage);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
else {
|
|
2247
|
+
if (!val.match(regex)) {
|
|
2248
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2249
|
+
state.info.failCause.assertionFailed = true;
|
|
2250
|
+
state.info.failCause.lastError = errorMessage;
|
|
2251
|
+
throw new Error(errorMessage);
|
|
2252
|
+
}
|
|
1980
2253
|
}
|
|
1981
2254
|
return state.info;
|
|
1982
2255
|
}
|
|
@@ -1984,7 +2257,7 @@ class StableBrowser {
|
|
|
1984
2257
|
await _commandError(state, e, this);
|
|
1985
2258
|
}
|
|
1986
2259
|
finally {
|
|
1987
|
-
_commandFinally(state, this);
|
|
2260
|
+
await _commandFinally(state, this);
|
|
1988
2261
|
}
|
|
1989
2262
|
}
|
|
1990
2263
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2144,56 +2417,49 @@ class StableBrowser {
|
|
|
2144
2417
|
console.debug(error);
|
|
2145
2418
|
}
|
|
2146
2419
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
// });
|
|
2190
|
-
// }
|
|
2191
|
-
// } catch (error) {
|
|
2192
|
-
// // console.debug(error);
|
|
2193
|
-
// }
|
|
2194
|
-
// }
|
|
2420
|
+
_matcher(text) {
|
|
2421
|
+
if (!text) {
|
|
2422
|
+
return { matcher: "contains", queryText: "" };
|
|
2423
|
+
}
|
|
2424
|
+
if (text.length < 2) {
|
|
2425
|
+
return { matcher: "contains", queryText: text };
|
|
2426
|
+
}
|
|
2427
|
+
const split = text.split(":");
|
|
2428
|
+
const matcher = split[0].toLowerCase();
|
|
2429
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2430
|
+
return { matcher, queryText };
|
|
2431
|
+
}
|
|
2432
|
+
_getDomain(url) {
|
|
2433
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2434
|
+
return "";
|
|
2435
|
+
}
|
|
2436
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2437
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2438
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2439
|
+
}
|
|
2440
|
+
let n = hostnameFragments.length;
|
|
2441
|
+
let fragments = [...hostnameFragments];
|
|
2442
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2443
|
+
hostnameFragments.pop();
|
|
2444
|
+
n = hostnameFragments.length;
|
|
2445
|
+
}
|
|
2446
|
+
if (n == 0) {
|
|
2447
|
+
if (fragments[0] === "www")
|
|
2448
|
+
fragments = fragments.slice(1);
|
|
2449
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2450
|
+
}
|
|
2451
|
+
if (hostnameFragments[0] === "www")
|
|
2452
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2453
|
+
return hostnameFragments.join(".");
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Verify the page path matches the given path.
|
|
2457
|
+
* @param {string} pathPart - The path to verify.
|
|
2458
|
+
* @param {object} options - Options for verification.
|
|
2459
|
+
* @param {object} world - The world context.
|
|
2460
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2461
|
+
*/
|
|
2195
2462
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2196
|
-
const startTime = Date.now();
|
|
2197
2463
|
let error = null;
|
|
2198
2464
|
let screenshotId = null;
|
|
2199
2465
|
let screenshotPath = null;
|
|
@@ -2207,74 +2473,235 @@ class StableBrowser {
|
|
|
2207
2473
|
pathPart = newValue;
|
|
2208
2474
|
}
|
|
2209
2475
|
info.pathPart = pathPart;
|
|
2476
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2477
|
+
const state = {
|
|
2478
|
+
text_search: queryText,
|
|
2479
|
+
options,
|
|
2480
|
+
world,
|
|
2481
|
+
locate: false,
|
|
2482
|
+
scroll: false,
|
|
2483
|
+
highlight: false,
|
|
2484
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2485
|
+
text: `Verify the page url is ${queryText}`,
|
|
2486
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2487
|
+
operation: "verifyPagePath",
|
|
2488
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2489
|
+
};
|
|
2210
2490
|
try {
|
|
2491
|
+
await _preCommand(state, this);
|
|
2492
|
+
state.info.text = queryText;
|
|
2211
2493
|
for (let i = 0; i < 30; i++) {
|
|
2212
2494
|
const url = await this.page.url();
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2495
|
+
switch (matcher) {
|
|
2496
|
+
case "exact":
|
|
2497
|
+
if (url !== queryText) {
|
|
2498
|
+
if (i === 29) {
|
|
2499
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2500
|
+
}
|
|
2501
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
break;
|
|
2505
|
+
case "contains":
|
|
2506
|
+
if (!url.includes(queryText)) {
|
|
2507
|
+
if (i === 29) {
|
|
2508
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2509
|
+
}
|
|
2510
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
break;
|
|
2514
|
+
case "starts-with":
|
|
2515
|
+
{
|
|
2516
|
+
const domain = this._getDomain(url);
|
|
2517
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2518
|
+
if (i === 29) {
|
|
2519
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2520
|
+
}
|
|
2521
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
break;
|
|
2526
|
+
case "ends-with":
|
|
2527
|
+
{
|
|
2528
|
+
const urlObj = new URL(url);
|
|
2529
|
+
let route = "/";
|
|
2530
|
+
if (urlObj.pathname !== "/") {
|
|
2531
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2532
|
+
}
|
|
2533
|
+
else {
|
|
2534
|
+
route = "/";
|
|
2535
|
+
}
|
|
2536
|
+
if (route !== queryText) {
|
|
2537
|
+
if (i === 29) {
|
|
2538
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2539
|
+
}
|
|
2540
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
break;
|
|
2545
|
+
case "regex":
|
|
2546
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2547
|
+
if (!regex.test(url)) {
|
|
2548
|
+
if (i === 29) {
|
|
2549
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2550
|
+
}
|
|
2551
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
break;
|
|
2555
|
+
default:
|
|
2556
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2557
|
+
if (!url.includes(pathPart)) {
|
|
2558
|
+
if (i === 29) {
|
|
2559
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2560
|
+
}
|
|
2561
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2562
|
+
continue;
|
|
2563
|
+
}
|
|
2219
2564
|
}
|
|
2220
|
-
|
|
2221
|
-
return info;
|
|
2565
|
+
await _screenshot(state, this);
|
|
2566
|
+
return state.info;
|
|
2222
2567
|
}
|
|
2223
2568
|
}
|
|
2224
2569
|
catch (e) {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
info.screenshotPath = screenshotPath;
|
|
2229
|
-
Object.assign(e, { info: info });
|
|
2230
|
-
error = e;
|
|
2231
|
-
// throw e;
|
|
2232
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2570
|
+
state.info.failCause.lastError = e.message;
|
|
2571
|
+
state.info.failCause.assertionFailed = true;
|
|
2572
|
+
await _commandError(state, e, this);
|
|
2233
2573
|
}
|
|
2234
2574
|
finally {
|
|
2235
|
-
|
|
2236
|
-
_reportToWorld(world, {
|
|
2237
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2238
|
-
text: "Verify page path",
|
|
2239
|
-
_text: "Verify the page path contains " + pathPart,
|
|
2240
|
-
screenshotId,
|
|
2241
|
-
result: error
|
|
2242
|
-
? {
|
|
2243
|
-
status: "FAILED",
|
|
2244
|
-
startTime,
|
|
2245
|
-
endTime,
|
|
2246
|
-
message: error?.message,
|
|
2247
|
-
}
|
|
2248
|
-
: {
|
|
2249
|
-
status: "PASSED",
|
|
2250
|
-
startTime,
|
|
2251
|
-
endTime,
|
|
2252
|
-
},
|
|
2253
|
-
info: info,
|
|
2254
|
-
});
|
|
2575
|
+
await _commandFinally(state, this);
|
|
2255
2576
|
}
|
|
2256
2577
|
}
|
|
2257
|
-
|
|
2578
|
+
/**
|
|
2579
|
+
* Verify the page title matches the given title.
|
|
2580
|
+
* @param {string} title - The title to verify.
|
|
2581
|
+
* @param {object} options - Options for verification.
|
|
2582
|
+
* @param {object} world - The world context.
|
|
2583
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2584
|
+
*/
|
|
2585
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2586
|
+
let error = null;
|
|
2587
|
+
let screenshotId = null;
|
|
2588
|
+
let screenshotPath = null;
|
|
2589
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2590
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2591
|
+
if (newValue !== title) {
|
|
2592
|
+
this.logger.info(title + "=" + newValue);
|
|
2593
|
+
title = newValue;
|
|
2594
|
+
}
|
|
2595
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2596
|
+
const state = {
|
|
2597
|
+
text_search: queryText,
|
|
2598
|
+
options,
|
|
2599
|
+
world,
|
|
2600
|
+
locate: false,
|
|
2601
|
+
scroll: false,
|
|
2602
|
+
highlight: false,
|
|
2603
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2604
|
+
text: `Verify the page title is ${queryText}`,
|
|
2605
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2606
|
+
operation: "verifyPageTitle",
|
|
2607
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2608
|
+
};
|
|
2609
|
+
try {
|
|
2610
|
+
await _preCommand(state, this);
|
|
2611
|
+
state.info.text = queryText;
|
|
2612
|
+
for (let i = 0; i < 30; i++) {
|
|
2613
|
+
const foundTitle = await this.page.title();
|
|
2614
|
+
switch (matcher) {
|
|
2615
|
+
case "exact":
|
|
2616
|
+
if (foundTitle !== queryText) {
|
|
2617
|
+
if (i === 29) {
|
|
2618
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2619
|
+
}
|
|
2620
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
break;
|
|
2624
|
+
case "contains":
|
|
2625
|
+
if (!foundTitle.includes(queryText)) {
|
|
2626
|
+
if (i === 29) {
|
|
2627
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2628
|
+
}
|
|
2629
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
break;
|
|
2633
|
+
case "starts-with":
|
|
2634
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2635
|
+
if (i === 29) {
|
|
2636
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2637
|
+
}
|
|
2638
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
break;
|
|
2642
|
+
case "ends-with":
|
|
2643
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2644
|
+
if (i === 29) {
|
|
2645
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2646
|
+
}
|
|
2647
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
break;
|
|
2651
|
+
case "regex":
|
|
2652
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2653
|
+
if (!regex.test(foundTitle)) {
|
|
2654
|
+
if (i === 29) {
|
|
2655
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2656
|
+
}
|
|
2657
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
break;
|
|
2661
|
+
default:
|
|
2662
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2663
|
+
if (!foundTitle.includes(title)) {
|
|
2664
|
+
if (i === 29) {
|
|
2665
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2666
|
+
}
|
|
2667
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
await _screenshot(state, this);
|
|
2672
|
+
return state.info;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
catch (e) {
|
|
2676
|
+
state.info.failCause.lastError = e.message;
|
|
2677
|
+
state.info.failCause.assertionFailed = true;
|
|
2678
|
+
await _commandError(state, e, this);
|
|
2679
|
+
}
|
|
2680
|
+
finally {
|
|
2681
|
+
await _commandFinally(state, this);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2258
2685
|
const frames = this.page.frames();
|
|
2259
2686
|
let results = [];
|
|
2260
|
-
let ignoreCase = false;
|
|
2687
|
+
// let ignoreCase = false;
|
|
2261
2688
|
for (let i = 0; i < frames.length; i++) {
|
|
2262
2689
|
if (dateAlternatives.date) {
|
|
2263
2690
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2264
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2691
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2265
2692
|
result.frame = frames[i];
|
|
2266
2693
|
results.push(result);
|
|
2267
2694
|
}
|
|
2268
2695
|
}
|
|
2269
2696
|
else if (numberAlternatives.number) {
|
|
2270
2697
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2271
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2698
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2272
2699
|
result.frame = frames[i];
|
|
2273
2700
|
results.push(result);
|
|
2274
2701
|
}
|
|
2275
2702
|
}
|
|
2276
2703
|
else {
|
|
2277
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2704
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2278
2705
|
result.frame = frames[i];
|
|
2279
2706
|
results.push(result);
|
|
2280
2707
|
}
|
|
@@ -2293,7 +2720,7 @@ class StableBrowser {
|
|
|
2293
2720
|
scroll: false,
|
|
2294
2721
|
highlight: false,
|
|
2295
2722
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2296
|
-
text: `Verify text exists in page`,
|
|
2723
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2297
2724
|
_text: `Verify the text '${text}' exists in page`,
|
|
2298
2725
|
operation: "verifyTextExistInPage",
|
|
2299
2726
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2335,27 +2762,10 @@ class StableBrowser {
|
|
|
2335
2762
|
const frame = resultWithElementsFound[0].frame;
|
|
2336
2763
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2337
2764
|
await this._highlightElements(frame, dataAttribute);
|
|
2338
|
-
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2339
|
-
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2340
|
-
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2341
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2342
|
-
// this._unhighlightElements(frame, dataAttribute)
|
|
2343
|
-
// .then(async () => {
|
|
2344
|
-
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2345
|
-
// })
|
|
2346
|
-
// .catch(
|
|
2347
|
-
// (e) => {}
|
|
2348
|
-
// console.error(e)
|
|
2349
|
-
// );
|
|
2350
|
-
// });
|
|
2351
|
-
// }
|
|
2352
2765
|
const element = await frame.locator(dataAttribute).first();
|
|
2353
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2354
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2355
2766
|
if (element) {
|
|
2356
2767
|
await this.scrollIfNeeded(element, state.info);
|
|
2357
2768
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2358
|
-
// await _screenshot(state, this, element);
|
|
2359
2769
|
}
|
|
2360
2770
|
}
|
|
2361
2771
|
await _screenshot(state, this);
|
|
@@ -2365,13 +2775,12 @@ class StableBrowser {
|
|
|
2365
2775
|
console.error(error);
|
|
2366
2776
|
}
|
|
2367
2777
|
}
|
|
2368
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2369
2778
|
}
|
|
2370
2779
|
catch (e) {
|
|
2371
2780
|
await _commandError(state, e, this);
|
|
2372
2781
|
}
|
|
2373
2782
|
finally {
|
|
2374
|
-
_commandFinally(state, this);
|
|
2783
|
+
await _commandFinally(state, this);
|
|
2375
2784
|
}
|
|
2376
2785
|
}
|
|
2377
2786
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2384,7 +2793,7 @@ class StableBrowser {
|
|
|
2384
2793
|
scroll: false,
|
|
2385
2794
|
highlight: false,
|
|
2386
2795
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2387
|
-
text: `Verify text does not exist in page`,
|
|
2796
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2388
2797
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2389
2798
|
operation: "verifyTextNotExistInPage",
|
|
2390
2799
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2428,7 +2837,7 @@ class StableBrowser {
|
|
|
2428
2837
|
await _commandError(state, e, this);
|
|
2429
2838
|
}
|
|
2430
2839
|
finally {
|
|
2431
|
-
_commandFinally(state, this);
|
|
2840
|
+
await _commandFinally(state, this);
|
|
2432
2841
|
}
|
|
2433
2842
|
}
|
|
2434
2843
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2470,7 +2879,7 @@ class StableBrowser {
|
|
|
2470
2879
|
};
|
|
2471
2880
|
while (true) {
|
|
2472
2881
|
try {
|
|
2473
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2882
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2474
2883
|
}
|
|
2475
2884
|
catch (error) {
|
|
2476
2885
|
// ignore
|
|
@@ -2498,7 +2907,7 @@ class StableBrowser {
|
|
|
2498
2907
|
const count = await frame.locator(css).count();
|
|
2499
2908
|
for (let j = 0; j < count; j++) {
|
|
2500
2909
|
const continer = await frame.locator(css).nth(j);
|
|
2501
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2910
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2502
2911
|
if (result.elementCount > 0) {
|
|
2503
2912
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2504
2913
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2539,9 +2948,33 @@ class StableBrowser {
|
|
|
2539
2948
|
await _commandError(state, e, this);
|
|
2540
2949
|
}
|
|
2541
2950
|
finally {
|
|
2542
|
-
_commandFinally(state, this);
|
|
2951
|
+
await _commandFinally(state, this);
|
|
2543
2952
|
}
|
|
2544
2953
|
}
|
|
2954
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2955
|
+
const frames = this.page.frames();
|
|
2956
|
+
let results = [];
|
|
2957
|
+
let ignoreCase = false;
|
|
2958
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2959
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2960
|
+
result.frame = frames[i];
|
|
2961
|
+
const climbArray = [];
|
|
2962
|
+
for (let i = 0; i < climb; i++) {
|
|
2963
|
+
climbArray.push("..");
|
|
2964
|
+
}
|
|
2965
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2966
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2967
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2968
|
+
if (count > 0) {
|
|
2969
|
+
result.elementCount = count;
|
|
2970
|
+
result.locator = newLocator;
|
|
2971
|
+
results.push(result);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
// state.info.results = results;
|
|
2975
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2976
|
+
return resultWithElementsFound;
|
|
2977
|
+
}
|
|
2545
2978
|
async visualVerification(text, options = {}, world = null) {
|
|
2546
2979
|
const startTime = Date.now();
|
|
2547
2980
|
let error = null;
|
|
@@ -2858,7 +3291,13 @@ class StableBrowser {
|
|
|
2858
3291
|
}
|
|
2859
3292
|
}
|
|
2860
3293
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2861
|
-
|
|
3294
|
+
try {
|
|
3295
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3296
|
+
}
|
|
3297
|
+
catch (error) {
|
|
3298
|
+
this.logger.debug(error);
|
|
3299
|
+
throw error;
|
|
3300
|
+
}
|
|
2862
3301
|
}
|
|
2863
3302
|
_getLoadTimeout(options) {
|
|
2864
3303
|
let timeout = 15000;
|
|
@@ -2881,6 +3320,7 @@ class StableBrowser {
|
|
|
2881
3320
|
}
|
|
2882
3321
|
async saveStoreState(path = null, world = null) {
|
|
2883
3322
|
const storageState = await this.page.context().storageState();
|
|
3323
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2884
3324
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2885
3325
|
if (path) {
|
|
2886
3326
|
// save { storageState: storageState } into the path
|
|
@@ -2891,10 +3331,14 @@ class StableBrowser {
|
|
|
2891
3331
|
}
|
|
2892
3332
|
}
|
|
2893
3333
|
async restoreSaveState(path = null, world = null) {
|
|
3334
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2894
3335
|
await refreshBrowser(this, path, world);
|
|
2895
3336
|
this.registerEventListeners(this.context);
|
|
2896
3337
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2897
3338
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3339
|
+
if (this.onRestoreSaveState) {
|
|
3340
|
+
this.onRestoreSaveState(path);
|
|
3341
|
+
}
|
|
2898
3342
|
}
|
|
2899
3343
|
async waitForPageLoad(options = {}, world = null) {
|
|
2900
3344
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -2977,7 +3421,7 @@ class StableBrowser {
|
|
|
2977
3421
|
await _commandError(state, e, this);
|
|
2978
3422
|
}
|
|
2979
3423
|
finally {
|
|
2980
|
-
_commandFinally(state, this);
|
|
3424
|
+
await _commandFinally(state, this);
|
|
2981
3425
|
}
|
|
2982
3426
|
}
|
|
2983
3427
|
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
@@ -3064,7 +3508,7 @@ class StableBrowser {
|
|
|
3064
3508
|
await _commandError(state, e, this);
|
|
3065
3509
|
}
|
|
3066
3510
|
finally {
|
|
3067
|
-
_commandFinally(state, this);
|
|
3511
|
+
await _commandFinally(state, this);
|
|
3068
3512
|
}
|
|
3069
3513
|
}
|
|
3070
3514
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3169,7 +3613,39 @@ class StableBrowser {
|
|
|
3169
3613
|
console.log("#-#");
|
|
3170
3614
|
}
|
|
3171
3615
|
}
|
|
3616
|
+
async beforeScenario(world, scenario) {
|
|
3617
|
+
this.beforeScenarioCalled = true;
|
|
3618
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3619
|
+
this.scenarioName = scenario.pickle.name;
|
|
3620
|
+
}
|
|
3621
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3622
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3623
|
+
}
|
|
3624
|
+
if (this.context) {
|
|
3625
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3626
|
+
}
|
|
3627
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3628
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3629
|
+
// check if @global_test_data tag is present
|
|
3630
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3631
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
// update test data based on feature/scenario
|
|
3635
|
+
let envName = null;
|
|
3636
|
+
if (this.context && this.context.environment) {
|
|
3637
|
+
envName = this.context.environment.name;
|
|
3638
|
+
}
|
|
3639
|
+
if (!process.env.TEMP_RUN) {
|
|
3640
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3641
|
+
}
|
|
3642
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3643
|
+
}
|
|
3644
|
+
async afterScenario(world, scenario) { }
|
|
3172
3645
|
async beforeStep(world, step) {
|
|
3646
|
+
if (!this.beforeScenarioCalled) {
|
|
3647
|
+
this.beforeScenario(world, step);
|
|
3648
|
+
}
|
|
3173
3649
|
if (this.stepIndex === undefined) {
|
|
3174
3650
|
this.stepIndex = 0;
|
|
3175
3651
|
}
|
|
@@ -3186,21 +3662,11 @@ class StableBrowser {
|
|
|
3186
3662
|
else {
|
|
3187
3663
|
this.stepName = "step " + this.stepIndex;
|
|
3188
3664
|
}
|
|
3189
|
-
if (this.context) {
|
|
3190
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3191
|
-
}
|
|
3192
3665
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3193
3666
|
if (this.context.browserObject.context) {
|
|
3194
3667
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3195
3668
|
}
|
|
3196
3669
|
}
|
|
3197
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3198
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3199
|
-
// check if @global_test_data tag is present
|
|
3200
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3201
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
3670
|
if (this.initSnapshotTaken === false) {
|
|
3205
3671
|
this.initSnapshotTaken = true;
|
|
3206
3672
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
@@ -3225,18 +3691,68 @@ class StableBrowser {
|
|
|
3225
3691
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3226
3692
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3227
3693
|
for (let i = 0; i < frames.length; i++) {
|
|
3228
|
-
content.push(`- frame: ${i}`);
|
|
3229
3694
|
const frame = frames[i];
|
|
3230
|
-
|
|
3231
|
-
|
|
3695
|
+
try {
|
|
3696
|
+
// Ensure frame is attached and has body
|
|
3697
|
+
const body = frame.locator("body");
|
|
3698
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3699
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3700
|
+
content.push(`- frame: ${i}`);
|
|
3701
|
+
content.push(snapshot);
|
|
3702
|
+
}
|
|
3703
|
+
catch (innerErr) { }
|
|
3232
3704
|
}
|
|
3233
3705
|
return content.join("\n");
|
|
3234
3706
|
}
|
|
3235
3707
|
catch (e) {
|
|
3236
|
-
console.
|
|
3708
|
+
console.log("Error in getAriaSnapshot");
|
|
3709
|
+
//console.debug(e);
|
|
3237
3710
|
}
|
|
3238
3711
|
return null;
|
|
3239
3712
|
}
|
|
3713
|
+
/**
|
|
3714
|
+
* Sends command with custom payload to report.
|
|
3715
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3716
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3717
|
+
* @param content - Content of the command to be shown in the report.
|
|
3718
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3719
|
+
* @param world - Optional world context.
|
|
3720
|
+
* @public
|
|
3721
|
+
*/
|
|
3722
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3723
|
+
const state = {
|
|
3724
|
+
options,
|
|
3725
|
+
world,
|
|
3726
|
+
locate: false,
|
|
3727
|
+
scroll: false,
|
|
3728
|
+
screenshot: options.screenshot ?? false,
|
|
3729
|
+
highlight: options.highlight ?? false,
|
|
3730
|
+
type: Types.REPORT_COMMAND,
|
|
3731
|
+
text: commandText,
|
|
3732
|
+
_text: commandText,
|
|
3733
|
+
operation: "report_command",
|
|
3734
|
+
log: "***** " + commandText + " *****\n",
|
|
3735
|
+
};
|
|
3736
|
+
try {
|
|
3737
|
+
await _preCommand(state, this);
|
|
3738
|
+
const payload = {
|
|
3739
|
+
type: options.type ?? "text",
|
|
3740
|
+
content: content,
|
|
3741
|
+
screenshotId: null,
|
|
3742
|
+
};
|
|
3743
|
+
state.payload = payload;
|
|
3744
|
+
if (commandStatus === "FAILED") {
|
|
3745
|
+
state.throwError = true;
|
|
3746
|
+
throw new Error("Command failed");
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
catch (e) {
|
|
3750
|
+
await _commandError(state, e, this);
|
|
3751
|
+
}
|
|
3752
|
+
finally {
|
|
3753
|
+
await _commandFinally(state, this);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3240
3756
|
async afterStep(world, step) {
|
|
3241
3757
|
this.stepName = null;
|
|
3242
3758
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3244,6 +3760,13 @@ class StableBrowser {
|
|
|
3244
3760
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3245
3761
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3246
3762
|
});
|
|
3763
|
+
if (world && world.attach) {
|
|
3764
|
+
await world.attach(JSON.stringify({
|
|
3765
|
+
type: "trace",
|
|
3766
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3767
|
+
}), "application/json+trace");
|
|
3768
|
+
}
|
|
3769
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3247
3770
|
}
|
|
3248
3771
|
}
|
|
3249
3772
|
if (this.context) {
|
|
@@ -3256,6 +3779,29 @@ class StableBrowser {
|
|
|
3256
3779
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3257
3780
|
}
|
|
3258
3781
|
}
|
|
3782
|
+
if (!process.env.TEMP_RUN) {
|
|
3783
|
+
const state = {
|
|
3784
|
+
world,
|
|
3785
|
+
locate: false,
|
|
3786
|
+
scroll: false,
|
|
3787
|
+
screenshot: true,
|
|
3788
|
+
highlight: true,
|
|
3789
|
+
type: Types.STEP_COMPLETE,
|
|
3790
|
+
text: "end of scenario",
|
|
3791
|
+
_text: "end of scenario",
|
|
3792
|
+
operation: "step_complete",
|
|
3793
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
3794
|
+
};
|
|
3795
|
+
try {
|
|
3796
|
+
await _preCommand(state, this);
|
|
3797
|
+
}
|
|
3798
|
+
catch (e) {
|
|
3799
|
+
await _commandError(state, e, this);
|
|
3800
|
+
}
|
|
3801
|
+
finally {
|
|
3802
|
+
await _commandFinally(state, this);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3259
3805
|
}
|
|
3260
3806
|
}
|
|
3261
3807
|
function createTimedPromise(promise, label) {
|