automation_model 1.0.640-dev → 1.0.640-stage

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