automation_model 1.0.631-dev → 1.0.631-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 -23
  45. package/lib/stable_browser.js +611 -167
  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 +19 -0
  50. package/lib/table_helper.js +116 -0
  51. package/lib/table_helper.js.map +1 -0
  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 +8 -4
  56. package/lib/utils.js +221 -20
  57. package/lib/utils.js.map +1 -1
  58. package/package.json +11 -6
@@ -10,33 +10,37 @@ 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, } 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";
21
22
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
23
  import { LocatorLog } from "./locator_log.js";
23
24
  import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { snapshotValidation } from "./snapshot_validation.js";
27
+ import { loadBrunoParams } from "./bruno.js";
24
28
  export const Types = {
25
29
  CLICK: "click_element",
26
30
  WAIT_ELEMENT: "wait_element",
27
- NAVIGATE: "navigate",
31
+ NAVIGATE: "navigate", ///
28
32
  FILL: "fill_element",
29
- EXECUTE: "execute_page_method",
30
- OPEN: "open_environment",
33
+ EXECUTE: "execute_page_method", //
34
+ OPEN: "open_environment", //
31
35
  COMPLETE: "step_complete",
32
36
  ASK: "information_needed",
33
- GET_PAGE_STATUS: "get_page_status",
34
- CLICK_ROW_ACTION: "click_row_action",
37
+ GET_PAGE_STATUS: "get_page_status", ///
38
+ CLICK_ROW_ACTION: "click_row_action", //
35
39
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
36
40
  VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
37
41
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
38
42
  ANALYZE_TABLE: "analyze_table",
39
- SELECT: "select_combobox",
43
+ SELECT: "select_combobox", //
40
44
  VERIFY_PAGE_PATH: "verify_page_path",
41
45
  TYPE_PRESS: "type_press",
42
46
  PRESS: "press_key",
@@ -45,6 +49,7 @@ export const Types = {
45
49
  UNCHECK: "uncheck_element",
46
50
  EXTRACT: "extract_attribute",
47
51
  CLOSE_PAGE: "close_page",
52
+ TABLE_OPERATION: "table_operation",
48
53
  SET_DATE_TIME: "set_date_time",
49
54
  SET_VIEWPORT: "set_viewport",
50
55
  VERIFY_VISUAL: "verify_visual",
@@ -53,6 +58,11 @@ export const Types = {
53
58
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
54
59
  VERIFY_ATTRIBUTE: "verify_element_attribute",
55
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",
56
66
  };
57
67
  export const apps = {};
58
68
  const formatElementName = (elementName) => {
@@ -178,6 +188,30 @@ class StableBrowser {
178
188
  await this.waitForPageLoad();
179
189
  }
180
190
  }
191
+ async switchTab(tabTitleOrIndex) {
192
+ // first check if the tabNameOrIndex is a number
193
+ let index = parseInt(tabTitleOrIndex);
194
+ if (!isNaN(index)) {
195
+ if (index >= 0 && index < this.context.pages.length) {
196
+ this.page = this.context.pages[index];
197
+ this.context.page = this.page;
198
+ await this.page.bringToFront();
199
+ return;
200
+ }
201
+ }
202
+ // if the tabNameOrIndex is a string, find the tab by name
203
+ for (let i = 0; i < this.context.pages.length; i++) {
204
+ let page = this.context.pages[i];
205
+ let title = await page.title();
206
+ if (title.includes(tabTitleOrIndex)) {
207
+ this.page = page;
208
+ this.context.page = this.page;
209
+ await this.page.bringToFront();
210
+ return;
211
+ }
212
+ }
213
+ throw new Error("Tab not found: " + tabTitleOrIndex);
214
+ }
181
215
  registerConsoleLogListener(page, context) {
182
216
  if (!this.context.webLogger) {
183
217
  this.context.webLogger = [];
@@ -273,7 +307,7 @@ class StableBrowser {
273
307
  _commandError(state, error, this);
274
308
  }
275
309
  finally {
276
- _commandFinally(state, this);
310
+ await _commandFinally(state, this);
277
311
  }
278
312
  }
279
313
  async _getLocator(locator, scope, _params) {
@@ -374,6 +408,12 @@ class StableBrowser {
374
408
  if (!el.setAttribute) {
375
409
  el = el.parentElement;
376
410
  }
411
+ // remove any attributes start with data-blinq-id
412
+ // for (let i = 0; i < el.attributes.length; i++) {
413
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
414
+ // el.removeAttribute(el.attributes[i].name);
415
+ // }
416
+ // }
377
417
  el.setAttribute("data-blinq-id-" + randomToken, "");
378
418
  return true;
379
419
  }, [tag1, randomToken]))) {
@@ -383,7 +423,7 @@ class StableBrowser {
383
423
  }
384
424
  return { elementCount: tagCount, randomToken };
385
425
  }
386
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
426
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
387
427
  if (!info) {
388
428
  info = {};
389
429
  }
@@ -450,7 +490,7 @@ class StableBrowser {
450
490
  }
451
491
  return;
452
492
  }
