automation_model 1.0.634-dev → 1.0.634-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 +32 -23
  45. package/lib/stable_browser.js +633 -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 +7 -4
  56. package/lib/utils.js +205 -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, 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";
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,12 @@ 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",
66
+ STEP_COMPLETE: "step_complete",
56
67
  };
57
68
  export const apps = {};
58
69
  const formatElementName = (elementName) => {
@@ -178,6 +189,30 @@ class StableBrowser {
178
189
  await this.waitForPageLoad();
179
190
  }
180
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
+ }
181
216
  registerConsoleLogListener(page, context) {
182
217
  if (!this.context.webLogger) {
183
218
  this.context.webLogger = [];
@@ -245,6 +280,7 @@ class StableBrowser {
245
280
  if (!url) {
246
281
  throw new Error("url is null, verify that the environment file is correct");
247
282
  }
283
+ url = await this._replaceWithLocalData(url, this.world);
248
284
  if (!url.startsWith("http")) {
249
285
  url = "https://" + url;
250
286
  }
@@ -273,7 +309,7 @@ class StableBrowser {
273
309
  _commandError(state, error, this);
274
310
  }
275
311
  finally {
276
- _commandFinally(state, this);
312
+ await _commandFinally(state, this);
277
313
  }
278
314
  }
279
315
  async _getLocator(locator, scope, _params) {
@@ -374,6 +410,12 @@ class StableBrowser {
374
410
  if (!el.setAttribute) {
375
411
  el = el.parentElement;
376
412
  }
413
+ // remove any attributes start with data-blinq-id
414
+ // for (let i = 0; i < el.attributes.length; i++) {
415
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
416
+ // el.removeAttribute(el.attributes[i].name);
417
+ // }
418
+ // }
377
419
  el.setAttribute("data-blinq-id-" + randomToken, "");
378
420
  return true;
379
421
  }, [tag1, randomToken]))) {
@@ -383,7 +425,7 @@ class StableBrowser {
383
425
  }
384
426
  return { elementCount: tagCount, randomToken };
385
427
  }
386
- 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) {
387
429
  if (!info) {
388
430
  info = {};
389
431
  }
@@ -450,7 +492,7 @@ class StableBrowser {
450
492
  }
451
493
  return;
452
494
  }
453
- if (info.locatorLog && count === 0) {
495
+ if (info.locatorLog && count === 0 && logErrors) {
454
496
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
455
497
  }
456
498
  for (let j = 0; j < count; j++) {
@@ -465,7 +507,7 @@ class StableBrowser {
465
507
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
466
508
  }
467
509
  }
468
- else {
510
+ else if (logErrors) {
469
511
  info.failCause.visible = visible;
470
512
  info.failCause.enabled = enabled;
471
513
  if (!info.printMessages) {
@@ -560,12 +602,24 @@ class StableBrowser {
560
602
  element.evaluate((el, randomToken) => {
561
603
  el.setAttribute("data-blinq-id-" + randomToken, "");
562
604
  }, 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;
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);
569
623
  }
570
624
  }
571
625
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -717,14 +771,9 @@ class StableBrowser {
717
771
  // info.log += "scanning locators in priority 2" + "\n";
718
772
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
719
773
  }
720
- if (result.foundElements.length === 0 && onlyPriority3) {
774
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
721
775
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
722
776
  }
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
777
  let foundElements = result.foundElements;
729
778
  if (foundElements.length === 1 && foundElements[0].unique) {
730
779
  info.box = foundElements[0].box;
@@ -779,6 +828,11 @@ class StableBrowser {
779
828
  visibleOnly = false;
780
829
  }
781
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
+ }
782
836
  }
783
837
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
784
838
  // if (info.locatorLog) {
@@ -794,7 +848,7 @@ class StableBrowser {
794
848
  }
795
849
  throw new Error("failed to locate first element no elements found, " + info.log);
796
850
  }
797
- 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) {
798
852
  let foundElements = [];
799
853
  const result = {
800
854
  foundElements: foundElements,
@@ -813,7 +867,9 @@ class StableBrowser {
813
867
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
814
868
  }
815
869
  catch (e) {
816
- 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
+ }
817
873
  }
818
874
  }
819
875
  if (foundLocators.length === 1) {
@@ -825,9 +881,40 @@ class StableBrowser {
825
881
  result.locatorIndex = i;
826
882
  }
827
883
  if (foundLocators.length > 1) {
828
- info.failCause.foundMultiple = true;
829
- if (info.locatorLog) {
830
- 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
+ }
831
918
  }
832
919
  }
