automation_model 1.0.637-dev → 1.0.637-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 +99 -28
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +38 -9
- 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 +33 -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 +52 -25
- package/lib/stable_browser.js +720 -229
- 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.d.ts +1 -1
- package/lib/table_helper.js +26 -5
- 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 +7 -4
- package/lib/utils.js +204 -20
- package/lib/utils.js.map +1 -1
- package/package.json +11 -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, } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } 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 { 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) => {
|
|
@@ -180,6 +190,30 @@ class StableBrowser {
|
|
|
180
190
|
await this.waitForPageLoad();
|
|
181
191
|
}
|
|
182
192
|
}
|
|
193
|
+
async switchTab(tabTitleOrIndex) {
|
|
194
|
+
// first check if the tabNameOrIndex is a number
|
|
195
|
+
let index = parseInt(tabTitleOrIndex);
|
|
196
|
+
if (!isNaN(index)) {
|
|
197
|
+
if (index >= 0 && index < this.context.pages.length) {
|
|
198
|
+
this.page = this.context.pages[index];
|
|
199
|
+
this.context.page = this.page;
|
|
200
|
+
await this.page.bringToFront();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// if the tabNameOrIndex is a string, find the tab by name
|
|
205
|
+
for (let i = 0; i < this.context.pages.length; i++) {
|
|
206
|
+
let page = this.context.pages[i];
|
|
207
|
+
let title = await page.title();
|
|
208
|
+
if (title.includes(tabTitleOrIndex)) {
|
|
209
|
+
this.page = page;
|
|
210
|
+
this.context.page = this.page;
|
|
211
|
+
await this.page.bringToFront();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
throw new Error("Tab not found: " + tabTitleOrIndex);
|
|
216
|
+
}
|
|
183
217
|
registerConsoleLogListener(page, context) {
|
|
184
218
|
if (!this.context.webLogger) {
|
|
185
219
|
this.context.webLogger = [];
|
|
@@ -247,6 +281,7 @@ class StableBrowser {
|
|
|
247
281
|
if (!url) {
|
|
248
282
|
throw new Error("url is null, verify that the environment file is correct");
|
|
249
283
|
}
|
|
284
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
250
285
|
if (!url.startsWith("http")) {
|
|
251
286
|
url = "https://" + url;
|
|
252
287
|
}
|
|
@@ -275,7 +310,7 @@ class StableBrowser {
|
|
|
275
310
|
_commandError(state, error, this);
|
|
276
311
|
}
|
|
277
312
|
finally {
|
|
278
|
-
_commandFinally(state, this);
|
|
313
|
+
await _commandFinally(state, this);
|
|
279
314
|
}
|
|
280
315
|
}
|
|
281
316
|
async _getLocator(locator, scope, _params) {
|
|
@@ -391,7 +426,7 @@ class StableBrowser {
|
|
|
391
426
|
}
|
|
392
427
|
return { elementCount: tagCount, randomToken };
|
|
393
428
|
}
|
|
394
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
429
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
395
430
|
if (!info) {
|
|
396
431
|
info = {};
|
|
397
432
|
}
|
|
@@ -458,7 +493,7 @@ class StableBrowser {
|
|
|
458
493
|
}
|
|
459
494
|
return;
|
|
460
495
|
}
|
|
461
|
-
if (info.locatorLog && count === 0) {
|
|
496
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
462
497
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
463
498
|
}
|
|
464
499
|
for (let j = 0; j < count; j++) {
|
|
@@ -473,7 +508,7 @@ class StableBrowser {
|
|
|
473
508
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
474
509
|
}
|
|
475
510
|
}
|
|
476
|
-
else {
|
|
511
|
+
else if (logErrors) {
|
|
477
512
|
info.failCause.visible = visible;
|
|
478
513
|
info.failCause.enabled = enabled;
|
|
479
514
|
if (!info.printMessages) {
|
|
@@ -565,15 +600,27 @@ class StableBrowser {
|
|
|
565
600
|
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
566
601
|
if (!element.rerun) {
|
|
567
602
|
const randomToken = Math.random().toString(36).substring(7);
|
|
568
|
-
element.evaluate((el, randomToken) => {
|
|
603
|
+
await element.evaluate((el, randomToken) => {
|
|
569
604
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
570
605
|
}, randomToken);
|
|
571
|
-
if (element._frame) {
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
const scope = element.page();
|
|
575
|
-
|
|
576
|
-
|
|
606
|
+
// if (element._frame) {
|
|
607
|
+
// return element;
|
|
608
|
+
// }
|
|
609
|
+
const scope = element._frame ?? element.page();
|
|
610
|
+
let newElementSelector = "[data-blinq-id-" + randomToken + "]";
|
|
611
|
+
let prefixSelector = "";
|
|
612
|
+
const frameControlSelector = " >> internal:control=enter-frame";
|
|
613
|
+
const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
|
|
614
|
+
if (frameSelectorIndex !== -1) {
|
|
615
|
+
// remove everything after the >> internal:control=enter-frame
|
|
616
|
+
const frameSelector = element._selector.substring(0, frameSelectorIndex);
|
|
617
|
+
prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
|
|
618
|
+
}
|
|
619
|
+
// if (element?._frame?._selector) {
|
|
620
|
+
// prefixSelector = element._frame._selector + " >> " + prefixSelector;
|
|
621
|
+
// }
|
|
622
|
+
const newSelector = prefixSelector + newElementSelector;
|
|
623
|
+
return scope.locator(newSelector);
|
|
577
624
|
}
|
|
578
625
|
}
|
|
579
626
|
throw new Error("unable to locate element " + JSON.stringify(selectors));
|
|
@@ -725,14 +772,9 @@ class StableBrowser {
|
|
|
725
772
|
// info.log += "scanning locators in priority 2" + "\n";
|
|
726
773
|
result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
727
774
|
}
|
|
728
|
-
if (result.foundElements.length === 0 && onlyPriority3) {
|
|
775
|
+
if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
|
|
729
776
|
result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
|
|
730
777
|
}
|
|
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
778
|
let foundElements = result.foundElements;
|
|
737
779
|
if (foundElements.length === 1 && foundElements[0].unique) {
|
|
738
780
|
info.box = foundElements[0].box;
|
|
@@ -787,6 +829,11 @@ class StableBrowser {
|
|
|
787
829
|
visibleOnly = false;
|
|
788
830
|
}
|
|
789
831
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
832
|
+
// sheck of more of half of the timeout has passed
|
|
833
|
+
if (Date.now() - startTime > timeout / 2) {
|
|
834
|
+
highPriorityOnly = false;
|
|
835
|
+
visibleOnly = false;
|
|
836
|
+
}
|
|
790
837
|
}
|
|
791
838
|
this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
|
|
792
839
|
// if (info.locatorLog) {
|
|
@@ -802,7 +849,7 @@ class StableBrowser {
|
|
|
802
849
|
}
|
|
803
850
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
804
851
|
}
|
|
805
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
852
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
806
853
|
let foundElements = [];
|
|
807
854
|
const result = {
|
|
808
855
|
foundElements: foundElements,
|
|
@@ -821,7 +868,9 @@ class StableBrowser {
|
|
|
821
868
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
822
869
|
}
|
|
823
870
|
catch (e) {
|
|
824
|
-
|
|
871
|
+
if (logErrors) {
|
|
872
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
873
|
+
}
|
|
825
874
|
}
|
|
826
875
|
}
|
|
827
876
|
if (foundLocators.length === 1) {
|
|
@@ -833,9 +882,40 @@ class StableBrowser {
|
|
|
833
882
|
result.locatorIndex = i;
|
|
834
883
|
}
|
|
835
884
|
if (foundLocators.length > 1) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
885
|
+
// remove elements that consume the same space with 10 pixels tolerance
|
|
886
|
+
const boxes = [];
|
|
887
|
+
for (let j = 0; j < foundLocators.length; j++) {
|
|
888
|
+
boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
|
|
889
|
+
}
|
|
890
|
+
for (let j = 0; j < boxes.length; j++) {
|
|
891
|
+
for (let k = 0; k < boxes.length; k++) {
|
|
892
|
+
if (j === k) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
// check if x, y, width, height are the same with 10 pixels tolerance
|
|
896
|
+
if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
|
|
897
|
+
Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
|
|
898
|
+
Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
|
|
899
|
+
Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
|
|
900
|
+
// as the element is not unique, will remove it
|
|
901
|
+
boxes.splice(k, 1);
|
|
902
|
+
k--;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (boxes.length === 1) {
|
|
907
|
+
result.foundElements.push({
|
|
908
|
+
locator: boxes[0].locator.first(),
|
|
909
|
+
box: boxes[0].box,
|
|
910
|
+
unique: true,
|
|
911
|
+
});
|
|
912
|
+
result.locatorIndex = i;
|
|
913
|
+
}
|
|
914
|
+
else if (logErrors) {
|
|
915
|
+
info.failCause.foundMultiple = true;
|
|
916
|
+
if (info.locatorLog) {
|
|
917
|
+
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
918
|
+
}
|
|
839
919
|
}
|
|
840
920
|
}
|
|
841
921
|
}
|
|
@@ -883,7 +963,7 @@ class StableBrowser {
|
|
|
883
963
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
884
964
|
}
|
|
885
965
|
finally {
|
|
886
|
-
_commandFinally(state, this);
|
|
966
|
+
await _commandFinally(state, this);
|
|
887
967
|
}
|
|
888
968
|
}
|
|
889
969
|
}
|
|
@@ -932,7 +1012,7 @@ class StableBrowser {
|
|
|
932
1012
|
await _commandError(state, "timeout looking for " + elementDescription, this);
|
|
933
1013
|
}
|
|
934
1014
|
finally {
|
|
935
|
-
_commandFinally(state, this);
|
|
1015
|
+
await _commandFinally(state, this);
|
|
936
1016
|
}
|
|
937
1017
|
}
|
|
938
1018
|
}
|
|
@@ -953,19 +1033,7 @@ class StableBrowser {
|
|
|
953
1033
|
};
|
|
954
1034
|
try {
|
|
955
1035
|
await _preCommand(state, this);
|
|
956
|
-
|
|
957
|
-
// state.selectors.locators[0].text = state.options.context;
|
|
958
|
-
// }
|
|
959
|
-
try {
|
|
960
|
-
await state.element.click();
|
|
961
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
962
|
-
}
|
|
963
|
-
catch (e) {
|
|
964
|
-
// await this.closeUnexpectedPopups();
|
|
965
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
966
|
-
await state.element.dispatchEvent("click");
|
|
967
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
968
|
-
}
|
|
1036
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
969
1037
|
await this.waitForPageLoad();
|
|
970
1038
|
return state.info;
|
|
971
1039
|
}
|
|
@@ -973,7 +1041,7 @@ class StableBrowser {
|
|
|
973
1041
|
await _commandError(state, e, this);
|
|
974
1042
|
}
|
|
975
1043
|
finally {
|
|
976
|
-
_commandFinally(state, this);
|
|
1044
|
+
await _commandFinally(state, this);
|
|
977
1045
|
}
|
|
978
1046
|
}
|
|
979
1047
|
async waitForElement(selectors, _params, options = {}, world = null) {
|
|
@@ -1004,7 +1072,7 @@ class StableBrowser {
|
|
|
1004
1072
|
// await _commandError(state, e, this);
|
|
1005
1073
|
}
|
|
1006
1074
|
finally {
|
|
1007
|
-
_commandFinally(state, this);
|
|
1075
|
+
await _commandFinally(state, this);
|
|
1008
1076
|
}
|
|
1009
1077
|
return found;
|
|
1010
1078
|
}
|
|
@@ -1028,7 +1096,7 @@ class StableBrowser {
|
|
|
1028
1096
|
try {
|
|
1029
1097
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1030
1098
|
// console.log(`Highlighting while running from recorder`);
|
|
1031
|
-
await this._highlightElements(element);
|
|
1099
|
+
await this._highlightElements(state.element);
|
|
1032
1100
|
await state.element.setChecked(checked);
|
|
1033
1101
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1034
1102
|
// await this._unHighlightElements(element);
|
|
@@ -1055,7 +1123,7 @@ class StableBrowser {
|
|
|
1055
1123
|
await _commandError(state, e, this);
|
|
1056
1124
|
}
|
|
1057
1125
|
finally {
|
|
1058
|
-
_commandFinally(state, this);
|
|
1126
|
+
await _commandFinally(state, this);
|
|
1059
1127
|
}
|
|
1060
1128
|
}
|
|
1061
1129
|
async hover(selectors, _params, options = {}, world = null) {
|
|
@@ -1072,19 +1140,7 @@ class StableBrowser {
|
|
|
1072
1140
|
};
|
|
1073
1141
|
try {
|
|
1074
1142
|
await _preCommand(state, this);
|
|
1075
|
-
|
|
1076
|
-
await state.element.hover();
|
|
1077
|
-
// await _screenshot(state, this);
|
|
1078
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1079
|
-
}
|
|
1080
|
-
catch (e) {
|
|
1081
|
-
//await this.closeUnexpectedPopups();
|
|
1082
|
-
state.info.log += "hover failed, will try again" + "\n";
|
|
1083
|
-
state.element = await this._locate(selectors, state.info, _params);
|
|
1084
|
-
await state.element.hover({ timeout: 10000 });
|
|
1085
|
-
// await _screenshot(state, this);
|
|
1086
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1087
|
-
}
|
|
1143
|
+
await performAction("hover", state.element, options, this, state, _params);
|
|
1088
1144
|
await _screenshot(state, this);
|
|
1089
1145
|
await this.waitForPageLoad();
|
|
1090
1146
|
return state.info;
|
|
@@ -1093,7 +1149,7 @@ class StableBrowser {
|
|
|
1093
1149
|
await _commandError(state, e, this);
|
|
1094
1150
|
}
|
|
1095
1151
|
finally {
|
|
1096
|
-
_commandFinally(state, this);
|
|
1152
|
+
await _commandFinally(state, this);
|
|
1097
1153
|
}
|
|
1098
1154
|
}
|
|
1099
1155
|
async selectOption(selectors, values, _params = null, options = {}, world = null) {
|
|
@@ -1129,7 +1185,7 @@ class StableBrowser {
|
|
|
1129
1185
|
await _commandError(state, e, this);
|
|
1130
1186
|
}
|
|
1131
1187
|
finally {
|
|
1132
|
-
_commandFinally(state, this);
|
|
1188
|
+
await _commandFinally(state, this);
|
|
1133
1189
|
}
|
|
1134
1190
|
}
|
|
1135
1191
|
async type(_value, _params = null, options = {}, world = null) {
|
|
@@ -1175,7 +1231,7 @@ class StableBrowser {
|
|
|
1175
1231
|
await _commandError(state, e, this);
|
|
1176
1232
|
}
|
|
1177
1233
|
finally {
|
|
1178
|
-
_commandFinally(state, this);
|
|
1234
|
+
await _commandFinally(state, this);
|
|
1179
1235
|
}
|
|
1180
1236
|
}
|
|
1181
1237
|
async setInputValue(selectors, value, _params = null, options = {}, world = null) {
|
|
@@ -1211,7 +1267,7 @@ class StableBrowser {
|
|
|
1211
1267
|
await _commandError(state, e, this);
|
|
1212
1268
|
}
|
|
1213
1269
|
finally {
|
|
1214
|
-
_commandFinally(state, this);
|
|
1270
|
+
await _commandFinally(state, this);
|
|
1215
1271
|
}
|
|
1216
1272
|
}
|
|
1217
1273
|
async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1231,7 +1287,7 @@ class StableBrowser {
|
|
|
1231
1287
|
try {
|
|
1232
1288
|
await _preCommand(state, this);
|
|
1233
1289
|
try {
|
|
1234
|
-
await state.element
|
|
1290
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1235
1291
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1236
1292
|
if (format) {
|
|
1237
1293
|
state.value = dayjs(state.value).format(format);
|
|
@@ -1280,7 +1336,7 @@ class StableBrowser {
|
|
|
1280
1336
|
await _commandError(state, e, this);
|
|
1281
1337
|
}
|
|
1282
1338
|
finally {
|
|
1283
|
-
_commandFinally(state, this);
|
|
1339
|
+
await _commandFinally(state, this);
|
|
1284
1340
|
}
|
|
1285
1341
|
}
|
|
1286
1342
|
async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1299,6 +1355,9 @@ class StableBrowser {
|
|
|
1299
1355
|
operation: "clickType",
|
|
1300
1356
|
log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
|
|
1301
1357
|
};
|
|
1358
|
+
if (!options) {
|
|
1359
|
+
options = {};
|
|
1360
|
+
}
|
|
1302
1361
|
if (newValue !== _value) {
|
|
1303
1362
|
//this.logger.info(_value + "=" + newValue);
|
|
1304
1363
|
_value = newValue;
|
|
@@ -1306,7 +1365,7 @@ class StableBrowser {
|
|
|
1306
1365
|
try {
|
|
1307
1366
|
await _preCommand(state, this);
|
|
1308
1367
|
state.info.value = _value;
|
|
1309
|
-
if (
|
|
1368
|
+
if (!options.press) {
|
|
1310
1369
|
try {
|
|
1311
1370
|
let currentValue = await state.element.inputValue();
|
|
1312
1371
|
if (currentValue) {
|
|
@@ -1317,13 +1376,9 @@ class StableBrowser {
|
|
|
1317
1376
|
this.logger.info("unable to clear input value");
|
|
1318
1377
|
}
|
|
1319
1378
|
}
|
|
1320
|
-
if (options
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
}
|
|
1324
|
-
catch (e) {
|
|
1325
|
-
await state.element.dispatchEvent("click");
|
|
1326
|
-
}
|
|
1379
|
+
if (options.press) {
|
|
1380
|
+
options.timeout = 5000;
|
|
1381
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
1327
1382
|
}
|
|
1328
1383
|
else {
|
|
1329
1384
|
try {
|
|
@@ -1381,7 +1436,7 @@ class StableBrowser {
|
|
|
1381
1436
|
await _commandError(state, e, this);
|
|
1382
1437
|
}
|
|
1383
1438
|
finally {
|
|
1384
|
-
_commandFinally(state, this);
|
|
1439
|
+
await _commandFinally(state, this);
|
|
1385
1440
|
}
|
|
1386
1441
|
}
|
|
1387
1442
|
async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
|
|
@@ -1411,7 +1466,42 @@ class StableBrowser {
|
|
|
1411
1466
|
await _commandError(state, e, this);
|
|
1412
1467
|
}
|
|
1413
1468
|
finally {
|
|
1414
|
-
_commandFinally(state, this);
|
|
1469
|
+
await _commandFinally(state, this);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
|
|
1473
|
+
const state = {
|
|
1474
|
+
selectors,
|
|
1475
|
+
_params,
|
|
1476
|
+
files,
|
|
1477
|
+
value: '"' + files.join('", "') + '"',
|
|
1478
|
+
options,
|
|
1479
|
+
world,
|
|
1480
|
+
type: Types.SET_INPUT_FILES,
|
|
1481
|
+
text: `Set input files`,
|
|
1482
|
+
_text: `Set input files on ${selectors.element_name}`,
|
|
1483
|
+
operation: "setInputFiles",
|
|
1484
|
+
log: "***** set input files " + selectors.element_name + " *****\n",
|
|
1485
|
+
};
|
|
1486
|
+
const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
|
|
1487
|
+
try {
|
|
1488
|
+
await _preCommand(state, this);
|
|
1489
|
+
for (let i = 0; i < files.length; i++) {
|
|
1490
|
+
const file = files[i];
|
|
1491
|
+
const filePath = path.join(uploadsFolder, file);
|
|
1492
|
+
if (!fs.existsSync(filePath)) {
|
|
1493
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1494
|
+
}
|
|
1495
|
+
state.files[i] = filePath;
|
|
1496
|
+
}
|
|
1497
|
+
await state.element.setInputFiles(files);
|
|
1498
|
+
return state.info;
|
|
1499
|
+
}
|
|
1500
|
+
catch (e) {
|
|
1501
|
+
await _commandError(state, e, this);
|
|
1502
|
+
}
|
|
1503
|
+
finally {
|
|
1504
|
+
await _commandFinally(state, this);
|
|
1415
1505
|
}
|
|
1416
1506
|
}
|
|
1417
1507
|
async getText(selectors, _params = null, options = {}, info = {}, world = null) {
|
|
@@ -1527,7 +1617,7 @@ class StableBrowser {
|
|
|
1527
1617
|
await _commandError(state, e, this);
|
|
1528
1618
|
}
|
|
1529
1619
|
finally {
|
|
1530
|
-
_commandFinally(state, this);
|
|
1620
|
+
await _commandFinally(state, this);
|
|
1531
1621
|
}
|
|
1532
1622
|
}
|
|
1533
1623
|
async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
|
|
@@ -1562,7 +1652,7 @@ class StableBrowser {
|
|
|
1562
1652
|
while (Date.now() - startTime < timeout) {
|
|
1563
1653
|
try {
|
|
1564
1654
|
await _preCommand(state, this);
|
|
1565
|
-
foundObj = await this._getText(selectors, climb, _params, { timeout:
|
|
1655
|
+
foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
|
|
1566
1656
|
if (foundObj && foundObj.element) {
|
|
1567
1657
|
await this.scrollIfNeeded(foundObj.element, state.info);
|
|
1568
1658
|
}
|
|
@@ -1604,7 +1694,79 @@ class StableBrowser {
|
|
|
1604
1694
|
throw e;
|
|
1605
1695
|
}
|
|
1606
1696
|
finally {
|
|
1607
|
-
_commandFinally(state, this);
|
|
1697
|
+
await _commandFinally(state, this);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
|
|
1701
|
+
const timeout = this._getFindElementTimeout(options);
|
|
1702
|
+
const startTime = Date.now();
|
|
1703
|
+
const state = {
|
|
1704
|
+
_params,
|
|
1705
|
+
value: referanceSnapshot,
|
|
1706
|
+
options,
|
|
1707
|
+
world,
|
|
1708
|
+
locate: false,
|
|
1709
|
+
scroll: false,
|
|
1710
|
+
screenshot: true,
|
|
1711
|
+
highlight: false,
|
|
1712
|
+
type: Types.SNAPSHOT_VALIDATION,
|
|
1713
|
+
text: `verify snapshot: ${referanceSnapshot}`,
|
|
1714
|
+
operation: "snapshotValidation",
|
|
1715
|
+
log: "***** verify snapshot *****\n",
|
|
1716
|
+
};
|
|
1717
|
+
if (!referanceSnapshot) {
|
|
1718
|
+
throw new Error("referanceSnapshot is null");
|
|
1719
|
+
}
|
|
1720
|
+
let text = null;
|
|
1721
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1722
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1723
|
+
}
|
|
1724
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1725
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1726
|
+
}
|
|
1727
|
+
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1728
|
+
text = referanceSnapshot.substring(5);
|
|
1729
|
+
}
|
|
1730
|
+
else {
|
|
1731
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1732
|
+
}
|
|
1733
|
+
state.text = text;
|
|
1734
|
+
const newValue = await this._replaceWithLocalData(text, world);
|
|
1735
|
+
await _preCommand(state, this);
|
|
1736
|
+
let foundObj = null;
|
|
1737
|
+
try {
|
|
1738
|
+
let matchResult = null;
|
|
1739
|
+
while (Date.now() - startTime < timeout) {
|
|
1740
|
+
try {
|
|
1741
|
+
let scope = null;
|
|
1742
|
+
if (!frameSelectors) {
|
|
1743
|
+
scope = this.page;
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1747
|
+
}
|
|
1748
|
+
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1749
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1750
|
+
if (matchResult.errorLine !== -1) {
|
|
1751
|
+
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1752
|
+
}
|
|
1753
|
+
// highlight and screenshot
|
|
1754
|
+
return state.info;
|
|
1755
|
+
}
|
|
1756
|
+
catch (e) {
|
|
1757
|
+
// Log error but continue retrying until timeout is reached
|
|
1758
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1759
|
+
}
|
|
1760
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1761
|
+
}
|
|
1762
|
+
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1763
|
+
}
|
|
1764
|
+
catch (e) {
|
|
1765
|
+
await _commandError(state, e, this);
|
|
1766
|
+
throw e;
|
|
1767
|
+
}
|
|
1768
|
+
finally {
|
|
1769
|
+
await _commandFinally(state, this);
|
|
1608
1770
|
}
|
|
1609
1771
|
}
|
|
1610
1772
|
async waitForUserInput(message, world = null) {
|
|
@@ -1642,6 +1804,15 @@ class StableBrowser {
|
|
|
1642
1804
|
// save the data to the file
|
|
1643
1805
|
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
|
1644
1806
|
}
|
|
1807
|
+
overwriteTestData(testData, world = null) {
|
|
1808
|
+
if (!testData) {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
// if data file exists, load it
|
|
1812
|
+
const dataFile = _getDataFile(world, this.context, this);
|
|
1813
|
+
// save the data to the file
|
|
1814
|
+
fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
|
|
1815
|
+
}
|
|
1645
1816
|
_getDataFilePath(fileName) {
|
|
1646
1817
|
let dataFile = path.join(this.project_path, "data", fileName);
|
|
1647
1818
|
if (fs.existsSync(dataFile)) {
|
|
@@ -1894,7 +2065,7 @@ class StableBrowser {
|
|
|
1894
2065
|
await _commandError(state, e, this);
|
|
1895
2066
|
}
|
|
1896
2067
|
finally {
|
|
1897
|
-
_commandFinally(state, this);
|
|
2068
|
+
await _commandFinally(state, this);
|
|
1898
2069
|
}
|
|
1899
2070
|
}
|
|
1900
2071
|
async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
|
|
@@ -1925,10 +2096,31 @@ class StableBrowser {
|
|
|
1925
2096
|
case "value":
|
|
1926
2097
|
state.value = await state.element.inputValue();
|
|
1927
2098
|
break;
|
|
2099
|
+
case "text":
|
|
2100
|
+
state.value = await state.element.textContent();
|
|
2101
|
+
break;
|
|
1928
2102
|
default:
|
|
1929
2103
|
state.value = await state.element.getAttribute(attribute);
|
|
1930
2104
|
break;
|
|
1931
2105
|
}
|
|
2106
|
+
if (options !== null) {
|
|
2107
|
+
if (options.regex && options.regex !== "") {
|
|
2108
|
+
// Construct a regex pattern from the provided string
|
|
2109
|
+
const regex = options.regex.slice(1, -1);
|
|
2110
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2111
|
+
const matches = state.value.match(regexPattern);
|
|
2112
|
+
if (matches) {
|
|
2113
|
+
let newValue = "";
|
|
2114
|
+
for (const match of matches) {
|
|
2115
|
+
newValue += match;
|
|
2116
|
+
}
|
|
2117
|
+
state.value = newValue;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2121
|
+
state.value = state.value.trim();
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
1932
2124
|
state.info.value = state.value;
|
|
1933
2125
|
this.setTestData({ [variable]: state.value }, world);
|
|
1934
2126
|
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
@@ -1939,7 +2131,7 @@ class StableBrowser {
|
|
|
1939
2131
|
await _commandError(state, e, this);
|
|
1940
2132
|
}
|
|
1941
2133
|
finally {
|
|
1942
|
-
_commandFinally(state, this);
|
|
2134
|
+
await _commandFinally(state, this);
|
|
1943
2135
|
}
|
|
1944
2136
|
}
|
|
1945
2137
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
@@ -1964,12 +2156,15 @@ class StableBrowser {
|
|
|
1964
2156
|
let expectedValue;
|
|
1965
2157
|
try {
|
|
1966
2158
|
await _preCommand(state, this);
|
|
1967
|
-
expectedValue = state.value;
|
|
2159
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
1968
2160
|
state.info.expectedValue = expectedValue;
|
|
1969
2161
|
switch (attribute) {
|
|
1970
2162
|
case "innerText":
|
|
1971
2163
|
val = String(await state.element.innerText());
|
|
1972
2164
|
break;
|
|
2165
|
+
case "text":
|
|
2166
|
+
val = String(await state.element.textContent());
|
|
2167
|
+
break;
|
|
1973
2168
|
case "value":
|
|
1974
2169
|
val = String(await state.element.inputValue());
|
|
1975
2170
|
break;
|
|
@@ -1991,17 +2186,42 @@ class StableBrowser {
|
|
|
1991
2186
|
let regex;
|
|
1992
2187
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
1993
2188
|
const patternBody = expectedValue.slice(1, -1);
|
|
1994
|
-
|
|
2189
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2190
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2191
|
+
state.info.regex = true;
|
|
1995
2192
|
}
|
|
1996
2193
|
else {
|
|
1997
2194
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1998
2195
|
regex = new RegExp(escapedPattern, "g");
|
|
1999
2196
|
}
|
|
2000
|
-
if (
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2197
|
+
if (attribute === "innerText") {
|
|
2198
|
+
if (state.info.regex) {
|
|
2199
|
+
if (!regex.test(val)) {
|
|
2200
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2201
|
+
state.info.failCause.assertionFailed = true;
|
|
2202
|
+
state.info.failCause.lastError = errorMessage;
|
|
2203
|
+
throw new Error(errorMessage);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
else {
|
|
2207
|
+
const valLines = val.split("\n");
|
|
2208
|
+
const expectedLines = expectedValue.split("\n");
|
|
2209
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2210
|
+
if (!isPart) {
|
|
2211
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2212
|
+
state.info.failCause.assertionFailed = true;
|
|
2213
|
+
state.info.failCause.lastError = errorMessage;
|
|
2214
|
+
throw new Error(errorMessage);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
else {
|
|
2219
|
+
if (!val.match(regex)) {
|
|
2220
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2221
|
+
state.info.failCause.assertionFailed = true;
|
|
2222
|
+
state.info.failCause.lastError = errorMessage;
|
|
2223
|
+
throw new Error(errorMessage);
|
|
2224
|
+
}
|
|
2005
2225
|
}
|
|
2006
2226
|
return state.info;
|
|
2007
2227
|
}
|
|
@@ -2009,7 +2229,7 @@ class StableBrowser {
|
|
|
2009
2229
|
await _commandError(state, e, this);
|
|
2010
2230
|
}
|
|
2011
2231
|
finally {
|
|
2012
|
-
_commandFinally(state, this);
|
|
2232
|
+
await _commandFinally(state, this);
|
|
2013
2233
|
}
|
|
2014
2234
|
}
|
|
2015
2235
|
async extractEmailData(emailAddress, options, world) {
|
|
@@ -2169,56 +2389,49 @@ class StableBrowser {
|
|
|
2169
2389
|
console.debug(error);
|
|
2170
2390
|
}
|
|
2171
2391
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
// });
|
|
2215
|
-
// }
|
|
2216
|
-
// } catch (error) {
|
|
2217
|
-
// // console.debug(error);
|
|
2218
|
-
// }
|
|
2219
|
-
// }
|
|
2392
|
+
_matcher(text) {
|
|
2393
|
+
if (!text) {
|
|
2394
|
+
return { matcher: "contains", queryText: "" };
|
|
2395
|
+
}
|
|
2396
|
+
if (text.length < 2) {
|
|
2397
|
+
return { matcher: "contains", queryText: text };
|
|
2398
|
+
}
|
|
2399
|
+
const split = text.split(":");
|
|
2400
|
+
const matcher = split[0].toLowerCase();
|
|
2401
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2402
|
+
return { matcher, queryText };
|
|
2403
|
+
}
|
|
2404
|
+
_getDomain(url) {
|
|
2405
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2406
|
+
return "";
|
|
2407
|
+
}
|
|
2408
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2409
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2410
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2411
|
+
}
|
|
2412
|
+
let n = hostnameFragments.length;
|
|
2413
|
+
let fragments = [...hostnameFragments];
|
|
2414
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2415
|
+
hostnameFragments.pop();
|
|
2416
|
+
n = hostnameFragments.length;
|
|
2417
|
+
}
|
|
2418
|
+
if (n == 0) {
|
|
2419
|
+
if (fragments[0] === "www")
|
|
2420
|
+
fragments = fragments.slice(1);
|
|
2421
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2422
|
+
}
|
|
2423
|
+
if (hostnameFragments[0] === "www")
|
|
2424
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2425
|
+
return hostnameFragments.join(".");
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Verify the page path matches the given path.
|
|
2429
|
+
* @param {string} pathPart - The path to verify.
|
|
2430
|
+
* @param {object} options - Options for verification.
|
|
2431
|
+
* @param {object} world - The world context.
|
|
2432
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2433
|
+
*/
|
|
2220
2434
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2221
|
-
const startTime = Date.now();
|
|
2222
2435
|
let error = null;
|
|
2223
2436
|
let screenshotId = null;
|
|
2224
2437
|
let screenshotPath = null;
|
|
@@ -2232,74 +2445,235 @@ class StableBrowser {
|
|
|
2232
2445
|
pathPart = newValue;
|
|
2233
2446
|
}
|
|
2234
2447
|
info.pathPart = pathPart;
|
|
2448
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2449
|
+
const state = {
|
|
2450
|
+
text_search: queryText,
|
|
2451
|
+
options,
|
|
2452
|
+
world,
|
|
2453
|
+
locate: false,
|
|
2454
|
+
scroll: false,
|
|
2455
|
+
highlight: false,
|
|
2456
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2457
|
+
text: `Verify the page url is ${queryText}`,
|
|
2458
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2459
|
+
operation: "verifyPagePath",
|
|
2460
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2461
|
+
};
|
|
2235
2462
|
try {
|
|
2463
|
+
await _preCommand(state, this);
|
|
2464
|
+
state.info.text = queryText;
|
|
2236
2465
|
for (let i = 0; i < 30; i++) {
|
|
2237
2466
|
const url = await this.page.url();
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2467
|
+
switch (matcher) {
|
|
2468
|
+
case "exact":
|
|
2469
|
+
if (url !== queryText) {
|
|
2470
|
+
if (i === 29) {
|
|
2471
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2472
|
+
}
|
|
2473
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
break;
|
|
2477
|
+
case "contains":
|
|
2478
|
+
if (!url.includes(queryText)) {
|
|
2479
|
+
if (i === 29) {
|
|
2480
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2481
|
+
}
|
|
2482
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
break;
|
|
2486
|
+
case "starts-with":
|
|
2487
|
+
{
|
|
2488
|
+
const domain = this._getDomain(url);
|
|
2489
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2490
|
+
if (i === 29) {
|
|
2491
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2492
|
+
}
|
|
2493
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
break;
|
|
2498
|
+
case "ends-with":
|
|
2499
|
+
{
|
|
2500
|
+
const urlObj = new URL(url);
|
|
2501
|
+
let route = "/";
|
|
2502
|
+
if (urlObj.pathname !== "/") {
|
|
2503
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2504
|
+
}
|
|
2505
|
+
else {
|
|
2506
|
+
route = "/";
|
|
2507
|
+
}
|
|
2508
|
+
if (route !== queryText) {
|
|
2509
|
+
if (i === 29) {
|
|
2510
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2511
|
+
}
|
|
2512
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
break;
|
|
2517
|
+
case "regex":
|
|
2518
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2519
|
+
if (!regex.test(url)) {
|
|
2520
|
+
if (i === 29) {
|
|
2521
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2522
|
+
}
|
|
2523
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2526
|
+
break;
|
|
2527
|
+
default:
|
|
2528
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2529
|
+
if (!url.includes(pathPart)) {
|
|
2530
|
+
if (i === 29) {
|
|
2531
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2532
|
+
}
|
|
2533
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2244
2536
|
}
|
|
2245
|
-
|
|
2246
|
-
return info;
|
|
2537
|
+
await _screenshot(state, this);
|
|
2538
|
+
return state.info;
|
|
2247
2539
|
}
|
|
2248
2540
|
}
|
|
2249
2541
|
catch (e) {
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
info.screenshotPath = screenshotPath;
|
|
2254
|
-
Object.assign(e, { info: info });
|
|
2255
|
-
error = e;
|
|
2256
|
-
// throw e;
|
|
2257
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2542
|
+
state.info.failCause.lastError = e.message;
|
|
2543
|
+
state.info.failCause.assertionFailed = true;
|
|
2544
|
+
await _commandError(state, e, this);
|
|
2258
2545
|
}
|
|
2259
2546
|
finally {
|
|
2260
|
-
|
|
2261
|
-
_reportToWorld(world, {
|
|
2262
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2263
|
-
text: "Verify page path",
|
|
2264
|
-
_text: "Verify the page path contains " + pathPart,
|
|
2265
|
-
screenshotId,
|
|
2266
|
-
result: error
|
|
2267
|
-
? {
|
|
2268
|
-
status: "FAILED",
|
|
2269
|
-
startTime,
|
|
2270
|
-
endTime,
|
|
2271
|
-
message: error?.message,
|
|
2272
|
-
}
|
|
2273
|
-
: {
|
|
2274
|
-
status: "PASSED",
|
|
2275
|
-
startTime,
|
|
2276
|
-
endTime,
|
|
2277
|
-
},
|
|
2278
|
-
info: info,
|
|
2279
|
-
});
|
|
2547
|
+
await _commandFinally(state, this);
|
|
2280
2548
|
}
|
|
2281
2549
|
}
|
|
2282
|
-
|
|
2550
|
+
/**
|
|
2551
|
+
* Verify the page title matches the given title.
|
|
2552
|
+
* @param {string} title - The title to verify.
|
|
2553
|
+
* @param {object} options - Options for verification.
|
|
2554
|
+
* @param {object} world - The world context.
|
|
2555
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2556
|
+
*/
|
|
2557
|
+
async verifyPageTitle(title, options = {}, world = null) {
|
|
2558
|
+
let error = null;
|
|
2559
|
+
let screenshotId = null;
|
|
2560
|
+
let screenshotPath = null;
|
|
2561
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2562
|
+
const newValue = await this._replaceWithLocalData(title, world);
|
|
2563
|
+
if (newValue !== title) {
|
|
2564
|
+
this.logger.info(title + "=" + newValue);
|
|
2565
|
+
title = newValue;
|
|
2566
|
+
}
|
|
2567
|
+
const { matcher, queryText } = this._matcher(title);
|
|
2568
|
+
const state = {
|
|
2569
|
+
text_search: queryText,
|
|
2570
|
+
options,
|
|
2571
|
+
world,
|
|
2572
|
+
locate: false,
|
|
2573
|
+
scroll: false,
|
|
2574
|
+
highlight: false,
|
|
2575
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
2576
|
+
text: `Verify the page title is ${queryText}`,
|
|
2577
|
+
_text: `Verify the page title is ${queryText}`,
|
|
2578
|
+
operation: "verifyPageTitle",
|
|
2579
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
2580
|
+
};
|
|
2581
|
+
try {
|
|
2582
|
+
await _preCommand(state, this);
|
|
2583
|
+
state.info.text = queryText;
|
|
2584
|
+
for (let i = 0; i < 30; i++) {
|
|
2585
|
+
const foundTitle = await this.page.title();
|
|
2586
|
+
switch (matcher) {
|
|
2587
|
+
case "exact":
|
|
2588
|
+
if (foundTitle !== queryText) {
|
|
2589
|
+
if (i === 29) {
|
|
2590
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
2591
|
+
}
|
|
2592
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2595
|
+
break;
|
|
2596
|
+
case "contains":
|
|
2597
|
+
if (!foundTitle.includes(queryText)) {
|
|
2598
|
+
if (i === 29) {
|
|
2599
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
2600
|
+
}
|
|
2601
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
break;
|
|
2605
|
+
case "starts-with":
|
|
2606
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
2607
|
+
if (i === 29) {
|
|
2608
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
2609
|
+
}
|
|
2610
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
break;
|
|
2614
|
+
case "ends-with":
|
|
2615
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
2616
|
+
if (i === 29) {
|
|
2617
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
2618
|
+
}
|
|
2619
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
break;
|
|
2623
|
+
case "regex":
|
|
2624
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2625
|
+
if (!regex.test(foundTitle)) {
|
|
2626
|
+
if (i === 29) {
|
|
2627
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
2628
|
+
}
|
|
2629
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
break;
|
|
2633
|
+
default:
|
|
2634
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2635
|
+
if (!foundTitle.includes(title)) {
|
|
2636
|
+
if (i === 29) {
|
|
2637
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
2638
|
+
}
|
|
2639
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
await _screenshot(state, this);
|
|
2644
|
+
return state.info;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
catch (e) {
|
|
2648
|
+
state.info.failCause.lastError = e.message;
|
|
2649
|
+
state.info.failCause.assertionFailed = true;
|
|
2650
|
+
await _commandError(state, e, this);
|
|
2651
|
+
}
|
|
2652
|
+
finally {
|
|
2653
|
+
await _commandFinally(state, this);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
2283
2657
|
const frames = this.page.frames();
|
|
2284
2658
|
let results = [];
|
|
2285
|
-
let ignoreCase = false;
|
|
2659
|
+
// let ignoreCase = false;
|
|
2286
2660
|
for (let i = 0; i < frames.length; i++) {
|
|
2287
2661
|
if (dateAlternatives.date) {
|
|
2288
2662
|
for (let j = 0; j < dateAlternatives.dates.length; j++) {
|
|
2289
|
-
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false,
|
|
2663
|
+
const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2290
2664
|
result.frame = frames[i];
|
|
2291
2665
|
results.push(result);
|
|
2292
2666
|
}
|
|
2293
2667
|
}
|
|
2294
2668
|
else if (numberAlternatives.number) {
|
|
2295
2669
|
for (let j = 0; j < numberAlternatives.numbers.length; j++) {
|
|
2296
|
-
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false,
|
|
2670
|
+
const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2297
2671
|
result.frame = frames[i];
|
|
2298
2672
|
results.push(result);
|
|
2299
2673
|
}
|
|
2300
2674
|
}
|
|
2301
2675
|
else {
|
|
2302
|
-
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false,
|
|
2676
|
+
const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
|
|
2303
2677
|
result.frame = frames[i];
|
|
2304
2678
|
results.push(result);
|
|
2305
2679
|
}
|
|
@@ -2318,7 +2692,7 @@ class StableBrowser {
|
|
|
2318
2692
|
scroll: false,
|
|
2319
2693
|
highlight: false,
|
|
2320
2694
|
type: Types.VERIFY_PAGE_CONTAINS_TEXT,
|
|
2321
|
-
text: `Verify text exists in page`,
|
|
2695
|
+
text: `Verify the text '${maskValue(text)}' exists in page`,
|
|
2322
2696
|
_text: `Verify the text '${text}' exists in page`,
|
|
2323
2697
|
operation: "verifyTextExistInPage",
|
|
2324
2698
|
log: "***** verify text " + text + " exists in page *****\n",
|
|
@@ -2360,27 +2734,10 @@ class StableBrowser {
|
|
|
2360
2734
|
const frame = resultWithElementsFound[0].frame;
|
|
2361
2735
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2362
2736
|
await this._highlightElements(frame, dataAttribute);
|
|
2363
|
-
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2364
|
-
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2365
|
-
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2366
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2367
|
-
// this._unhighlightElements(frame, dataAttribute)
|
|
2368
|
-
// .then(async () => {
|
|
2369
|
-
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2370
|
-
// })
|
|
2371
|
-
// .catch(
|
|
2372
|
-
// (e) => {}
|
|
2373
|
-
// console.error(e)
|
|
2374
|
-
// );
|
|
2375
|
-
// });
|
|
2376
|
-
// }
|
|
2377
2737
|
const element = await frame.locator(dataAttribute).first();
|
|
2378
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2379
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2380
2738
|
if (element) {
|
|
2381
2739
|
await this.scrollIfNeeded(element, state.info);
|
|
2382
2740
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2383
|
-
// await _screenshot(state, this, element);
|
|
2384
2741
|
}
|
|
2385
2742
|
}
|
|
2386
2743
|
await _screenshot(state, this);
|
|
@@ -2390,13 +2747,12 @@ class StableBrowser {
|
|
|
2390
2747
|
console.error(error);
|
|
2391
2748
|
}
|
|
2392
2749
|
}
|
|
2393
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2394
2750
|
}
|
|
2395
2751
|
catch (e) {
|
|
2396
2752
|
await _commandError(state, e, this);
|
|
2397
2753
|
}
|
|
2398
2754
|
finally {
|
|
2399
|
-
_commandFinally(state, this);
|
|
2755
|
+
await _commandFinally(state, this);
|
|
2400
2756
|
}
|
|
2401
2757
|
}
|
|
2402
2758
|
async waitForTextToDisappear(text, options = {}, world = null) {
|
|
@@ -2409,7 +2765,7 @@ class StableBrowser {
|
|
|
2409
2765
|
scroll: false,
|
|
2410
2766
|
highlight: false,
|
|
2411
2767
|
type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
|
|
2412
|
-
text: `Verify text does not exist in page`,
|
|
2768
|
+
text: `Verify the text '${maskValue(text)}' does not exist in page`,
|
|
2413
2769
|
_text: `Verify the text '${text}' does not exist in page`,
|
|
2414
2770
|
operation: "verifyTextNotExistInPage",
|
|
2415
2771
|
log: "***** verify text " + text + " does not exist in page *****\n",
|
|
@@ -2453,7 +2809,7 @@ class StableBrowser {
|
|
|
2453
2809
|
await _commandError(state, e, this);
|
|
2454
2810
|
}
|
|
2455
2811
|
finally {
|
|
2456
|
-
_commandFinally(state, this);
|
|
2812
|
+
await _commandFinally(state, this);
|
|
2457
2813
|
}
|
|
2458
2814
|
}
|
|
2459
2815
|
async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
|
|
@@ -2495,7 +2851,7 @@ class StableBrowser {
|
|
|
2495
2851
|
};
|
|
2496
2852
|
while (true) {
|
|
2497
2853
|
try {
|
|
2498
|
-
resultWithElementsFound = await this.findTextInAllFrames(
|
|
2854
|
+
resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
|
|
2499
2855
|
}
|
|
2500
2856
|
catch (error) {
|
|
2501
2857
|
// ignore
|
|
@@ -2523,7 +2879,7 @@ class StableBrowser {
|
|
|
2523
2879
|
const count = await frame.locator(css).count();
|
|
2524
2880
|
for (let j = 0; j < count; j++) {
|
|
2525
2881
|
const continer = await frame.locator(css).nth(j);
|
|
2526
|
-
const result = await this._locateElementByText(continer, textToVerify, "
|
|
2882
|
+
const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
|
|
2527
2883
|
if (result.elementCount > 0) {
|
|
2528
2884
|
const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
|
|
2529
2885
|
await this._highlightElements(frame, dataAttribute);
|
|
@@ -2564,9 +2920,33 @@ class StableBrowser {
|
|
|
2564
2920
|
await _commandError(state, e, this);
|
|
2565
2921
|
}
|
|
2566
2922
|
finally {
|
|
2567
|
-
_commandFinally(state, this);
|
|
2923
|
+
await _commandFinally(state, this);
|
|
2568
2924
|
}
|
|
2569
2925
|
}
|
|
2926
|
+
async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
|
|
2927
|
+
const frames = this.page.frames();
|
|
2928
|
+
let results = [];
|
|
2929
|
+
let ignoreCase = false;
|
|
2930
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2931
|
+
const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
|
|
2932
|
+
result.frame = frames[i];
|
|
2933
|
+
const climbArray = [];
|
|
2934
|
+
for (let i = 0; i < climb; i++) {
|
|
2935
|
+
climbArray.push("..");
|
|
2936
|
+
}
|
|
2937
|
+
let climbXpath = "xpath=" + climbArray.join("/");
|
|
2938
|
+
const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
|
|
2939
|
+
const count = await frames[i].locator(newLocator).count();
|
|
2940
|
+
if (count > 0) {
|
|
2941
|
+
result.elementCount = count;
|
|
2942
|
+
result.locator = newLocator;
|
|
2943
|
+
results.push(result);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
// state.info.results = results;
|
|
2947
|
+
const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
|
|
2948
|
+
return resultWithElementsFound;
|
|
2949
|
+
}
|
|
2570
2950
|
async visualVerification(text, options = {}, world = null) {
|
|
2571
2951
|
const startTime = Date.now();
|
|
2572
2952
|
let error = null;
|
|
@@ -2883,7 +3263,13 @@ class StableBrowser {
|
|
|
2883
3263
|
}
|
|
2884
3264
|
}
|
|
2885
3265
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
2886
|
-
|
|
3266
|
+
try {
|
|
3267
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3268
|
+
}
|
|
3269
|
+
catch (error) {
|
|
3270
|
+
this.logger.debug(error);
|
|
3271
|
+
throw error;
|
|
3272
|
+
}
|
|
2887
3273
|
}
|
|
2888
3274
|
_getLoadTimeout(options) {
|
|
2889
3275
|
let timeout = 15000;
|
|
@@ -2906,6 +3292,7 @@ class StableBrowser {
|
|
|
2906
3292
|
}
|
|
2907
3293
|
async saveStoreState(path = null, world = null) {
|
|
2908
3294
|
const storageState = await this.page.context().storageState();
|
|
3295
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2909
3296
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
2910
3297
|
if (path) {
|
|
2911
3298
|
// save { storageState: storageState } into the path
|
|
@@ -2916,10 +3303,14 @@ class StableBrowser {
|
|
|
2916
3303
|
}
|
|
2917
3304
|
}
|
|
2918
3305
|
async restoreSaveState(path = null, world = null) {
|
|
3306
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
2919
3307
|
await refreshBrowser(this, path, world);
|
|
2920
3308
|
this.registerEventListeners(this.context);
|
|
2921
3309
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
2922
3310
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3311
|
+
if (this.onRestoreSaveState) {
|
|
3312
|
+
this.onRestoreSaveState(path);
|
|
3313
|
+
}
|
|
2923
3314
|
}
|
|
2924
3315
|
async waitForPageLoad(options = {}, world = null) {
|
|
2925
3316
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -3002,10 +3393,10 @@ class StableBrowser {
|
|
|
3002
3393
|
await _commandError(state, e, this);
|
|
3003
3394
|
}
|
|
3004
3395
|
finally {
|
|
3005
|
-
_commandFinally(state, this);
|
|
3396
|
+
await _commandFinally(state, this);
|
|
3006
3397
|
}
|
|
3007
3398
|
}
|
|
3008
|
-
async tableCellOperation(headerText, rowText, options, world = null) {
|
|
3399
|
+
async tableCellOperation(headerText, rowText, options, _params, world = null) {
|
|
3009
3400
|
let operation = null;
|
|
3010
3401
|
if (!options || !options.operation) {
|
|
3011
3402
|
throw new Error("operation is not defined");
|
|
@@ -3062,26 +3453,24 @@ class StableBrowser {
|
|
|
3062
3453
|
await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
|
|
3063
3454
|
}
|
|
3064
3455
|
else {
|
|
3065
|
-
const results = await findElementsInArea(options.css, cellArea, this
|
|
3456
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3066
3457
|
if (results.length === 0) {
|
|
3067
3458
|
throw new Error(`Element not found in cell area`);
|
|
3068
3459
|
}
|
|
3069
3460
|
state.element = results[0];
|
|
3070
|
-
await
|
|
3461
|
+
await performAction("click", state.element, options, this, state, _params);
|
|
3071
3462
|
}
|
|
3072
3463
|
break;
|
|
3073
3464
|
case "hover+click":
|
|
3074
3465
|
if (!options.css) {
|
|
3075
3466
|
throw new Error("css is not defined");
|
|
3076
3467
|
}
|
|
3077
|
-
const results = await findElementsInArea(options.css, cellArea, this
|
|
3468
|
+
const results = await findElementsInArea(options.css, cellArea, this, options);
|
|
3078
3469
|
if (results.length === 0) {
|
|
3079
3470
|
throw new Error(`Element not found in cell area`);
|
|
3080
3471
|
}
|
|
3081
3472
|
state.element = results[0];
|
|
3082
|
-
await
|
|
3083
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3084
|
-
await results[0].click();
|
|
3473
|
+
await performAction("hover+click", state.element, options, this, state, _params);
|
|
3085
3474
|
break;
|
|
3086
3475
|
default:
|
|
3087
3476
|
throw new Error("operation is not supported");
|
|
@@ -3091,7 +3480,7 @@ class StableBrowser {
|
|
|
3091
3480
|
await _commandError(state, e, this);
|
|
3092
3481
|
}
|
|
3093
3482
|
finally {
|
|
3094
|
-
_commandFinally(state, this);
|
|
3483
|
+
await _commandFinally(state, this);
|
|
3095
3484
|
}
|
|
3096
3485
|
}
|
|
3097
3486
|
saveTestDataAsGlobal(options, world) {
|
|
@@ -3196,7 +3585,39 @@ class StableBrowser {
|
|
|
3196
3585
|
console.log("#-#");
|
|
3197
3586
|
}
|
|
3198
3587
|
}
|
|
3588
|
+
async beforeScenario(world, scenario) {
|
|
3589
|
+
this.beforeScenarioCalled = true;
|
|
3590
|
+
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3591
|
+
this.scenarioName = scenario.pickle.name;
|
|
3592
|
+
}
|
|
3593
|
+
if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
|
|
3594
|
+
this.featureName = scenario.gherkinDocument.feature.name;
|
|
3595
|
+
}
|
|
3596
|
+
if (this.context) {
|
|
3597
|
+
this.context.examplesRow = extractStepExampleParameters(scenario);
|
|
3598
|
+
}
|
|
3599
|
+
if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
|
|
3600
|
+
this.tags = scenario.pickle.tags.map((tag) => tag.name);
|
|
3601
|
+
// check if @global_test_data tag is present
|
|
3602
|
+
if (this.tags.includes("@global_test_data")) {
|
|
3603
|
+
this.saveTestDataAsGlobal({}, world);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
// update test data based on feature/scenario
|
|
3607
|
+
let envName = null;
|
|
3608
|
+
if (this.context && this.context.environment) {
|
|
3609
|
+
envName = this.context.environment.name;
|
|
3610
|
+
}
|
|
3611
|
+
if (!process.env.TEMP_RUN) {
|
|
3612
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
3613
|
+
}
|
|
3614
|
+
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3615
|
+
}
|
|
3616
|
+
async afterScenario(world, scenario) { }
|
|
3199
3617
|
async beforeStep(world, step) {
|
|
3618
|
+
if (!this.beforeScenarioCalled) {
|
|
3619
|
+
this.beforeScenario(world, step);
|
|
3620
|
+
}
|
|
3200
3621
|
if (this.stepIndex === undefined) {
|
|
3201
3622
|
this.stepIndex = 0;
|
|
3202
3623
|
}
|
|
@@ -3213,21 +3634,11 @@ class StableBrowser {
|
|
|
3213
3634
|
else {
|
|
3214
3635
|
this.stepName = "step " + this.stepIndex;
|
|
3215
3636
|
}
|
|
3216
|
-
if (this.context) {
|
|
3217
|
-
this.context.examplesRow = extractStepExampleParameters(step);
|
|
3218
|
-
}
|
|
3219
3637
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
3220
3638
|
if (this.context.browserObject.context) {
|
|
3221
3639
|
await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
|
|
3222
3640
|
}
|
|
3223
3641
|
}
|
|
3224
|
-
if (this.tags === null && step && step.pickle && step.pickle.tags) {
|
|
3225
|
-
this.tags = step.pickle.tags.map((tag) => tag.name);
|
|
3226
|
-
// check if @global_test_data tag is present
|
|
3227
|
-
if (this.tags.includes("@global_test_data")) {
|
|
3228
|
-
this.saveTestDataAsGlobal({}, world);
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
3642
|
if (this.initSnapshotTaken === false) {
|
|
3232
3643
|
this.initSnapshotTaken = true;
|
|
3233
3644
|
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
@@ -3252,18 +3663,68 @@ class StableBrowser {
|
|
|
3252
3663
|
const content = [`- path: ${path}`, `- title: ${title}`];
|
|
3253
3664
|
const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
|
|
3254
3665
|
for (let i = 0; i < frames.length; i++) {
|
|
3255
|
-
content.push(`- frame: ${i}`);
|
|
3256
3666
|
const frame = frames[i];
|
|
3257
|
-
|
|
3258
|
-
|
|
3667
|
+
try {
|
|
3668
|
+
// Ensure frame is attached and has body
|
|
3669
|
+
const body = frame.locator("body");
|
|
3670
|
+
await body.waitFor({ timeout: 200 }); // wait explicitly
|
|
3671
|
+
const snapshot = await body.ariaSnapshot({ timeout });
|
|
3672
|
+
content.push(`- frame: ${i}`);
|
|
3673
|
+
content.push(snapshot);
|
|
3674
|
+
}
|
|
3675
|
+
catch (innerErr) { }
|
|
3259
3676
|
}
|
|
3260
3677
|
return content.join("\n");
|
|
3261
3678
|
}
|
|
3262
3679
|
catch (e) {
|
|
3263
|
-
console.
|
|
3680
|
+
console.log("Error in getAriaSnapshot");
|
|
3681
|
+
//console.debug(e);
|
|
3264
3682
|
}
|
|
3265
3683
|
return null;
|
|
3266
3684
|
}
|
|
3685
|
+
/**
|
|
3686
|
+
* Sends command with custom payload to report.
|
|
3687
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
3688
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
3689
|
+
* @param content - Content of the command to be shown in the report.
|
|
3690
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
3691
|
+
* @param world - Optional world context.
|
|
3692
|
+
* @public
|
|
3693
|
+
*/
|
|
3694
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
3695
|
+
const state = {
|
|
3696
|
+
options,
|
|
3697
|
+
world,
|
|
3698
|
+
locate: false,
|
|
3699
|
+
scroll: false,
|
|
3700
|
+
screenshot: options.screenshot ?? false,
|
|
3701
|
+
highlight: options.highlight ?? false,
|
|
3702
|
+
type: Types.REPORT_COMMAND,
|
|
3703
|
+
text: commandText,
|
|
3704
|
+
_text: commandText,
|
|
3705
|
+
operation: "report_command",
|
|
3706
|
+
log: "***** " + commandText + " *****\n",
|
|
3707
|
+
};
|
|
3708
|
+
try {
|
|
3709
|
+
await _preCommand(state, this);
|
|
3710
|
+
const payload = {
|
|
3711
|
+
type: options.type ?? "text",
|
|
3712
|
+
content: content,
|
|
3713
|
+
screenshotId: null,
|
|
3714
|
+
};
|
|
3715
|
+
state.payload = payload;
|
|
3716
|
+
if (commandStatus === "FAILED") {
|
|
3717
|
+
state.throwError = true;
|
|
3718
|
+
throw new Error("Command failed");
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
catch (e) {
|
|
3722
|
+
await _commandError(state, e, this);
|
|
3723
|
+
}
|
|
3724
|
+
finally {
|
|
3725
|
+
await _commandFinally(state, this);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3267
3728
|
async afterStep(world, step) {
|
|
3268
3729
|
this.stepName = null;
|
|
3269
3730
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3271,6 +3732,13 @@ class StableBrowser {
|
|
|
3271
3732
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3272
3733
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3273
3734
|
});
|
|
3735
|
+
if (world && world.attach) {
|
|
3736
|
+
await world.attach(JSON.stringify({
|
|
3737
|
+
type: "trace",
|
|
3738
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
3739
|
+
}), "application/json+trace");
|
|
3740
|
+
}
|
|
3741
|
+
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3274
3742
|
}
|
|
3275
3743
|
}
|
|
3276
3744
|
if (this.context) {
|
|
@@ -3283,6 +3751,29 @@ class StableBrowser {
|
|
|
3283
3751
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3284
3752
|
}
|
|
3285
3753
|
}
|
|
3754
|
+
if (!process.env.TEMP_RUN) {
|
|
3755
|
+
const state = {
|
|
3756
|
+
world,
|
|
3757
|
+
locate: false,
|
|
3758
|
+
scroll: false,
|
|
3759
|
+
screenshot: true,
|
|
3760
|
+
highlight: true,
|
|
3761
|
+
type: Types.STEP_COMPLETE,
|
|
3762
|
+
text: "end of scenario",
|
|
3763
|
+
_text: "end of scenario",
|
|
3764
|
+
operation: "step_complete",
|
|
3765
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
3766
|
+
};
|
|
3767
|
+
try {
|
|
3768
|
+
await _preCommand(state, this);
|
|
3769
|
+
}
|
|
3770
|
+
catch (e) {
|
|
3771
|
+
await _commandError(state, e, this);
|
|
3772
|
+
}
|
|
3773
|
+
finally {
|
|
3774
|
+
await _commandFinally(state, this);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3286
3777
|
}
|
|
3287
3778
|
}
|
|
3288
3779
|
function createTimedPromise(promise, label) {
|