automation_model 1.0.635-dev → 1.0.635-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 (58) 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 +99 -28
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.js +38 -9
  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 +33 -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 +31 -24
  45. package/lib/stable_browser.js +555 -173
  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.d.ts +1 -1
  50. package/lib/table_helper.js +26 -5
  51. package/lib/table_helper.js.map +1 -1
  52. package/lib/test_context.d.ts +2 -0
  53. package/lib/test_context.js +2 -0
  54. package/lib/test_context.js.map +1 -1
  55. package/lib/utils.d.ts +7 -4
  56. package/lib/utils.js +205 -20
  57. package/lib/utils.js.map +1 -1
  58. package/package.json +11 -6
@@ -10,11 +10,12 @@ import { getDateTimeValue } from "./date_time.js";
10
10
  import drawRectangle from "./drawRect.js";
11
11
  //import { closeUnexpectedPopups } from "./popups.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
14
14
  import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
17
  import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
18
19
  import { locate_element } from "./locate_element.js";
19
20
  import { randomUUID } from "crypto";
20
21
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
@@ -22,22 +23,24 @@ 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",
42
45
  TYPE_PRESS: "type_press",
43
46
  PRESS: "press_key",
@@ -55,6 +58,12 @@ export const Types = {
55
58
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
56
59
  VERIFY_ATTRIBUTE: "verify_element_attribute",
57
60
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
61
+ BRUNO: "bruno",
62
+ VERIFY_FILE_EXISTS: "verify_file_exists",
63
+ SET_INPUT_FILES: "set_input_files",
64
+ SNAPSHOT_VALIDATION: "snapshot_validation",
65
+ REPORT_COMMAND: "report_command",
66
+ STEP_COMPLETE: "step_complete",
58
67
  };
59
68
  export const apps = {};
60
69
  const formatElementName = (elementName) => {
@@ -180,6 +189,30 @@ class StableBrowser {
180
189
  await this.waitForPageLoad();
181
190
  }
182
191
  }