833
920
  }
@@ -875,7 +962,7 @@ class StableBrowser {
875
962
  await _commandError(state, "timeout looking for " + elementDescription, this);
876
963
  }
877
964
  finally {
878
- _commandFinally(state, this);
965
+ await _commandFinally(state, this);
879
966
  }
880
967
  }
881
968
  }
@@ -924,7 +1011,7 @@ class StableBrowser {
924
1011
  await _commandError(state, "timeout looking for " + elementDescription, this);
925
1012
  }
926
1013
  finally {
927
- _commandFinally(state, this);
1014
+ await _commandFinally(state, this);
928
1015
  }
929
1016
  }
930
1017
  }
@@ -945,19 +1032,7 @@ class StableBrowser {
945
1032
  };
946
1033
  try {
947
1034
  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
- }
1035
+ await performAction("click", state.element, options, this, state, _params);
961
1036
  await this.waitForPageLoad();
962
1037
  return state.info;
963
1038
  }
@@ -965,7 +1040,7 @@ class StableBrowser {
965
1040
  await _commandError(state, e, this);
966
1041
  }
967
1042
  finally {
968
- _commandFinally(state, this);
1043
+ await _commandFinally(state, this);
969
1044
  }
970
1045
  }
971
1046
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -996,7 +1071,7 @@ class StableBrowser {
996
1071
  // await _commandError(state, e, this);
997
1072
  }
998
1073
  finally {
999
- _commandFinally(state, this);
1074
+ await _commandFinally(state, this);
1000
1075
  }
1001
1076
  return found;
1002
1077
  }
@@ -1020,7 +1095,7 @@ class StableBrowser {
1020
1095
  try {
1021
1096
  // if (world && world.screenshot && !world.screenshotPath) {
1022
1097
  // console.log(`Highlighting while running from recorder`);
1023
- await this._highlightElements(element);
1098
+ await this._highlightElements(state.element);
1024
1099
  await state.element.setChecked(checked);
1025
1100
  await new Promise((resolve) => setTimeout(resolve, 1000));
1026
1101
  // await this._unHighlightElements(element);
@@ -1047,7 +1122,7 @@ class StableBrowser {
1047
1122
  await _commandError(state, e, this);
1048
1123
  }
1049
1124
  finally {
1050
- _commandFinally(state, this);
1125
+ await _commandFinally(state, this);
1051
1126
  }
1052
1127
  }
1053
1128
  async hover(selectors, _params, options = {}, world = null) {
@@ -1064,19 +1139,7 @@ class StableBrowser {
1064
1139
  };
1065
1140
  try {
1066
1141
  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
- }
1142
+ await performAction("hover", state.element, options, this, state, _params);
1080
1143
  await _screenshot(state, this);
1081
1144
  await this.waitForPageLoad();
1082
1145
  return state.info;
@@ -1085,7 +1148,7 @@ class StableBrowser {
1085
1148
  await _commandError(state, e, this);
1086
1149
  }
1087
1150
  finally {
1088
- _commandFinally(state, this);
1151
+ await _commandFinally(state, this);
1089
1152
  }
1090
1153
  }
1091
1154
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1121,7 +1184,7 @@ class StableBrowser {
1121
1184
  await _commandError(state, e, this);
1122
1185
  }
1123
1186
  finally {
1124
- _commandFinally(state, this);
1187
+ await _commandFinally(state, this);
1125
1188
  }
1126
1189
  }
1127
1190
  async type(_value, _params = null, options = {}, world = null) {
@@ -1167,7 +1230,7 @@ class StableBrowser {
1167
1230
  await _commandError(state, e, this);
1168
1231
  }
1169
1232
  finally {
1170
- _commandFinally(state, this);
1233
+ await _commandFinally(state, this);
1171
1234
  }
1172
1235
  }
1173
1236
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1203,7 +1266,7 @@ class StableBrowser {
1203
1266
  await _commandError(state, e, this);
1204
1267
  }
1205
1268
  finally {
1206
- _commandFinally(state, this);
1269
+ await _commandFinally(state, this);
1207
1270
  }
1208
1271
  }
