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