192
+ async switchTab(tabTitleOrIndex) {
193
+ // first check if the tabNameOrIndex is a number
194
+ let index = parseInt(tabTitleOrIndex);
195
+ if (!isNaN(index)) {
196
+ if (index >= 0 && index < this.context.pages.length) {
197
+ this.page = this.context.pages[index];
198
+ this.context.page = this.page;
199
+ await this.page.bringToFront();
200
+ return;
201
+ }
202
+ }
203
+ // if the tabNameOrIndex is a string, find the tab by name
204
+ for (let i = 0; i < this.context.pages.length; i++) {
205
+ let page = this.context.pages[i];
206
+ let title = await page.title();
207
+ if (title.includes(tabTitleOrIndex)) {
208
+ this.page = page;
209
+ this.context.page = this.page;
210
+ await this.page.bringToFront();
211
+ return;
212
+ }
213
+ }
214
+ throw new Error("Tab not found: " + tabTitleOrIndex);
215
+ }
183
216
  registerConsoleLogListener(page, context) {
184
217
  if (!this.context.webLogger) {
185
218
  this.context.webLogger = [];
@@ -247,6 +280,7 @@ class StableBrowser {
247
280
  if (!url) {
248
281
  throw new Error("url is null, verify that the environment file is correct");
249
282
  }
283
+ url = await this._replaceWithLocalData(url, this.world);
250
284
  if (!url.startsWith("http")) {
251
285
  url = "https://" + url;
252
286
  }
@@ -275,7 +309,7 @@ class StableBrowser {
275
309
  _commandError(state, error, this);
276
310
  }
277
311
  finally {
278
- _commandFinally(state, this);
312
+ await _commandFinally(state, this);
279
313
  }
280
314
  }
281
315
  async _getLocator(locator, scope, _params) {
@@ -391,7 +425,7 @@ class StableBrowser {
391
425
  }
392
426
  return { elementCount: tagCount, randomToken };
393
427
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
428
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
429
  if (!info) {
396
430
  info = {};
397
431
  }
@@ -458,7 +492,7 @@ class StableBrowser {
458
492
  }
459
493
  return;
460
494
  }
461
- if (info.locatorLog && count === 0) {
495
+ if (info.locatorLog && count === 0 && logErrors) {
462
496
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
497
  }
464
498
  for (let j = 0; j < count; j++) {
@@ -473,7 +507,7 @@ class StableBrowser {
473
507
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
508
  }
475
509
  }
476
- else {
510
+ else if (logErrors) {
477
511
  info.failCause.visible = visible;
478
512
  info.failCause.enabled = enabled;
479
513
  if (!info.printMessages) {
@@ -565,15 +599,27 @@ class StableBrowser {
565
599
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
600
  if (!element.rerun) {
567
601
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
602
+ await element.evaluate((el, randomToken) => {
569
603
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
604
  }, 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;
605
+ // if (element._frame) {
606
+ // return element;
607
+ // }
608
+ const scope = element._frame ?? element.page();
609
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
610
+ let prefixSelector = "";
611
+ const frameControlSelector = " >> internal:control=enter-frame";
612
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
613
+ if (frameSelectorIndex !== -1) {
614
+ // remove everything after the >> internal:control=enter-frame
615
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
616
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
617
+ }
618
+ // if (element?._frame?._selector) {
619
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
620
+ // }
621
+ const newSelector = prefixSelector + newElementSelector;
622
+ return scope.locator(newSelector);
577
623
  }
578
624
  }
579
625
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +771,9 @@ class StableBrowser {
725
771
  // info.log += "scanning locators in priority 2" + "\n";
726
772
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
773
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
774
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
775
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
776
  }
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
777
  let foundElements = result.foundElements;
737
778
  if (foundElements.length === 1 && foundElements[0].unique) {
738
779
  info.box = foundElements[0].box;
@@ -787,6 +828,11 @@ class StableBrowser {
787
828
  visibleOnly = false;
788
829
  }
789
830
  await new Promise((resolve) => setTimeout(resolve, 1000));
831
+ // sheck of more of half of the timeout has passed
832
+ if (Date.now() - startTime > timeout / 2) {
833
+ highPriorityOnly = false;
834
+ visibleOnly = false;
835
+ }
790
836
  }
791
837
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
838
  // if (info.locatorLog) {
@@ -802,7 +848,7 @@ class StableBrowser {
802
848
  }
803
849
  throw new Error("failed to locate first element no elements found, " + info.log);
804
850
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
851
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
852
  let foundElements = [];
807
853
  const result = {
808
854
  foundElements: foundElements,
@@ -821,7 +867,9 @@ class StableBrowser {
821
867
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
868
  }
823
869
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
870
+ if (logErrors) {
871
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
872
+ }
825
873
  }
826
874
  }
827
875
  if (foundLocators.length === 1) {
@@ -833,9 +881,40 @@ class StableBrowser {
833
881
  result.locatorIndex = i;
834
882
  }
835
883
  if (foundLocators.length > 1) {
836
- info.failCause.foundMultiple = true;
837
- if (info.locatorLog) {
838
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
884
+ // remove elements that consume the same space with 10 pixels tolerance
885
+ const boxes = [];
886
+ for (let j = 0; j < foundLocators.length; j++) {
887
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
888
+ }
889
+ for (let j = 0; j < boxes.length; j++) {
890
+ for (let k = 0; k < boxes.length; k++) {
891
+ if (j === k) {
892
+ continue;
893
+ }
894
+ // check if x, y, width, height are the same with 10 pixels tolerance
895
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
896
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
897
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
898
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
899
+ // as the element is not unique, will remove it
900
+ boxes.splice(k, 1);
901
+ k--;
902
+ }
903
+ }
904
+ }
905
+ if (boxes.length === 1) {
906
+ result.foundElements.push({
907
+ locator: boxes[0].locator.first(),
908
+ box: boxes[0].box,
909
+ unique: true,
910
+ });
911
+ result.locatorIndex = i;
912
+ }
913
+ else if (logErrors) {
914
+ info.failCause.foundMultiple = true;
915
+ if (info.locatorLog) {
916
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
917
+ }
839
918
  }
840
919
  }
841
920
  }
@@ -883,7 +962,7 @@ class StableBrowser {
883
962
  await _commandError(state, "timeout looking for " + elementDescription, this);
884
963
  }
885
964
  finally {
886
- _commandFinally(state, this);
965
+ await _commandFinally(state, this);
887
966
  }
888
967
  }
889
968
  }
@@ -932,7 +1011,7 @@ class StableBrowser {
932
1011
  await _commandError(state, "timeout looking for " + elementDescription, this);
933
1012
  }
934
1013
  finally {
935
- _commandFinally(state, this);
1014
+ await _commandFinally(state, this);
936
1015
  }
937
1016
  }
938
1017
  }
@@ -953,19 +1032,7 @@ class StableBrowser {
953
1032
  };
954
1033
  try {
955
1034
  await _preCommand(state, this);
956
- // if (state.options && state.options.context) {
957
- // state.selectors.locators[0].text = state.options.context;
958
- // }
959
- try {
960
- await state.element.click();
961
- // await new Promise((resolve) => setTimeout(resolve, 1000));
962
- }
963
- catch (e) {
964
- // await this.closeUnexpectedPopups();
965
- state.element = await this._locate(selectors, state.info, _params);
966
- await state.element.dispatchEvent("click");
967
- // await new Promise((resolve) => setTimeout(resolve, 1000));
968
- }
1035
+ await performAction("click", state.element, options, this, state, _params);
969
1036
  await this.waitForPageLoad();
970
1037
  return state.info;
971
1038
  }
@@ -973,7 +1040,7 @@ class StableBrowser {
973
1040
  await _commandError(state, e, this);
974
1041
  }
975
1042
  finally {
976
- _commandFinally(state, this);
1043
+ await _commandFinally(state, this);
977
1044
  }
978
1045
  }
979
1046
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1004,7 +1071,7 @@ class StableBrowser {
1004
1071
  // await _commandError(state, e, this);
1005
1072
  }
1006
1073
  finally {
1007
- _commandFinally(state, this);
1074
+ await _commandFinally(state, this);
1008
1075
  }
1009
1076
  return found;
1010
1077
  }
