automation_model 1.0.641-dev → 1.0.641-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.
Files changed (57) hide show
  1. package/README.md +16 -16
  2. package/lib/analyze_helper.js.map +1 -1
  3. package/lib/api.d.ts +0 -1
  4. package/lib/api.js +35 -21
  5. package/lib/api.js.map +1 -1
  6. package/lib/auto_page.d.ts +1 -1
  7. package/lib/auto_page.js +96 -26
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.js +28 -8
  10. package/lib/browser_manager.js.map +1 -1
  11. package/lib/bruno.d.ts +2 -0
  12. package/lib/bruno.js +381 -0
  13. package/lib/bruno.js.map +1 -0
  14. package/lib/command_common.d.ts +4 -4
  15. package/lib/command_common.js +36 -16
  16. package/lib/command_common.js.map +1 -1
  17. package/lib/date_time.js.map +1 -1
  18. package/lib/drawRect.js.map +1 -1
  19. package/lib/environment.d.ts +1 -0
  20. package/lib/environment.js +1 -0
  21. package/lib/environment.js.map +1 -1
  22. package/lib/error-messages.js.map +1 -1
  23. package/lib/file_checker.d.ts +1 -0
  24. package/lib/file_checker.js +61 -0
  25. package/lib/file_checker.js.map +1 -0
  26. package/lib/find_function.js.map +1 -1
  27. package/lib/index.d.ts +2 -0
  28. package/lib/index.js +2 -0
  29. package/lib/index.js.map +1 -1
  30. package/lib/init_browser.d.ts +2 -2
  31. package/lib/init_browser.js +29 -27
  32. package/lib/init_browser.js.map +1 -1
  33. package/lib/locate_element.js +2 -2
  34. package/lib/locate_element.js.map +1 -1
  35. package/lib/locator.js +1 -1
  36. package/lib/locator.js.map +1 -1
  37. package/lib/locator_log.js.map +1 -1
  38. package/lib/network.d.ts +1 -1
  39. package/lib/network.js +5 -5
  40. package/lib/network.js.map +1 -1
  41. package/lib/snapshot_validation.d.ts +37 -0
  42. package/lib/snapshot_validation.js +357 -0
  43. package/lib/snapshot_validation.js.map +1 -0
  44. package/lib/stable_browser.d.ts +51 -24
  45. package/lib/stable_browser.js +714 -194
  46. package/lib/stable_browser.js.map +1 -1
  47. package/lib/table.js.map +1 -1
  48. package/lib/table_analyze.js.map +1 -1
  49. package/lib/table_helper.js +15 -0
  50. package/lib/table_helper.js.map +1 -1
  51. package/lib/test_context.d.ts +2 -0
  52. package/lib/test_context.js +2 -0
  53. package/lib/test_context.js.map +1 -1
  54. package/lib/utils.d.ts +6 -4
  55. package/lib/utils.js +131 -20
  56. package/lib/utils.js.map +1 -1
  57. package/package.json +11 -6
@@ -15,6 +15,7 @@ import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
17
  import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
18
19
  import { locate_element } from "./locate_element.js";
19
20
  import { randomUUID } from "crypto";
20
21
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
@@ -22,23 +23,26 @@ import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
23
  import { LocatorLog } from "./locator_log.js";
23
24
  import axios from "axios";
24
25
  import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