453
- if (info.locatorLog && count === 0) {
493
+ if (info.locatorLog && count === 0 && logErrors) {
454
494
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
455
495
  }
456
496
  for (let j = 0; j < count; j++) {
@@ -465,7 +505,7 @@ class StableBrowser {
465
505
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
466
506
  }
467
507
  }
468
- else {
508
+ else if (logErrors) {
469
509
  info.failCause.visible = visible;
470
510
  info.failCause.enabled = enabled;
471
511
  if (!info.printMessages) {
@@ -560,12 +600,24 @@ class StableBrowser {
560
600
  element.evaluate((el, randomToken) => {
561
601
  el.setAttribute("data-blinq-id-" + randomToken, "");
562
602
  }, randomToken);
563
- if (element._frame) {
564
- return element;
565
- }
566
- const scope = element.page();
567
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
568
- return newSelector;
603
+ // if (element._frame) {
604
+ // return element;
605
+ // }
606
+ const scope = element._frame ?? element.page();
607
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
608
+ let prefixSelector = "";
609
+ const frameControlSelector = " >> internal:control=enter-frame";
610
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
611
+ if (frameSelectorIndex !== -1) {
612
+ // remove everything after the >> internal:control=enter-frame
613
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
614
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
615
+ }
616
+ // if (element?._frame?._selector) {
617
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
618
+ // }
619
+ const newSelector = prefixSelector + newElementSelector;
620
+ return scope.locator(newSelector);
569
621
  }
570
622
  }
571
623
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -717,14 +769,9 @@ class StableBrowser {
717
769
  // info.log += "scanning locators in priority 2" + "\n";
718
770
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
719
771
  }
720
- if (result.foundElements.length === 0 && onlyPriority3) {
772
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
721
773
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
722
774
  }
723
- else {
724
- if (result.foundElements.length === 0 && !highPriorityOnly) {
725
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
726
- }
727
- }
728
775
  let foundElements = result.foundElements;
729
776
  if (foundElements.length === 1 && foundElements[0].unique) {
730
777
  info.box = foundElements[0].box;
@@ -779,6 +826,11 @@ class StableBrowser {
779
826
  visibleOnly = false;
780
827
  }
781
828
  await new Promise((resolve) => setTimeout(resolve, 1000));
829
+ // sheck of more of half of the timeout has passed
830
+ if (Date.now() - startTime > timeout / 2) {
831
+ highPriorityOnly = false;
832
+ visibleOnly = false;
833
+ }
782
834
  }
783
835
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
784
836
  // if (info.locatorLog) {
@@ -794,7 +846,7 @@ class StableBrowser {
794
846
  }
795
847
  throw new Error("failed to locate first element no elements found, " + info.log);
796
848
  }
797
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
849
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
798
850
  let foundElements = [];
799
851
  const result = {
800
852
  foundElements: foundElements,
@@ -813,7 +865,9 @@ class StableBrowser {
813
865
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
814
866
  }
815
867
  catch (e) {
816
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
868
+ if (logErrors) {
869
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
870
+ }
817
871
  }
818
872
  }
819
873
  if (foundLocators.length === 1) {
@@ -825,9 +879,40 @@ class StableBrowser {
825
879
  result.locatorIndex = i;
826
880
  }
827
881
  if (foundLocators.length > 1) {
828
- info.failCause.foundMultiple = true;
829
- if (info.locatorLog) {
830
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
882
+ // remove elements that consume the same space with 10 pixels tolerance
883
+ const boxes = [];
884
+ for (let j = 0; j < foundLocators.length; j++) {
885
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
886
+ }
887
+ for (let j = 0; j < boxes.length; j++) {
888
+ for (let k = 0; k < boxes.length; k++) {
889
+ if (j === k) {
890
+ continue;
891
+ }
892
+ // check if x, y, width, height are the same with 10 pixels tolerance
893
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
894
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
895
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
896
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
897
+ // as the element is not unique, will remove it
898
+ boxes.splice(k, 1);
899
+ k--;
900
+ }
901
+ }
902
+ }
903
+ if (boxes.length === 1) {
904
+ result.foundElements.push({
905
+ locator: boxes[0].locator.first(),
906
+ box: boxes[0].box,
907
+ unique: true,
908
+ });
909
+ result.locatorIndex = i;
910
+ }
911
+ else if (logErrors) {
912
+ info.failCause.foundMultiple = true;
913
+ if (info.locatorLog) {
914
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
915
+ }
831
916
  }
832
917
  }
833
918
  }
@@ -875,7 +960,7 @@ class StableBrowser {
875
960
  await _commandError(state, "timeout looking for " + elementDescription, this);
876
961
  }
877
962
  finally {
878
- _commandFinally(state, this);
963
+ await _commandFinally(state, this);
879
964
  }
880
965
  }
881
966
  }
@@ -924,7 +1009,7 @@ class StableBrowser {
924
1009
  await _commandError(state, "timeout looking for " + elementDescription, this);
925
1010
  }
926
1011
  finally {
927
- _commandFinally(state, this);
1012
+ await _commandFinally(state, this);
928
1013
  }
929
1014
  }
930
1015
  }
@@ -945,19 +1030,7 @@ class StableBrowser {
945
1030
  };
946
1031
  try {
947
1032
  await _preCommand(state, this);
948
- // if (state.options && state.options.context) {
949
- // state.selectors.locators[0].text = state.options.context;
950
- // }
951
- try {
952
- await state.element.click();
953
- // await new Promise((resolve) => setTimeout(resolve, 1000));
954
- }
955
- catch (e) {
956
- // await this.closeUnexpectedPopups();
957
- state.element = await this._locate(selectors, state.info, _params);
958
- await state.element.dispatchEvent("click");
959
- // await new Promise((resolve) => setTimeout(resolve, 1000));
960
- }
1033
+ await performAction("click", state.element, options, this, state, _params);
961
1034
  await this.waitForPageLoad();
962
1035
  return state.info;
963
1036
  }
@@ -965,7 +1038,7 @@ class StableBrowser {
965
1038
  await _commandError(state, e, this);
966
1039
  }
967
1040
  finally {
968
- _commandFinally(state, this);
1041
+ await _commandFinally(state, this);
969
1042
  }
970
1043
  }
971
1044
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -996,7 +1069,7 @@ class StableBrowser {
996
1069
  // await _commandError(state, e, this);
997
1070
  }