@@ -1028,7 +1095,7 @@ class StableBrowser {
1028
1095
  try {
1029
1096
  // if (world && world.screenshot && !world.screenshotPath) {
1030
1097
  // console.log(`Highlighting while running from recorder`);
1031
- await this._highlightElements(element);
1098
+ await this._highlightElements(state.element);
1032
1099
  await state.element.setChecked(checked);
1033
1100
  await new Promise((resolve) => setTimeout(resolve, 1000));
1034
1101
  // await this._unHighlightElements(element);
@@ -1055,7 +1122,7 @@ class StableBrowser {
1055
1122
  await _commandError(state, e, this);
1056
1123
  }
1057
1124
  finally {
1058
- _commandFinally(state, this);
1125
+ await _commandFinally(state, this);
1059
1126
  }
1060
1127
  }
1061
1128
  async hover(selectors, _params, options = {}, world = null) {
@@ -1072,19 +1139,7 @@ class StableBrowser {
1072
1139
  };
1073
1140
  try {
1074
1141
  await _preCommand(state, this);
1075
- try {
1076
- await state.element.hover();
1077
- // await _screenshot(state, this);
1078
- await new Promise((resolve) => setTimeout(resolve, 1000));
1079
- }
1080
- catch (e) {
1081
- //await this.closeUnexpectedPopups();
1082
- state.info.log += "hover failed, will try again" + "\n";
1083
- state.element = await this._locate(selectors, state.info, _params);
1084
- await state.element.hover({ timeout: 10000 });
1085
- // await _screenshot(state, this);
1086
- await new Promise((resolve) => setTimeout(resolve, 1000));
1087
- }
1142
+ await performAction("hover", state.element, options, this, state, _params);
1088
1143
  await _screenshot(state, this);
1089
1144
  await this.waitForPageLoad();
1090
1145
  return state.info;
@@ -1093,7 +1148,7 @@ class StableBrowser {
1093
1148
  await _commandError(state, e, this);
1094
1149
  }
1095
1150
  finally {
1096
- _commandFinally(state, this);
1151
+ await _commandFinally(state, this);
1097
1152
  }
1098
1153
  }
1099
1154
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1129,7 +1184,7 @@ class StableBrowser {
1129
1184
  await _commandError(state, e, this);
1130
1185
  }
1131
1186
  finally {
1132
- _commandFinally(state, this);
1187
+ await _commandFinally(state, this);
1133
1188
  }
1134
1189
  }
1135
1190
  async type(_value, _params = null, options = {}, world = null) {
@@ -1175,7 +1230,7 @@ class StableBrowser {
1175
1230
  await _commandError(state, e, this);
1176
1231
  }
1177
1232
  finally {
1178
- _commandFinally(state, this);
1233
+ await _commandFinally(state, this);
1179
1234
  }
1180
1235
  }
1181
1236
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1211,7 +1266,7 @@ class StableBrowser {
1211
1266
  await _commandError(state, e, this);
1212
1267
  }
1213
1268
  finally {
1214
- _commandFinally(state, this);
1269
+ await _commandFinally(state, this);
1215
1270
  }
1216
1271
  }