27
+ import { loadBrunoParams } from "./bruno.js";
25
28
  export const Types = {
26
29
  CLICK: "click_element",
27
30
  WAIT_ELEMENT: "wait_element",
28
- NAVIGATE: "navigate",
31
+ NAVIGATE: "navigate", ///
29
32
  FILL: "fill_element",
30
- EXECUTE: "execute_page_method",
31
- OPEN: "open_environment",
33
+ EXECUTE: "execute_page_method", //
34
+ OPEN: "open_environment", //
32
35
  COMPLETE: "step_complete",
33
36
  ASK: "information_needed",
34
- GET_PAGE_STATUS: "get_page_status",
35
- CLICK_ROW_ACTION: "click_row_action",
37
+ GET_PAGE_STATUS: "get_page_status", ///
38
+ CLICK_ROW_ACTION: "click_row_action", //
36
39
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
37
40
  VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
38
41
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
39
42
  ANALYZE_TABLE: "analyze_table",
40
- SELECT: "select_combobox",
43
+ SELECT: "select_combobox", //
41
44
  VERIFY_PAGE_PATH: "verify_page_path",
45
+ VERIFY_PAGE_TITLE: "verify_page_title",
42
46
  TYPE_PRESS: "type_press",
43
47
  PRESS: "press_key",
44
48
  HOVER: "hover_element",
@@ -55,6 +59,12 @@ export const Types = {
55
59
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
56
60
  VERIFY_ATTRIBUTE: "verify_element_attribute",
57
61
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
62
+ BRUNO: "bruno",
63
+ VERIFY_FILE_EXISTS: "verify_file_exists",
64
+ SET_INPUT_FILES: "set_input_files",
65
+ SNAPSHOT_VALIDATION: "snapshot_validation",
66
+ REPORT_COMMAND: "report_command",
67
+ STEP_COMPLETE: "step_complete",
58
68
  };
59
69
  export const apps = {};
60
70
  const formatElementName = (elementName) => {
@@ -118,6 +128,9 @@ class StableBrowser {
118
128
  if (!context.pageLoading) {
119
129
  context.pageLoading = { status: false };
120
130
  }
131
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
132
+ this.page.on("dialog", (dialog) => dialog.accept());
133
+ }
121
134
  context.playContext.on("page", async function (page) {
122
135
  if (this.configuration && this.configuration.closePopups === true) {
123
136
  console.log("close unexpected popups");
@@ -126,6 +139,14 @@ class StableBrowser {
126
139
  }
127
140
  context.pageLoading.status = true;
128
141
  this.page = page;
142
+ try {
143
+ if (this.configuration && this.configuration.acceptDialog) {
144
+ await page.on("dialog", (dialog) => dialog.accept());
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.error("Error on dialog accept registration", error);
149
+ }
129
150
  context.page = page;
130
151
  context.pages.push(page);
131
152
  registerNetworkEvents(this.world, this, context, this.page);
@@ -180,6 +201,30 @@ class StableBrowser {
180
201
  await this.waitForPageLoad();
181
202
  }
182
203
  }
204
+ async switchTab(tabTitleOrIndex) {
205
+ // first check if the tabNameOrIndex is a number
206
+ let index = parseInt(tabTitleOrIndex);
207
+ if (!isNaN(index)) {
208
+ if (index >= 0 && index < this.context.pages.length) {
209
+ this.page = this.context.pages[index];
210
+ this.context.page = this.page;
211
+ await this.page.bringToFront();
212
+ return;
213
+ }
214
+ }
215
+ // if the tabNameOrIndex is a string, find the tab by name
216
+ for (let i = 0; i < this.context.pages.length; i++) {
217
+ let page = this.context.pages[i];
218
+ let title = await page.title();
219
+ if (title.includes(tabTitleOrIndex)) {
220
+ this.page = page;
221
+ this.context.page = this.page;
222
+ await this.page.bringToFront();
223
+ return;
224
+ }
225
+ }
226
+ throw new Error("Tab not found: " + tabTitleOrIndex);
227
+ }
183
228
  registerConsoleLogListener(page, context) {
184
229
  if (!this.context.webLogger) {
185
230
  this.context.webLogger = [];
@@ -247,6 +292,7 @@ class StableBrowser {
247
292
  if (!url) {
248
293
  throw new Error("url is null, verify that the environment file is correct");
249
294
  }
295
+ url = await this._replaceWithLocalData(url, this.world);
250
296
  if (!url.startsWith("http")) {
251
297
  url = "https://" + url;
252
298
  }
@@ -275,7 +321,7 @@ class StableBrowser {
275
321
  _commandError(state, error, this);
276
322
  }
277
323
  finally {
278
- _commandFinally(state, this);
324
+ await _commandFinally(state, this);
279
325
  }
280
326
  }
281
327
  async _getLocator(locator, scope, _params) {
@@ -391,7 +437,7 @@ class StableBrowser {
391
437
  }
392
438
  return { elementCount: tagCount, randomToken };
393
439
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
440
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
441
  if (!info) {
396
442
  info = {};
397
443
  }
@@ -458,7 +504,7 @@ class StableBrowser {
458
504
  }
459
505
  return;
460
506
  }
461
- if (info.locatorLog && count === 0) {
507
+ if (info.locatorLog && count === 0 && logErrors) {
462
508
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
509
  }
464
510
  for (let j = 0; j < count; j++) {
@@ -473,7 +519,7 @@ class StableBrowser {
473
519
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
520
  }
475
521
  }
476
- else {
522
+ else if (logErrors) {
477
523
  info.failCause.visible = visible;
478
524
  info.failCause.enabled = enabled;
479
525
  if (!info.printMessages) {
@@ -565,15 +611,27 @@ class StableBrowser {
565
611
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
612
  if (!element.rerun) {
567
613
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
614
+ await element.evaluate((el, randomToken) => {
569
615
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
616
  }, randomToken);
571
- if (element._frame) {
572
- return element;
573
- }
574
- const scope = element.page();
575
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
576
- return newSelector;
617
+ // if (element._frame) {
618
+ // return element;
619
+ // }
620
+ const scope = element._frame ?? element.page();
621
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
622
+ let prefixSelector = "";
623
+ const frameControlSelector = " >> internal:control=enter-frame";
624
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
625
+ if (frameSelectorIndex !== -1) {
626
+ // remove everything after the >> internal:control=enter-frame
627
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
628
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
629
+ }
630
+ // if (element?._frame?._selector) {
631
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
632
+ // }
633
+ const newSelector = prefixSelector + newElementSelector;
634
+ return scope.locator(newSelector);
577
635
  }
578
636
  }
579
637
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +783,9 @@ class StableBrowser {
725
783
  // info.log += "scanning locators in priority 2" + "\n";
726
784
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
785
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
786
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
787
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
788
  }
731
- else {
732
- if (result.foundElements.length === 0 && !highPriorityOnly) {
733
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
734
- }
735
- }
736
789
  let foundElements = result.foundElements;
737
790
  if (foundElements.length === 1 && foundElements[0].unique) {
738
791
  info.box = foundElements[0].box;
@@ -787,6 +840,11 @@ class StableBrowser {
787
840
  visibleOnly = false;
788
841
  }
789
842
  await new Promise((resolve) => setTimeout(resolve, 1000));
843
+ // sheck of more of half of the timeout has passed
844
+ if (Date.now() - startTime > timeout / 2) {
845
+ highPriorityOnly = false;
846
+ visibleOnly = false;
847
+ }
790
848
  }
791
849
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
850
  // if (info.locatorLog) {
@@ -802,7 +860,7 @@ class StableBrowser {
802
860
  }
803
861
  throw new Error("failed to locate first element no elements found, " + info.log);
804
862
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
863
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
864
  let foundElements = [];
807
865
  const result = {
808
866
  foundElements: foundElements,
@@ -821,7 +879,9 @@ class StableBrowser {
821
879
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
880
  }
823
881
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
882
+ if (logErrors) {
883
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
884
+ }
825
885
  }
826
886
  }
827
887
  if (foundLocators.length === 1) {
@@ -856,13 +916,13 @@ class StableBrowser {
856
916
  }
857
917
  if (boxes.length === 1) {
858
918
  result.foundElements.push({
859
- locator: boxes[0].locator,
919
+ locator: boxes[0].locator.first(),
860
920
  box: boxes[0].box,
861
921
  unique: true,
862
922
  });
863
923
  result.locatorIndex = i;
864
924
  }
865
- else {
925
+ else if (logErrors) {
866
926
  info.failCause.foundMultiple = true;
867
927
  if (info.locatorLog) {
868
928
  info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
@@ -914,7 +974,7 @@ class StableBrowser {
914
974
  await _commandError(state, "timeout looking for " + elementDescription, this);
915
975
  }
916
976
  finally {
917
- _commandFinally(state, this);
977
+ await _commandFinally(state, this);
918
978
  }
919
979
  }
920
980
  }
@@ -963,7 +1023,7 @@ class StableBrowser {
963
1023
  await _commandError(state, "timeout looking for " + elementDescription, this);
964
1024
  }
965
1025
  finally {
966
- _commandFinally(state, this);
1026
+ await _commandFinally(state, this);
967
1027
  }
968
1028
  }
969
1029
  }
@@ -992,7 +1052,7 @@ class StableBrowser {
992
1052
  await _commandError(state, e, this);
993
1053
  }
994
1054
  finally {
995
- _commandFinally(state, this);
1055
+ await _commandFinally(state, this);
996
1056
  }
997
1057
  }
998
1058
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1023,7 +1083,7 @@ class StableBrowser {
1023
1083
  // await _commandError(state, e, this);
1024
1084
  }
1025
1085
  finally {
1026
- _commandFinally(state, this);
1086
+ await _commandFinally(state, this);
1027
1087
  }
1028
1088
  return found;
1029
1089
  }
@@ -1047,8 +1107,8 @@ class StableBrowser {
1047
1107
  try {
1048
1108
  // if (world && world.screenshot && !world.screenshotPath) {
1049
1109
  // console.log(`Highlighting while running from recorder`);
1050
- await this._highlightElements(element);
1051
- await state.element.setChecked(checked);
1110
+ await this._highlightElements(state.element);
1111
+ await state.element.setChecked(checked, { timeout: 2000 });
1052
1112
  await new Promise((resolve) => setTimeout(resolve, 1000));
1053
1113
  // await this._unHighlightElements(element);
1054
1114
  // }
@@ -1060,11 +1120,28 @@ class StableBrowser {
1060
1120
  this.logger.info("element did not change its state, ignoring...");
1061
1121
  }
1062
1122
  else {
1123
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1063
1124
  //await this.closeUnexpectedPopups();
1064
1125
  state.info.log += "setCheck failed, will try again" + "\n";
1065
- state.element = await this._locate(selectors, state.info, _params);
1066
- await state.element.setChecked(checked, { timeout: 5000, force: true });
1067
- await new Promise((resolve) => setTimeout(resolve, 1000));
1126
+ state.element_found = false;
1127
+ try {
1128
+ state.element = await this._locate(selectors, state.info, _params, 100);
1129
+ state.element_found = true;
1130
+ // check the check state
1131
+ }
1132
+ catch (error) {
1133
+ // element dismissed
1134
+ }
1135
+ if (state.element_found) {
1136
+ const isChecked = await state.element.isChecked();
1137
+ if (isChecked !== checked) {
1138
+ // perform click
1139
+ await state.element.click({ timeout: 2000, force: true });
1140
+ }
1141
+ else {
1142
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1143
+ }
1144
+ }
1068
1145
  }
1069
1146
  }
1070
1147
  await this.waitForPageLoad();
@@ -1074,7 +1151,7 @@ class StableBrowser {
1074
1151
  await _commandError(state, e, this);
1075
1152
  }
1076
1153
  finally {
1077
- _commandFinally(state, this);
1154
+ await _commandFinally(state, this);
1078
1155
  }
1079
1156
  }
1080
1157
  async hover(selectors, _params, options = {}, world = null) {
@@ -1100,7 +1177,7 @@ class StableBrowser {
1100
1177
  await _commandError(state, e, this);
1101
1178
  }
1102
1179
  finally {
1103
- _commandFinally(state, this);
1180
+ await _commandFinally(state, this);
1104
1181
  }
1105
1182
  }
1106
1183
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1136,7 +1213,7 @@ class StableBrowser {
1136
1213
  await _commandError(state, e, this);
1137
1214
  }
1138
1215
  finally {
1139
- _commandFinally(state, this);
1216
+ await _commandFinally(state, this);
1140
1217
  }
1141
1218
  }
1142
1219
  async type(_value, _params = null, options = {}, world = null) {
@@ -1182,7 +1259,7 @@ class StableBrowser {
1182
1259
  await _commandError(state, e, this);
1183
1260
  }
1184
1261
  finally {
1185
- _commandFinally(state, this);
1262
+ await _commandFinally(state, this);
1186
1263
  }
1187
1264
  }
1188
1265
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1218,7 +1295,7 @@ class StableBrowser {
1218
1295
  await _commandError(state, e, this);
1219
1296
  }
1220
1297
  finally {
1221
- _commandFinally(state, this);
1298
+ await _commandFinally(state, this);
1222
1299
  }
1223
1300
  }
1224
1301
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1287,7 +1364,7 @@ class StableBrowser {
1287
1364
  await _commandError(state, e, this);
1288
1365
  }
1289
1366
  finally {
1290
- _commandFinally(state, this);
1367
+ await _commandFinally(state, this);
1291
1368
  }
1292
1369
  }
1293
1370
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1306,6 +1383,9 @@ class StableBrowser {
1306
1383
  operation: "clickType",
1307
1384
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1308
1385
  };
1386
+ if (!options) {
1387
+ options = {};
1388
+ }
1309
1389
  if (newValue !== _value) {
1310
1390
  //this.logger.info(_value + "=" + newValue);
1311
1391
  _value = newValue;
@@ -1313,7 +1393,7 @@ class StableBrowser {
1313
1393
  try {
1314
1394
  await _preCommand(state, this);
1315
1395
  state.info.value = _value;
1316
- if (options === null || options === undefined || !options.press) {
1396
+ if (!options.press) {
1317
1397
  try {
1318
1398
  let currentValue = await state.element.inputValue();
1319
1399
  if (currentValue) {
@@ -1324,10 +1404,7 @@ class StableBrowser {
1324
1404
  this.logger.info("unable to clear input value");
1325
1405
  }
1326
1406
  }
1327
- if (options === null || options === undefined || options.press) {
1328
- if (!options) {
1329
- options = {};
1330
- }
1407
+ if (options.press) {
1331
1408
  options.timeout = 5000;
1332
1409
  await performAction("click", state.element, options, this, state, _params);
1333
1410
  }
@@ -1387,7 +1464,7 @@ class StableBrowser {
1387
1464
  await _commandError(state, e, this);
1388
1465
  }
1389
1466
  finally {
1390
- _commandFinally(state, this);
1467
+ await _commandFinally(state, this);
1391
1468
  }
1392
1469
  }
1393
1470
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1417,7 +1494,42 @@ class StableBrowser {
1417
1494
  await _commandError(state, e, this);
1418
1495
  }
1419
1496
  finally {
1420
- _commandFinally(state, this);
1497
+ await _commandFinally(state, this);
1498
+ }
1499
+ }
1500
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1501
+ const state = {
1502
+ selectors,
1503
+ _params,
1504
+ files,
1505
+ value: '"' + files.join('", "') + '"',
1506
+ options,
1507
+ world,
1508
+ type: Types.SET_INPUT_FILES,
1509
+ text: `Set input files`,
1510
+ _text: `Set input files on ${selectors.element_name}`,
1511
+ operation: "setInputFiles",
1512
+ log: "***** set input files " + selectors.element_name + " *****\n",
1513
+ };
1514
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1515
+ try {
1516
+ await _preCommand(state, this);
1517
+ for (let i = 0; i < files.length; i++) {
1518
+ const file = files[i];
1519
+ const filePath = path.join(uploadsFolder, file);
1520
+ if (!fs.existsSync(filePath)) {
1521
+ throw new Error(`File not found: ${filePath}`);
1522
+ }
1523
+ state.files[i] = filePath;
1524
+ }
1525
+ await state.element.setInputFiles(files);
1526
+ return state.info;
1527
+ }
1528
+ catch (e) {
1529
+ await _commandError(state, e, this);
1530
+ }
1531
+ finally {
1532
+ await _commandFinally(state, this);
1421
1533
  }
1422
1534
  }
1423
1535
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1533,7 +1645,7 @@ class StableBrowser {
1533
1645
  await _commandError(state, e, this);
1534
1646
  }
1535
1647
  finally {
1536
- _commandFinally(state, this);
1648
+ await _commandFinally(state, this);
1537
1649
  }
1538
1650
  }
1539
1651
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1568,7 +1680,7 @@ class StableBrowser {
1568
1680
  while (Date.now() - startTime < timeout) {
1569
1681
  try {
1570
1682
  await _preCommand(state, this);
1571
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1683
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1572
1684
  if (foundObj && foundObj.element) {
1573
1685
  await this.scrollIfNeeded(foundObj.element, state.info);
1574
1686
  }
@@ -1610,7 +1722,84 @@ class StableBrowser {
1610
1722
  throw e;
1611
1723
  }
1612
1724
  finally {
1613
- _commandFinally(state, this);
1725
+ await _commandFinally(state, this);
1726
+ }
1727
+ }
1728
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1729
+ const timeout = this._getFindElementTimeout(options);
1730
+ const startTime = Date.now();
1731
+ const state = {
1732
+ _params,
1733
+ value: referanceSnapshot,
1734
+ options,
1735
+ world,
1736
+ locate: false,
1737
+ scroll: false,
1738
+ screenshot: true,
1739
+ highlight: false,
1740
+ type: Types.SNAPSHOT_VALIDATION,
1741
+ text: `verify snapshot: ${referanceSnapshot}`,
1742
+ operation: "snapshotValidation",
1743
+ log: "***** verify snapshot *****\n",
1744
+ };
1745
+ if (!referanceSnapshot) {
1746
+ throw new Error("referanceSnapshot is null");
1747
+ }
1748
+ let text = null;
1749
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1750
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1751
+ }
1752
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1753
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1754
+ }
1755
+ else if (referanceSnapshot.startsWith("yaml:")) {
1756
+ text = referanceSnapshot.substring(5);
1757
+ }
1758
+ else {
1759
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1760
+ }
1761
+ state.text = text;
1762
+ const newValue = await this._replaceWithLocalData(text, world);
1763
+ await _preCommand(state, this);
1764
+ let foundObj = null;
1765
+ try {
1766
+ let matchResult = null;
1767
+ while (Date.now() - startTime < timeout) {
1768
+ try {
1769
+ let scope = null;
1770
+ if (!frameSelectors) {
1771
+ scope = this.page;
1772
+ }
1773
+ else {
1774
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1775
+ }
1776
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1777
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1778
+ if (matchResult.errorLine !== -1) {
1779
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1780
+ }
1781
+ // highlight and screenshot
1782
+ try {
1783
+ await await highlightSnapshot(newValue, scope);
1784
+ await _screenshot(state, this);
1785
+ }
1786
+ catch (e) { }
1787
+ return state.info;
1788
+ }
1789
+ catch (e) {
1790
+ // Log error but continue retrying until timeout is reached
1791
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1792
+ }
1793
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1794
+ }
1795
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1796
+ }
1797
+ catch (e) {
1798
+ await _commandError(state, e, this);
1799
+ throw e;
1800
+ }
1801
+ finally {
1802
+ await _commandFinally(state, this);
1614
1803
  }
1615
1804
  }
1616
1805
  async waitForUserInput(message, world = null) {
@@ -1648,6 +1837,15 @@ class StableBrowser {
1648
1837
  // save the data to the file
1649
1838
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1650
1839
  }
1840
+ overwriteTestData(testData, world = null) {
1841
+ if (!testData) {
1842
+ return;
1843
+ }
1844
+ // if data file exists, load it
1845
+ const dataFile = _getDataFile(world, this.context, this);
1846
+ // save the data to the file
1847
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1848
+ }
1651
1849
  _getDataFilePath(fileName) {
1652
1850
  let dataFile = path.join(this.project_path, "data", fileName);
1653
1851
  if (fs.existsSync(dataFile)) {
@@ -1900,7 +2098,7 @@ class StableBrowser {
1900
2098
  await _commandError(state, e, this);
1901
2099
  }
1902
2100
  finally {
1903
- _commandFinally(state, this);
2101
+ await _commandFinally(state, this);
1904
2102
  }
1905
2103
  }
1906
2104
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1931,10 +2129,31 @@ class StableBrowser {
1931
2129
  case "value":
1932
2130
  state.value = await state.element.inputValue();
1933
2131
  break;
2132
+ case "text":
2133
+ state.value = await state.element.textContent();
2134
+ break;
1934
2135
  default:
1935
2136
  state.value = await state.element.getAttribute(attribute);
1936
2137
  break;
1937
2138
  }
2139
+ if (options !== null) {
2140
+ if (options.regex && options.regex !== "") {
2141
+ // Construct a regex pattern from the provided string
2142
+ const regex = options.regex.slice(1, -1);
2143
+ const regexPattern = new RegExp(regex, "g");
2144
+ const matches = state.value.match(regexPattern);
2145
+ if (matches) {
2146
+ let newValue = "";
2147
+ for (const match of matches) {
2148
+ newValue += match;
2149
+ }
2150
+ state.value = newValue;
2151
+ }
2152
+ }
2153
+ if (options.trimSpaces && options.trimSpaces === true) {
2154
+ state.value = state.value.trim();
2155
+ }
2156
+ }
1938
2157
  state.info.value = state.value;
1939
2158
  this.setTestData({ [variable]: state.value }, world);
1940
2159
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1945,7 +2164,7 @@ class StableBrowser {
1945
2164
  await _commandError(state, e, this);
1946
2165
  }
1947
2166
  finally {
1948
- _commandFinally(state, this);
2167
+ await _commandFinally(state, this);
1949
2168
  }
1950
2169
  }
1951
2170
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1970,12 +2189,15 @@ class StableBrowser {
1970
2189
  let expectedValue;
1971
2190
  try {
1972
2191
  await _preCommand(state, this);
1973
- expectedValue = state.value;
2192
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1974
2193
  state.info.expectedValue = expectedValue;
1975
2194
  switch (attribute) {
1976
2195
  case "innerText":
1977
2196
  val = String(await state.element.innerText());
1978
2197
  break;
2198
+ case "text":
2199
+ val = String(await state.element.textContent());
2200
+ break;
1979
2201
  case "value":
1980
2202
  val = String(await state.element.inputValue());
1981
2203
  break;
@@ -1997,17 +2219,42 @@ class StableBrowser {
1997
2219
  let regex;
1998
2220
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1999
2221
  const patternBody = expectedValue.slice(1, -1);
2000
- regex = new RegExp(patternBody, "g");
2222
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2223
+ regex = new RegExp(processedPattern, "gs");
2224
+ state.info.regex = true;
2001
2225
  }
2002
2226
  else {
2003
2227
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2004
2228
  regex = new RegExp(escapedPattern, "g");
2005
2229
  }
2006
- if (!val.match(regex)) {
2007
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2008
- state.info.failCause.assertionFailed = true;
2009
- state.info.failCause.lastError = errorMessage;
2010
- throw new Error(errorMessage);
2230
+ if (attribute === "innerText") {
2231
+ if (state.info.regex) {
2232
+ if (!regex.test(val)) {
2233
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2234
+ state.info.failCause.assertionFailed = true;
2235
+ state.info.failCause.lastError = errorMessage;
2236
+ throw new Error(errorMessage);
2237
+ }
2238
+ }
2239
+ else {
2240
+ const valLines = val.split("\n");
2241
+ const expectedLines = expectedValue.split("\n");
2242
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2243
+ if (!isPart) {
2244
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2245
+ state.info.failCause.assertionFailed = true;
2246
+ state.info.failCause.lastError = errorMessage;
2247
+ throw new Error(errorMessage);
2248
+ }
2249
+ }
2250
+ }
2251
+ else {
2252
+ if (!val.match(regex)) {
2253
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2254
+ state.info.failCause.assertionFailed = true;
2255
+ state.info.failCause.lastError = errorMessage;
2256
+ throw new Error(errorMessage);
2257
+ }
2011
2258
  }
2012
2259
  return state.info;
2013
2260
  }
@@ -2015,7 +2262,7 @@ class StableBrowser {
2015
2262
  await _commandError(state, e, this);
2016
2263
  }
2017
2264
  finally {
2018
- _commandFinally(state, this);
2265
+ await _commandFinally(state, this);
2019
2266
  }
2020
2267
  }
2021
2268
  async extractEmailData(emailAddress, options, world) {
@@ -2175,56 +2422,49 @@ class StableBrowser {
2175
2422
  console.debug(error);
2176
2423
  }
2177
2424
  }
2178
- // async _unhighlightElements(scope, css) {
2179
- // try {
2180
- // if (!scope) {
2181
- // return;
2182
- // }
2183
- // if (!css) {
2184
- // scope
2185
- // .evaluate((node) => {
2186
- // if (node && node.style) {
2187
- // if (!node.__previousOutline) {
2188
- // node.style.outline = "";
2189
- // } else {
2190
- // node.style.outline = node.__previousOutline;
2191
- // }
2192
- // }
2193
- // })
2194
- // .then(() => {})
2195
- // .catch((e) => {
2196
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2197
- // });
2198
- // } else {
2199
- // scope
2200
- // .evaluate(([css]) => {
2201
- // if (!css) {
2202
- // return;
2203
- // }
2204
- // let elements = Array.from(document.querySelectorAll(css));
2205
- // for (i = 0; i < elements.length; i++) {
2206
- // let element = elements[i];
2207
- // if (!element.style) {
2208
- // return;
2209
- // }
2210
- // if (!element.__previousOutline) {
2211
- // element.style.outline = "";
2212
- // } else {
2213
- // element.style.outline = element.__previousOutline;
2214
- // }
2215
- // }
2216
- // })
2217
- // .then(() => {})
2218
- // .catch((e) => {
2219
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2220
- // });
2221
- // }
2222
- // } catch (error) {
2223
- // // console.debug(error);
2224
- // }
2225
- // }
2425
+ _matcher(text) {
2426
+ if (!text) {
2427
+ return { matcher: "contains", queryText: "" };
2428
+ }
2429
+ if (text.length < 2) {
2430
+ return { matcher: "contains", queryText: text };
2431
+ }
2432
+ const split = text.split(":");
2433
+ const matcher = split[0].toLowerCase();
2434
+ const queryText = split.slice(1).join(":").trim();
2435
+ return { matcher, queryText };
2436
+ }
2437
+ _getDomain(url) {
2438
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2439
+ return "";
2440
+ }
2441
+ let hostnameFragments = url.split("/")[2].split(".");
2442
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2443
+ return hostnameFragments.join("-").split(":").join("-");
2444
+ }
2445
+ let n = hostnameFragments.length;
2446
+ let fragments = [...hostnameFragments];
2447
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2448
+ hostnameFragments.pop();
2449
+ n = hostnameFragments.length;
2450
+ }
2451
+ if (n == 0) {
2452
+ if (fragments[0] === "www")
2453
+ fragments = fragments.slice(1);
2454
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2455
+ }
2456
+ if (hostnameFragments[0] === "www")
2457
+ hostnameFragments = hostnameFragments.slice(1);
2458
+ return hostnameFragments.join(".");
2459
+ }
2460
+ /**
2461
+ * Verify the page path matches the given path.
2462
+ * @param {string} pathPart - The path to verify.
2463
+ * @param {object} options - Options for verification.
2464
+ * @param {object} world - The world context.
2465
+ * @returns {Promise<object>} - The state info after verification.
2466
+ */
2226
2467
  async verifyPagePath(pathPart, options = {}, world = null) {
2227
- const startTime = Date.now();
2228
2468
  let error = null;
2229
2469
  let screenshotId = null;
2230
2470
  let screenshotPath = null;
@@ -2238,74 +2478,235 @@ class StableBrowser {
2238
2478
  pathPart = newValue;
2239
2479
  }
2240
2480
  info.pathPart = pathPart;
2481
+ const { matcher, queryText } = this._matcher(pathPart);
2482
+ const state = {
2483
+ text_search: queryText,
2484
+ options,
2485
+ world,
2486
+ locate: false,
2487
+ scroll: false,
2488
+ highlight: false,
2489
+ type: Types.VERIFY_PAGE_PATH,
2490
+ text: `Verify the page url is ${queryText}`,
2491
+ _text: `Verify the page url is ${queryText}`,
2492
+ operation: "verifyPagePath",
2493
+ log: "***** verify page url is " + queryText + " *****\n",
2494
+ };
2241
2495
  try {
2496
+ await _preCommand(state, this);
2497
+ state.info.text = queryText;
2242
2498
  for (let i = 0; i < 30; i++) {
2243
2499
  const url = await this.page.url();
2244
- if (!url.includes(pathPart)) {
2245
- if (i === 29) {
2246
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2247
- }
2248
- await new Promise((resolve) => setTimeout(resolve, 1000));
2249
- continue;
2500
+ switch (matcher) {
2501
+ case "exact":
2502
+ if (url !== queryText) {
2503
+ if (i === 29) {
2504
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2505
+ }
2506
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2507
+ continue;
2508
+ }
2509
+ break;
2510
+ case "contains":
2511
+ if (!url.includes(queryText)) {
2512
+ if (i === 29) {
2513
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2514
+ }
2515
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2516
+ continue;
2517
+ }
2518
+ break;
2519
+ case "starts-with":
2520
+ {
2521
+ const domain = this._getDomain(url);
2522
+ if (domain.length > 0 && domain !== queryText) {
2523
+ if (i === 29) {
2524
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2525
+ }
2526
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2527
+ continue;
2528
+ }
2529
+ }
2530
+ break;
2531
+ case "ends-with":
2532
+ {
2533
+ const urlObj = new URL(url);
2534
+ let route = "/";
2535
+ if (urlObj.pathname !== "/") {
2536
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2537
+ }
2538
+ else {
2539
+ route = "/";
2540
+ }
2541
+ if (route !== queryText) {
2542
+ if (i === 29) {
2543
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2544
+ }
2545
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2546
+ continue;
2547
+ }
2548
+ }
2549
+ break;
2550
+ case "regex":
2551
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2552
+ if (!regex.test(url)) {
2553
+ if (i === 29) {
2554
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2555
+ }
2556
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2557
+ continue;
2558
+ }
2559
+ break;
2560
+ default:
2561
+ console.log("Unknown matching type, defaulting to contains matching");
2562
+ if (!url.includes(pathPart)) {
2563
+ if (i === 29) {
2564
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2565
+ }
2566
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2567
+ continue;
2568
+ }
2250
2569
  }
2251
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2252
- return info;
2570
+ await _screenshot(state, this);
2571
+ return state.info;
2253
2572
  }
2254
2573
  }
2255
2574
  catch (e) {
2256
- //await this.closeUnexpectedPopups();
2257
- this.logger.error("verify page path failed " + info.log);
2258
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2259
- info.screenshotPath = screenshotPath;
2260
- Object.assign(e, { info: info });
2261
- error = e;
2262
- // throw e;
2263
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2575
+ state.info.failCause.lastError = e.message;
2576
+ state.info.failCause.assertionFailed = true;
2577
+ await _commandError(state, e, this);
2264
2578
  }
2265
2579
  finally {
2266
- const endTime = Date.now();
2267
- _reportToWorld(world, {
2268
- type: Types.VERIFY_PAGE_PATH,
2269
- text: "Verify page path",
2270
- _text: "Verify the page path contains " + pathPart,
2271
- screenshotId,
2272
- result: error
2273
- ? {
2274
- status: "FAILED",
2275
- startTime,
2276
- endTime,
2277
- message: error?.message,
2278
- }
2279
- : {
2280
- status: "PASSED",
2281
- startTime,
2282
- endTime,
2283
- },
2284
- info: info,
2285
- });
2580
+ await _commandFinally(state, this);
2581
+ }
2582
+ }
2583
+ /**
2584
+ * Verify the page title matches the given title.
2585
+ * @param {string} title - The title to verify.
2586
+ * @param {object} options - Options for verification.
2587
+ * @param {object} world - The world context.
2588
+ * @returns {Promise<object>} - The state info after verification.
2589
+ */
2590
+ async verifyPageTitle(title, options = {}, world = null) {
2591
+ let error = null;
2592
+ let screenshotId = null;
2593
+ let screenshotPath = null;
2594
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2595
+ const newValue = await this._replaceWithLocalData(title, world);
2596
+ if (newValue !== title) {
2597
+ this.logger.info(title + "=" + newValue);
2598
+ title = newValue;
2599
+ }
2600
+ const { matcher, queryText } = this._matcher(title);
2601
+ const state = {
2602
+ text_search: queryText,
2603
+ options,
2604
+ world,
2605
+ locate: false,
2606
+ scroll: false,
2607
+ highlight: false,
2608
+ type: Types.VERIFY_PAGE_TITLE,
2609
+ text: `Verify the page title is ${queryText}`,
2610
+ _text: `Verify the page title is ${queryText}`,
2611
+ operation: "verifyPageTitle",
2612
+ log: "***** verify page title is " + queryText + " *****\n",
2613
+ };
2614
+ try {
2615
+ await _preCommand(state, this);
2616
+ state.info.text = queryText;
2617
+ for (let i = 0; i < 30; i++) {
2618
+ const foundTitle = await this.page.title();
2619
+ switch (matcher) {
2620
+ case "exact":
2621
+ if (foundTitle !== queryText) {
2622
+ if (i === 29) {
2623
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2624
+ }
2625
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2626
+ continue;
2627
+ }
2628
+ break;
2629
+ case "contains":
2630
+ if (!foundTitle.includes(queryText)) {
2631
+ if (i === 29) {
2632
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2633
+ }
2634
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2635
+ continue;
2636
+ }
2637
+ break;
2638
+ case "starts-with":
2639
+ if (!foundTitle.startsWith(queryText)) {
2640
+ if (i === 29) {
2641
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2642
+ }
2643
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2644
+ continue;
2645
+ }
2646
+ break;
2647
+ case "ends-with":
2648
+ if (!foundTitle.endsWith(queryText)) {
2649
+ if (i === 29) {
2650
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2651
+ }
2652
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2653
+ continue;
2654
+ }
2655
+ break;
2656
+ case "regex":
2657
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2658
+ if (!regex.test(foundTitle)) {
2659
+ if (i === 29) {
2660
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2661
+ }
2662
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2663
+ continue;
2664
+ }
2665
+ break;
2666
+ default:
2667
+ console.log("Unknown matching type, defaulting to contains matching");
2668
+ if (!foundTitle.includes(title)) {
2669
+ if (i === 29) {
2670
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2671
+ }
2672
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2673
+ continue;
2674
+ }
2675
+ }
2676
+ await _screenshot(state, this);
2677
+ return state.info;
2678
+ }
2679
+ }
2680
+ catch (e) {
2681
+ state.info.failCause.lastError = e.message;
2682
+ state.info.failCause.assertionFailed = true;
2683
+ await _commandError(state, e, this);
2684
+ }
2685
+ finally {
2686
+ await _commandFinally(state, this);
2286
2687
  }
2287
2688
  }
2288
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2689
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2289
2690
  const frames = this.page.frames();
2290
2691
  let results = [];
2291
- let ignoreCase = false;
2692
+ // let ignoreCase = false;
2292
2693
  for (let i = 0; i < frames.length; i++) {
2293
2694
  if (dateAlternatives.date) {
2294
2695
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2295
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2696
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2296
2697
  result.frame = frames[i];
2297
2698
  results.push(result);
2298
2699
  }
2299
2700
  }
2300
2701
  else if (numberAlternatives.number) {
2301
2702
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2302
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2703
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2303
2704
  result.frame = frames[i];
2304
2705
  results.push(result);
2305
2706
  }
2306
2707
  }
2307
2708
  else {
2308
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2709
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2309
2710
  result.frame = frames[i];
2310
2711
  results.push(result);
2311
2712
  }
@@ -2324,7 +2725,7 @@ class StableBrowser {
2324
2725
  scroll: false,
2325
2726
  highlight: false,
2326
2727
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2327
- text: `Verify text exists in page`,
2728
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2328
2729
  _text: `Verify the text '${text}' exists in page`,
2329
2730
  operation: "verifyTextExistInPage",
2330
2731
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2366,27 +2767,10 @@ class StableBrowser {
2366
2767
  const frame = resultWithElementsFound[0].frame;
2367
2768
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2368
2769
  await this._highlightElements(frame, dataAttribute);
2369
- // if (world && world.screenshot && !world.screenshotPath) {
2370
- // console.log(`Highlighting for verify text is found while running from recorder`);
2371
- // this._highlightElements(frame, dataAttribute).then(async () => {
2372
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2373
- // this._unhighlightElements(frame, dataAttribute)
2374
- // .then(async () => {
2375
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2376
- // })
2377
- // .catch(
2378
- // (e) => {}
2379
- // console.error(e)
2380
- // );
2381
- // });
2382
- // }
2383
2770
  const element = await frame.locator(dataAttribute).first();
2384
- // await new Promise((resolve) => setTimeout(resolve, 100));
2385
- // await this._unhighlightElements(frame, dataAttribute);
2386
2771
  if (element) {
2387
2772
  await this.scrollIfNeeded(element, state.info);
2388
2773
  await element.dispatchEvent("bvt_verify_page_contains_text");
2389
- // await _screenshot(state, this, element);
2390
2774
  }
2391
2775
  }
2392
2776
  await _screenshot(state, this);
@@ -2396,13 +2780,12 @@ class StableBrowser {
2396
2780
  console.error(error);
2397
2781
  }
2398
2782
  }
2399
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2400
2783
  }
2401
2784
  catch (e) {
2402
2785
  await _commandError(state, e, this);
2403
2786
  }
2404
2787
  finally {
2405
- _commandFinally(state, this);
2788
+ await _commandFinally(state, this);
2406
2789
  }
2407
2790
  }
2408
2791
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2415,7 +2798,7 @@ class StableBrowser {
2415
2798
  scroll: false,
2416
2799
  highlight: false,
2417
2800
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2418
- text: `Verify text does not exist in page`,
2801
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2419
2802
  _text: `Verify the text '${text}' does not exist in page`,
2420
2803
  operation: "verifyTextNotExistInPage",
2421
2804
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2459,7 +2842,7 @@ class StableBrowser {
2459
2842
  await _commandError(state, e, this);
2460
2843
  }
2461
2844
  finally {
2462
- _commandFinally(state, this);
2845
+ await _commandFinally(state, this);
2463
2846
  }
2464
2847
  }
2465
2848
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2501,7 +2884,7 @@ class StableBrowser {
2501
2884
  };
2502
2885
  while (true) {
2503
2886
  try {
2504
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2887
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2505
2888
  }
2506
2889
  catch (error) {
2507
2890
  // ignore
@@ -2529,7 +2912,7 @@ class StableBrowser {
2529
2912
  const count = await frame.locator(css).count();
2530
2913
  for (let j = 0; j < count; j++) {
2531
2914
  const continer = await frame.locator(css).nth(j);
2532
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2915
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2533
2916
  if (result.elementCount > 0) {
2534
2917
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2535
2918
  await this._highlightElements(frame, dataAttribute);
@@ -2570,8 +2953,32 @@ class StableBrowser {
2570
2953
  await _commandError(state, e, this);
2571
2954
  }
2572
2955
  finally {
2573
- _commandFinally(state, this);
2956
+ await _commandFinally(state, this);
2957
+ }
2958
+ }
2959
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2960
+ const frames = this.page.frames();
2961
+ let results = [];
2962
+ let ignoreCase = false;
2963
+ for (let i = 0; i < frames.length; i++) {
2964
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2965
+ result.frame = frames[i];
2966
+ const climbArray = [];
2967
+ for (let i = 0; i < climb; i++) {
2968
+ climbArray.push("..");
2969
+ }
2970
+ let climbXpath = "xpath=" + climbArray.join("/");
2971
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2972
+ const count = await frames[i].locator(newLocator).count();
2973
+ if (count > 0) {
2974
+ result.elementCount = count;
2975
+ result.locator = newLocator;
2976
+ results.push(result);
2977
+ }
2574
2978
  }
2979
+ // state.info.results = results;
2980
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2981
+ return resultWithElementsFound;
2575
2982
  }
2576
2983
  async visualVerification(text, options = {}, world = null) {
2577
2984
  const startTime = Date.now();
@@ -2889,7 +3296,13 @@ class StableBrowser {
2889
3296
  }
2890
3297
  }
2891
3298
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2892
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3299
+ try {
3300
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3301
+ }
3302
+ catch (error) {
3303
+ this.logger.debug(error);
3304
+ throw error;
3305
+ }
2893
3306
  }
2894
3307
  _getLoadTimeout(options) {
2895
3308
  let timeout = 15000;
@@ -2912,6 +3325,7 @@ class StableBrowser {
2912
3325
  }
2913
3326
  async saveStoreState(path = null, world = null) {
2914
3327
  const storageState = await this.page.context().storageState();
3328
+ path = await this._replaceWithLocalData(path, this.world);
2915
3329
  //const testDataFile = _getDataFile(world, this.context, this);
2916
3330
  if (path) {
2917
3331
  // save { storageState: storageState } into the path
@@ -2922,10 +3336,14 @@ class StableBrowser {
2922
3336
  }
2923
3337
  }
2924
3338
  async restoreSaveState(path = null, world = null) {
3339
+ path = await this._replaceWithLocalData(path, this.world);
2925
3340
  await refreshBrowser(this, path, world);
2926
3341
  this.registerEventListeners(this.context);
2927
3342
  registerNetworkEvents(this.world, this, this.context, this.page);
2928
3343
  registerDownloadEvent(this.page, this.world, this.context);
3344
+ if (this.onRestoreSaveState) {
3345
+ this.onRestoreSaveState(path);
3346
+ }
2929
3347
  }
2930
3348
  async waitForPageLoad(options = {}, world = null) {
2931
3349
  let timeout = this._getLoadTimeout(options);
@@ -3008,7 +3426,7 @@ class StableBrowser {
3008
3426
  await _commandError(state, e, this);
3009
3427
  }
3010
3428
  finally {
3011
- _commandFinally(state, this);
3429
+ await _commandFinally(state, this);
3012
3430
  }
3013
3431
  }
3014
3432
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3095,7 +3513,7 @@ class StableBrowser {
3095
3513
  await _commandError(state, e, this);
3096
3514
  }
3097
3515
  finally {
3098
- _commandFinally(state, this);
3516
+ await _commandFinally(state, this);
3099
3517
  }
3100
3518
  }
3101
3519
  saveTestDataAsGlobal(options, world) {
@@ -3200,7 +3618,39 @@ class StableBrowser {
3200
3618
  console.log("#-#");
3201
3619
  }
3202
3620
  }
3621
+ async beforeScenario(world, scenario) {
3622
+ this.beforeScenarioCalled = true;
3623
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3624
+ this.scenarioName = scenario.pickle.name;
3625
+ }
3626
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3627
+ this.featureName = scenario.gherkinDocument.feature.name;
3628
+ }
3629
+ if (this.context) {
3630
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3631
+ }
3632
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3633
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3634
+ // check if @global_test_data tag is present
3635
+ if (this.tags.includes("@global_test_data")) {
3636
+ this.saveTestDataAsGlobal({}, world);
3637
+ }
3638
+ }
3639
+ // update test data based on feature/scenario
3640
+ let envName = null;
3641
+ if (this.context && this.context.environment) {
3642
+ envName = this.context.environment.name;
3643
+ }
3644
+ if (!process.env.TEMP_RUN) {
3645
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3646
+ }
3647
+ await loadBrunoParams(this.context, this.context.environment.name);
3648
+ }
3649
+ async afterScenario(world, scenario) { }
3203
3650
  async beforeStep(world, step) {
3651
+ if (!this.beforeScenarioCalled) {
3652
+ this.beforeScenario(world, step);
3653
+ }
3204
3654
  if (this.stepIndex === undefined) {
3205
3655
  this.stepIndex = 0;
3206
3656
  }
@@ -3217,21 +3667,11 @@ class StableBrowser {
3217
3667
  else {
3218
3668
  this.stepName = "step " + this.stepIndex;
3219
3669
  }
3220
- if (this.context) {
3221
- this.context.examplesRow = extractStepExampleParameters(step);
3222
- }
3223
3670
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3224
3671
  if (this.context.browserObject.context) {
3225
3672
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3226
3673
  }
3227
3674
  }
3228
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3229
- this.tags = step.pickle.tags.map((tag) => tag.name);
3230
- // check if @global_test_data tag is present
3231
- if (this.tags.includes("@global_test_data")) {
3232
- this.saveTestDataAsGlobal({}, world);
3233
- }
3234
- }
3235
3675
  if (this.initSnapshotTaken === false) {
3236
3676
  this.initSnapshotTaken = true;
3237
3677
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3256,18 +3696,68 @@ class StableBrowser {
3256
3696
  const content = [`- path: ${path}`, `- title: ${title}`];
3257
3697
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3258
3698
  for (let i = 0; i < frames.length; i++) {
3259
- content.push(`- frame: ${i}`);
3260
3699
  const frame = frames[i];
3261
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3262
- content.push(snapshot);
3700
+ try {
3701
+ // Ensure frame is attached and has body
3702
+ const body = frame.locator("body");
3703
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3704
+ const snapshot = await body.ariaSnapshot({ timeout });
3705
+ content.push(`- frame: ${i}`);
3706
+ content.push(snapshot);
3707
+ }
3708
+ catch (innerErr) { }
3263
3709
  }
3264
3710
  return content.join("\n");
3265
3711
  }
3266
3712
  catch (e) {
3267
- console.error(e);
3713
+ console.log("Error in getAriaSnapshot");
3714
+ //console.debug(e);
3268
3715
  }
3269
3716
  return null;
3270
3717
  }
3718
+ /**
3719
+ * Sends command with custom payload to report.
3720
+ * @param commandText - Title of the command to be shown in the report.
3721
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3722
+ * @param content - Content of the command to be shown in the report.
3723
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3724
+ * @param world - Optional world context.
3725
+ * @public
3726
+ */
3727
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3728
+ const state = {
3729
+ options,
3730
+ world,
3731
+ locate: false,
3732
+ scroll: false,
3733
+ screenshot: options.screenshot ?? false,
3734
+ highlight: options.highlight ?? false,
3735
+ type: Types.REPORT_COMMAND,
3736
+ text: commandText,
3737
+ _text: commandText,
3738
+ operation: "report_command",
3739
+ log: "***** " + commandText + " *****\n",
3740
+ };
3741
+ try {
3742
+ await _preCommand(state, this);
3743
+ const payload = {
3744
+ type: options.type ?? "text",
3745
+ content: content,
3746
+ screenshotId: null,
3747
+ };
3748
+ state.payload = payload;
3749
+ if (commandStatus === "FAILED") {
3750
+ state.throwError = true;
3751
+ throw new Error("Command failed");
3752
+ }
3753
+ }
3754
+ catch (e) {
3755
+ await _commandError(state, e, this);
3756
+ }
3757
+ finally {
3758
+ await _commandFinally(state, this);
3759
+ }
3760
+ }
3271
3761
  async afterStep(world, step) {
3272
3762
  this.stepName = null;
3273
3763
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3275,6 +3765,13 @@ class StableBrowser {
3275
3765
  await this.context.browserObject.context.tracing.stopChunk({
3276
3766
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3277
3767
  });
3768
+ if (world && world.attach) {
3769
+ await world.attach(JSON.stringify({
3770
+ type: "trace",
3771
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3772
+ }), "application/json+trace");
3773
+ }
3774
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3278
3775
  }
3279
3776
  }
3280
3777
  if (this.context) {
@@ -3287,6 +3784,29 @@ class StableBrowser {
3287
3784
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3288
3785
  }
3289
3786
  }
3787
+ if (!process.env.TEMP_RUN) {
3788
+ const state = {
3789
+ world,
3790
+ locate: false,
3791
+ scroll: false,
3792
+ screenshot: true,
3793
+ highlight: true,
3794
+ type: Types.STEP_COMPLETE,
3795
+ text: "end of scenario",
3796
+ _text: "end of scenario",
3797
+ operation: "step_complete",
3798
+ log: "***** " + "end of scenario" + " *****\n",
3799
+ };
3800
+ try {
3801
+ await _preCommand(state, this);
3802
+ }
3803
+ catch (e) {
3804
+ await _commandError(state, e, this);
3805
+ }
3806
+ finally {
3807
+ await _commandFinally(state, this);
3808
+ }
3809
+ }
3290
3810
  }
3291
3811
  }
3292
3812
  function createTimedPromise(promise, label) {