998
1071
  finally {
999
- _commandFinally(state, this);
1072
+ await _commandFinally(state, this);
1000
1073
  }
1001
1074
  return found;
1002
1075
  }
@@ -1020,7 +1093,7 @@ class StableBrowser {
1020
1093
  try {
1021
1094
  // if (world && world.screenshot && !world.screenshotPath) {
1022
1095
  // console.log(`Highlighting while running from recorder`);
1023
- await this._highlightElements(element);
1096
+ await this._highlightElements(state.element);
1024
1097
  await state.element.setChecked(checked);
1025
1098
  await new Promise((resolve) => setTimeout(resolve, 1000));
1026
1099
  // await this._unHighlightElements(element);
@@ -1047,7 +1120,7 @@ class StableBrowser {
1047
1120
  await _commandError(state, e, this);
1048
1121
  }
1049
1122
  finally {
1050
- _commandFinally(state, this);
1123
+ await _commandFinally(state, this);
1051
1124
  }
1052
1125
  }
1053
1126
  async hover(selectors, _params, options = {}, world = null) {
@@ -1064,19 +1137,7 @@ class StableBrowser {
1064
1137
  };
1065
1138
  try {
1066
1139
  await _preCommand(state, this);
1067
- try {
1068
- await state.element.hover();
1069
- // await _screenshot(state, this);
1070
- await new Promise((resolve) => setTimeout(resolve, 1000));
1071
- }
1072
- catch (e) {
1073
- //await this.closeUnexpectedPopups();
1074
- state.info.log += "hover failed, will try again" + "\n";
1075
- state.element = await this._locate(selectors, state.info, _params);
1076
- await state.element.hover({ timeout: 10000 });
1077
- // await _screenshot(state, this);
1078
- await new Promise((resolve) => setTimeout(resolve, 1000));
1079
- }
1140
+ await performAction("hover", state.element, options, this, state, _params);
1080
1141
  await _screenshot(state, this);
1081
1142
  await this.waitForPageLoad();
1082
1143
  return state.info;
@@ -1085,7 +1146,7 @@ class StableBrowser {
1085
1146
  await _commandError(state, e, this);
1086
1147
  }
1087
1148
  finally {
1088
- _commandFinally(state, this);
1149
+ await _commandFinally(state, this);
1089
1150
  }
1090
1151
  }
1091
1152
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1121,7 +1182,7 @@ class StableBrowser {
1121
1182
  await _commandError(state, e, this);
1122
1183
  }
1123
1184
  finally {
1124
- _commandFinally(state, this);
1185
+ await _commandFinally(state, this);
1125
1186
  }
1126
1187
  }
1127
1188
  async type(_value, _params = null, options = {}, world = null) {
@@ -1167,7 +1228,7 @@ class StableBrowser {
1167
1228
  await _commandError(state, e, this);
1168
1229
  }
1169
1230
  finally {
1170
- _commandFinally(state, this);
1231
+ await _commandFinally(state, this);
1171
1232
  }
1172
1233
  }
1173
1234
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1203,7 +1264,7 @@ class StableBrowser {
1203
1264
  await _commandError(state, e, this);
1204
1265
  }
1205
1266
  finally {
1206
- _commandFinally(state, this);
1267
+ await _commandFinally(state, this);
1207
1268
  }
1208
1269
  }