1209
1272
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1223,7 +1286,7 @@ class StableBrowser {
1223
1286
  try {
1224
1287
  await _preCommand(state, this);
1225
1288
  try {
1226
- await state.element.click();
1289
+ await performAction("click", state.element, options, this, state, _params);
1227
1290
  await new Promise((resolve) => setTimeout(resolve, 500));
1228
1291
  if (format) {
1229
1292
  state.value = dayjs(state.value).format(format);
@@ -1272,7 +1335,7 @@ class StableBrowser {
1272
1335
  await _commandError(state, e, this);
1273
1336
  }
1274
1337
  finally {
1275
- _commandFinally(state, this);
1338
+ await _commandFinally(state, this);
1276
1339
  }
1277
1340
  }
1278
1341
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1291,6 +1354,9 @@ class StableBrowser {
1291
1354
  operation: "clickType",
1292
1355
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1293
1356
  };
1357
+ if (!options) {
1358
+ options = {};
1359
+ }
1294
1360
  if (newValue !== _value) {
1295
1361
  //this.logger.info(_value + "=" + newValue);
1296
1362
  _value = newValue;
@@ -1298,7 +1364,7 @@ class StableBrowser {
1298
1364
  try {
1299
1365
  await _preCommand(state, this);
1300
1366
  state.info.value = _value;
1301
- if (options === null || options === undefined || !options.press) {
1367
+ if (!options.press) {
1302
1368
  try {
1303
1369
  let currentValue = await state.element.inputValue();
1304
1370
  if (currentValue) {
@@ -1309,13 +1375,9 @@ class StableBrowser {
1309
1375
  this.logger.info("unable to clear input value");
1310
1376
  }
1311
1377
  }
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
- }
1378
+ if (options.press) {
1379
+ options.timeout = 5000;
1380
+ await performAction("click", state.element, options, this, state, _params);
1319
1381
  }
1320
1382
  else {
1321
1383
  try {
@@ -1373,7 +1435,7 @@ class StableBrowser {
1373
1435
  await _commandError(state, e, this);
1374
1436
  }
1375
1437
  finally {
1376
- _commandFinally(state, this);
1438
+ await _commandFinally(state, this);
1377
1439
  }
1378
1440
  }
1379
1441
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1403,7 +1465,42 @@ class StableBrowser {
1403
1465
  await _commandError(state, e, this);
1404
1466
  }
1405
1467
  finally {
1406
- _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);
1407
1504
  }
1408
1505
  }
1409
1506
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1519,7 +1616,7 @@ class StableBrowser {
1519
1616
  await _commandError(state, e, this);
1520
1617
  }
1521
1618
  finally {
1522
- _commandFinally(state, this);
1619
+ await _commandFinally(state, this);
1523
1620
  }
1524
1621
  }
1525
1622
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1554,7 +1651,7 @@ class StableBrowser {
1554
1651
  while (Date.now() - startTime < timeout) {
1555
1652
  try {
1556
1653
  await _preCommand(state, this);
1557
- 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);
1558
1655
  if (foundObj && foundObj.element) {
1559
1656
  await this.scrollIfNeeded(foundObj.element, state.info);
1560
1657
  }
@@ -1596,7 +1693,79 @@ class StableBrowser {
1596
1693
  throw e;
1597
1694
  }
1598
1695
  finally {
1599
- _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);
1600
1769
  }
1601
1770
  }
1602
1771
  async waitForUserInput(message, world = null) {
@@ -1634,6 +1803,15 @@ class StableBrowser {
1634
1803
  // save the data to the file
1635
1804
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1636
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
+ }
1637
1815
  _getDataFilePath(fileName) {
1638
1816
  let dataFile = path.join(this.project_path, "data", fileName);
1639
1817
  if (fs.existsSync(dataFile)) {
@@ -1886,7 +2064,7 @@ class StableBrowser {
1886
2064
  await _commandError(state, e, this);
1887
2065
  }
1888
2066
  finally {
1889
- _commandFinally(state, this);
2067
+ await _commandFinally(state, this);
1890
2068
  }
1891
2069
  }
1892
2070
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1917,10 +2095,31 @@ class StableBrowser {
1917
2095
  case "value":
1918
2096
  state.value = await state.element.inputValue();
1919
2097
  break;
2098
+ case "text":
2099
+ state.value = await state.element.textContent();
2100
+ break;
1920
2101
  default:
1921
2102
  state.value = await state.element.getAttribute(attribute);
1922
2103
  break;
1923
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
+ }
1924
2123
  state.info.value = state.value;
1925
2124
  this.setTestData({ [variable]: state.value }, world);
1926
2125
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1931,7 +2130,7 @@ class StableBrowser {
1931
2130
  await _commandError(state, e, this);
1932
2131
  }
1933
2132
  finally {
1934
- _commandFinally(state, this);
2133
+ await _commandFinally(state, this);
1935
2134
  }
1936
2135
  }
1937
2136
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1956,12 +2155,15 @@ class StableBrowser {
1956
2155
  let expectedValue;
1957
2156
  try {
1958
2157
  await _preCommand(state, this);
1959
- expectedValue = state.value;
2158
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1960
2159
  state.info.expectedValue = expectedValue;
1961
2160
  switch (attribute) {
1962
2161
  case "innerText":
1963
2162
  val = String(await state.element.innerText());
1964
2163
  break;
2164
+ case "text":
2165
+ val = String(await state.element.textContent());
2166
+ break;
1965
2167
  case "value":
1966
2168
  val = String(await state.element.inputValue());
1967
2169
  break;
@@ -1983,17 +2185,42 @@ class StableBrowser {
1983
2185
  let regex;
1984
2186
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1985
2187
  const patternBody = expectedValue.slice(1, -1);
1986
- regex = new RegExp(patternBody, "g");
2188
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2189
+ regex = new RegExp(processedPattern, "gs");
2190
+ state.info.regex = true;
1987
2191
  }
1988
2192
  else {
1989
2193
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1990
2194
  regex = new RegExp(escapedPattern, "g");
1991
2195
  }
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);
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
+ }
1997
2224
  }
1998
2225
  return state.info;
1999
2226
  }
@@ -2001,7 +2228,7 @@ class StableBrowser {
2001
2228
  await _commandError(state, e, this);
2002
2229
  }
2003
2230
  finally {
2004
- _commandFinally(state, this);
2231
+ await _commandFinally(state, this);
2005
2232
  }
2006
2233
  }
2007
2234
  async extractEmailData(emailAddress, options, world) {
@@ -2161,54 +2388,6 @@ class StableBrowser {
2161
2388
  console.debug(error);
2162
2389
  }
2163
2390
  }
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
2391
  async verifyPagePath(pathPart, options = {}, world = null) {
2213
2392
  const startTime = Date.now();
2214
2393
  let error = null;
@@ -2246,7 +2425,7 @@ class StableBrowser {
2246
2425
  Object.assign(e, { info: info });
2247
2426
  error = e;
2248
2427
  // throw e;
2249
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2428
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
2250
2429
  }
2251
2430
  finally {
2252
2431
  const endTime = Date.now();
@@ -2271,27 +2450,89 @@ class StableBrowser {
2271
2450
  });
2272
2451
  }
2273
2452
  }
2274
- 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) {
2275
2516
  const frames = this.page.frames();
2276
2517
  let results = [];
2277
- let ignoreCase = false;
2518
+ // let ignoreCase = false;
2278
2519
  for (let i = 0; i < frames.length; i++) {
2279
2520
  if (dateAlternatives.date) {
2280
2521
  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, {});
2522
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2282
2523
  result.frame = frames[i];
2283
2524
  results.push(result);
2284
2525
  }
2285
2526
  }
2286
2527
  else if (numberAlternatives.number) {
2287
2528
  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, {});
2529
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2289
2530
  result.frame = frames[i];
2290
2531
  results.push(result);
2291
2532
  }
2292
2533
  }
2293
2534
  else {
2294
- 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, {});
2295
2536
  result.frame = frames[i];
2296
2537
  results.push(result);
2297
2538
  }
