automation_model 1.0.633-dev → 1.0.633-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 +635 -166
  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,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,11 +2551,14 @@ 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
  };
2559
+ if (testForRegex(text)) {
2560
+ text = text.replace(/\\"/g, '"');
2561
+ }
2318
2562
  const timeout = this._getFindElementTimeout(options);
2319
2563
  await new Promise((resolve) => setTimeout(resolve, 2000));
2320
2564
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2385,7 +2629,7 @@ class StableBrowser {
2385
2629
  await _commandError(state, e, this);
2386
2630
  }
2387
2631
  finally {
2388
- _commandFinally(state, this);
2632
+ await _commandFinally(state, this);
2389
2633
  }
2390
2634
  }
2391
2635
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2398,11 +2642,14 @@ class StableBrowser {
2398
2642
  scroll: false,
2399
2643
  highlight: false,
2400
2644
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2401
- text: `Verify text does not exist in page`,
2645
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2402
2646
  _text: `Verify the text '${text}' does not exist in page`,
2403
2647
  operation: "verifyTextNotExistInPage",
2404
2648
  log: "***** verify text " + text + " does not exist in page *****\n",
2405
2649
  };
2650
+ if (testForRegex(text)) {
2651
+ text = text.replace(/\\"/g, '"');
2652
+ }
2406
2653
  const timeout = this._getFindElementTimeout(options);
2407
2654
  await new Promise((resolve) => setTimeout(resolve, 2000));
2408
2655
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2439,7 +2686,7 @@ class StableBrowser {
2439
2686
  await _commandError(state, e, this);
2440
2687
  }
2441
2688
  finally {
2442
- _commandFinally(state, this);
2689
+ await _commandFinally(state, this);
2443
2690
  }
2444
2691
  }
2445
2692
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2481,7 +2728,7 @@ class StableBrowser {
2481
2728
  };
2482
2729
  while (true) {
2483
2730
  try {
2484
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2731
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2485
2732
  }
2486
2733
  catch (error) {
2487
2734
  // ignore
@@ -2509,7 +2756,7 @@ class StableBrowser {
2509
2756
  const count = await frame.locator(css).count();
2510
2757
  for (let j = 0; j < count; j++) {
2511
2758
  const continer = await frame.locator(css).nth(j);
2512
- 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, {});
2513
2760
  if (result.elementCount > 0) {
2514
2761
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2515
2762
  await this._highlightElements(frame, dataAttribute);
@@ -2550,8 +2797,32 @@ class StableBrowser {
2550
2797
  await _commandError(state, e, this);
2551
2798
  }
2552
2799
  finally {
2553
- _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
+ }
2554
2822
  }
2823
+ // state.info.results = results;
2824
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2825
+ return resultWithElementsFound;
2555
2826
  }
2556
2827
  async visualVerification(text, options = {}, world = null) {
2557
2828
  const startTime = Date.now();
@@ -2869,7 +3140,13 @@ class StableBrowser {
2869
3140
  }
2870
3141
  }
2871
3142
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2872
- 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
+ }
2873
3150
  }
2874
3151
  _getLoadTimeout(options) {
2875
3152
  let timeout = 15000;
@@ -2892,6 +3169,7 @@ class StableBrowser {
2892
3169
  }
2893
3170
  async saveStoreState(path = null, world = null) {
2894
3171
  const storageState = await this.page.context().storageState();
3172
+ path = await this._replaceWithLocalData(path, this.world);
2895
3173
  //const testDataFile = _getDataFile(world, this.context, this);
2896
3174
  if (path) {
2897
3175
  // save { storageState: storageState } into the path
@@ -2902,10 +3180,14 @@ class StableBrowser {
2902
3180
  }
2903
3181
  }
2904
3182
  async restoreSaveState(path = null, world = null) {
3183
+ path = await this._replaceWithLocalData(path, this.world);
2905
3184
  await refreshBrowser(this, path, world);
2906
3185
  this.registerEventListeners(this.context);
2907
3186
  registerNetworkEvents(this.world, this, this.context, this.page);
2908
3187
  registerDownloadEvent(this.page, this.world, this.context);
3188
+ if (this.onRestoreSaveState) {
3189
+ this.onRestoreSaveState(path);
3190
+ }
2909
3191
  }
2910
3192
  async waitForPageLoad(options = {}, world = null) {
2911
3193
  let timeout = this._getLoadTimeout(options);
@@ -2988,7 +3270,94 @@ class StableBrowser {
2988
3270
  await _commandError(state, e, this);
2989
3271
  }
2990
3272
  finally {
2991
- _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);
2992
3361
  }
2993
3362
  }
2994
3363
  saveTestDataAsGlobal(options, world) {
@@ -3093,7 +3462,39 @@ class StableBrowser {
3093
3462
  console.log("#-#");
3094
3463
  }
3095
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) { }
3096
3494
  async beforeStep(world, step) {
3495
+ if (!this.beforeScenarioCalled) {
3496
+ this.beforeScenario(world, step);
3497
+ }
3097
3498
  if (this.stepIndex === undefined) {
3098
3499
  this.stepIndex = 0;
3099
3500
  }
@@ -3110,21 +3511,11 @@ class StableBrowser {
3110
3511
  else {
3111
3512
  this.stepName = "step " + this.stepIndex;
3112
3513
  }
3113
- if (this.context) {
3114
- this.context.examplesRow = extractStepExampleParameters(step);
3115
- }
3116
3514
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3117
3515
  if (this.context.browserObject.context) {
3118
3516
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3119
3517
  }
3120
3518
  }
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
3519
  if (this.initSnapshotTaken === false) {
3129
3520
  this.initSnapshotTaken = true;
3130
3521
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3149,18 +3540,68 @@ class StableBrowser {
3149
3540
  const content = [`- path: ${path}`, `- title: ${title}`];
3150
3541
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3151
3542
  for (let i = 0; i < frames.length; i++) {
3152
- content.push(`- frame: ${i}`);
3153
3543
  const frame = frames[i];
3154
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3155
- 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) { }
3156
3553
  }
3157
3554
  return content.join("\n");
3158
3555
  }
3159
3556
  catch (e) {
3160
- console.error(e);
3557
+ console.log("Error in getAriaSnapshot");
3558
+ //console.debug(e);
3161
3559
  }
3162
3560
  return null;
3163
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
+ }
3164
3605
  async afterStep(world, step) {
3165
3606
  this.stepName = null;
3166
3607
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3168,6 +3609,13 @@ class StableBrowser {
3168
3609
  await this.context.browserObject.context.tracing.stopChunk({
3169
3610
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3170
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`);
3171
3619
  }
3172
3620
  }
3173
3621
  if (this.context) {
@@ -3180,6 +3628,27 @@ class StableBrowser {
3180
3628
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3181
3629
  }
3182
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
+ }
3183
3652
  }
3184
3653
  }
3185
3654
  function createTimedPromise(promise, label) {