1209
1270
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1223,7 +1284,7 @@ class StableBrowser {
1223
1284
  try {
1224
1285
  await _preCommand(state, this);
1225
1286
  try {
1226
- await state.element.click();
1287
+ await performAction("click", state.element, options, this, state, _params);
1227
1288
  await new Promise((resolve) => setTimeout(resolve, 500));
1228
1289
  if (format) {
1229
1290
  state.value = dayjs(state.value).format(format);
@@ -1272,7 +1333,7 @@ class StableBrowser {
1272
1333
  await _commandError(state, e, this);
1273
1334
  }
1274
1335
  finally {
1275
- _commandFinally(state, this);
1336
+ await _commandFinally(state, this);
1276
1337
  }
1277
1338
  }
1278
1339
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1291,6 +1352,9 @@ class StableBrowser {
1291
1352
  operation: "clickType",
1292
1353
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1293
1354
  };
1355
+ if (!options) {
1356
+ options = {};
1357
+ }
1294
1358
  if (newValue !== _value) {
1295
1359
  //this.logger.info(_value + "=" + newValue);
1296
1360
  _value = newValue;
@@ -1298,7 +1362,7 @@ class StableBrowser {
1298
1362
  try {
1299
1363
  await _preCommand(state, this);
1300
1364
  state.info.value = _value;
1301
- if (options === null || options === undefined || !options.press) {
1365
+ if (!options.press) {
1302
1366
  try {
1303
1367
  let currentValue = await state.element.inputValue();
1304
1368
  if (currentValue) {
@@ -1309,13 +1373,9 @@ class StableBrowser {
1309
1373
  this.logger.info("unable to clear input value");
1310
1374
  }
1311
1375
  }
1312
- if (options === null || options === undefined || options.press) {
1313
- try {
1314
- await state.element.click({ timeout: 5000 });
1315
- }
1316
- catch (e) {
1317
- await state.element.dispatchEvent("click");
1318
- }
1376
+ if (options.press) {
1377
+ options.timeout = 5000;
1378
+ await performAction("click", state.element, options, this, state, _params);
1319
1379
  }
1320
1380
  else {
1321
1381
  try {
@@ -1373,7 +1433,7 @@ class StableBrowser {
1373
1433
  await _commandError(state, e, this);
1374
1434
  }
1375
1435
  finally {
1376
- _commandFinally(state, this);
1436
+ await _commandFinally(state, this);
1377
1437
  }
1378
1438
  }
1379
1439
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1403,7 +1463,42 @@ class StableBrowser {
1403
1463
  await _commandError(state, e, this);
1404
1464
  }
1405
1465
  finally {
1406
- _commandFinally(state, this);
1466
+ await _commandFinally(state, this);
1467
+ }
1468
+ }
1469
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1470
+ const state = {
1471
+ selectors,
1472
+ _params,
1473
+ files,
1474
+ value: '"' + files.join('", "') + '"',
1475
+ options,
1476
+ world,
1477
+ type: Types.SET_INPUT_FILES,
1478
+ text: `Set input files`,
1479
+ _text: `Set input files on ${selectors.element_name}`,
1480
+ operation: "setInputFiles",
1481
+ log: "***** set input files " + selectors.element_name + " *****\n",
1482
+ };
1483
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1484
+ try {
1485
+ await _preCommand(state, this);
1486
+ for (let i = 0; i < files.length; i++) {
1487
+ const file = files[i];
1488
+ const filePath = path.join(uploadsFolder, file);
1489
+ if (!fs.existsSync(filePath)) {
1490
+ throw new Error(`File not found: ${filePath}`);
1491
+ }
1492
+ state.files[i] = filePath;
1493
+ }
1494
+ await state.element.setInputFiles(files);
1495
+ return state.info;
1496
+ }
1497
+ catch (e) {
1498
+ await _commandError(state, e, this);
1499
+ }
1500
+ finally {
1501
+ await _commandFinally(state, this);
1407
1502
  }
1408
1503
  }
1409
1504
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1519,7 +1614,7 @@ class StableBrowser {
1519
1614
  await _commandError(state, e, this);
1520
1615
  }
1521
1616
  finally {
1522
- _commandFinally(state, this);
1617
+ await _commandFinally(state, this);
1523
1618
  }
1524
1619
  }
1525
1620
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1554,7 +1649,7 @@ class StableBrowser {
1554
1649
  while (Date.now() - startTime < timeout) {
1555
1650
  try {
1556
1651
  await _preCommand(state, this);
1557
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1652
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1558
1653
  if (foundObj && foundObj.element) {
1559
1654
  await this.scrollIfNeeded(foundObj.element, state.info);
1560
1655
  }
@@ -1596,7 +1691,79 @@ class StableBrowser {
1596
1691
  throw e;
1597
1692
  }
1598
1693
  finally {
1599
- _commandFinally(state, this);
1694
+ await _commandFinally(state, this);
1695
+ }
1696
+ }
1697
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1698
+ const timeout = this._getFindElementTimeout(options);
1699
+ const startTime = Date.now();
1700
+ const state = {
1701
+ _params,
1702
+ value: referanceSnapshot,
1703
+ options,
1704
+ world,
1705
+ locate: false,
1706
+ scroll: false,
1707
+ screenshot: true,
1708
+ highlight: false,
1709
+ type: Types.SNAPSHOT_VALIDATION,
1710
+ text: `verify snapshot: ${referanceSnapshot}`,
1711
+ operation: "snapshotValidation",
1712
+ log: "***** verify snapshot *****\n",
1713
+ };
1714
+ if (!referanceSnapshot) {
1715
+ throw new Error("referanceSnapshot is null");
1716
+ }
1717
+ let text = null;
1718
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1719
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1720
+ }
1721
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1722
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1723
+ }
1724
+ else if (referanceSnapshot.startsWith("yaml:")) {
1725
+ text = referanceSnapshot.substring(5);
1726
+ }
1727
+ else {
1728
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1729
+ }
1730
+ state.text = text;
1731
+ const newValue = await this._replaceWithLocalData(text, world);
1732
+ await _preCommand(state, this);
1733
+ let foundObj = null;
1734
+ try {
1735
+ let matchResult = null;
1736
+ while (Date.now() - startTime < timeout) {
1737
+ try {
1738
+ let scope = null;
1739
+ if (!frameSelectors) {
1740
+ scope = this.page;
1741
+ }
1742
+ else {
1743
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1744
+ }
1745
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1746
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1747
+ if (matchResult.errorLine !== -1) {
1748
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1749
+ }
1750
+ // highlight and screenshot
1751
+ return state.info;
1752
+ }
1753
+ catch (e) {
1754
+ // Log error but continue retrying until timeout is reached
1755
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1756
+ }
1757
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1758
+ }
1759
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1760
+ }
1761
+ catch (e) {
1762
+ await _commandError(state, e, this);
1763
+ throw e;
1764
+ }
1765
+ finally {
1766
+ await _commandFinally(state, this);
1600
1767
  }
1601
1768
  }
1602
1769
  async waitForUserInput(message, world = null) {
@@ -1634,6 +1801,15 @@ class StableBrowser {
1634
1801
  // save the data to the file
1635
1802
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1636
1803
  }
1804
+ overwriteTestData(testData, world = null) {
1805
+ if (!testData) {
1806
+ return;
1807
+ }
1808
+ // if data file exists, load it
1809
+ const dataFile = _getDataFile(world, this.context, this);
1810
+ // save the data to the file
1811
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1812
+ }
1637
1813
  _getDataFilePath(fileName) {
1638
1814
  let dataFile = path.join(this.project_path, "data", fileName);
1639
1815
  if (fs.existsSync(dataFile)) {
@@ -1886,7 +2062,7 @@ class StableBrowser {
1886
2062
  await _commandError(state, e, this);
1887
2063
  }
1888
2064
  finally {
1889
- _commandFinally(state, this);
2065
+ await _commandFinally(state, this);
1890
2066
  }
1891
2067
  }
1892
2068
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1917,10 +2093,31 @@ class StableBrowser {
1917
2093
  case "value":
1918
2094
  state.value = await state.element.inputValue();
1919
2095
  break;
2096
+ case "text":
2097
+ state.value = await state.element.textContent();
2098
+ break;
1920
2099
  default:
1921
2100
  state.value = await state.element.getAttribute(attribute);
1922
2101
  break;
1923
2102
  }
2103
+ if (options !== null) {
2104
+ if (options.regex && options.regex !== "") {
2105
+ // Construct a regex pattern from the provided string
2106
+ const regex = options.regex.slice(1, -1);
2107
+ const regexPattern = new RegExp(regex, "g");
2108
+ const matches = state.value.match(regexPattern);
2109
+ if (matches) {
2110
+ let newValue = "";
2111
+ for (const match of matches) {
2112
+ newValue += match;
2113
+ }
2114
+ state.value = newValue;
2115
+ }
2116
+ }
2117
+ if (options.trimSpaces && options.trimSpaces === true) {
2118
+ state.value = state.value.trim();
2119
+ }
2120
+ }
1924
2121
  state.info.value = state.value;
1925
2122
  this.setTestData({ [variable]: state.value }, world);
1926
2123
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1931,7 +2128,7 @@ class StableBrowser {
1931
2128
  await _commandError(state, e, this);
1932
2129
  }
1933
2130
  finally {
1934
- _commandFinally(state, this);
2131
+ await _commandFinally(state, this);
1935
2132
  }
1936
2133
  }
1937
2134
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1956,12 +2153,15 @@ class StableBrowser {
1956
2153
  let expectedValue;
1957
2154
  try {
1958
2155
  await _preCommand(state, this);
1959
- expectedValue = state.value;
2156
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1960
2157
  state.info.expectedValue = expectedValue;
1961
2158
  switch (attribute) {
1962
2159
  case "innerText":
1963
2160
  val = String(await state.element.innerText());
1964
2161
  break;
2162
+ case "text":
2163
+ val = String(await state.element.textContent());
2164
+ break;
1965
2165
  case "value":
1966
2166
  val = String(await state.element.inputValue());
1967
2167
  break;
@@ -1983,17 +2183,42 @@ class StableBrowser {
1983
2183
  let regex;
1984
2184
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1985
2185
  const patternBody = expectedValue.slice(1, -1);
1986
- regex = new RegExp(patternBody, "g");
2186
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2187
+ regex = new RegExp(processedPattern, "gs");
2188
+ state.info.regex = true;
1987
2189
  }
1988
2190
  else {
1989
2191
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1990
2192
  regex = new RegExp(escapedPattern, "g");
1991
2193
  }
1992
- if (!val.match(regex)) {
1993
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
1994
- state.info.failCause.assertionFailed = true;
1995
- state.info.failCause.lastError = errorMessage;
1996
- throw new Error(errorMessage);
2194
+ if (attribute === "innerText") {
2195
+ if (state.info.regex) {
2196
+ if (!regex.test(val)) {
2197
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2198
+ state.info.failCause.assertionFailed = true;
2199
+ state.info.failCause.lastError = errorMessage;
2200
+ throw new Error(errorMessage);
2201
+ }
2202
+ }
2203
+ else {
2204
+ const valLines = val.split("\n");
2205
+ const expectedLines = expectedValue.split("\n");
2206
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2207
+ if (!isPart) {
2208
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2209
+ state.info.failCause.assertionFailed = true;
2210
+ state.info.failCause.lastError = errorMessage;
2211
+ throw new Error(errorMessage);
2212
+ }
2213
+ }
2214
+ }
2215
+ else {
2216
+ if (!val.match(regex)) {
2217
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2218
+ state.info.failCause.assertionFailed = true;
2219
+ state.info.failCause.lastError = errorMessage;
2220
+ throw new Error(errorMessage);
2221
+ }
1997
2222
  }
1998
2223
  return state.info;
1999
2224
  }
@@ -2001,7 +2226,7 @@ class StableBrowser {
2001
2226
  await _commandError(state, e, this);
2002
2227
  }
2003
2228
  finally {
2004
- _commandFinally(state, this);
2229
+ await _commandFinally(state, this);
2005
2230
  }
2006
2231
  }
2007
2232
  async extractEmailData(emailAddress, options, world) {
@@ -2161,54 +2386,6 @@ class StableBrowser {
2161
2386
  console.debug(error);
2162
2387
  }
2163
2388
  }
2164
- // async _unhighlightElements(scope, css) {
2165
- // try {
2166
- // if (!scope) {
2167
- // return;
2168
- // }
2169
- // if (!css) {
2170
- // scope
2171
- // .evaluate((node) => {
2172
- // if (node && node.style) {
2173
- // if (!node.__previousOutline) {
2174
- // node.style.outline = "";
2175
- // } else {
2176
- // node.style.outline = node.__previousOutline;
2177
- // }
2178
- // }
2179
- // })
2180
- // .then(() => {})
2181
- // .catch((e) => {
2182
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2183
- // });
2184
- // } else {
2185
- // scope
2186
- // .evaluate(([css]) => {
2187
- // if (!css) {
2188
- // return;
2189
- // }
2190
- // let elements = Array.from(document.querySelectorAll(css));
2191
- // for (i = 0; i < elements.length; i++) {
2192
- // let element = elements[i];
2193
- // if (!element.style) {
2194
- // return;
2195
- // }
2196
- // if (!element.__previousOutline) {
2197
- // element.style.outline = "";
2198
- // } else {
2199
- // element.style.outline = element.__previousOutline;
2200
- // }
2201
- // }
2202
- // })
2203
- // .then(() => {})
2204
- // .catch((e) => {
2205
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2206
- // });
2207
- // }
2208
- // } catch (error) {
2209
- // // console.debug(error);
2210
- // }
2211
- // }
2212
2389
  async verifyPagePath(pathPart, options = {}, world = null) {
2213
2390
  const startTime = Date.now();
2214
2391
  let error = null;
@@ -2246,7 +2423,7 @@ class StableBrowser {
2246
2423
  Object.assign(e, { info: info });
2247
2424
  error = e;
2248
2425
  // throw e;
2249
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2426
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
2250
2427
  }
2251
2428
  finally {
2252
2429
  const endTime = Date.now();
@@ -2271,27 +2448,89 @@ class StableBrowser {
2271
2448
  });
2272
2449
  }
2273
2450
  }
2274
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2451
+ async verifyPageTitle(title, options = {}, world = null) {
2452
+ const startTime = Date.now();
2453
+ let error = null;
2454
+ let screenshotId = null;
2455
+ let screenshotPath = null;
2456
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2457
+ const info = {};
2458
+ info.log = "***** verify page title " + title + " *****\n";
2459
+ info.operation = "verifyPageTitle";
2460
+ const newValue = await this._replaceWithLocalData(title, world);
2461
+ if (newValue !== title) {
2462
+ this.logger.info(title + "=" + newValue);
2463
+ title = newValue;
2464
+ }
2465
+ info.title = title;
2466
+ try {
2467
+ for (let i = 0; i < 30; i++) {
2468
+ const foundTitle = await this.page.title();
2469
+ if (!foundTitle.includes(title)) {
2470
+ if (i === 29) {
2471
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2472
+ }
2473
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2474
+ continue;
2475
+ }
2476
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2477
+ return info;
2478
+ }
2479
+ }
2480
+ catch (e) {
2481
+ //await this.closeUnexpectedPopups();
2482
+ this.logger.error("verify page title failed " + info.log);
2483
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2484
+ info.screenshotPath = screenshotPath;
2485
+ Object.assign(e, { info: info });
2486
+ error = e;
2487
+ // throw e;
2488
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2489
+ }
2490
+ finally {
2491
+ const endTime = Date.now();
2492
+ _reportToWorld(world, {
2493
+ type: Types.VERIFY_PAGE_PATH,
2494
+ text: "Verify page title",
2495
+ _text: "Verify the page title contains " + title,
2496
+ screenshotId,
2497
+ result: error
2498
+ ? {
2499
+ status: "FAILED",
2500
+ startTime,
2501
+ endTime,
2502
+ message: error?.message,
2503
+ }
2504
+ : {
2505
+ status: "PASSED",
2506
+ startTime,
2507
+ endTime,
2508
+ },
2509
+ info: info,
2510
+ });
2511
+ }
2512
+ }
2513
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2275
2514
  const frames = this.page.frames();
2276
2515
  let results = [];
2277
- let ignoreCase = false;
2516
+ // let ignoreCase = false;
2278
2517
  for (let i = 0; i < frames.length; i++) {
2279
2518
  if (dateAlternatives.date) {
2280
2519
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2281
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2520
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2282
2521
  result.frame = frames[i];
2283
2522
  results.push(result);
2284
2523
  }
2285
2524
  }
2286
2525
  else if (numberAlternatives.number) {
2287
2526
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2288
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2527
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2289
2528
  result.frame = frames[i];
2290
2529
  results.push(result);
2291
2530
  }
2292
2531
  }
2293
2532
  else {
2294
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2533
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2295
2534
  result.frame = frames[i];
2296
2535
  results.push(result);
2297
2536
  }
@@ -2310,11 +2549,14 @@ class StableBrowser {
2310
2549
  scroll: false,
2311
2550
  highlight: false,
2312
2551
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2313
- text: `Verify text exists in page`,
2552
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2314
2553
  _text: `Verify the text '${text}' exists in page`,
2315
2554
  operation: "verifyTextExistInPage",
2316
2555
  log: "***** verify text " + text + " exists in page *****\n",
2317
2556
  };
2557
+ if (testForRegex(text)) {
2558
+ text = text.replace(/\\"/g, '"');
2559
+ }
2318
2560
  const timeout = this._getFindElementTimeout(options);
2319
2561
  await new Promise((resolve) => setTimeout(resolve, 2000));
2320
2562
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2385,7 +2627,7 @@ class StableBrowser {
2385
2627
  await _commandError(state, e, this);
2386
2628
  }
2387
2629
  finally {
2388
- _commandFinally(state, this);
2630
+ await _commandFinally(state, this);
2389
2631
  }
2390
2632
  }
2391
2633
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2398,11 +2640,14 @@ class StableBrowser {
2398
2640
  scroll: false,
2399
2641
  highlight: false,
2400
2642
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2401
- text: `Verify text does not exist in page`,
2643
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2402
2644
  _text: `Verify the text '${text}' does not exist in page`,
2403
2645
  operation: "verifyTextNotExistInPage",
2404
2646
  log: "***** verify text " + text + " does not exist in page *****\n",
2405
2647
  };
2648
+ if (testForRegex(text)) {
2649
+ text = text.replace(/\\"/g, '"');
2650
+ }
2406
2651
  const timeout = this._getFindElementTimeout(options);
2407
2652
  await new Promise((resolve) => setTimeout(resolve, 2000));
2408
2653
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2439,7 +2684,7 @@ class StableBrowser {
2439
2684
  await _commandError(state, e, this);
2440
2685
  }
2441
2686
  finally {
2442
- _commandFinally(state, this);
2687
+ await _commandFinally(state, this);
2443
2688
  }
2444
2689
  }
2445
2690
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2481,7 +2726,7 @@ class StableBrowser {
2481
2726
  };
2482
2727
  while (true) {
2483
2728
  try {
2484
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2729
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2485
2730
  }
2486
2731
  catch (error) {
2487
2732
  // ignore
@@ -2509,7 +2754,7 @@ class StableBrowser {
2509
2754
  const count = await frame.locator(css).count();
2510
2755
  for (let j = 0; j < count; j++) {
2511
2756
  const continer = await frame.locator(css).nth(j);
2512
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2757
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2513
2758
  if (result.elementCount > 0) {
2514
2759
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2515
2760
  await this._highlightElements(frame, dataAttribute);
@@ -2550,8 +2795,32 @@ class StableBrowser {
2550
2795
  await _commandError(state, e, this);
2551
2796
  }
2552
2797
  finally {
2553
- _commandFinally(state, this);
2798
+ await _commandFinally(state, this);
2799
+ }
2800
+ }
2801
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2802
+ const frames = this.page.frames();
2803
+ let results = [];
2804
+ let ignoreCase = false;
2805
+ for (let i = 0; i < frames.length; i++) {
2806
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2807
+ result.frame = frames[i];
2808
+ const climbArray = [];
2809
+ for (let i = 0; i < climb; i++) {
2810
+ climbArray.push("..");
2811
+ }
2812
+ let climbXpath = "xpath=" + climbArray.join("/");
2813
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2814
+ const count = await frames[i].locator(newLocator).count();
2815
+ if (count > 0) {
2816
+ result.elementCount = count;
2817
+ result.locator = newLocator;
2818
+ results.push(result);
2819
+ }
2554
2820
  }
2821
+ // state.info.results = results;
2822
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2823
+ return resultWithElementsFound;
2555
2824
  }
2556
2825
  async visualVerification(text, options = {}, world = null) {
2557
2826
  const startTime = Date.now();
@@ -2869,7 +3138,13 @@ class StableBrowser {
2869
3138
  }
2870
3139
  }
2871
3140
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2872
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3141
+ try {
3142
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3143
+ }
3144
+ catch (error) {
3145
+ this.logger.debug(error);
3146
+ throw error;
3147
+ }
2873
3148
  }
2874
3149
  _getLoadTimeout(options) {
2875
3150
  let timeout = 15000;
@@ -2906,6 +3181,9 @@ class StableBrowser {
2906
3181
  this.registerEventListeners(this.context);
2907
3182
  registerNetworkEvents(this.world, this, this.context, this.page);
2908
3183
  registerDownloadEvent(this.page, this.world, this.context);
3184
+ if (this.onRestoreSaveState) {
3185
+ this.onRestoreSaveState(path);
3186
+ }
2909
3187
  }
2910
3188
  async waitForPageLoad(options = {}, world = null) {
2911
3189
  let timeout = this._getLoadTimeout(options);
@@ -2988,11 +3266,98 @@ class StableBrowser {
2988
3266
  await _commandError(state, e, this);
2989
3267
  }
2990
3268
  finally {
2991
- _commandFinally(state, this);
3269
+ await _commandFinally(state, this);
3270
+ }
3271
+ }
3272
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3273
+ let operation = null;
3274
+ if (!options || !options.operation) {
3275
+ throw new Error("operation is not defined");
3276
+ }
3277
+ operation = options.operation;
3278
+ // validate operation is one of the supported operations
3279
+ if (operation != "click" && operation != "hover+click") {
3280
+ throw new Error("operation is not supported");
3281
+ }
3282
+ const state = {
3283
+ options,
3284
+ world,
3285
+ locate: false,
3286
+ scroll: false,
3287
+ highlight: false,
3288
+ type: Types.TABLE_OPERATION,
3289
+ text: `Table operation`,
3290
+ _text: `Table ${operation} operation`,
3291
+ operation: operation,
3292
+ log: "***** Table operation *****\n",
3293
+ };
3294
+ const timeout = this._getFindElementTimeout(options);
3295
+ try {
3296
+ await _preCommand(state, this);
3297
+ const start = Date.now();
3298
+ let cellArea = null;
3299
+ while (true) {
3300
+ try {
3301
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3302
+ if (cellArea) {
3303
+ break;
3304
+ }
3305
+ }
3306
+ catch (e) {
3307
+ // ignore
3308
+ }
3309
+ if (Date.now() - start > timeout) {
3310
+ throw new Error(`Cell not found in table`);
3311
+ }
3312
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3313
+ }
3314
+ switch (operation) {
3315
+ case "click":
3316
+ if (!options.css) {
3317
+ // will click in the center of the cell
3318
+ let xOffset = 0;
3319
+ let yOffset = 0;
3320
+ if (options.xOffset) {
3321
+ xOffset = options.xOffset;
3322
+ }
3323
+ if (options.yOffset) {
3324
+ yOffset = options.yOffset;
3325
+ }
3326
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3327
+ }
3328
+ else {
3329
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3330
+ if (results.length === 0) {
3331
+ throw new Error(`Element not found in cell area`);
3332
+ }
3333
+ state.element = results[0];
3334
+ await performAction("click", state.element, options, this, state, _params);
3335
+ }
3336
+ break;
3337
+ case "hover+click":
3338
+ if (!options.css) {
3339
+ throw new Error("css is not defined");
3340
+ }
3341
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3342
+ if (results.length === 0) {
3343
+ throw new Error(`Element not found in cell area`);
3344
+ }
3345
+ state.element = results[0];
3346
+ await performAction("hover+click", state.element, options, this, state, _params);
3347
+ break;
3348
+ default:
3349
+ throw new Error("operation is not supported");
3350
+ }
3351
+ }
3352
+ catch (e) {
3353
+ await _commandError(state, e, this);
3354
+ }
3355
+ finally {
3356
+ await _commandFinally(state, this);
2992
3357
  }
2993
3358
  }
2994
3359
  saveTestDataAsGlobal(options, world) {
2995
- const dataFile = this._getDataFile(world);
3360
+ const dataFile = _getDataFile(world, this.context, this);
2996
3361
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2997
3362
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2998
3363
  }
@@ -3093,7 +3458,39 @@ class StableBrowser {
3093
3458
  console.log("#-#");
3094
3459
  }
3095
3460
  }
3461
+ async beforeScenario(world, scenario) {
3462
+ this.beforeScenarioCalled = true;
3463
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3464
+ this.scenarioName = scenario.pickle.name;
3465
+ }
3466
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3467
+ this.featureName = scenario.gherkinDocument.feature.name;
3468
+ }
3469
+ if (this.context) {
3470
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3471
+ }
3472
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3473
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3474
+ // check if @global_test_data tag is present
3475
+ if (this.tags.includes("@global_test_data")) {
3476
+ this.saveTestDataAsGlobal({}, world);
3477
+ }
3478
+ }
3479
+ // update test data based on feature/scenario
3480
+ let envName = null;
3481
+ if (this.context && this.context.environment) {
3482
+ envName = this.context.environment.name;
3483
+ }
3484
+ if (!process.env.TEMP_RUN) {
3485
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3486
+ }
3487
+ await loadBrunoParams(this.context, this.context.environment.name);
3488
+ }
3489
+ async afterScenario(world, scenario) { }
3096
3490
  async beforeStep(world, step) {
3491
+ if (!this.beforeScenarioCalled) {
3492
+ this.beforeScenario(world, step);
3493
+ }
3097
3494
  if (this.stepIndex === undefined) {
3098
3495
  this.stepIndex = 0;
3099
3496
  }
@@ -3110,21 +3507,11 @@ class StableBrowser {
3110
3507
  else {
3111
3508
  this.stepName = "step " + this.stepIndex;
3112
3509
  }
3113
- if (this.context) {
3114
- this.context.examplesRow = extractStepExampleParameters(step);
3115
- }
3116
3510
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3117
3511
  if (this.context.browserObject.context) {
3118
3512
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3119
3513
  }
3120
3514
  }
3121
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3122
- this.tags = step.pickle.tags.map((tag) => tag.name);
3123
- // check if @global_test_data tag is present
3124
- if (this.tags.includes("@global_test_data")) {
3125
- this.saveTestDataAsGlobal({}, world);
3126
- }
3127
- }
3128
3515
  if (this.initSnapshotTaken === false) {
3129
3516
  this.initSnapshotTaken = true;
3130
3517
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3149,18 +3536,68 @@ class StableBrowser {
3149
3536
  const content = [`- path: ${path}`, `- title: ${title}`];
3150
3537
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3151
3538
  for (let i = 0; i < frames.length; i++) {
3152
- content.push(`- frame: ${i}`);
3153
3539
  const frame = frames[i];
3154
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3155
- content.push(snapshot);
3540
+ try {
3541
+ // Ensure frame is attached and has body
3542
+ const body = frame.locator("body");
3543
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3544
+ const snapshot = await body.ariaSnapshot({ timeout });
3545
+ content.push(`- frame: ${i}`);
3546
+ content.push(snapshot);
3547
+ }
3548
+ catch (innerErr) { }
3156
3549
  }
3157
3550
  return content.join("\n");
3158
3551
  }
3159
3552
  catch (e) {
3160
- console.error(e);
3553
+ console.log("Error in getAriaSnapshot");
3554
+ //console.debug(e);
3161
3555
  }
3162
3556
  return null;
3163
3557
  }
3558
+ /**
3559
+ * Sends command with custom payload to report.
3560
+ * @param commandText - Title of the command to be shown in the report.
3561
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3562
+ * @param content - Content of the command to be shown in the report.
3563
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3564
+ * @param world - Optional world context.
3565
+ * @public
3566
+ */
3567
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3568
+ const state = {
3569
+ options,
3570
+ world,
3571
+ locate: false,
3572
+ scroll: false,
3573
+ screenshot: options.screenshot ?? false,
3574
+ highlight: options.highlight ?? false,
3575
+ type: Types.REPORT_COMMAND,
3576
+ text: commandText,
3577
+ _text: commandText,
3578
+ operation: "report_command",
3579
+ log: "***** " + commandText + " *****\n",
3580
+ };
3581
+ try {
3582
+ await _preCommand(state, this);
3583
+ const payload = {
3584
+ type: options.type ?? "text",
3585
+ content: content,
3586
+ screenshotId: null,
3587
+ };
3588
+ state.payload = payload;
3589
+ if (commandStatus === "FAILED") {
3590
+ state.throwError = true;
3591
+ throw new Error("Command failed");
3592
+ }
3593
+ }
3594
+ catch (e) {
3595
+ await _commandError(state, e, this);
3596
+ }
3597
+ finally {
3598
+ await _commandFinally(state, this);
3599
+ }
3600
+ }
3164
3601
  async afterStep(world, step) {
3165
3602
  this.stepName = null;
3166
3603
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3168,6 +3605,13 @@ class StableBrowser {
3168
3605
  await this.context.browserObject.context.tracing.stopChunk({
3169
3606
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3170
3607
  });
3608
+ if (world && world.attach) {
3609
+ await world.attach(JSON.stringify({
3610
+ type: "trace",
3611
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3612
+ }), "application/json+trace");
3613
+ }
3614
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3171
3615
  }
3172
3616
  }
3173
3617
  if (this.context) {