@@ -2310,13 +2551,13 @@ class StableBrowser {
2310
2551
  scroll: false,
2311
2552
  highlight: false,
2312
2553
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2313
- text: `Verify text exists in page`,
2554
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2314
2555
  _text: `Verify the text '${text}' exists in page`,
2315
2556
  operation: "verifyTextExistInPage",
2316
2557
  log: "***** verify text " + text + " exists in page *****\n",
2317
2558
  };
2318
2559
  if (testForRegex(text)) {
2319
- text = text.replace(/"/g, '\\"');
2560
+ text = text.replace(/\\"/g, '"');
2320
2561
  }
2321
2562
  const timeout = this._getFindElementTimeout(options);
2322
2563
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -2388,7 +2629,7 @@ class StableBrowser {
2388
2629
  await _commandError(state, e, this);
2389
2630
  }
2390
2631
  finally {
2391
- _commandFinally(state, this);
2632
+ await _commandFinally(state, this);
2392
2633
  }
2393
2634
  }
2394
2635
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2401,11 +2642,14 @@ class StableBrowser {
2401
2642
  scroll: false,
2402
2643
  highlight: false,
2403
2644
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2404
- text: `Verify text does not exist in page`,
2645
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2405
2646
  _text: `Verify the text '${text}' does not exist in page`,
2406
2647
  operation: "verifyTextNotExistInPage",
2407
2648
  log: "***** verify text " + text + " does not exist in page *****\n",
2408
2649
  };
2650
+ if (testForRegex(text)) {
2651
+ text = text.replace(/\\"/g, '"');
2652
+ }
2409
2653
  const timeout = this._getFindElementTimeout(options);
2410
2654
  await new Promise((resolve) => setTimeout(resolve, 2000));
2411
2655
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2442,7 +2686,7 @@ class StableBrowser {
2442
2686
  await _commandError(state, e, this);
2443
2687
  }
2444
2688
  finally {
2445
- _commandFinally(state, this);
2689
+ await _commandFinally(state, this);
2446
2690
  }
2447
2691
  }
2448
2692
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2484,7 +2728,7 @@ class StableBrowser {
2484
2728
  };
2485
2729
  while (true) {
2486
2730
  try {
2487
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2731
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2488
2732
  }
2489
2733
  catch (error) {
2490
2734
  // ignore
@@ -2512,7 +2756,7 @@ class StableBrowser {
2512
2756
  const count = await frame.locator(css).count();
2513
2757
  for (let j = 0; j < count; j++) {
2514
2758
  const continer = await frame.locator(css).nth(j);
2515
- 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, {});
2516
2760
  if (result.elementCount > 0) {
2517
2761
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2518
2762
  await this._highlightElements(frame, dataAttribute);
@@ -2553,8 +2797,32 @@ class StableBrowser {
2553
2797
  await _commandError(state, e, this);
2554
2798
  }
2555
2799
  finally {
2556
- _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
+ }
2557
2822
  }
2823
+ // state.info.results = results;
2824
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2825
+ return resultWithElementsFound;
2558
2826
  }
2559
2827
  async visualVerification(text, options = {}, world = null) {
2560
2828
  const startTime = Date.now();
@@ -2872,7 +3140,13 @@ class StableBrowser {
2872
3140
  }
2873
3141
  }
2874
3142
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2875
- 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
+ }
2876
3150
  }
2877
3151
  _getLoadTimeout(options) {
2878
3152
  let timeout = 15000;
@@ -2895,6 +3169,7 @@ class StableBrowser {
2895
3169
  }
2896
3170
  async saveStoreState(path = null, world = null) {
2897
3171
  const storageState = await this.page.context().storageState();
3172
+ path = await this._replaceWithLocalData(path, this.world);
2898
3173
  //const testDataFile = _getDataFile(world, this.context, this);
2899
3174
  if (path) {
2900
3175
  // save { storageState: storageState } into the path
@@ -2905,10 +3180,14 @@ class StableBrowser {
2905
3180
  }
2906
3181
  }
2907
3182
  async restoreSaveState(path = null, world = null) {
3183
+ path = await this._replaceWithLocalData(path, this.world);
2908
3184
  await refreshBrowser(this, path, world);
2909
3185
  this.registerEventListeners(this.context);
2910
3186
  registerNetworkEvents(this.world, this, this.context, this.page);
2911
3187
  registerDownloadEvent(this.page, this.world, this.context);
3188
+ if (this.onRestoreSaveState) {
3189
+ this.onRestoreSaveState(path);
3190
+ }
2912
3191
  }
2913
3192
  async waitForPageLoad(options = {}, world = null) {
2914
3193
  let timeout = this._getLoadTimeout(options);
@@ -2991,7 +3270,94 @@ class StableBrowser {
2991
3270
  await _commandError(state, e, this);
2992
3271
  }
2993
3272
  finally {
2994
- _commandFinally(state, this);
3273
+ await _commandFinally(state, this);
3274
+ }
3275
+ }
3276
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3277
+ let operation = null;
3278
+ if (!options || !options.operation) {
3279
+ throw new Error("operation is not defined");
3280
+ }
3281
+ operation = options.operation;
3282
+ // validate operation is one of the supported operations
3283
+ if (operation != "click" && operation != "hover+click") {
3284
+ throw new Error("operation is not supported");
3285
+ }
3286
+ const state = {
3287
+ options,
3288
+ world,
3289
+ locate: false,
3290
+ scroll: false,
3291
+ highlight: false,
3292
+ type: Types.TABLE_OPERATION,
3293
+ text: `Table operation`,
3294
+ _text: `Table ${operation} operation`,
3295
+ operation: operation,
3296
+ log: "***** Table operation *****\n",
3297
+ };
3298
+ const timeout = this._getFindElementTimeout(options);
3299
+ try {
3300
+ await _preCommand(state, this);
3301
+ const start = Date.now();
3302
+ let cellArea = null;
3303
+ while (true) {
3304
+ try {
3305
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3306
+ if (cellArea) {
3307
+ break;
3308
+ }
3309
+ }
3310
+ catch (e) {
3311
+ // ignore
3312
+ }
3313
+ if (Date.now() - start > timeout) {
3314
+ throw new Error(`Cell not found in table`);
3315
+ }
3316
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3317
+ }
3318
+ switch (operation) {
3319
+ case "click":
3320
+ if (!options.css) {
3321
+ // will click in the center of the cell
3322
+ let xOffset = 0;
3323
+ let yOffset = 0;
3324
+ if (options.xOffset) {
3325
+ xOffset = options.xOffset;
3326
+ }
3327
+ if (options.yOffset) {
3328
+ yOffset = options.yOffset;
3329
+ }
3330
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3331
+ }
3332
+ else {
3333
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3334
+ if (results.length === 0) {
3335
+ throw new Error(`Element not found in cell area`);
3336
+ }
3337
+ state.element = results[0];
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");
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);
3351
+ break;
3352
+ default:
3353
+ throw new Error("operation is not supported");
3354
+ }
3355
+ }
3356
+ catch (e) {
3357
+ await _commandError(state, e, this);
3358
+ }
3359
+ finally {
3360
+ await _commandFinally(state, this);
2995
3361
  }
2996
3362
  }
2997
3363
  saveTestDataAsGlobal(options, world) {
@@ -3096,7 +3462,39 @@ class StableBrowser {
3096
3462
  console.log("#-#");
3097
3463
  }
3098
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) { }
3099
3494
  async beforeStep(world, step) {
3495
+ if (!this.beforeScenarioCalled) {
3496
+ this.beforeScenario(world, step);
3497
+ }
3100
3498
  if (this.stepIndex === undefined) {
3101
3499
  this.stepIndex = 0;
3102
3500
  }
@@ -3113,21 +3511,11 @@ class StableBrowser {
3113
3511
  else {
3114
3512
  this.stepName = "step " + this.stepIndex;
3115
3513
  }
3116
- if (this.context) {
3117
- this.context.examplesRow = extractStepExampleParameters(step);
3118
- }
3119
3514
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3120
3515
  if (this.context.browserObject.context) {
3121
3516
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3122
3517
  }
3123
3518
  }
3124
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3125
- this.tags = step.pickle.tags.map((tag) => tag.name);
3126
- // check if @global_test_data tag is present
3127
- if (this.tags.includes("@global_test_data")) {
3128
- this.saveTestDataAsGlobal({}, world);
3129
- }
3130
- }
3131
3519
  if (this.initSnapshotTaken === false) {
3132
3520
  this.initSnapshotTaken = true;
3133
3521
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3152,18 +3540,68 @@ class StableBrowser {
3152
3540
  const content = [`- path: ${path}`, `- title: ${title}`];
3153
3541
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3154
3542
  for (let i = 0; i < frames.length; i++) {
3155
- content.push(`- frame: ${i}`);
3156
3543
  const frame = frames[i];
3157
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3158
- 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) { }
3159
3553
  }
3160
3554
  return content.join("\n");
3161
3555
  }
3162
3556
  catch (e) {
3163
- console.error(e);
3557
+ console.log("Error in getAriaSnapshot");
3558
+ //console.debug(e);
3164
3559
  }
3165
3560
  return null;
3166
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
+ }
3167
3605
  async afterStep(world, step) {
3168
3606
  this.stepName = null;
3169
3607
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3171,6 +3609,13 @@ class StableBrowser {
3171
3609
  await this.context.browserObject.context.tracing.stopChunk({
3172
3610
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3173
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`);
3174
3619
  }
3175
3620
  }
3176
3621
  if (this.context) {
@@ -3183,6 +3628,27 @@ class StableBrowser {
3183
3628
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3184
3629
  }
3185
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
+ }
3186
3652
  }
3187
3653
  }
3188
3654
  function createTimedPromise(promise, label) {