1217
1272
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1231,7 +1286,7 @@ class StableBrowser {
1231
1286
  try {
1232
1287
  await _preCommand(state, this);
1233
1288
  try {
1234
- await state.element.click();
1289
+ await performAction("click", state.element, options, this, state, _params);
1235
1290
  await new Promise((resolve) => setTimeout(resolve, 500));
1236
1291
  if (format) {
1237
1292
  state.value = dayjs(state.value).format(format);
@@ -1280,7 +1335,7 @@ class StableBrowser {
1280
1335
  await _commandError(state, e, this);
1281
1336
  }
1282
1337
  finally {
1283
- _commandFinally(state, this);
1338
+ await _commandFinally(state, this);
1284
1339
  }
1285
1340
  }
1286
1341
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1299,6 +1354,9 @@ class StableBrowser {
1299
1354
  operation: "clickType",
1300
1355
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1301
1356
  };
1357
+ if (!options) {
1358
+ options = {};
1359
+ }
1302
1360
  if (newValue !== _value) {
1303
1361
  //this.logger.info(_value + "=" + newValue);
1304
1362
  _value = newValue;
@@ -1306,7 +1364,7 @@ class StableBrowser {
1306
1364
  try {
1307
1365
  await _preCommand(state, this);
1308
1366
  state.info.value = _value;
1309
- if (options === null || options === undefined || !options.press) {
1367
+ if (!options.press) {
1310
1368
  try {
1311
1369
  let currentValue = await state.element.inputValue();
1312
1370
  if (currentValue) {
@@ -1317,13 +1375,9 @@ class StableBrowser {
1317
1375
  this.logger.info("unable to clear input value");
1318
1376
  }
1319
1377
  }
1320
- if (options === null || options === undefined || options.press) {
1321
- try {
1322
- await state.element.click({ timeout: 5000 });
1323
- }
1324
- catch (e) {
1325
- await state.element.dispatchEvent("click");
1326
- }
1378
+ if (options.press) {
1379
+ options.timeout = 5000;
1380
+ await performAction("click", state.element, options, this, state, _params);
1327
1381
  }
1328
1382
  else {
1329
1383
  try {
@@ -1381,7 +1435,7 @@ class StableBrowser {
1381
1435
  await _commandError(state, e, this);
1382
1436
  }
1383
1437
  finally {
1384
- _commandFinally(state, this);
1438
+ await _commandFinally(state, this);
1385
1439
  }
1386
1440
  }
1387
1441
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1411,7 +1465,42 @@ class StableBrowser {
1411
1465
  await _commandError(state, e, this);
1412
1466
  }
1413
1467
  finally {
1414
- _commandFinally(state, this);
1468
+ await _commandFinally(state, this);
1469
+ }
1470
+ }
1471
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1472
+ const state = {
1473
+ selectors,
1474
+ _params,
1475
+ files,
1476
+ value: '"' + files.join('", "') + '"',
1477
+ options,
1478
+ world,
1479
+ type: Types.SET_INPUT_FILES,
1480
+ text: `Set input files`,
1481
+ _text: `Set input files on ${selectors.element_name}`,
1482
+ operation: "setInputFiles",
1483
+ log: "***** set input files " + selectors.element_name + " *****\n",
1484
+ };
1485
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1486
+ try {
1487
+ await _preCommand(state, this);
1488
+ for (let i = 0; i < files.length; i++) {
1489
+ const file = files[i];
1490
+ const filePath = path.join(uploadsFolder, file);
1491
+ if (!fs.existsSync(filePath)) {
1492
+ throw new Error(`File not found: ${filePath}`);
1493
+ }
1494
+ state.files[i] = filePath;
1495
+ }
1496
+ await state.element.setInputFiles(files);
1497
+ return state.info;
1498
+ }
1499
+ catch (e) {
1500
+ await _commandError(state, e, this);
1501
+ }
1502
+ finally {
1503
+ await _commandFinally(state, this);
1415
1504
  }
1416
1505
  }
1417
1506
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1527,7 +1616,7 @@ class StableBrowser {
1527
1616
  await _commandError(state, e, this);
1528
1617
  }
1529
1618
  finally {
1530
- _commandFinally(state, this);
1619
+ await _commandFinally(state, this);
1531
1620
  }
1532
1621
  }
1533
1622
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1562,7 +1651,7 @@ class StableBrowser {
1562
1651
  while (Date.now() - startTime < timeout) {
1563
1652
  try {
1564
1653
  await _preCommand(state, this);
1565
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1654
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1566
1655
  if (foundObj && foundObj.element) {
1567
1656
  await this.scrollIfNeeded(foundObj.element, state.info);
1568
1657
  }
@@ -1604,7 +1693,79 @@ class StableBrowser {
1604
1693
  throw e;
1605
1694
  }
1606
1695
  finally {
1607
- _commandFinally(state, this);
1696
+ await _commandFinally(state, this);
1697
+ }
1698
+ }
1699
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1700
+ const timeout = this._getFindElementTimeout(options);
1701
+ const startTime = Date.now();
1702
+ const state = {
1703
+ _params,
1704
+ value: referanceSnapshot,
1705
+ options,
1706
+ world,
1707
+ locate: false,
1708
+ scroll: false,
1709
+ screenshot: true,
1710
+ highlight: false,
1711
+ type: Types.SNAPSHOT_VALIDATION,
1712
+ text: `verify snapshot: ${referanceSnapshot}`,
1713
+ operation: "snapshotValidation",
1714
+ log: "***** verify snapshot *****\n",
1715
+ };
1716
+ if (!referanceSnapshot) {
1717
+ throw new Error("referanceSnapshot is null");
1718
+ }
1719
+ let text = null;
1720
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1721
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1722
+ }
1723
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1724
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1725
+ }
1726
+ else if (referanceSnapshot.startsWith("yaml:")) {
1727
+ text = referanceSnapshot.substring(5);
1728
+ }
1729
+ else {
1730
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1731
+ }
1732
+ state.text = text;
1733
+ const newValue = await this._replaceWithLocalData(text, world);
1734
+ await _preCommand(state, this);
1735
+ let foundObj = null;
1736
+ try {
1737
+ let matchResult = null;
1738
+ while (Date.now() - startTime < timeout) {
1739
+ try {
1740
+ let scope = null;
1741
+ if (!frameSelectors) {
1742
+ scope = this.page;
1743
+ }
1744
+ else {
1745
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1746
+ }
1747
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1748
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1749
+ if (matchResult.errorLine !== -1) {
1750
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1751
+ }
1752
+ // highlight and screenshot
1753
+ return state.info;
1754
+ }
1755
+ catch (e) {
1756
+ // Log error but continue retrying until timeout is reached
1757
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1758
+ }
1759
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1760
+ }
1761
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1762
+ }
1763
+ catch (e) {
1764
+ await _commandError(state, e, this);
1765
+ throw e;
1766
+ }
1767
+ finally {
1768
+ await _commandFinally(state, this);
1608
1769
  }
1609
1770
  }
1610
1771
  async waitForUserInput(message, world = null) {
@@ -1642,6 +1803,15 @@ class StableBrowser {
1642
1803
  // save the data to the file
1643
1804
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1644
1805
  }
1806
+ overwriteTestData(testData, world = null) {
1807
+ if (!testData) {
1808
+ return;
1809
+ }
1810
+ // if data file exists, load it
1811
+ const dataFile = _getDataFile(world, this.context, this);
1812
+ // save the data to the file
1813
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1814
+ }
1645
1815
  _getDataFilePath(fileName) {
1646
1816
  let dataFile = path.join(this.project_path, "data", fileName);
1647
1817
  if (fs.existsSync(dataFile)) {
@@ -1894,7 +2064,7 @@ class StableBrowser {
1894
2064
  await _commandError(state, e, this);
1895
2065
  }
1896
2066
  finally {
1897
- _commandFinally(state, this);
2067
+ await _commandFinally(state, this);
1898
2068
  }
1899
2069
  }
1900
2070
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1925,10 +2095,31 @@ class StableBrowser {
1925
2095
  case "value":
1926
2096
  state.value = await state.element.inputValue();
1927
2097
  break;
2098
+ case "text":
2099
+ state.value = await state.element.textContent();
2100
+ break;
1928
2101
  default:
1929
2102
  state.value = await state.element.getAttribute(attribute);
1930
2103
  break;
1931
2104
  }
2105
+ if (options !== null) {
2106
+ if (options.regex && options.regex !== "") {
2107
+ // Construct a regex pattern from the provided string
2108
+ const regex = options.regex.slice(1, -1);
2109
+ const regexPattern = new RegExp(regex, "g");
2110
+ const matches = state.value.match(regexPattern);
2111
+ if (matches) {
2112
+ let newValue = "";
2113
+ for (const match of matches) {
2114
+ newValue += match;
2115
+ }
2116
+ state.value = newValue;
2117
+ }
2118
+ }
2119
+ if (options.trimSpaces && options.trimSpaces === true) {
2120
+ state.value = state.value.trim();
2121
+ }
2122
+ }
1932
2123
  state.info.value = state.value;
1933
2124
  this.setTestData({ [variable]: state.value }, world);
1934
2125
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1939,7 +2130,7 @@ class StableBrowser {
1939
2130
  await _commandError(state, e, this);
1940
2131
  }
1941
2132
  finally {
1942
- _commandFinally(state, this);
2133
+ await _commandFinally(state, this);
1943
2134
  }
1944
2135
  }
1945
2136
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1964,12 +2155,15 @@ class StableBrowser {
1964
2155
  let expectedValue;
1965
2156
  try {
1966
2157
  await _preCommand(state, this);
1967
- expectedValue = state.value;
2158
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1968
2159
  state.info.expectedValue = expectedValue;
1969
2160
  switch (attribute) {
1970
2161
  case "innerText":
1971
2162
  val = String(await state.element.innerText());
1972
2163
  break;
2164
+ case "text":
2165
+ val = String(await state.element.textContent());
2166
+ break;
1973
2167
  case "value":
1974
2168
  val = String(await state.element.inputValue());
1975
2169
  break;
@@ -1991,17 +2185,42 @@ class StableBrowser {
1991
2185
  let regex;
1992
2186
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1993
2187
  const patternBody = expectedValue.slice(1, -1);
1994
- regex = new RegExp(patternBody, "g");
2188
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2189
+ regex = new RegExp(processedPattern, "gs");
2190
+ state.info.regex = true;
1995
2191
  }
1996
2192
  else {
1997
2193
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1998
2194
  regex = new RegExp(escapedPattern, "g");
1999
2195
  }
2000
- if (!val.match(regex)) {
2001
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2002
- state.info.failCause.assertionFailed = true;
2003
- state.info.failCause.lastError = errorMessage;
2004
- throw new Error(errorMessage);
2196
+ if (attribute === "innerText") {
2197
+ if (state.info.regex) {
2198
+ if (!regex.test(val)) {
2199
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2200
+ state.info.failCause.assertionFailed = true;
2201
+ state.info.failCause.lastError = errorMessage;
2202
+ throw new Error(errorMessage);
2203
+ }
2204
+ }
2205
+ else {
2206
+ const valLines = val.split("\n");
2207
+ const expectedLines = expectedValue.split("\n");
2208
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2209
+ if (!isPart) {
2210
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2211
+ state.info.failCause.assertionFailed = true;
2212
+ state.info.failCause.lastError = errorMessage;
2213
+ throw new Error(errorMessage);
2214
+ }
2215
+ }
2216
+ }
2217
+ else {
2218
+ if (!val.match(regex)) {
2219
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2220
+ state.info.failCause.assertionFailed = true;
2221
+ state.info.failCause.lastError = errorMessage;
2222
+ throw new Error(errorMessage);
2223
+ }
2005
2224
  }
2006
2225
  return state.info;
2007
2226
  }
@@ -2009,7 +2228,7 @@ class StableBrowser {
2009
2228
  await _commandError(state, e, this);
2010
2229
  }
2011
2230
  finally {
2012
- _commandFinally(state, this);
2231
+ await _commandFinally(state, this);
2013
2232
  }
2014
2233
  }
2015
2234
  async extractEmailData(emailAddress, options, world) {
@@ -2169,54 +2388,6 @@ class StableBrowser {
2169
2388
  console.debug(error);
2170
2389
  }
2171
2390
  }
2172
- // async _unhighlightElements(scope, css) {
2173
- // try {
2174
- // if (!scope) {
2175
- // return;
2176
- // }
2177
- // if (!css) {
2178
- // scope
2179
- // .evaluate((node) => {
2180
- // if (node && node.style) {
2181
- // if (!node.__previousOutline) {
2182
- // node.style.outline = "";
2183
- // } else {
2184
- // node.style.outline = node.__previousOutline;
2185
- // }
2186
- // }
2187
- // })
2188
- // .then(() => {})
2189
- // .catch((e) => {
2190
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2191
- // });
2192
- // } else {
2193
- // scope
2194
- // .evaluate(([css]) => {
2195
- // if (!css) {
2196
- // return;
2197
- // }
2198
- // let elements = Array.from(document.querySelectorAll(css));
2199
- // for (i = 0; i < elements.length; i++) {
2200
- // let element = elements[i];
2201
- // if (!element.style) {
2202
- // return;
2203
- // }
2204
- // if (!element.__previousOutline) {
2205
- // element.style.outline = "";
2206
- // } else {
2207
- // element.style.outline = element.__previousOutline;
2208
- // }
2209
- // }
2210
- // })
2211
- // .then(() => {})
2212
- // .catch((e) => {
2213
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2214
- // });
2215
- // }
2216
- // } catch (error) {
2217
- // // console.debug(error);
2218
- // }
2219
- // }
2220
2391
  async verifyPagePath(pathPart, options = {}, world = null) {
2221
2392
  const startTime = Date.now();
2222
2393
  let error = null;
@@ -2254,7 +2425,7 @@ class StableBrowser {
2254
2425
  Object.assign(e, { info: info });
2255
2426
  error = e;
2256
2427
  // throw e;
2257
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2428
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
2258
2429
  }
2259
2430
  finally {
2260
2431
  const endTime = Date.now();
@@ -2279,27 +2450,89 @@ class StableBrowser {
2279
2450
  });
2280
2451
  }
2281
2452
  }
2282
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2453
+ async verifyPageTitle(title, options = {}, world = null) {
2454
+ const startTime = Date.now();
2455
+ let error = null;
2456
+ let screenshotId = null;
2457
+ let screenshotPath = null;
2458
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2459
+ const info = {};
2460
+ info.log = "***** verify page title " + title + " *****\n";
2461
+ info.operation = "verifyPageTitle";
2462
+ const newValue = await this._replaceWithLocalData(title, world);
2463
+ if (newValue !== title) {
2464
+ this.logger.info(title + "=" + newValue);
2465
+ title = newValue;
2466
+ }
2467
+ info.title = title;
2468
+ try {
2469
+ for (let i = 0; i < 30; i++) {
2470
+ const foundTitle = await this.page.title();
2471
+ if (!foundTitle.includes(title)) {
2472
+ if (i === 29) {
2473
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2474
+ }
2475
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2476
+ continue;
2477
+ }
2478
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2479
+ return info;
2480
+ }
2481
+ }
2482
+ catch (e) {
2483
+ //await this.closeUnexpectedPopups();
2484
+ this.logger.error("verify page title failed " + info.log);
2485
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2486
+ info.screenshotPath = screenshotPath;
2487
+ Object.assign(e, { info: info });
2488
+ error = e;
2489
+ // throw e;
2490
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2491
+ }
2492
+ finally {
2493
+ const endTime = Date.now();
2494
+ _reportToWorld(world, {
2495
+ type: Types.VERIFY_PAGE_PATH,
2496
+ text: "Verify page title",
2497
+ _text: "Verify the page title contains " + title,
2498
+ screenshotId,
2499
+ result: error
2500
+ ? {
2501
+ status: "FAILED",
2502
+ startTime,
2503
+ endTime,
2504
+ message: error?.message,
2505
+ }
2506
+ : {
2507
+ status: "PASSED",
2508
+ startTime,
2509
+ endTime,
2510
+ },
2511
+ info: info,
2512
+ });
2513
+ }
2514
+ }
2515
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2283
2516
  const frames = this.page.frames();
2284
2517
  let results = [];
2285
- let ignoreCase = false;
2518
+ // let ignoreCase = false;
2286
2519
  for (let i = 0; i < frames.length; i++) {
2287
2520
  if (dateAlternatives.date) {
2288
2521
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2289
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2522
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2290
2523
  result.frame = frames[i];
2291
2524
  results.push(result);
2292
2525
  }
2293
2526
  }
2294
2527
  else if (numberAlternatives.number) {
2295
2528
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2296
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2529
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2297
2530
  result.frame = frames[i];
2298
2531
  results.push(result);
2299
2532
  }
2300
2533
  }
2301
2534
  else {
2302
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2535
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2303
2536
  result.frame = frames[i];
2304
2537
  results.push(result);
2305
2538
  }
@@ -2318,13 +2551,13 @@ class StableBrowser {
2318
2551
  scroll: false,
2319
2552
  highlight: false,
2320
2553
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2321
- text: `Verify text exists in page`,
2554
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2322
2555
  _text: `Verify the text '${text}' exists in page`,
2323
2556
  operation: "verifyTextExistInPage",
2324
2557
  log: "***** verify text " + text + " exists in page *****\n",
2325
2558
  };
2326
2559
  if (testForRegex(text)) {
2327
- text = text.replace(/"/g, '\\"');
2560
+ text = text.replace(/\\"/g, '"');
2328
2561
  }
2329
2562
  const timeout = this._getFindElementTimeout(options);
2330
2563
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -2396,7 +2629,7 @@ class StableBrowser {
2396
2629
  await _commandError(state, e, this);
2397
2630
  }
2398
2631
  finally {
2399
- _commandFinally(state, this);
2632
+ await _commandFinally(state, this);
2400
2633
  }
2401
2634
  }
2402
2635
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2409,11 +2642,14 @@ class StableBrowser {
2409
2642
  scroll: false,
2410
2643
  highlight: false,
2411
2644
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2412
- text: `Verify text does not exist in page`,
2645
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2413
2646
  _text: `Verify the text '${text}' does not exist in page`,
2414
2647
  operation: "verifyTextNotExistInPage",
2415
2648
  log: "***** verify text " + text + " does not exist in page *****\n",
2416
2649
  };
2650
+ if (testForRegex(text)) {
2651
+ text = text.replace(/\\"/g, '"');
2652
+ }
2417
2653
  const timeout = this._getFindElementTimeout(options);
2418
2654
  await new Promise((resolve) => setTimeout(resolve, 2000));
2419
2655
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2450,7 +2686,7 @@ class StableBrowser {
2450
2686
  await _commandError(state, e, this);
2451
2687
  }
2452
2688
  finally {
2453
- _commandFinally(state, this);
2689
+ await _commandFinally(state, this);
2454
2690
  }
2455
2691
  }
2456
2692
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2492,7 +2728,7 @@ class StableBrowser {
2492
2728
  };
2493
2729
  while (true) {
2494
2730
  try {
2495
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2731
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2496
2732
  }
2497
2733
  catch (error) {
2498
2734
  // ignore
@@ -2520,7 +2756,7 @@ class StableBrowser {
2520
2756
  const count = await frame.locator(css).count();
2521
2757
  for (let j = 0; j < count; j++) {
2522
2758
  const continer = await frame.locator(css).nth(j);
2523
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2759
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2524
2760
  if (result.elementCount > 0) {
2525
2761
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2526
2762
  await this._highlightElements(frame, dataAttribute);
@@ -2561,8 +2797,32 @@ class StableBrowser {
2561
2797
  await _commandError(state, e, this);
2562
2798
  }
2563
2799
  finally {
2564
- _commandFinally(state, this);
2800
+ await _commandFinally(state, this);
2801
+ }
2802
+ }
2803
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2804
+ const frames = this.page.frames();
2805
+ let results = [];
2806
+ let ignoreCase = false;
2807
+ for (let i = 0; i < frames.length; i++) {
2808
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2809
+ result.frame = frames[i];
2810
+ const climbArray = [];
2811
+ for (let i = 0; i < climb; i++) {
2812
+ climbArray.push("..");
2813
+ }
2814
+ let climbXpath = "xpath=" + climbArray.join("/");
2815
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2816
+ const count = await frames[i].locator(newLocator).count();
2817
+ if (count > 0) {
2818
+ result.elementCount = count;
2819
+ result.locator = newLocator;
2820
+ results.push(result);
2821
+ }
2565
2822
  }
2823
+ // state.info.results = results;
2824
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2825
+ return resultWithElementsFound;
2566
2826
  }
2567
2827
  async visualVerification(text, options = {}, world = null) {
2568
2828
  const startTime = Date.now();
@@ -2880,7 +3140,13 @@ class StableBrowser {
2880
3140
  }
2881
3141
  }
2882
3142
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2883
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3143
+ try {
3144
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3145
+ }
3146
+ catch (error) {
3147
+ this.logger.debug(error);
3148
+ throw error;
3149
+ }
2884
3150
  }
2885
3151
  _getLoadTimeout(options) {
2886
3152
  let timeout = 15000;
@@ -2903,6 +3169,7 @@ class StableBrowser {
2903
3169
  }
2904
3170
  async saveStoreState(path = null, world = null) {
2905
3171
  const storageState = await this.page.context().storageState();
3172
+ path = await this._replaceWithLocalData(path, this.world);
2906
3173
  //const testDataFile = _getDataFile(world, this.context, this);
2907
3174
  if (path) {
2908
3175
  // save { storageState: storageState } into the path
@@ -2913,10 +3180,14 @@ class StableBrowser {
2913
3180
  }
2914
3181
  }
2915
3182
  async restoreSaveState(path = null, world = null) {
3183
+ path = await this._replaceWithLocalData(path, this.world);
2916
3184
  await refreshBrowser(this, path, world);
2917
3185
  this.registerEventListeners(this.context);
2918
3186
  registerNetworkEvents(this.world, this, this.context, this.page);
2919
3187
  registerDownloadEvent(this.page, this.world, this.context);
3188
+ if (this.onRestoreSaveState) {
3189
+ this.onRestoreSaveState(path);
3190
+ }
2920
3191
  }
2921
3192
  async waitForPageLoad(options = {}, world = null) {
2922
3193
  let timeout = this._getLoadTimeout(options);
@@ -2999,17 +3270,17 @@ class StableBrowser {
2999
3270
  await _commandError(state, e, this);
3000
3271
  }
3001
3272
  finally {
3002
- _commandFinally(state, this);
3273
+ await _commandFinally(state, this);
3003
3274
  }
3004
3275
  }
3005
- async tableCellOperation(headerText, rowText, options, world = null) {
3276
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3006
3277
  let operation = null;
3007
3278
  if (!options || !options.operation) {
3008
3279
  throw new Error("operation is not defined");
3009
3280
  }
3010
3281
  operation = options.operation;
3011
3282
  // validate operation is one of the supported operations
3012
- if (operation != "click") {
3283
+ if (operation != "click" && operation != "hover+click") {
3013
3284
  throw new Error("operation is not supported");
3014
3285
  }
3015
3286
  const state = {
@@ -3059,13 +3330,24 @@ class StableBrowser {
3059
3330
  await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3060
3331
  }
3061
3332
  else {
3062
- const results = await findElementsInArea(options.css, cellArea, this.page, this, options);
3333
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3063
3334
  if (results.length === 0) {
3064
3335
  throw new Error(`Element not found in cell area`);
3065
3336
  }
3066
3337
  state.element = results[0];
3067
- await results[0].click();
3338
+ await performAction("click", state.element, options, this, state, _params);
3339
+ }
3340
+ break;
3341
+ case "hover+click":
3342
+ if (!options.css) {
3343
+ throw new Error("css is not defined");
3068
3344
  }
3345
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3346
+ if (results.length === 0) {
3347
+ throw new Error(`Element not found in cell area`);
3348
+ }
3349
+ state.element = results[0];
3350
+ await performAction("hover+click", state.element, options, this, state, _params);
3069
3351
  break;
3070
3352
  default:
3071
3353
  throw new Error("operation is not supported");
@@ -3075,7 +3357,7 @@ class StableBrowser {
3075
3357
  await _commandError(state, e, this);
3076
3358
  }
3077
3359
  finally {
3078
- _commandFinally(state, this);
3360
+ await _commandFinally(state, this);
3079
3361
  }
3080
3362
  }
3081
3363
  saveTestDataAsGlobal(options, world) {
@@ -3180,7 +3462,39 @@ class StableBrowser {
3180
3462
  console.log("#-#");
3181
3463
  }
3182
3464
  }
3465
+ async beforeScenario(world, scenario) {
3466
+ this.beforeScenarioCalled = true;
3467
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3468
+ this.scenarioName = scenario.pickle.name;
3469
+ }
3470
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3471
+ this.featureName = scenario.gherkinDocument.feature.name;
3472
+ }
3473
+ if (this.context) {
3474
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3475
+ }
3476
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3477
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3478
+ // check if @global_test_data tag is present
3479
+ if (this.tags.includes("@global_test_data")) {
3480
+ this.saveTestDataAsGlobal({}, world);
3481
+ }
3482
+ }
3483
+ // update test data based on feature/scenario
3484
+ let envName = null;
3485
+ if (this.context && this.context.environment) {
3486
+ envName = this.context.environment.name;
3487
+ }
3488
+ if (!process.env.TEMP_RUN) {
3489
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3490
+ }
3491
+ await loadBrunoParams(this.context, this.context.environment.name);
3492
+ }
3493
+ async afterScenario(world, scenario) { }
3183
3494
  async beforeStep(world, step) {
3495
+ if (!this.beforeScenarioCalled) {
3496
+ this.beforeScenario(world, step);
3497
+ }
3184
3498
  if (this.stepIndex === undefined) {
3185
3499
  this.stepIndex = 0;
3186
3500
  }
@@ -3197,21 +3511,11 @@ class StableBrowser {
3197
3511
  else {
3198
3512
  this.stepName = "step " + this.stepIndex;
3199
3513
  }
3200
- if (this.context) {
3201
- this.context.examplesRow = extractStepExampleParameters(step);
3202
- }
3203
3514
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3204
3515
  if (this.context.browserObject.context) {
3205
3516
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3206
3517
  }
3207
3518
  }
3208
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3209
- this.tags = step.pickle.tags.map((tag) => tag.name);
3210
- // check if @global_test_data tag is present
3211
- if (this.tags.includes("@global_test_data")) {
3212
- this.saveTestDataAsGlobal({}, world);
3213
- }
3214
- }
3215
3519
  if (this.initSnapshotTaken === false) {
3216
3520
  this.initSnapshotTaken = true;
3217
3521
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3236,18 +3540,68 @@ class StableBrowser {
3236
3540
  const content = [`- path: ${path}`, `- title: ${title}`];
3237
3541
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3238
3542
  for (let i = 0; i < frames.length; i++) {
3239
- content.push(`- frame: ${i}`);
3240
3543
  const frame = frames[i];
3241
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3242
- content.push(snapshot);
3544
+ try {
3545
+ // Ensure frame is attached and has body
3546
+ const body = frame.locator("body");
3547
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3548
+ const snapshot = await body.ariaSnapshot({ timeout });
3549
+ content.push(`- frame: ${i}`);
3550
+ content.push(snapshot);
3551
+ }
3552
+ catch (innerErr) { }
3243
3553
  }
3244
3554
  return content.join("\n");
3245
3555
  }
3246
3556
  catch (e) {
3247
- console.error(e);
3557
+ console.log("Error in getAriaSnapshot");
3558
+ //console.debug(e);
3248
3559
  }
3249
3560
  return null;
3250
3561
  }
3562
+ /**
3563
+ * Sends command with custom payload to report.
3564
+ * @param commandText - Title of the command to be shown in the report.
3565
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3566
+ * @param content - Content of the command to be shown in the report.
3567
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3568
+ * @param world - Optional world context.
3569
+ * @public
3570
+ */
3571
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3572
+ const state = {
3573
+ options,
3574
+ world,
3575
+ locate: false,
3576
+ scroll: false,
3577
+ screenshot: options.screenshot ?? false,
3578
+ highlight: options.highlight ?? false,
3579
+ type: Types.REPORT_COMMAND,
3580
+ text: commandText,
3581
+ _text: commandText,
3582
+ operation: "report_command",
3583
+ log: "***** " + commandText + " *****\n",
3584
+ };
3585
+ try {
3586
+ await _preCommand(state, this);
3587
+ const payload = {
3588
+ type: options.type ?? "text",
3589
+ content: content,
3590
+ screenshotId: null,
3591
+ };
3592
+ state.payload = payload;
3593
+ if (commandStatus === "FAILED") {
3594
+ state.throwError = true;
3595
+ throw new Error("Command failed");
3596
+ }
3597
+ }
3598
+ catch (e) {
3599
+ await _commandError(state, e, this);
3600
+ }
3601
+ finally {
3602
+ await _commandFinally(state, this);
3603
+ }
3604
+ }
3251
3605
  async afterStep(world, step) {
3252
3606
  this.stepName = null;
3253
3607
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3255,6 +3609,13 @@ class StableBrowser {
3255
3609
  await this.context.browserObject.context.tracing.stopChunk({
3256
3610
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3257
3611
  });
3612
+ if (world && world.attach) {
3613
+ await world.attach(JSON.stringify({
3614
+ type: "trace",
3615
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3616
+ }), "application/json+trace");
3617
+ }
3618
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3258
3619
  }
3259
3620
  }
3260
3621
  if (this.context) {
@@ -3267,6 +3628,27 @@ class StableBrowser {
3267
3628
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3268
3629
  }
3269
3630
  }
3631
+ const state = {
3632
+ world,
3633
+ locate: false,
3634
+ scroll: false,
3635
+ screenshot: true,
3636
+ highlight: true,
3637
+ type: Types.STEP_COMPLETE,
3638
+ text: "end of scenario",
3639
+ _text: "end of scenario",
3640
+ operation: "step_complete",
3641
+ log: "***** " + "end of scenario" + " *****\n",
3642
+ };
3643
+ try {
3644
+ await _preCommand(state, this);
3645
+ }
3646
+ catch (e) {
3647
+ await _commandError(state, e, this);
3648
+ }
3649
+ finally {
3650
+ await _commandFinally(state, this);
3651
+ }
3270
3652
  }
3271
3653
  }
3272
3654
  function createTimedPromise(promise, label) {