automation_model 1.0.701-dev → 1.0.701-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 +4 -1
- package/lib/analyze_helper.js.map +1 -1
- package/lib/api.d.ts +0 -1
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +4 -2
- package/lib/auto_page.js +210 -126
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +1 -0
- package/lib/browser_manager.js +54 -9
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.js.map +1 -1
- package/lib/command_common.d.ts +1 -1
- package/lib/command_common.js +18 -1
- 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.js +129 -25
- package/lib/file_checker.js.map +1 -1
- package/lib/find_function.js.map +1 -1
- package/lib/init_browser.js +4 -4
- package/lib/init_browser.js.map +1 -1
- package/lib/locate_element.js.map +1 -1
- package/lib/locator.d.ts +1 -0
- package/lib/locator.js +10 -3
- package/lib/locator.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +2 -0
- package/lib/network.js +334 -86
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +21 -0
- package/lib/route.js +450 -0
- package/lib/route.js.map +1 -0
- package/lib/scripts/axe.mini.js +3 -3
- package/lib/snapshot_validation.d.ts +4 -2
- package/lib/snapshot_validation.js +160 -42
- package/lib/snapshot_validation.js.map +1 -1
- package/lib/stable_browser.d.ts +59 -26
- package/lib/stable_browser.js +897 -201
- package/lib/stable_browser.js.map +1 -1
- package/lib/table.d.ts +9 -7
- package/lib/table.js +82 -12
- package/lib/table.js.map +1 -1
- package/lib/table_analyze.js.map +1 -1
- package/lib/table_helper.js.map +1 -1
- package/lib/test_context.d.ts +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +3 -1
- package/lib/utils.js +88 -63
- package/lib/utils.js.map +1 -1
- package/package.json +12 -7
package/lib/stable_browser.js
CHANGED
|
@@ -10,7 +10,7 @@ import { getDateTimeValue } from "./date_time.js";
|
|
|
10
10
|
import drawRectangle from "./drawRect.js";
|
|
11
11
|
//import { closeUnexpectedPopups } from "./popups.js";
|
|
12
12
|
import { getTableCells, getTableData } from "./table_analyze.js";
|
|
13
|
-
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
|
|
13
|
+
import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
|
|
14
14
|
import csv from "csv-parser";
|
|
15
15
|
import { Readable } from "node:stream";
|
|
16
16
|
import readline from "readline";
|
|
@@ -19,35 +19,41 @@ import { getTestData } from "./auto_page.js";
|
|
|
19
19
|
import { locate_element } from "./locate_element.js";
|
|
20
20
|
import { randomUUID } from "crypto";
|
|
21
21
|
import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
|
|
22
|
-
import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
22
|
+
import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
|
|
23
23
|
import { LocatorLog } from "./locator_log.js";
|
|
24
24
|
import axios from "axios";
|
|
25
25
|
import { _findCellArea, findElementsInArea } from "./table_helper.js";
|
|
26
|
-
import { snapshotValidation } from "./snapshot_validation.js";
|
|
26
|
+
import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
|
|
27
27
|
import { loadBrunoParams } from "./bruno.js";
|
|
28
|
+
import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
|
|
28
29
|
export const Types = {
|
|
29
30
|
CLICK: "click_element",
|
|
30
31
|
WAIT_ELEMENT: "wait_element",
|
|
31
32
|
NAVIGATE: "navigate",
|
|
33
|
+
GO_BACK: "go_back",
|
|
34
|
+
GO_FORWARD: "go_forward",
|
|
32
35
|
FILL: "fill_element",
|
|
33
|
-
EXECUTE: "execute_page_method",
|
|
34
|
-
OPEN: "open_environment",
|
|
36
|
+
EXECUTE: "execute_page_method", //
|
|
37
|
+
OPEN: "open_environment", //
|
|
35
38
|
COMPLETE: "step_complete",
|
|
36
39
|
ASK: "information_needed",
|
|
37
|
-
GET_PAGE_STATUS: "get_page_status",
|
|
38
|
-
CLICK_ROW_ACTION: "click_row_action",
|
|
40
|
+
GET_PAGE_STATUS: "get_page_status", ///
|
|
41
|
+
CLICK_ROW_ACTION: "click_row_action", //
|
|
39
42
|
VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
|
|
40
43
|
VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
|
|
41
44
|
VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
|
|
42
45
|
ANALYZE_TABLE: "analyze_table",
|
|
43
|
-
SELECT: "select_combobox",
|
|
46
|
+
SELECT: "select_combobox", //
|
|
47
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
44
48
|
VERIFY_PAGE_PATH: "verify_page_path",
|
|
49
|
+
VERIFY_PAGE_TITLE: "verify_page_title",
|
|
45
50
|
TYPE_PRESS: "type_press",
|
|
46
51
|
PRESS: "press_key",
|
|
47
52
|
HOVER: "hover_element",
|
|
48
53
|
CHECK: "check_element",
|
|
49
54
|
UNCHECK: "uncheck_element",
|
|
50
55
|
EXTRACT: "extract_attribute",
|
|
56
|
+
EXTRACT_PROPERTY: "extract_property",
|
|
51
57
|
CLOSE_PAGE: "close_page",
|
|
52
58
|
TABLE_OPERATION: "table_operation",
|
|
53
59
|
SET_DATE_TIME: "set_date_time",
|
|
@@ -59,9 +65,13 @@ export const Types = {
|
|
|
59
65
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
60
66
|
VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
|
|
61
67
|
BRUNO: "bruno",
|
|
62
|
-
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
63
68
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
64
69
|
SET_INPUT_FILES: "set_input_files",
|
|
70
|
+
SNAPSHOT_VALIDATION: "snapshot_validation",
|
|
71
|
+
REPORT_COMMAND: "report_command",
|
|
72
|
+
STEP_COMPLETE: "step_complete",
|
|
73
|
+
SLEEP: "sleep",
|
|
74
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
65
75
|
};
|
|
66
76
|
export const apps = {};
|
|
67
77
|
const formatElementName = (elementName) => {
|
|
@@ -73,6 +83,7 @@ class StableBrowser {
|
|
|
73
83
|
logger;
|
|
74
84
|
context;
|
|
75
85
|
world;
|
|
86
|
+
fastMode;
|
|
76
87
|
project_path = null;
|
|
77
88
|
webLogFile = null;
|
|
78
89
|
networkLogger = null;
|
|
@@ -81,12 +92,14 @@ class StableBrowser {
|
|
|
81
92
|
tags = null;
|
|
82
93
|
isRecording = false;
|
|
83
94
|
initSnapshotTaken = false;
|
|
84
|
-
|
|
95
|
+
abortedExecution = false;
|
|
96
|
+
constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
|
|
85
97
|
this.browser = browser;
|
|
86
98
|
this.page = page;
|
|
87
99
|
this.logger = logger;
|
|
88
100
|
this.context = context;
|
|
89
101
|
this.world = world;
|
|
102
|
+
this.fastMode = fastMode;
|
|
90
103
|
if (!this.logger) {
|
|
91
104
|
this.logger = console;
|
|
92
105
|
}
|
|
@@ -115,6 +128,19 @@ class StableBrowser {
|
|
|
115
128
|
context.pages = [this.page];
|
|
116
129
|
const logFolder = path.join(this.project_path, "logs", "web");
|
|
117
130
|
this.world = world;
|
|
131
|
+
if (this.configuration && this.configuration.fastMode === true) {
|
|
132
|
+
this.fastMode = true;
|
|
133
|
+
}
|
|
134
|
+
if (process.env.FAST_MODE === "true") {
|
|
135
|
+
// console.log("Fast mode enabled from environment variable");
|
|
136
|
+
this.fastMode = true;
|
|
137
|
+
}
|
|
138
|
+
if (process.env.FAST_MODE === "false") {
|
|
139
|
+
this.fastMode = false;
|
|
140
|
+
}
|
|
141
|
+
if (this.context) {
|
|
142
|
+
this.context.fastMode = this.fastMode;
|
|
143
|
+
}
|
|
118
144
|
this.registerEventListeners(this.context);
|
|
119
145
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
120
146
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
@@ -125,6 +151,9 @@ class StableBrowser {
|
|
|
125
151
|
if (!context.pageLoading) {
|
|
126
152
|
context.pageLoading = { status: false };
|
|
127
153
|
}
|
|
154
|
+
if (this.configuration && this.configuration.acceptDialog && this.page) {
|
|
155
|
+
this.page.on("dialog", (dialog) => dialog.accept());
|
|
156
|
+
}
|
|
128
157
|
context.playContext.on("page", async function (page) {
|
|
129
158
|
if (this.configuration && this.configuration.closePopups === true) {
|
|
130
159
|
console.log("close unexpected popups");
|
|
@@ -133,6 +162,14 @@ class StableBrowser {
|
|
|
133
162
|
}
|
|
134
163
|
context.pageLoading.status = true;
|
|
135
164
|
this.page = page;
|
|
165
|
+
try {
|
|
166
|
+
if (this.configuration && this.configuration.acceptDialog) {
|
|
167
|
+
await page.on("dialog", (dialog) => dialog.accept());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.error("Error on dialog accept registration", error);
|
|
172
|
+
}
|
|
136
173
|
context.page = page;
|
|
137
174
|
context.pages.push(page);
|
|
138
175
|
registerNetworkEvents(this.world, this, context, this.page);
|
|
@@ -184,7 +221,9 @@ class StableBrowser {
|
|
|
184
221
|
if (newContextCreated) {
|
|
185
222
|
this.registerEventListeners(this.context);
|
|
186
223
|
await this.goto(this.context.environment.baseUrl);
|
|
187
|
-
|
|
224
|
+
if (!this.fastMode) {
|
|
225
|
+
await this.waitForPageLoad();
|
|
226
|
+
}
|
|
188
227
|
}
|
|
189
228
|
}
|
|
190
229
|
async switchTab(tabTitleOrIndex) {
|
|
@@ -278,6 +317,7 @@ class StableBrowser {
|
|
|
278
317
|
if (!url) {
|
|
279
318
|
throw new Error("url is null, verify that the environment file is correct");
|
|
280
319
|
}
|
|
320
|
+
url = await this._replaceWithLocalData(url, this.world);
|
|
281
321
|
if (!url.startsWith("http")) {
|
|
282
322
|
url = "https://" + url;
|
|
283
323
|
}
|
|
@@ -309,6 +349,64 @@ class StableBrowser {
|
|
|
309
349
|
await _commandFinally(state, this);
|
|
310
350
|
}
|
|
311
351
|
}
|
|
352
|
+
async goBack(options, world = null) {
|
|
353
|
+
const state = {
|
|
354
|
+
value: "",
|
|
355
|
+
world: world,
|
|
356
|
+
type: Types.GO_BACK,
|
|
357
|
+
text: `Browser navigate back`,
|
|
358
|
+
operation: "goBack",
|
|
359
|
+
log: "***** navigate back *****\n",
|
|
360
|
+
info: {},
|
|
361
|
+
locate: false,
|
|
362
|
+
scroll: false,
|
|
363
|
+
screenshot: false,
|
|
364
|
+
highlight: false,
|
|
365
|
+
};
|
|
366
|
+
try {
|
|
367
|
+
await _preCommand(state, this);
|
|
368
|
+
await this.page.goBack({
|
|
369
|
+
waitUntil: "load",
|
|
370
|
+
});
|
|
371
|
+
await _screenshot(state, this);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
console.error("Error on goBack", error);
|
|
375
|
+
_commandError(state, error, this);
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
await _commandFinally(state, this);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async goForward(options, world = null) {
|
|
382
|
+
const state = {
|
|
383
|
+
value: "",
|
|
384
|
+
world: world,
|
|
385
|
+
type: Types.GO_FORWARD,
|
|
386
|
+
text: `Browser navigate forward`,
|
|
387
|
+
operation: "goForward",
|
|
388
|
+
log: "***** navigate forward *****\n",
|
|
389
|
+
info: {},
|
|
390
|
+
locate: false,
|
|
391
|
+
scroll: false,
|
|
392
|
+
screenshot: false,
|
|
393
|
+
highlight: false,
|
|
394
|
+
};
|
|
395
|
+
try {
|
|
396
|
+
await _preCommand(state, this);
|
|
397
|
+
await this.page.goForward({
|
|
398
|
+
waitUntil: "load",
|
|
399
|
+
});
|
|
400
|
+
await _screenshot(state, this);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
console.error("Error on goForward", error);
|
|
404
|
+
_commandError(state, error, this);
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
await _commandFinally(state, this);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
312
410
|
async _getLocator(locator, scope, _params) {
|
|
313
411
|
locator = _fixLocatorUsingParams(locator, _params);
|
|
314
412
|
// locator = await this._replaceWithLocalData(locator);
|
|
@@ -422,7 +520,7 @@ class StableBrowser {
|
|
|
422
520
|
}
|
|
423
521
|
return { elementCount: tagCount, randomToken };
|
|
424
522
|
}
|
|
425
|
-
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
|
|
523
|
+
async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
|
|
426
524
|
if (!info) {
|
|
427
525
|
info = {};
|
|
428
526
|
}
|
|
@@ -434,14 +532,13 @@ class StableBrowser {
|
|
|
434
532
|
info.locatorLog = new LocatorLog(selectorHierarchy);
|
|
435
533
|
}
|
|
436
534
|
let locatorSearch = selectorHierarchy[index];
|
|
437
|
-
let originalLocatorSearch = "";
|
|
438
535
|
try {
|
|
439
|
-
|
|
440
|
-
locatorSearch = JSON.parse(originalLocatorSearch);
|
|
536
|
+
locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
|
|
441
537
|
}
|
|
442
538
|
catch (e) {
|
|
443
539
|
console.error(e);
|
|
444
540
|
}
|
|
541
|
+
let originalLocatorSearch = JSON.stringify(locatorSearch);
|
|
445
542
|
//info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
|
|
446
543
|
let locator = null;
|
|
447
544
|
if (locatorSearch.climb && locatorSearch.climb >= 0) {
|
|
@@ -489,7 +586,7 @@ class StableBrowser {
|
|
|
489
586
|
}
|
|
490
587
|
return;
|
|
491
588
|
}
|
|
492
|
-
if (info.locatorLog && count === 0) {
|
|
589
|
+
if (info.locatorLog && count === 0 && logErrors) {
|
|
493
590
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
|
|
494
591
|
}
|
|
495
592
|
for (let j = 0; j < count; j++) {
|
|
@@ -504,7 +601,7 @@ class StableBrowser {
|
|
|
504
601
|
info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
|
|
505
602
|
}
|
|
506
603
|
}
|
|
507
|
-
else {
|
|
604
|
+
else if (logErrors) {
|
|
508
605
|
info.failCause.visible = visible;
|
|
509
606
|
info.failCause.enabled = enabled;
|
|
510
607
|
if (!info.printMessages) {
|
|
@@ -596,7 +693,7 @@ class StableBrowser {
|
|
|
596
693
|
let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
|
|
597
694
|
if (!element.rerun) {
|
|
598
695
|
const randomToken = Math.random().toString(36).substring(7);
|
|
599
|
-
element.evaluate((el, randomToken) => {
|
|
696
|
+
await element.evaluate((el, randomToken) => {
|
|
600
697
|
el.setAttribute("data-blinq-id-" + randomToken, "");
|
|
601
698
|
}, randomToken);
|
|
602
699
|
// if (element._frame) {
|
|
@@ -649,7 +746,7 @@ class StableBrowser {
|
|
|
649
746
|
break;
|
|
650
747
|
}
|
|
651
748
|
catch (error) {
|
|
652
|
-
console.error("frame not found " + frameLocator.css);
|
|
749
|
+
// console.error("frame not found " + frameLocator.css);
|
|
653
750
|
}
|
|
654
751
|
}
|
|
655
752
|
}
|
|
@@ -727,7 +824,6 @@ class StableBrowser {
|
|
|
727
824
|
let locatorsCount = 0;
|
|
728
825
|
let lazy_scroll = false;
|
|
729
826
|
//let arrayMode = Array.isArray(selectors);
|
|
730
|
-
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
731
827
|
let selectorsLocators = null;
|
|
732
828
|
selectorsLocators = selectors.locators;
|
|
733
829
|
// group selectors by priority
|
|
@@ -755,6 +851,7 @@ class StableBrowser {
|
|
|
755
851
|
let highPriorityOnly = true;
|
|
756
852
|
let visibleOnly = true;
|
|
757
853
|
while (true) {
|
|
854
|
+
let scope = await this._findFrameScope(selectors, timeout, info);
|
|
758
855
|
locatorsCount = 0;
|
|
759
856
|
let result = [];
|
|
760
857
|
let popupResult = await this.closeUnexpectedPopups(info, _params);
|
|
@@ -845,7 +942,7 @@ class StableBrowser {
|
|
|
845
942
|
}
|
|
846
943
|
throw new Error("failed to locate first element no elements found, " + info.log);
|
|
847
944
|
}
|
|
848
|
-
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
|
|
945
|
+
async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
|
|
849
946
|
let foundElements = [];
|
|
850
947
|
const result = {
|
|
851
948
|
foundElements: foundElements,
|
|
@@ -864,7 +961,9 @@ class StableBrowser {
|
|
|
864
961
|
await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
|
|
865
962
|
}
|
|
866
963
|
catch (e) {
|
|
867
|
-
|
|
964
|
+
if (logErrors) {
|
|
965
|
+
this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
|
|
966
|
+
}
|
|
868
967
|
}
|
|
869
968
|
}
|
|
870
969
|
if (foundLocators.length === 1) {
|
|
@@ -905,7 +1004,7 @@ class StableBrowser {
|
|
|
905
1004
|
});
|
|
906
1005
|
result.locatorIndex = i;
|
|
907
1006
|
}
|
|
908
|
-
else {
|
|
1007
|
+
else if (logErrors) {
|
|
909
1008
|
info.failCause.foundMultiple = true;
|
|
910
1009
|
if (info.locatorLog) {
|
|
911
1010
|
info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
|
|
@@ -1028,7 +1127,9 @@ class StableBrowser {
|
|
|
1028
1127
|
try {
|
|
1029
1128
|
await _preCommand(state, this);
|
|
1030
1129
|
await performAction("click", state.element, options, this, state, _params);
|
|
1031
|
-
|
|
1130
|
+
if (!this.fastMode) {
|
|
1131
|
+
await this.waitForPageLoad();
|
|
1132
|
+
}
|
|
1032
1133
|
return state.info;
|
|
1033
1134
|
}
|
|
1034
1135
|
catch (e) {
|
|
@@ -1091,7 +1192,7 @@ class StableBrowser {
|
|
|
1091
1192
|
// if (world && world.screenshot && !world.screenshotPath) {
|
|
1092
1193
|
// console.log(`Highlighting while running from recorder`);
|
|
1093
1194
|
await this._highlightElements(state.element);
|
|
1094
|
-
await state.element.setChecked(checked);
|
|
1195
|
+
await state.element.setChecked(checked, { timeout: 2000 });
|
|
1095
1196
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1096
1197
|
// await this._unHighlightElements(element);
|
|
1097
1198
|
// }
|
|
@@ -1103,11 +1204,28 @@ class StableBrowser {
|
|
|
1103
1204
|
this.logger.info("element did not change its state, ignoring...");
|
|
1104
1205
|
}
|
|
1105
1206
|
else {
|
|
1207
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1106
1208
|
//await this.closeUnexpectedPopups();
|
|
1107
1209
|
state.info.log += "setCheck failed, will try again" + "\n";
|
|
1108
|
-
state.
|
|
1109
|
-
|
|
1110
|
-
|
|
1210
|
+
state.element_found = false;
|
|
1211
|
+
try {
|
|
1212
|
+
state.element = await this._locate(selectors, state.info, _params, 100);
|
|
1213
|
+
state.element_found = true;
|
|
1214
|
+
// check the check state
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
// element dismissed
|
|
1218
|
+
}
|
|
1219
|
+
if (state.element_found) {
|
|
1220
|
+
const isChecked = await state.element.isChecked();
|
|
1221
|
+
if (isChecked !== checked) {
|
|
1222
|
+
// perform click
|
|
1223
|
+
await state.element.click({ timeout: 2000, force: true });
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1111
1229
|
}
|
|
1112
1230
|
}
|
|
1113
1231
|
await this.waitForPageLoad();
|
|
@@ -1403,7 +1521,9 @@ class StableBrowser {
|
|
|
1403
1521
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1404
1522
|
}
|
|
1405
1523
|
}
|
|
1524
|
+
//if (!this.fastMode) {
|
|
1406
1525
|
await _screenshot(state, this);
|
|
1526
|
+
//}
|
|
1407
1527
|
if (enter === true) {
|
|
1408
1528
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1409
1529
|
await this.page.keyboard.press("Enter");
|
|
@@ -1712,14 +1832,17 @@ class StableBrowser {
|
|
|
1712
1832
|
throw new Error("referanceSnapshot is null");
|
|
1713
1833
|
}
|
|
1714
1834
|
let text = null;
|
|
1715
|
-
if (fs.existsSync(path.join(this.project_path, "snapshots", referanceSnapshot + ".yml"))) {
|
|
1716
|
-
text = fs.readFileSync(path.join(this.project_path, "snapshots", referanceSnapshot + ".yml"), "utf8");
|
|
1835
|
+
if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
|
|
1836
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
|
|
1837
|
+
}
|
|
1838
|
+
else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
|
|
1839
|
+
text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
|
|
1717
1840
|
}
|
|
1718
1841
|
else if (referanceSnapshot.startsWith("yaml:")) {
|
|
1719
1842
|
text = referanceSnapshot.substring(5);
|
|
1720
1843
|
}
|
|
1721
1844
|
else {
|
|
1722
|
-
throw new Error("
|
|
1845
|
+
throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
|
|
1723
1846
|
}
|
|
1724
1847
|
state.text = text;
|
|
1725
1848
|
const newValue = await this._replaceWithLocalData(text, world);
|
|
@@ -1737,18 +1860,23 @@ class StableBrowser {
|
|
|
1737
1860
|
scope = await this._findFrameScope(frameSelectors, timeout, state.info);
|
|
1738
1861
|
}
|
|
1739
1862
|
const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
|
|
1740
|
-
matchResult = snapshotValidation(snapshot, newValue);
|
|
1863
|
+
matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
|
|
1741
1864
|
if (matchResult.errorLine !== -1) {
|
|
1742
1865
|
throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
|
|
1743
1866
|
}
|
|
1744
1867
|
// highlight and screenshot
|
|
1868
|
+
try {
|
|
1869
|
+
await await highlightSnapshot(newValue, scope);
|
|
1870
|
+
await _screenshot(state, this);
|
|
1871
|
+
}
|
|
1872
|
+
catch (e) { }
|
|
1745
1873
|
return state.info;
|
|
1746
1874
|
}
|
|
1747
1875
|
catch (e) {
|
|
1748
1876
|
// Log error but continue retrying until timeout is reached
|
|
1749
|
-
this.logger.warn("Retrying
|
|
1877
|
+
//this.logger.warn("Retrying snapshot validation due to: " + e.message);
|
|
1750
1878
|
}
|
|
1751
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1879
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
|
|
1752
1880
|
}
|
|
1753
1881
|
throw new Error("No snapshot match " + matchResult?.errorLineText);
|
|
1754
1882
|
}
|
|
@@ -1900,12 +2028,7 @@ class StableBrowser {
|
|
|
1900
2028
|
}
|
|
1901
2029
|
}
|
|
1902
2030
|
getTestData(world = null) {
|
|
1903
|
-
|
|
1904
|
-
let data = {};
|
|
1905
|
-
if (fs.existsSync(dataFile)) {
|
|
1906
|
-
data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
1907
|
-
}
|
|
1908
|
-
return data;
|
|
2031
|
+
return _getTestData(world, this.context, this);
|
|
1909
2032
|
}
|
|
1910
2033
|
async _screenShot(options = {}, world = null, info = null) {
|
|
1911
2034
|
// collect url/path/title
|
|
@@ -2125,6 +2248,77 @@ class StableBrowser {
|
|
|
2125
2248
|
await _commandFinally(state, this);
|
|
2126
2249
|
}
|
|
2127
2250
|
}
|
|
2251
|
+
async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
|
|
2252
|
+
const state = {
|
|
2253
|
+
selectors,
|
|
2254
|
+
_params,
|
|
2255
|
+
property,
|
|
2256
|
+
variable,
|
|
2257
|
+
options,
|
|
2258
|
+
world,
|
|
2259
|
+
type: Types.EXTRACT_PROPERTY,
|
|
2260
|
+
text: `Extract property from element`,
|
|
2261
|
+
_text: `Extract property ${property} from ${selectors.element_name}`,
|
|
2262
|
+
operation: "extractProperty",
|
|
2263
|
+
log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
|
|
2264
|
+
allowDisabled: true,
|
|
2265
|
+
};
|
|
2266
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2267
|
+
try {
|
|
2268
|
+
await _preCommand(state, this);
|
|
2269
|
+
switch (property) {
|
|
2270
|
+
case "inner_text":
|
|
2271
|
+
state.value = await state.element.innerText();
|
|
2272
|
+
break;
|
|
2273
|
+
case "href":
|
|
2274
|
+
state.value = await state.element.getAttribute("href");
|
|
2275
|
+
break;
|
|
2276
|
+
case "value":
|
|
2277
|
+
state.value = await state.element.inputValue();
|
|
2278
|
+
break;
|
|
2279
|
+
case "text":
|
|
2280
|
+
state.value = await state.element.textContent();
|
|
2281
|
+
break;
|
|
2282
|
+
default:
|
|
2283
|
+
if (property.startsWith("dataset.")) {
|
|
2284
|
+
const dataAttribute = property.substring(8);
|
|
2285
|
+
state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2286
|
+
}
|
|
2287
|
+
else {
|
|
2288
|
+
state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (options !== null) {
|
|
2292
|
+
if (options.regex && options.regex !== "") {
|
|
2293
|
+
// Construct a regex pattern from the provided string
|
|
2294
|
+
const regex = options.regex.slice(1, -1);
|
|
2295
|
+
const regexPattern = new RegExp(regex, "g");
|
|
2296
|
+
const matches = state.value.match(regexPattern);
|
|
2297
|
+
if (matches) {
|
|
2298
|
+
let newValue = "";
|
|
2299
|
+
for (const match of matches) {
|
|
2300
|
+
newValue += match;
|
|
2301
|
+
}
|
|
2302
|
+
state.value = newValue;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
if (options.trimSpaces && options.trimSpaces === true) {
|
|
2306
|
+
state.value = state.value.trim();
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
state.info.value = state.value;
|
|
2310
|
+
this.setTestData({ [variable]: state.value }, world);
|
|
2311
|
+
this.logger.info("set test data: " + variable + "=" + state.value);
|
|
2312
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2313
|
+
return state.info;
|
|
2314
|
+
}
|
|
2315
|
+
catch (e) {
|
|
2316
|
+
await _commandError(state, e, this);
|
|
2317
|
+
}
|
|
2318
|
+
finally {
|
|
2319
|
+
await _commandFinally(state, this);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2128
2322
|
async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
|
|
2129
2323
|
const state = {
|
|
2130
2324
|
selectors,
|
|
@@ -2177,14 +2371,167 @@ class StableBrowser {
|
|
|
2177
2371
|
let regex;
|
|
2178
2372
|
if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
|
|
2179
2373
|
const patternBody = expectedValue.slice(1, -1);
|
|
2180
|
-
|
|
2374
|
+
const processedPattern = patternBody.replace(/\n/g, ".*");
|
|
2375
|
+
regex = new RegExp(processedPattern, "gs");
|
|
2376
|
+
state.info.regex = true;
|
|
2181
2377
|
}
|
|
2182
2378
|
else {
|
|
2183
2379
|
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2184
2380
|
regex = new RegExp(escapedPattern, "g");
|
|
2185
2381
|
}
|
|
2186
|
-
if (
|
|
2187
|
-
|
|
2382
|
+
if (attribute === "innerText") {
|
|
2383
|
+
if (state.info.regex) {
|
|
2384
|
+
if (!regex.test(val)) {
|
|
2385
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2386
|
+
state.info.failCause.assertionFailed = true;
|
|
2387
|
+
state.info.failCause.lastError = errorMessage;
|
|
2388
|
+
throw new Error(errorMessage);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
else {
|
|
2392
|
+
const valLines = val.split("\n");
|
|
2393
|
+
const expectedLines = expectedValue.split("\n");
|
|
2394
|
+
const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
|
|
2395
|
+
if (!isPart) {
|
|
2396
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2397
|
+
state.info.failCause.assertionFailed = true;
|
|
2398
|
+
state.info.failCause.lastError = errorMessage;
|
|
2399
|
+
throw new Error(errorMessage);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
else {
|
|
2404
|
+
if (!val.match(regex)) {
|
|
2405
|
+
let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2406
|
+
state.info.failCause.assertionFailed = true;
|
|
2407
|
+
state.info.failCause.lastError = errorMessage;
|
|
2408
|
+
throw new Error(errorMessage);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return state.info;
|
|
2412
|
+
}
|
|
2413
|
+
catch (e) {
|
|
2414
|
+
await _commandError(state, e, this);
|
|
2415
|
+
}
|
|
2416
|
+
finally {
|
|
2417
|
+
await _commandFinally(state, this);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
|
|
2421
|
+
const state = {
|
|
2422
|
+
selectors,
|
|
2423
|
+
_params,
|
|
2424
|
+
property,
|
|
2425
|
+
value,
|
|
2426
|
+
options,
|
|
2427
|
+
world,
|
|
2428
|
+
type: Types.VERIFY_PROPERTY,
|
|
2429
|
+
highlight: true,
|
|
2430
|
+
screenshot: true,
|
|
2431
|
+
text: `Verify element property`,
|
|
2432
|
+
_text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
|
|
2433
|
+
operation: "verifyProperty",
|
|
2434
|
+
log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
|
|
2435
|
+
allowDisabled: true,
|
|
2436
|
+
};
|
|
2437
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2438
|
+
let val;
|
|
2439
|
+
let expectedValue;
|
|
2440
|
+
try {
|
|
2441
|
+
await _preCommand(state, this);
|
|
2442
|
+
expectedValue = await replaceWithLocalTestData(state.value, world);
|
|
2443
|
+
state.info.expectedValue = expectedValue;
|
|
2444
|
+
switch (property) {
|
|
2445
|
+
case "innerText":
|
|
2446
|
+
val = String(await state.element.innerText());
|
|
2447
|
+
break;
|
|
2448
|
+
case "text":
|
|
2449
|
+
val = String(await state.element.textContent());
|
|
2450
|
+
break;
|
|
2451
|
+
case "value":
|
|
2452
|
+
val = String(await state.element.inputValue());
|
|
2453
|
+
break;
|
|
2454
|
+
case "checked":
|
|
2455
|
+
val = String(await state.element.isChecked());
|
|
2456
|
+
break;
|
|
2457
|
+
case "disabled":
|
|
2458
|
+
val = String(await state.element.isDisabled());
|
|
2459
|
+
break;
|
|
2460
|
+
case "readOnly":
|
|
2461
|
+
const isEditable = await state.element.isEditable();
|
|
2462
|
+
val = String(!isEditable);
|
|
2463
|
+
break;
|
|
2464
|
+
case "innerHTML":
|
|
2465
|
+
val = String(await state.element.innerHTML());
|
|
2466
|
+
break;
|
|
2467
|
+
case "outerHTML":
|
|
2468
|
+
val = String(await state.element.evaluate((element) => element.outerHTML));
|
|
2469
|
+
break;
|
|
2470
|
+
default:
|
|
2471
|
+
if (property.startsWith("dataset.")) {
|
|
2472
|
+
const dataAttribute = property.substring(8);
|
|
2473
|
+
val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
|
|
2474
|
+
}
|
|
2475
|
+
else {
|
|
2476
|
+
val = String(await state.element.evaluate((element, prop) => element[prop], property));
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
// Helper function to remove all style="" attributes
|
|
2480
|
+
const removeStyleAttributes = (htmlString) => {
|
|
2481
|
+
return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
|
|
2482
|
+
};
|
|
2483
|
+
// Remove style attributes for innerHTML and outerHTML properties
|
|
2484
|
+
if (property === "innerHTML" || property === "outerHTML") {
|
|
2485
|
+
val = removeStyleAttributes(val);
|
|
2486
|
+
expectedValue = removeStyleAttributes(expectedValue);
|
|
2487
|
+
}
|
|
2488
|
+
state.info.value = val;
|
|
2489
|
+
let regex;
|
|
2490
|
+
state.info.value = val;
|
|
2491
|
+
const isRegex = expectedValue.startsWith("regex:");
|
|
2492
|
+
const isContains = expectedValue.startsWith("contains:");
|
|
2493
|
+
const isExact = expectedValue.startsWith("exact:");
|
|
2494
|
+
let matchPassed = false;
|
|
2495
|
+
if (isRegex) {
|
|
2496
|
+
const rawPattern = expectedValue.slice(6); // remove "regex:"
|
|
2497
|
+
const lastSlashIndex = rawPattern.lastIndexOf("/");
|
|
2498
|
+
if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
|
|
2499
|
+
const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
|
|
2500
|
+
const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
|
|
2501
|
+
const regex = new RegExp(patternBody, flags);
|
|
2502
|
+
state.info.regex = true;
|
|
2503
|
+
matchPassed = regex.test(val);
|
|
2504
|
+
}
|
|
2505
|
+
else {
|
|
2506
|
+
// Fallback: treat as literal
|
|
2507
|
+
const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2508
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2509
|
+
matchPassed = regex.test(val);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
else if (isContains) {
|
|
2513
|
+
const containsValue = expectedValue.slice(9); // remove "contains:"
|
|
2514
|
+
matchPassed = val.includes(containsValue);
|
|
2515
|
+
}
|
|
2516
|
+
else if (isExact) {
|
|
2517
|
+
const exactValue = expectedValue.slice(6); // remove "exact:"
|
|
2518
|
+
matchPassed = val === exactValue;
|
|
2519
|
+
}
|
|
2520
|
+
else if (property === "innerText") {
|
|
2521
|
+
// Default innerText logic
|
|
2522
|
+
const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
|
|
2523
|
+
const valLines = val.split("\n");
|
|
2524
|
+
const expectedLines = normalizedExpectedValue.split("\n");
|
|
2525
|
+
matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
|
|
2526
|
+
}
|
|
2527
|
+
else {
|
|
2528
|
+
// Fallback exact or loose match
|
|
2529
|
+
const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2530
|
+
const regex = new RegExp(escapedPattern, "g");
|
|
2531
|
+
matchPassed = regex.test(val);
|
|
2532
|
+
}
|
|
2533
|
+
if (!matchPassed) {
|
|
2534
|
+
let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
|
|
2188
2535
|
state.info.failCause.assertionFailed = true;
|
|
2189
2536
|
state.info.failCause.lastError = errorMessage;
|
|
2190
2537
|
throw new Error(errorMessage);
|
|
@@ -2198,6 +2545,132 @@ class StableBrowser {
|
|
|
2198
2545
|
await _commandFinally(state, this);
|
|
2199
2546
|
}
|
|
2200
2547
|
}
|
|
2548
|
+
async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
|
|
2549
|
+
// Convert timeout from seconds to milliseconds
|
|
2550
|
+
const timeoutMs = timeout * 1000;
|
|
2551
|
+
const state = {
|
|
2552
|
+
selectors,
|
|
2553
|
+
_params,
|
|
2554
|
+
condition,
|
|
2555
|
+
timeout: timeoutMs, // Store as milliseconds for internal use
|
|
2556
|
+
options,
|
|
2557
|
+
world,
|
|
2558
|
+
type: Types.CONDITIONAL_WAIT,
|
|
2559
|
+
highlight: true,
|
|
2560
|
+
screenshot: true,
|
|
2561
|
+
text: `Conditional wait for element`,
|
|
2562
|
+
_text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
|
|
2563
|
+
operation: "conditionalWait",
|
|
2564
|
+
log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
|
|
2565
|
+
allowDisabled: true,
|
|
2566
|
+
info: {},
|
|
2567
|
+
};
|
|
2568
|
+
// Initialize startTime outside try block to ensure it's always accessible
|
|
2569
|
+
const startTime = Date.now();
|
|
2570
|
+
let conditionMet = false;
|
|
2571
|
+
let currentValue = null;
|
|
2572
|
+
let lastError = null;
|
|
2573
|
+
// Main retry loop - continues until timeout or condition is met
|
|
2574
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2575
|
+
const elapsedTime = Date.now() - startTime;
|
|
2576
|
+
const remainingTime = timeoutMs - elapsedTime;
|
|
2577
|
+
try {
|
|
2578
|
+
// Try to execute _preCommand (element location)
|
|
2579
|
+
await _preCommand(state, this);
|
|
2580
|
+
// If _preCommand succeeds, start condition checking
|
|
2581
|
+
const checkCondition = async () => {
|
|
2582
|
+
try {
|
|
2583
|
+
switch (condition.toLowerCase()) {
|
|
2584
|
+
case "checked":
|
|
2585
|
+
currentValue = await state.element.isChecked();
|
|
2586
|
+
return currentValue === true;
|
|
2587
|
+
case "unchecked":
|
|
2588
|
+
currentValue = await state.element.isChecked();
|
|
2589
|
+
return currentValue === false;
|
|
2590
|
+
case "visible":
|
|
2591
|
+
currentValue = await state.element.isVisible();
|
|
2592
|
+
return currentValue === true;
|
|
2593
|
+
case "hidden":
|
|
2594
|
+
currentValue = await state.element.isVisible();
|
|
2595
|
+
return currentValue === false;
|
|
2596
|
+
case "enabled":
|
|
2597
|
+
currentValue = await state.element.isDisabled();
|
|
2598
|
+
return currentValue === false;
|
|
2599
|
+
case "disabled":
|
|
2600
|
+
currentValue = await state.element.isDisabled();
|
|
2601
|
+
return currentValue === true;
|
|
2602
|
+
case "editable":
|
|
2603
|
+
// currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
|
|
2604
|
+
currentValue = await state.element.isContentEditable();
|
|
2605
|
+
return currentValue === true;
|
|
2606
|
+
default:
|
|
2607
|
+
state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
|
|
2608
|
+
state.info.success = false;
|
|
2609
|
+
return false;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
catch (error) {
|
|
2613
|
+
// Don't throw here, just return false to continue retrying
|
|
2614
|
+
return false;
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
// Inner loop for condition checking (once element is located)
|
|
2618
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2619
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2620
|
+
conditionMet = await checkCondition();
|
|
2621
|
+
if (conditionMet) {
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
// Check if we still have time for another attempt
|
|
2625
|
+
if (Date.now() - startTime + 50 < timeoutMs) {
|
|
2626
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
2627
|
+
}
|
|
2628
|
+
else {
|
|
2629
|
+
break;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
// If we got here and condition is met, break out of main loop
|
|
2633
|
+
if (conditionMet) {
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2636
|
+
// If condition not met but no exception, we've timed out
|
|
2637
|
+
break;
|
|
2638
|
+
}
|
|
2639
|
+
catch (e) {
|
|
2640
|
+
lastError = e;
|
|
2641
|
+
const currentElapsedTime = Date.now() - startTime;
|
|
2642
|
+
const timeLeft = timeoutMs - currentElapsedTime;
|
|
2643
|
+
// Check if we have enough time left to retry
|
|
2644
|
+
if (timeLeft > 100) {
|
|
2645
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2646
|
+
}
|
|
2647
|
+
else {
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
const actualWaitTime = Date.now() - startTime;
|
|
2653
|
+
state.info = {
|
|
2654
|
+
success: conditionMet,
|
|
2655
|
+
conditionMet,
|
|
2656
|
+
actualWaitTime,
|
|
2657
|
+
currentValue,
|
|
2658
|
+
lastError: lastError?.message || null,
|
|
2659
|
+
message: conditionMet
|
|
2660
|
+
? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
|
|
2661
|
+
: `Condition '${condition}' not met within ${timeout}s timeout`,
|
|
2662
|
+
};
|
|
2663
|
+
if (lastError) {
|
|
2664
|
+
state.log += `Last error: ${lastError.message}\n`;
|
|
2665
|
+
}
|
|
2666
|
+
try {
|
|
2667
|
+
await _commandFinally(state, this);
|
|
2668
|
+
}
|
|
2669
|
+
catch (finallyError) {
|
|
2670
|
+
state.log += `Error in _commandFinally: ${finallyError.message}\n`;
|
|
2671
|
+
}
|
|
2672
|
+
return state.info;
|
|
2673
|
+
}
|
|
2201
2674
|
async extractEmailData(emailAddress, options, world) {
|
|
2202
2675
|
if (!emailAddress) {
|
|
2203
2676
|
throw new Error("email address is null");
|
|
@@ -2355,56 +2828,49 @@ class StableBrowser {
|
|
|
2355
2828
|
console.debug(error);
|
|
2356
2829
|
}
|
|
2357
2830
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
// });
|
|
2401
|
-
// }
|
|
2402
|
-
// } catch (error) {
|
|
2403
|
-
// // console.debug(error);
|
|
2404
|
-
// }
|
|
2405
|
-
// }
|
|
2831
|
+
_matcher(text) {
|
|
2832
|
+
if (!text) {
|
|
2833
|
+
return { matcher: "contains", queryText: "" };
|
|
2834
|
+
}
|
|
2835
|
+
if (text.length < 2) {
|
|
2836
|
+
return { matcher: "contains", queryText: text };
|
|
2837
|
+
}
|
|
2838
|
+
const split = text.split(":");
|
|
2839
|
+
const matcher = split[0].toLowerCase();
|
|
2840
|
+
const queryText = split.slice(1).join(":").trim();
|
|
2841
|
+
return { matcher, queryText };
|
|
2842
|
+
}
|
|
2843
|
+
_getDomain(url) {
|
|
2844
|
+
if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
|
|
2845
|
+
return "";
|
|
2846
|
+
}
|
|
2847
|
+
let hostnameFragments = url.split("/")[2].split(".");
|
|
2848
|
+
if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
|
|
2849
|
+
return hostnameFragments.join("-").split(":").join("-");
|
|
2850
|
+
}
|
|
2851
|
+
let n = hostnameFragments.length;
|
|
2852
|
+
let fragments = [...hostnameFragments];
|
|
2853
|
+
while (n > 0 && hostnameFragments[n - 1].length <= 3) {
|
|
2854
|
+
hostnameFragments.pop();
|
|
2855
|
+
n = hostnameFragments.length;
|
|
2856
|
+
}
|
|
2857
|
+
if (n == 0) {
|
|
2858
|
+
if (fragments[0] === "www")
|
|
2859
|
+
fragments = fragments.slice(1);
|
|
2860
|
+
return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
|
|
2861
|
+
}
|
|
2862
|
+
if (hostnameFragments[0] === "www")
|
|
2863
|
+
hostnameFragments = hostnameFragments.slice(1);
|
|
2864
|
+
return hostnameFragments.join(".");
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Verify the page path matches the given path.
|
|
2868
|
+
* @param {string} pathPart - The path to verify.
|
|
2869
|
+
* @param {object} options - Options for verification.
|
|
2870
|
+
* @param {object} world - The world context.
|
|
2871
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2872
|
+
*/
|
|
2406
2873
|
async verifyPagePath(pathPart, options = {}, world = null) {
|
|
2407
|
-
const startTime = Date.now();
|
|
2408
2874
|
let error = null;
|
|
2409
2875
|
let screenshotId = null;
|
|
2410
2876
|
let screenshotPath = null;
|
|
@@ -2418,113 +2884,212 @@ class StableBrowser {
|
|
|
2418
2884
|
pathPart = newValue;
|
|
2419
2885
|
}
|
|
2420
2886
|
info.pathPart = pathPart;
|
|
2887
|
+
const { matcher, queryText } = this._matcher(pathPart);
|
|
2888
|
+
const state = {
|
|
2889
|
+
text_search: queryText,
|
|
2890
|
+
options,
|
|
2891
|
+
world,
|
|
2892
|
+
locate: false,
|
|
2893
|
+
scroll: false,
|
|
2894
|
+
highlight: false,
|
|
2895
|
+
type: Types.VERIFY_PAGE_PATH,
|
|
2896
|
+
text: `Verify the page url is ${queryText}`,
|
|
2897
|
+
_text: `Verify the page url is ${queryText}`,
|
|
2898
|
+
operation: "verifyPagePath",
|
|
2899
|
+
log: "***** verify page url is " + queryText + " *****\n",
|
|
2900
|
+
};
|
|
2421
2901
|
try {
|
|
2902
|
+
await _preCommand(state, this);
|
|
2903
|
+
state.info.text = queryText;
|
|
2422
2904
|
for (let i = 0; i < 30; i++) {
|
|
2423
2905
|
const url = await this.page.url();
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2906
|
+
switch (matcher) {
|
|
2907
|
+
case "exact":
|
|
2908
|
+
if (url !== queryText) {
|
|
2909
|
+
if (i === 29) {
|
|
2910
|
+
throw new Error(`Page URL ${url} is not equal to ${queryText}`);
|
|
2911
|
+
}
|
|
2912
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2913
|
+
continue;
|
|
2914
|
+
}
|
|
2915
|
+
break;
|
|
2916
|
+
case "contains":
|
|
2917
|
+
if (!url.includes(queryText)) {
|
|
2918
|
+
if (i === 29) {
|
|
2919
|
+
throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
|
|
2920
|
+
}
|
|
2921
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
break;
|
|
2925
|
+
case "starts-with":
|
|
2926
|
+
{
|
|
2927
|
+
const domain = this._getDomain(url);
|
|
2928
|
+
if (domain.length > 0 && domain !== queryText) {
|
|
2929
|
+
if (i === 29) {
|
|
2930
|
+
throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
|
|
2931
|
+
}
|
|
2932
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2933
|
+
continue;
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
break;
|
|
2937
|
+
case "ends-with":
|
|
2938
|
+
{
|
|
2939
|
+
const urlObj = new URL(url);
|
|
2940
|
+
let route = "/";
|
|
2941
|
+
if (urlObj.pathname !== "/") {
|
|
2942
|
+
route = urlObj.pathname.split("/").slice(-1)[0].trim();
|
|
2943
|
+
}
|
|
2944
|
+
else {
|
|
2945
|
+
route = "/";
|
|
2946
|
+
}
|
|
2947
|
+
if (route !== queryText) {
|
|
2948
|
+
if (i === 29) {
|
|
2949
|
+
throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
|
|
2950
|
+
}
|
|
2951
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
break;
|
|
2956
|
+
case "regex":
|
|
2957
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
2958
|
+
if (!regex.test(url)) {
|
|
2959
|
+
if (i === 29) {
|
|
2960
|
+
throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
|
|
2961
|
+
}
|
|
2962
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
break;
|
|
2966
|
+
default:
|
|
2967
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
2968
|
+
if (!url.includes(pathPart)) {
|
|
2969
|
+
if (i === 29) {
|
|
2970
|
+
throw new Error(`Page URL ${url} does not contain ${pathPart}`);
|
|
2971
|
+
}
|
|
2972
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2430
2975
|
}
|
|
2431
|
-
|
|
2432
|
-
return info;
|
|
2976
|
+
await _screenshot(state, this);
|
|
2977
|
+
return state.info;
|
|
2433
2978
|
}
|
|
2434
2979
|
}
|
|
2435
2980
|
catch (e) {
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
info.screenshotPath = screenshotPath;
|
|
2440
|
-
Object.assign(e, { info: info });
|
|
2441
|
-
error = e;
|
|
2442
|
-
// throw e;
|
|
2443
|
-
await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
|
|
2981
|
+
state.info.failCause.lastError = e.message;
|
|
2982
|
+
state.info.failCause.assertionFailed = true;
|
|
2983
|
+
await _commandError(state, e, this);
|
|
2444
2984
|
}
|
|
2445
2985
|
finally {
|
|
2446
|
-
|
|
2447
|
-
_reportToWorld(world, {
|
|
2448
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2449
|
-
text: "Verify page path",
|
|
2450
|
-
_text: "Verify the page path contains " + pathPart,
|
|
2451
|
-
screenshotId,
|
|
2452
|
-
result: error
|
|
2453
|
-
? {
|
|
2454
|
-
status: "FAILED",
|
|
2455
|
-
startTime,
|
|
2456
|
-
endTime,
|
|
2457
|
-
message: error?.message,
|
|
2458
|
-
}
|
|
2459
|
-
: {
|
|
2460
|
-
status: "PASSED",
|
|
2461
|
-
startTime,
|
|
2462
|
-
endTime,
|
|
2463
|
-
},
|
|
2464
|
-
info: info,
|
|
2465
|
-
});
|
|
2986
|
+
await _commandFinally(state, this);
|
|
2466
2987
|
}
|
|
2467
2988
|
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Verify the page title matches the given title.
|
|
2991
|
+
* @param {string} title - The title to verify.
|
|
2992
|
+
* @param {object} options - Options for verification.
|
|
2993
|
+
* @param {object} world - The world context.
|
|
2994
|
+
* @returns {Promise<object>} - The state info after verification.
|
|
2995
|
+
*/
|
|
2468
2996
|
async verifyPageTitle(title, options = {}, world = null) {
|
|
2469
|
-
const startTime = Date.now();
|
|
2470
2997
|
let error = null;
|
|
2471
2998
|
let screenshotId = null;
|
|
2472
2999
|
let screenshotPath = null;
|
|
2473
3000
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2474
|
-
const info = {};
|
|
2475
|
-
info.log = "***** verify page title " + title + " *****\n";
|
|
2476
|
-
info.operation = "verifyPageTitle";
|
|
2477
3001
|
const newValue = await this._replaceWithLocalData(title, world);
|
|
2478
3002
|
if (newValue !== title) {
|
|
2479
3003
|
this.logger.info(title + "=" + newValue);
|
|
2480
3004
|
title = newValue;
|
|
2481
3005
|
}
|
|
2482
|
-
|
|
3006
|
+
const { matcher, queryText } = this._matcher(title);
|
|
3007
|
+
const state = {
|
|
3008
|
+
text_search: queryText,
|
|
3009
|
+
options,
|
|
3010
|
+
world,
|
|
3011
|
+
locate: false,
|
|
3012
|
+
scroll: false,
|
|
3013
|
+
highlight: false,
|
|
3014
|
+
type: Types.VERIFY_PAGE_TITLE,
|
|
3015
|
+
text: `Verify the page title is ${queryText}`,
|
|
3016
|
+
_text: `Verify the page title is ${queryText}`,
|
|
3017
|
+
operation: "verifyPageTitle",
|
|
3018
|
+
log: "***** verify page title is " + queryText + " *****\n",
|
|
3019
|
+
};
|
|
2483
3020
|
try {
|
|
3021
|
+
await _preCommand(state, this);
|
|
3022
|
+
state.info.text = queryText;
|
|
2484
3023
|
for (let i = 0; i < 30; i++) {
|
|
2485
3024
|
const foundTitle = await this.page.title();
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
3025
|
+
switch (matcher) {
|
|
3026
|
+
case "exact":
|
|
3027
|
+
if (foundTitle !== queryText) {
|
|
3028
|
+
if (i === 29) {
|
|
3029
|
+
throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
|
|
3030
|
+
}
|
|
3031
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
break;
|
|
3035
|
+
case "contains":
|
|
3036
|
+
if (!foundTitle.includes(queryText)) {
|
|
3037
|
+
if (i === 29) {
|
|
3038
|
+
throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
|
|
3039
|
+
}
|
|
3040
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
break;
|
|
3044
|
+
case "starts-with":
|
|
3045
|
+
if (!foundTitle.startsWith(queryText)) {
|
|
3046
|
+
if (i === 29) {
|
|
3047
|
+
throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
|
|
3048
|
+
}
|
|
3049
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
break;
|
|
3053
|
+
case "ends-with":
|
|
3054
|
+
if (!foundTitle.endsWith(queryText)) {
|
|
3055
|
+
if (i === 29) {
|
|
3056
|
+
throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
|
|
3057
|
+
}
|
|
3058
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
break;
|
|
3062
|
+
case "regex":
|
|
3063
|
+
const regex = new RegExp(queryText.slice(1, -1), "g");
|
|
3064
|
+
if (!regex.test(foundTitle)) {
|
|
3065
|
+
if (i === 29) {
|
|
3066
|
+
throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
|
|
3067
|
+
}
|
|
3068
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
break;
|
|
3072
|
+
default:
|
|
3073
|
+
console.log("Unknown matching type, defaulting to contains matching");
|
|
3074
|
+
if (!foundTitle.includes(title)) {
|
|
3075
|
+
if (i === 29) {
|
|
3076
|
+
throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
|
|
3077
|
+
}
|
|
3078
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
2492
3081
|
}
|
|
2493
|
-
|
|
2494
|
-
return info;
|
|
3082
|
+
await _screenshot(state, this);
|
|
3083
|
+
return state.info;
|
|
2495
3084
|
}
|
|
2496
3085
|
}
|
|
2497
3086
|
catch (e) {
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
info.screenshotPath = screenshotPath;
|
|
2502
|
-
Object.assign(e, { info: info });
|
|
2503
|
-
error = e;
|
|
2504
|
-
// throw e;
|
|
2505
|
-
await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
|
|
3087
|
+
state.info.failCause.lastError = e.message;
|
|
3088
|
+
state.info.failCause.assertionFailed = true;
|
|
3089
|
+
await _commandError(state, e, this);
|
|
2506
3090
|
}
|
|
2507
3091
|
finally {
|
|
2508
|
-
|
|
2509
|
-
_reportToWorld(world, {
|
|
2510
|
-
type: Types.VERIFY_PAGE_PATH,
|
|
2511
|
-
text: "Verify page title",
|
|
2512
|
-
_text: "Verify the page title contains " + title,
|
|
2513
|
-
screenshotId,
|
|
2514
|
-
result: error
|
|
2515
|
-
? {
|
|
2516
|
-
status: "FAILED",
|
|
2517
|
-
startTime,
|
|
2518
|
-
endTime,
|
|
2519
|
-
message: error?.message,
|
|
2520
|
-
}
|
|
2521
|
-
: {
|
|
2522
|
-
status: "PASSED",
|
|
2523
|
-
startTime,
|
|
2524
|
-
endTime,
|
|
2525
|
-
},
|
|
2526
|
-
info: info,
|
|
2527
|
-
});
|
|
3092
|
+
await _commandFinally(state, this);
|
|
2528
3093
|
}
|
|
2529
3094
|
}
|
|
2530
3095
|
async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
|
|
@@ -2608,27 +3173,10 @@ class StableBrowser {
|
|
|
2608
3173
|
const frame = resultWithElementsFound[0].frame;
|
|
2609
3174
|
const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
|
|
2610
3175
|
await this._highlightElements(frame, dataAttribute);
|
|
2611
|
-
// if (world && world.screenshot && !world.screenshotPath) {
|
|
2612
|
-
// console.log(`Highlighting for verify text is found while running from recorder`);
|
|
2613
|
-
// this._highlightElements(frame, dataAttribute).then(async () => {
|
|
2614
|
-
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2615
|
-
// this._unhighlightElements(frame, dataAttribute)
|
|
2616
|
-
// .then(async () => {
|
|
2617
|
-
// console.log(`Unhighlighted frame dataAttribute successfully`);
|
|
2618
|
-
// })
|
|
2619
|
-
// .catch(
|
|
2620
|
-
// (e) => {}
|
|
2621
|
-
// console.error(e)
|
|
2622
|
-
// );
|
|
2623
|
-
// });
|
|
2624
|
-
// }
|
|
2625
3176
|
const element = await frame.locator(dataAttribute).first();
|
|
2626
|
-
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2627
|
-
// await this._unhighlightElements(frame, dataAttribute);
|
|
2628
3177
|
if (element) {
|
|
2629
3178
|
await this.scrollIfNeeded(element, state.info);
|
|
2630
3179
|
await element.dispatchEvent("bvt_verify_page_contains_text");
|
|
2631
|
-
// await _screenshot(state, this, element);
|
|
2632
3180
|
}
|
|
2633
3181
|
}
|
|
2634
3182
|
await _screenshot(state, this);
|
|
@@ -2638,7 +3186,6 @@ class StableBrowser {
|
|
|
2638
3186
|
console.error(error);
|
|
2639
3187
|
}
|
|
2640
3188
|
}
|
|
2641
|
-
// await expect(element).toHaveCount(1, { timeout: 10000 });
|
|
2642
3189
|
}
|
|
2643
3190
|
catch (e) {
|
|
2644
3191
|
await _commandError(state, e, this);
|
|
@@ -2720,6 +3267,8 @@ class StableBrowser {
|
|
|
2720
3267
|
operation: "verify_text_with_relation",
|
|
2721
3268
|
log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
|
|
2722
3269
|
};
|
|
3270
|
+
const cmdStartTime = Date.now();
|
|
3271
|
+
let cmdEndTime = null;
|
|
2723
3272
|
const timeout = this._getFindElementTimeout(options);
|
|
2724
3273
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2725
3274
|
let newValue = await this._replaceWithLocalData(textAnchor, world);
|
|
@@ -2755,6 +3304,17 @@ class StableBrowser {
|
|
|
2755
3304
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2756
3305
|
continue;
|
|
2757
3306
|
}
|
|
3307
|
+
else {
|
|
3308
|
+
cmdEndTime = Date.now();
|
|
3309
|
+
if (cmdEndTime - cmdStartTime > 55000) {
|
|
3310
|
+
if (foundAncore) {
|
|
3311
|
+
throw new Error(`Text ${textToVerify} not found in page`);
|
|
3312
|
+
}
|
|
3313
|
+
else {
|
|
3314
|
+
throw new Error(`Text ${textAnchor} not found in page`);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
2758
3318
|
try {
|
|
2759
3319
|
for (let i = 0; i < resultWithElementsFound.length; i++) {
|
|
2760
3320
|
foundAncore = true;
|
|
@@ -3154,8 +3714,51 @@ class StableBrowser {
|
|
|
3154
3714
|
});
|
|
3155
3715
|
}
|
|
3156
3716
|
}
|
|
3717
|
+
/**
|
|
3718
|
+
* Explicit wait/sleep function that pauses execution for a specified duration
|
|
3719
|
+
* @param duration - Duration to sleep in milliseconds (default: 1000ms)
|
|
3720
|
+
* @param options - Optional configuration object
|
|
3721
|
+
* @param world - Optional world context
|
|
3722
|
+
* @returns Promise that resolves after the specified duration
|
|
3723
|
+
*/
|
|
3724
|
+
async sleep(duration = 1000, options = {}, world = null) {
|
|
3725
|
+
const state = {
|
|
3726
|
+
duration,
|
|
3727
|
+
options,
|
|
3728
|
+
world,
|
|
3729
|
+
locate: false,
|
|
3730
|
+
scroll: false,
|
|
3731
|
+
screenshot: false,
|
|
3732
|
+
highlight: false,
|
|
3733
|
+
type: Types.SLEEP,
|
|
3734
|
+
text: `Sleep for ${duration} ms`,
|
|
3735
|
+
_text: `Sleep for ${duration} ms`,
|
|
3736
|
+
operation: "sleep",
|
|
3737
|
+
log: `***** Sleep for ${duration} ms *****\n`,
|
|
3738
|
+
};
|
|
3739
|
+
try {
|
|
3740
|
+
await _preCommand(state, this);
|
|
3741
|
+
if (duration < 0) {
|
|
3742
|
+
throw new Error("Sleep duration cannot be negative");
|
|
3743
|
+
}
|
|
3744
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
3745
|
+
return state.info;
|
|
3746
|
+
}
|
|
3747
|
+
catch (e) {
|
|
3748
|
+
await _commandError(state, e, this);
|
|
3749
|
+
}
|
|
3750
|
+
finally {
|
|
3751
|
+
await _commandFinally(state, this);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3157
3754
|
async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
|
|
3158
|
-
|
|
3755
|
+
try {
|
|
3756
|
+
return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
|
|
3757
|
+
}
|
|
3758
|
+
catch (error) {
|
|
3759
|
+
this.logger.debug(error);
|
|
3760
|
+
throw error;
|
|
3761
|
+
}
|
|
3159
3762
|
}
|
|
3160
3763
|
_getLoadTimeout(options) {
|
|
3161
3764
|
let timeout = 15000;
|
|
@@ -3178,6 +3781,7 @@ class StableBrowser {
|
|
|
3178
3781
|
}
|
|
3179
3782
|
async saveStoreState(path = null, world = null) {
|
|
3180
3783
|
const storageState = await this.page.context().storageState();
|
|
3784
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
3181
3785
|
//const testDataFile = _getDataFile(world, this.context, this);
|
|
3182
3786
|
if (path) {
|
|
3183
3787
|
// save { storageState: storageState } into the path
|
|
@@ -3188,10 +3792,14 @@ class StableBrowser {
|
|
|
3188
3792
|
}
|
|
3189
3793
|
}
|
|
3190
3794
|
async restoreSaveState(path = null, world = null) {
|
|
3795
|
+
path = await this._replaceWithLocalData(path, this.world);
|
|
3191
3796
|
await refreshBrowser(this, path, world);
|
|
3192
3797
|
this.registerEventListeners(this.context);
|
|
3193
3798
|
registerNetworkEvents(this.world, this, this.context, this.page);
|
|
3194
3799
|
registerDownloadEvent(this.page, this.world, this.context);
|
|
3800
|
+
if (this.onRestoreSaveState) {
|
|
3801
|
+
this.onRestoreSaveState(path);
|
|
3802
|
+
}
|
|
3195
3803
|
}
|
|
3196
3804
|
async waitForPageLoad(options = {}, world = null) {
|
|
3197
3805
|
let timeout = this._getLoadTimeout(options);
|
|
@@ -3226,7 +3834,6 @@ class StableBrowser {
|
|
|
3226
3834
|
else if (e.label === "domcontentloaded") {
|
|
3227
3835
|
console.log("waited for the domcontent loaded timeout");
|
|
3228
3836
|
}
|
|
3229
|
-
console.log(".");
|
|
3230
3837
|
}
|
|
3231
3838
|
finally {
|
|
3232
3839
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -3270,7 +3877,6 @@ class StableBrowser {
|
|
|
3270
3877
|
await this.page.close();
|
|
3271
3878
|
}
|
|
3272
3879
|
catch (e) {
|
|
3273
|
-
console.log(".");
|
|
3274
3880
|
await _commandError(state, e, this);
|
|
3275
3881
|
}
|
|
3276
3882
|
finally {
|
|
@@ -3385,7 +3991,6 @@ class StableBrowser {
|
|
|
3385
3991
|
await this.page.setViewportSize({ width: width, height: hight });
|
|
3386
3992
|
}
|
|
3387
3993
|
catch (e) {
|
|
3388
|
-
console.log(".");
|
|
3389
3994
|
await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
|
|
3390
3995
|
}
|
|
3391
3996
|
finally {
|
|
@@ -3423,7 +4028,6 @@ class StableBrowser {
|
|
|
3423
4028
|
await this.page.reload();
|
|
3424
4029
|
}
|
|
3425
4030
|
catch (e) {
|
|
3426
|
-
console.log(".");
|
|
3427
4031
|
await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
|
|
3428
4032
|
}
|
|
3429
4033
|
finally {
|
|
@@ -3467,6 +4071,10 @@ class StableBrowser {
|
|
|
3467
4071
|
}
|
|
3468
4072
|
}
|
|
3469
4073
|
async beforeScenario(world, scenario) {
|
|
4074
|
+
if (world && world.attach) {
|
|
4075
|
+
world.attach(this.context.reportFolder, { mediaType: "text/plain" });
|
|
4076
|
+
}
|
|
4077
|
+
this.context.loadedRoutes = null;
|
|
3470
4078
|
this.beforeScenarioCalled = true;
|
|
3471
4079
|
if (scenario && scenario.pickle && scenario.pickle.name) {
|
|
3472
4080
|
this.scenarioName = scenario.pickle.name;
|
|
@@ -3490,14 +4098,18 @@ class StableBrowser {
|
|
|
3490
4098
|
envName = this.context.environment.name;
|
|
3491
4099
|
}
|
|
3492
4100
|
if (!process.env.TEMP_RUN) {
|
|
3493
|
-
await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
|
|
4101
|
+
await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
|
|
3494
4102
|
}
|
|
3495
4103
|
await loadBrunoParams(this.context, this.context.environment.name);
|
|
3496
4104
|
}
|
|
3497
4105
|
async afterScenario(world, scenario) { }
|
|
3498
4106
|
async beforeStep(world, step) {
|
|
4107
|
+
if (this.abortedExecution) {
|
|
4108
|
+
throw new Error("Aborted");
|
|
4109
|
+
}
|
|
3499
4110
|
if (!this.beforeScenarioCalled) {
|
|
3500
4111
|
this.beforeScenario(world, step);
|
|
4112
|
+
this.context.loadedRoutes = null;
|
|
3501
4113
|
}
|
|
3502
4114
|
if (this.stepIndex === undefined) {
|
|
3503
4115
|
this.stepIndex = 0;
|
|
@@ -3522,13 +4134,16 @@ class StableBrowser {
|
|
|
3522
4134
|
}
|
|
3523
4135
|
if (this.initSnapshotTaken === false) {
|
|
3524
4136
|
this.initSnapshotTaken = true;
|
|
3525
|
-
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
4137
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3526
4138
|
const snapshot = await this.getAriaSnapshot();
|
|
3527
4139
|
if (snapshot) {
|
|
3528
4140
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
|
|
3529
4141
|
}
|
|
3530
4142
|
}
|
|
3531
4143
|
}
|
|
4144
|
+
this.context.routeResults = null;
|
|
4145
|
+
await registerBeforeStepRoutes(this.context, this.stepName);
|
|
4146
|
+
networkBeforeStep(this.stepName);
|
|
3532
4147
|
}
|
|
3533
4148
|
async getAriaSnapshot() {
|
|
3534
4149
|
try {
|
|
@@ -3548,12 +4163,18 @@ class StableBrowser {
|
|
|
3548
4163
|
try {
|
|
3549
4164
|
// Ensure frame is attached and has body
|
|
3550
4165
|
const body = frame.locator("body");
|
|
3551
|
-
await body.waitFor({ timeout }); // wait explicitly
|
|
4166
|
+
//await body.waitFor({ timeout: 2000 }); // wait explicitly
|
|
3552
4167
|
const snapshot = await body.ariaSnapshot({ timeout });
|
|
4168
|
+
if (!snapshot) {
|
|
4169
|
+
continue;
|
|
4170
|
+
}
|
|
3553
4171
|
content.push(`- frame: ${i}`);
|
|
3554
4172
|
content.push(snapshot);
|
|
3555
4173
|
}
|
|
3556
|
-
catch (innerErr) {
|
|
4174
|
+
catch (innerErr) {
|
|
4175
|
+
console.warn(`Frame ${i} snapshot failed:`, innerErr);
|
|
4176
|
+
content.push(`- frame: ${i} - error: ${innerErr.message}`);
|
|
4177
|
+
}
|
|
3557
4178
|
}
|
|
3558
4179
|
return content.join("\n");
|
|
3559
4180
|
}
|
|
@@ -3563,6 +4184,49 @@ class StableBrowser {
|
|
|
3563
4184
|
}
|
|
3564
4185
|
return null;
|
|
3565
4186
|
}
|
|
4187
|
+
/**
|
|
4188
|
+
* Sends command with custom payload to report.
|
|
4189
|
+
* @param commandText - Title of the command to be shown in the report.
|
|
4190
|
+
* @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
|
|
4191
|
+
* @param content - Content of the command to be shown in the report.
|
|
4192
|
+
* @param options - Options for the command. Example: { type: "json", screenshot: true }
|
|
4193
|
+
* @param world - Optional world context.
|
|
4194
|
+
* @public
|
|
4195
|
+
*/
|
|
4196
|
+
async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
|
|
4197
|
+
const state = {
|
|
4198
|
+
options,
|
|
4199
|
+
world,
|
|
4200
|
+
locate: false,
|
|
4201
|
+
scroll: false,
|
|
4202
|
+
screenshot: options.screenshot ?? false,
|
|
4203
|
+
highlight: options.highlight ?? false,
|
|
4204
|
+
type: Types.REPORT_COMMAND,
|
|
4205
|
+
text: commandText,
|
|
4206
|
+
_text: commandText,
|
|
4207
|
+
operation: "report_command",
|
|
4208
|
+
log: "***** " + commandText + " *****\n",
|
|
4209
|
+
};
|
|
4210
|
+
try {
|
|
4211
|
+
await _preCommand(state, this);
|
|
4212
|
+
const payload = {
|
|
4213
|
+
type: options.type ?? "text",
|
|
4214
|
+
content: content,
|
|
4215
|
+
screenshotId: null,
|
|
4216
|
+
};
|
|
4217
|
+
state.payload = payload;
|
|
4218
|
+
if (commandStatus === "FAILED") {
|
|
4219
|
+
state.throwError = true;
|
|
4220
|
+
throw new Error("Command failed");
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
catch (e) {
|
|
4224
|
+
await _commandError(state, e, this);
|
|
4225
|
+
}
|
|
4226
|
+
finally {
|
|
4227
|
+
await _commandFinally(state, this);
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
3566
4230
|
async afterStep(world, step) {
|
|
3567
4231
|
this.stepName = null;
|
|
3568
4232
|
if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
|
|
@@ -3570,23 +4234,55 @@ class StableBrowser {
|
|
|
3570
4234
|
await this.context.browserObject.context.tracing.stopChunk({
|
|
3571
4235
|
path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
|
|
3572
4236
|
});
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
4237
|
+
if (world && world.attach) {
|
|
4238
|
+
await world.attach(JSON.stringify({
|
|
4239
|
+
type: "trace",
|
|
4240
|
+
traceFilePath: `trace-${this.stepIndex}.zip`,
|
|
4241
|
+
}), "application/json+trace");
|
|
4242
|
+
}
|
|
3577
4243
|
// console.log("trace file created", `trace-${this.stepIndex}.zip`);
|
|
3578
4244
|
}
|
|
3579
4245
|
}
|
|
3580
4246
|
if (this.context) {
|
|
3581
4247
|
this.context.examplesRow = null;
|
|
3582
4248
|
}
|
|
3583
|
-
if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
|
|
4249
|
+
if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
|
|
3584
4250
|
const snapshot = await this.getAriaSnapshot();
|
|
3585
4251
|
if (snapshot) {
|
|
3586
4252
|
const obj = {};
|
|
3587
4253
|
await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
|
|
3588
4254
|
}
|
|
3589
4255
|
}
|
|
4256
|
+
this.context.routeResults = await registerAfterStepRoutes(this.context, world);
|
|
4257
|
+
if (this.context.routeResults) {
|
|
4258
|
+
if (world && world.attach) {
|
|
4259
|
+
await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
if (!process.env.TEMP_RUN) {
|
|
4263
|
+
const state = {
|
|
4264
|
+
world,
|
|
4265
|
+
locate: false,
|
|
4266
|
+
scroll: false,
|
|
4267
|
+
screenshot: true,
|
|
4268
|
+
highlight: true,
|
|
4269
|
+
type: Types.STEP_COMPLETE,
|
|
4270
|
+
text: "end of scenario",
|
|
4271
|
+
_text: "end of scenario",
|
|
4272
|
+
operation: "step_complete",
|
|
4273
|
+
log: "***** " + "end of scenario" + " *****\n",
|
|
4274
|
+
};
|
|
4275
|
+
try {
|
|
4276
|
+
await _preCommand(state, this);
|
|
4277
|
+
}
|
|
4278
|
+
catch (e) {
|
|
4279
|
+
await _commandError(state, e, this);
|
|
4280
|
+
}
|
|
4281
|
+
finally {
|
|
4282
|
+
await _commandFinally(state, this);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
networkAfterStep(this.stepName);
|
|
3590
4286
|
}
|
|
3591
4287
|
}
|
|
3592
4288
|
function createTimedPromise(promise, label) {
|