automation_model 1.0.627-dev → 1.0.627-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 (48) hide show
  1. package/README.md +130 -0
  2. package/lib/api.js +35 -21
  3. package/lib/api.js.map +1 -1
  4. package/lib/auto_page.d.ts +1 -1
  5. package/lib/auto_page.js +99 -28
  6. package/lib/auto_page.js.map +1 -1
  7. package/lib/browser_manager.js +68 -23
  8. package/lib/browser_manager.js.map +1 -1
  9. package/lib/bruno.d.ts +2 -0
  10. package/lib/bruno.js +381 -0
  11. package/lib/bruno.js.map +1 -0
  12. package/lib/command_common.d.ts +4 -4
  13. package/lib/command_common.js +33 -16
  14. package/lib/command_common.js.map +1 -1
  15. package/lib/environment.d.ts +1 -0
  16. package/lib/environment.js +1 -0
  17. package/lib/environment.js.map +1 -1
  18. package/lib/file_checker.d.ts +1 -0
  19. package/lib/file_checker.js +61 -0
  20. package/lib/file_checker.js.map +1 -0
  21. package/lib/index.d.ts +2 -0
  22. package/lib/index.js +2 -0
  23. package/lib/index.js.map +1 -1
  24. package/lib/init_browser.d.ts +2 -2
  25. package/lib/init_browser.js +33 -27
  26. package/lib/init_browser.js.map +1 -1
  27. package/lib/locate_element.js +2 -2
  28. package/lib/locate_element.js.map +1 -1
  29. package/lib/locator_log.js.map +1 -1
  30. package/lib/network.d.ts +1 -1
  31. package/lib/network.js +5 -5
  32. package/lib/network.js.map +1 -1
  33. package/lib/snapshot_validation.d.ts +37 -0
  34. package/lib/snapshot_validation.js +280 -0
  35. package/lib/snapshot_validation.js.map +1 -0
  36. package/lib/stable_browser.d.ts +22 -3
  37. package/lib/stable_browser.js +594 -113
  38. package/lib/stable_browser.js.map +1 -1
  39. package/lib/table_helper.d.ts +19 -0
  40. package/lib/table_helper.js +116 -0
  41. package/lib/table_helper.js.map +1 -0
  42. package/lib/test_context.d.ts +2 -0
  43. package/lib/test_context.js +2 -0
  44. package/lib/test_context.js.map +1 -1
  45. package/lib/utils.d.ts +8 -4
  46. package/lib/utils.js +221 -20
  47. package/lib/utils.js.map +1 -1
  48. package/package.json +9 -8
@@ -10,19 +10,24 @@ 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 { loadBrunoParams } from "./bruno.js";
27
+ import { snapshotValidation } from "./snapshot_validation.js";
24
28
  export const Types = {
25
29
  CLICK: "click_element",
30
+ WAIT_ELEMENT: "wait_element",
26
31
  NAVIGATE: "navigate",
27
32
  FILL: "fill_element",
28
33
  EXECUTE: "execute_page_method",
@@ -44,6 +49,7 @@ export const Types = {
44
49
  UNCHECK: "uncheck_element",
45
50
  EXTRACT: "extract_attribute",
46
51
  CLOSE_PAGE: "close_page",
52
+ TABLE_OPERATION: "table_operation",
47
53
  SET_DATE_TIME: "set_date_time",
48
54
  SET_VIEWPORT: "set_viewport",
49
55
  VERIFY_VISUAL: "verify_visual",
@@ -52,6 +58,10 @@ export const Types = {
52
58
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
53
59
  VERIFY_ATTRIBUTE: "verify_element_attribute",
54
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",
55
65
  };
56
66
  export const apps = {};
57
67
  const formatElementName = (elementName) => {
@@ -177,6 +187,30 @@ class StableBrowser {
177
187
  await this.waitForPageLoad();
178
188
  }
179
189
  }
190
+ async switchTab(tabTitleOrIndex) {
191
+ // first check if the tabNameOrIndex is a number
192
+ let index = parseInt(tabTitleOrIndex);
193
+ if (!isNaN(index)) {
194
+ if (index >= 0 && index < this.context.pages.length) {
195
+ this.page = this.context.pages[index];
196
+ this.context.page = this.page;
197
+ await this.page.bringToFront();
198
+ return;
199
+ }
200
+ }
201
+ // if the tabNameOrIndex is a string, find the tab by name
202
+ for (let i = 0; i < this.context.pages.length; i++) {
203
+ let page = this.context.pages[i];
204
+ let title = await page.title();
205
+ if (title.includes(tabTitleOrIndex)) {
206
+ this.page = page;
207
+ this.context.page = this.page;
208
+ await this.page.bringToFront();
209
+ return;
210
+ }
211
+ }
212
+ throw new Error("Tab not found: " + tabTitleOrIndex);
213
+ }
180
214
  registerConsoleLogListener(page, context) {
181
215
  if (!this.context.webLogger) {
182
216
  this.context.webLogger = [];
@@ -272,7 +306,7 @@ class StableBrowser {
272
306
  _commandError(state, error, this);
273
307
  }
274
308
  finally {
275
- _commandFinally(state, this);
309
+ await _commandFinally(state, this);
276
310
  }
277
311
  }
278
312
  async _getLocator(locator, scope, _params) {
@@ -353,7 +387,7 @@ class StableBrowser {
353
387
  return resultCss;
354
388
  }
355
389
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
356
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
390
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
357
391
  const locator = scope.locator(query);
358
392
  const count = await locator.count();
359
393
  if (!tag1) {
@@ -373,6 +407,12 @@ class StableBrowser {
373
407
  if (!el.setAttribute) {
374
408
  el = el.parentElement;
375
409
  }
410
+ // remove any attributes start with data-blinq-id
411
+ // for (let i = 0; i < el.attributes.length; i++) {
412
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
413
+ // el.removeAttribute(el.attributes[i].name);
414
+ // }
415
+ // }
376
416
  el.setAttribute("data-blinq-id-" + randomToken, "");
377
417
  return true;
378
418
  }, [tag1, randomToken]))) {
@@ -382,7 +422,7 @@ class StableBrowser {
382
422
  }
383
423
  return { elementCount: tagCount, randomToken };
384
424
  }
385
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
425
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
386
426
  if (!info) {
387
427
  info = {};
388
428
  }
@@ -449,7 +489,7 @@ class StableBrowser {
449
489
  }
450
490
  return;
451
491
  }
452
- if (info.locatorLog && count === 0) {
492
+ if (info.locatorLog && count === 0 && logErrors) {
453
493
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
454
494
  }
455
495
  for (let j = 0; j < count; j++) {
@@ -464,7 +504,7 @@ class StableBrowser {
464
504
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
465
505
  }
466
506
  }
467
- else {
507
+ else if (logErrors) {
468
508
  info.failCause.visible = visible;
469
509
  info.failCause.enabled = enabled;
470
510
  if (!info.printMessages) {
@@ -559,12 +599,24 @@ class StableBrowser {
559
599
  element.evaluate((el, randomToken) => {
560
600
  el.setAttribute("data-blinq-id-" + randomToken, "");
561
601
  }, randomToken);
562
- if (element._frame) {
563
- return element;
564
- }
565
- const scope = element.page();
566
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
567
- return newSelector;
602
+ // if (element._frame) {
603
+ // return element;
604
+ // }
605
+ const scope = element._frame ?? element.page();
606
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
607
+ let prefixSelector = "";
608
+ const frameControlSelector = " >> internal:control=enter-frame";
609
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
610
+ if (frameSelectorIndex !== -1) {
611
+ // remove everything after the >> internal:control=enter-frame
612
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
613
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
614
+ }
615
+ // if (element?._frame?._selector) {
616
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
617
+ // }
618
+ const newSelector = prefixSelector + newElementSelector;
619
+ return scope.locator(newSelector);
568
620
  }
569
621
  }
570
622
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -716,14 +768,9 @@ class StableBrowser {
716
768
  // info.log += "scanning locators in priority 2" + "\n";
717
769
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
718
770
  }
719
- if (result.foundElements.length === 0 && onlyPriority3) {
771
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
720
772
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
721
773
  }
722
- else {
723
- if (result.foundElements.length === 0 && !highPriorityOnly) {
724
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
725
- }
726
- }
727
774
  let foundElements = result.foundElements;
728
775
  if (foundElements.length === 1 && foundElements[0].unique) {
729
776
  info.box = foundElements[0].box;
@@ -778,6 +825,11 @@ class StableBrowser {
778
825
  visibleOnly = false;
779
826
  }
780
827
  await new Promise((resolve) => setTimeout(resolve, 1000));
828
+ // sheck of more of half of the timeout has passed
829
+ if (Date.now() - startTime > timeout / 2) {
830
+ highPriorityOnly = false;
831
+ visibleOnly = false;
832
+ }
781
833
  }
782
834
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
783
835
  // if (info.locatorLog) {
@@ -793,7 +845,7 @@ class StableBrowser {
793
845
  }
794
846
  throw new Error("failed to locate first element no elements found, " + info.log);
795
847
  }
796
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
848
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
797
849
  let foundElements = [];
798
850
  const result = {
799
851
  foundElements: foundElements,
@@ -812,7 +864,9 @@ class StableBrowser {
812
864
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
813
865
  }
814
866
  catch (e) {
815
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
867
+ if (logErrors) {
868
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
869
+ }
816
870
  }
817
871
  }
818
872
  if (foundLocators.length === 1) {
@@ -824,9 +878,40 @@ class StableBrowser {
824
878
  result.locatorIndex = i;
825
879
  }
826
880
  if (foundLocators.length > 1) {
827
- info.failCause.foundMultiple = true;
828
- if (info.locatorLog) {
829
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
881
+ // remove elements that consume the same space with 10 pixels tolerance
882
+ const boxes = [];
883
+ for (let j = 0; j < foundLocators.length; j++) {
884
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
885
+ }
886
+ for (let j = 0; j < boxes.length; j++) {
887
+ for (let k = 0; k < boxes.length; k++) {
888
+ if (j === k) {
889
+ continue;
890
+ }
891
+ // check if x, y, width, height are the same with 10 pixels tolerance
892
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
893
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
894
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
895
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
896
+ // as the element is not unique, will remove it
897
+ boxes.splice(k, 1);
898
+ k--;
899
+ }
900
+ }
901
+ }
902
+ if (boxes.length === 1) {
903
+ result.foundElements.push({
904
+ locator: boxes[0].locator.first(),
905
+ box: boxes[0].box,
906
+ unique: true,
907
+ });
908
+ result.locatorIndex = i;
909
+ }
910
+ else if (logErrors) {
911
+ info.failCause.foundMultiple = true;
912
+ if (info.locatorLog) {
913
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
914
+ }
830
915
  }
831
916
  }
832
917
  }
@@ -874,7 +959,7 @@ class StableBrowser {
874
959
  await _commandError(state, "timeout looking for " + elementDescription, this);
875
960
  }
876
961
  finally {
877
- _commandFinally(state, this);
962
+ await _commandFinally(state, this);
878
963
  }
879
964
  }
880
965
  }
@@ -923,7 +1008,7 @@ class StableBrowser {
923
1008
  await _commandError(state, "timeout looking for " + elementDescription, this);
924
1009
  }
925
1010
  finally {
926
- _commandFinally(state, this);
1011
+ await _commandFinally(state, this);
927
1012
  }
928
1013
  }
929
1014
  }
@@ -944,19 +1029,7 @@ class StableBrowser {
944
1029
  };
945
1030
  try {
946
1031
  await _preCommand(state, this);
947
- // if (state.options && state.options.context) {
948
- // state.selectors.locators[0].text = state.options.context;
949
- // }
950
- try {
951
- await state.element.click();
952
- // await new Promise((resolve) => setTimeout(resolve, 1000));
953
- }
954
- catch (e) {
955
- // await this.closeUnexpectedPopups();
956
- state.element = await this._locate(selectors, state.info, _params);
957
- await state.element.dispatchEvent("click");
958
- // await new Promise((resolve) => setTimeout(resolve, 1000));
959
- }
1032
+ await performAction("click", state.element, options, this, state, _params);
960
1033
  await this.waitForPageLoad();
961
1034
  return state.info;
962
1035
  }
@@ -964,9 +1037,41 @@ class StableBrowser {
964
1037
  await _commandError(state, e, this);
965
1038
  }
966
1039
  finally {
967
- _commandFinally(state, this);
1040
+ await _commandFinally(state, this);
968
1041
  }
969
1042
  }
1043
+ async waitForElement(selectors, _params, options = {}, world = null) {
1044
+ const timeout = this._getFindElementTimeout(options);
1045
+ const state = {
1046
+ selectors,
1047
+ _params,
1048
+ options,
1049
+ world,
1050
+ text: "Wait for element",
1051
+ _text: "Wait for " + selectors.element_name,
1052
+ type: Types.WAIT_ELEMENT,
1053
+ operation: "waitForElement",
1054
+ log: "***** wait for " + selectors.element_name + " *****\n",
1055
+ };
1056
+ let found = false;
1057
+ try {
1058
+ await _preCommand(state, this);
1059
+ // if (state.options && state.options.context) {
1060
+ // state.selectors.locators[0].text = state.options.context;
1061
+ // }
1062
+ await state.element.waitFor({ timeout: timeout });
1063
+ found = true;
1064
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1065
+ }
1066
+ catch (e) {
1067
+ console.error("Error on waitForElement", e);
1068
+ // await _commandError(state, e, this);
1069
+ }
1070
+ finally {
1071
+ await _commandFinally(state, this);
1072
+ }
1073
+ return found;
1074
+ }
970
1075
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
971
1076
  const state = {
972
1077
  selectors,
@@ -987,7 +1092,7 @@ class StableBrowser {
987
1092
  try {
988
1093
  // if (world && world.screenshot && !world.screenshotPath) {
989
1094
  // console.log(`Highlighting while running from recorder`);
990
- await this._highlightElements(element);
1095
+ await this._highlightElements(state.element);
991
1096
  await state.element.setChecked(checked);
992
1097
  await new Promise((resolve) => setTimeout(resolve, 1000));
993
1098
  // await this._unHighlightElements(element);
@@ -1014,7 +1119,7 @@ class StableBrowser {
1014
1119
  await _commandError(state, e, this);
1015
1120
  }
1016
1121
  finally {
1017
- _commandFinally(state, this);
1122
+ await _commandFinally(state, this);
1018
1123
  }
1019
1124
  }
1020
1125
  async hover(selectors, _params, options = {}, world = null) {
@@ -1031,19 +1136,7 @@ class StableBrowser {
1031
1136
  };
1032
1137
  try {
1033
1138
  await _preCommand(state, this);
1034
- try {
1035
- await state.element.hover();
1036
- // await _screenshot(state, this);
1037
- await new Promise((resolve) => setTimeout(resolve, 1000));
1038
- }
1039
- catch (e) {
1040
- //await this.closeUnexpectedPopups();
1041
- state.info.log += "hover failed, will try again" + "\n";
1042
- state.element = await this._locate(selectors, state.info, _params);
1043
- await state.element.hover({ timeout: 10000 });
1044
- // await _screenshot(state, this);
1045
- await new Promise((resolve) => setTimeout(resolve, 1000));
1046
- }
1139
+ await performAction("hover", state.element, options, this, state, _params);
1047
1140
  await _screenshot(state, this);
1048
1141
  await this.waitForPageLoad();
1049
1142
  return state.info;
@@ -1052,7 +1145,7 @@ class StableBrowser {
1052
1145
  await _commandError(state, e, this);
1053
1146
  }
1054
1147
  finally {
1055
- _commandFinally(state, this);
1148
+ await _commandFinally(state, this);
1056
1149
  }
1057
1150
  }
1058
1151
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1088,7 +1181,7 @@ class StableBrowser {
1088
1181
  await _commandError(state, e, this);
1089
1182
  }
1090
1183
  finally {
1091
- _commandFinally(state, this);
1184
+ await _commandFinally(state, this);
1092
1185
  }
1093
1186
  }
1094
1187
  async type(_value, _params = null, options = {}, world = null) {
@@ -1134,7 +1227,7 @@ class StableBrowser {
1134
1227
  await _commandError(state, e, this);
1135
1228
  }
1136
1229
  finally {
1137
- _commandFinally(state, this);
1230
+ await _commandFinally(state, this);
1138
1231
  }
1139
1232
  }
1140
1233
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1170,7 +1263,7 @@ class StableBrowser {
1170
1263
  await _commandError(state, e, this);
1171
1264
  }
1172
1265
  finally {
1173
- _commandFinally(state, this);
1266
+ await _commandFinally(state, this);
1174
1267
  }
1175
1268
  }
1176
1269
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1190,7 +1283,7 @@ class StableBrowser {
1190
1283
  try {
1191
1284
  await _preCommand(state, this);
1192
1285
  try {
1193
- await state.element.click();
1286
+ await performAction("click", state.element, options, this, state, _params);
1194
1287
  await new Promise((resolve) => setTimeout(resolve, 500));
1195
1288
  if (format) {
1196
1289
  state.value = dayjs(state.value).format(format);
@@ -1239,7 +1332,7 @@ class StableBrowser {
1239
1332
  await _commandError(state, e, this);
1240
1333
  }
1241
1334
  finally {
1242
- _commandFinally(state, this);
1335
+ await _commandFinally(state, this);
1243
1336
  }
1244
1337
  }
1245
1338
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1258,6 +1351,9 @@ class StableBrowser {
1258
1351
  operation: "clickType",
1259
1352
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1260
1353
  };
1354
+ if (!options) {
1355
+ options = {};
1356
+ }
1261
1357
  if (newValue !== _value) {
1262
1358
  //this.logger.info(_value + "=" + newValue);
1263
1359
  _value = newValue;
@@ -1265,7 +1361,7 @@ class StableBrowser {
1265
1361
  try {
1266
1362
  await _preCommand(state, this);
1267
1363
  state.info.value = _value;
1268
- if (options === null || options === undefined || !options.press) {
1364
+ if (!options.press) {
1269
1365
  try {
1270
1366
  let currentValue = await state.element.inputValue();
1271
1367
  if (currentValue) {
@@ -1276,13 +1372,9 @@ class StableBrowser {
1276
1372
  this.logger.info("unable to clear input value");
1277
1373
  }
1278
1374
  }
1279
- if (options === null || options === undefined || options.press) {
1280
- try {
1281
- await state.element.click({ timeout: 5000 });
1282
- }
1283
- catch (e) {
1284
- await state.element.dispatchEvent("click");
1285
- }
1375
+ if (options.press) {
1376
+ options.timeout = 5000;
1377
+ await performAction("click", state.element, options, this, state, _params);
1286
1378
  }
1287
1379
  else {
1288
1380
  try {
@@ -1340,7 +1432,7 @@ class StableBrowser {
1340
1432
  await _commandError(state, e, this);
1341
1433
  }
1342
1434
  finally {
1343
- _commandFinally(state, this);
1435
+ await _commandFinally(state, this);
1344
1436
  }
1345
1437
  }
1346
1438
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1370,7 +1462,42 @@ class StableBrowser {
1370
1462
  await _commandError(state, e, this);
1371
1463
  }
1372
1464
  finally {
1373
- _commandFinally(state, this);
1465
+ await _commandFinally(state, this);
1466
+ }
1467
+ }
1468
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1469
+ const state = {
1470
+ selectors,
1471
+ _params,
1472
+ files,
1473
+ value: '"' + files.join('", "') + '"',
1474
+ options,
1475
+ world,
1476
+ type: Types.SET_INPUT_FILES,
1477
+ text: `Set input files`,
1478
+ _text: `Set input files on ${selectors.element_name}`,
1479
+ operation: "setInputFiles",
1480
+ log: "***** set input files " + selectors.element_name + " *****\n",
1481
+ };
1482
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1483
+ try {
1484
+ await _preCommand(state, this);
1485
+ for (let i = 0; i < files.length; i++) {
1486
+ const file = files[i];
1487
+ const filePath = path.join(uploadsFolder, file);
1488
+ if (!fs.existsSync(filePath)) {
1489
+ throw new Error(`File not found: ${filePath}`);
1490
+ }
1491
+ state.files[i] = filePath;
1492
+ }
1493
+ await state.element.setInputFiles(files);
1494
+ return state.info;
1495
+ }
1496
+ catch (e) {
1497
+ await _commandError(state, e, this);
1498
+ }
1499
+ finally {
1500
+ await _commandFinally(state, this);
1374
1501
  }
1375
1502
  }
1376
1503
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1486,7 +1613,7 @@ class StableBrowser {
1486
1613
  await _commandError(state, e, this);
1487
1614
  }
1488
1615
  finally {
1489
- _commandFinally(state, this);
1616
+ await _commandFinally(state, this);
1490
1617
  }
1491
1618
  }
1492
1619
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1521,7 +1648,7 @@ class StableBrowser {
1521
1648
  while (Date.now() - startTime < timeout) {
1522
1649
  try {
1523
1650
  await _preCommand(state, this);
1524
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1651
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1525
1652
  if (foundObj && foundObj.element) {
1526
1653
  await this.scrollIfNeeded(foundObj.element, state.info);
1527
1654
  }
@@ -1563,7 +1690,79 @@ class StableBrowser {
1563
1690
  throw e;
1564
1691
  }
1565
1692
  finally {
1566
- _commandFinally(state, this);
1693
+ await _commandFinally(state, this);
1694
+ }
1695
+ }
1696
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1697
+ const timeout = this._getFindElementTimeout(options);
1698
+ const startTime = Date.now();
1699
+ const state = {
1700
+ _params,
1701
+ value: referanceSnapshot,
1702
+ options,
1703
+ world,
1704
+ locate: false,
1705
+ scroll: false,
1706
+ screenshot: true,
1707
+ highlight: false,
1708
+ type: Types.SNAPSHOT_VALIDATION,
1709
+ text: `verify snapshot: ${referanceSnapshot}`,
1710
+ operation: "snapshotValidation",
1711
+ log: "***** verify snapshot *****\n",
1712
+ };
1713
+ if (!referanceSnapshot) {
1714
+ throw new Error("referanceSnapshot is null");
1715
+ }
1716
+ let text = null;
1717
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1718
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1719
+ }
1720
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1721
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1722
+ }
1723
+ else if (referanceSnapshot.startsWith("yaml:")) {
1724
+ text = referanceSnapshot.substring(5);
1725
+ }
1726
+ else {
1727
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1728
+ }
1729
+ state.text = text;
1730
+ const newValue = await this._replaceWithLocalData(text, world);
1731
+ await _preCommand(state, this);
1732
+ let foundObj = null;
1733
+ try {
1734
+ let matchResult = null;
1735
+ while (Date.now() - startTime < timeout) {
1736
+ try {
1737
+ let scope = null;
1738
+ if (!frameSelectors) {
1739
+ scope = this.page;
1740
+ }
1741
+ else {
1742
+ scope = await this._findFrameScope({ nestFrmLoc: frameSelectors }, timeout, state.info);
1743
+ }
1744
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1745
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1746
+ if (matchResult.errorLine !== -1) {
1747
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1748
+ }
1749
+ // highlight and screenshot
1750
+ return state.info;
1751
+ }
1752
+ catch (e) {
1753
+ // Log error but continue retrying until timeout is reached
1754
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1755
+ }
1756
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1757
+ }
1758
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1759
+ }
1760
+ catch (e) {
1761
+ await _commandError(state, e, this);
1762
+ throw e;
1763
+ }
1764
+ finally {
1765
+ await _commandFinally(state, this);
1567
1766
  }
1568
1767
  }
1569
1768
  async waitForUserInput(message, world = null) {
@@ -1601,6 +1800,15 @@ class StableBrowser {
1601
1800
  // save the data to the file
1602
1801
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1603
1802
  }
1803
+ overwriteTestData(testData, world = null) {
1804
+ if (!testData) {
1805
+ return;
1806
+ }
1807
+ // if data file exists, load it
1808
+ const dataFile = _getDataFile(world, this.context, this);
1809
+ // save the data to the file
1810
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1811
+ }
1604
1812
  _getDataFilePath(fileName) {
1605
1813
  let dataFile = path.join(this.project_path, "data", fileName);
1606
1814
  if (fs.existsSync(dataFile)) {
@@ -1853,7 +2061,7 @@ class StableBrowser {
1853
2061
  await _commandError(state, e, this);
1854
2062
  }
1855
2063
  finally {
1856
- _commandFinally(state, this);
2064
+ await _commandFinally(state, this);
1857
2065
  }
1858
2066
  }
1859
2067
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1884,10 +2092,31 @@ class StableBrowser {
1884
2092
  case "value":
1885
2093
  state.value = await state.element.inputValue();
1886
2094
  break;
2095
+ case "text":
2096
+ state.value = await state.element.textContent();
2097
+ break;
1887
2098
  default:
1888
2099
  state.value = await state.element.getAttribute(attribute);
1889
2100
  break;
1890
2101
  }
2102
+ if (options !== null) {
2103
+ if (options.regex && options.regex !== "") {
2104
+ // Construct a regex pattern from the provided string
2105
+ const regex = options.regex.slice(1, -1);
2106
+ const regexPattern = new RegExp(regex, "g");
2107
+ const matches = state.value.match(regexPattern);
2108
+ if (matches) {
2109
+ let newValue = "";
2110
+ for (const match of matches) {
2111
+ newValue += match;
2112
+ }
2113
+ state.value = newValue;
2114
+ }
2115
+ }
2116
+ if (options.trimSpaces && options.trimSpaces === true) {
2117
+ state.value = state.value.trim();
2118
+ }
2119
+ }
1891
2120
  state.info.value = state.value;
1892
2121
  this.setTestData({ [variable]: state.value }, world);
1893
2122
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1898,7 +2127,7 @@ class StableBrowser {
1898
2127
  await _commandError(state, e, this);
1899
2128
  }
1900
2129
  finally {
1901
- _commandFinally(state, this);
2130
+ await _commandFinally(state, this);
1902
2131
  }
1903
2132
  }
1904
2133
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1923,12 +2152,15 @@ class StableBrowser {
1923
2152
  let expectedValue;
1924
2153
  try {
1925
2154
  await _preCommand(state, this);
1926
- expectedValue = state.value;
2155
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1927
2156
  state.info.expectedValue = expectedValue;
1928
2157
  switch (attribute) {
1929
2158
  case "innerText":
1930
2159
  val = String(await state.element.innerText());
1931
2160
  break;
2161
+ case "text":
2162
+ val = String(await state.element.textContent());
2163
+ break;
1932
2164
  case "value":
1933
2165
  val = String(await state.element.inputValue());
1934
2166
  break;
@@ -1950,17 +2182,42 @@ class StableBrowser {
1950
2182
  let regex;
1951
2183
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1952
2184
  const patternBody = expectedValue.slice(1, -1);
1953
- regex = new RegExp(patternBody, "g");
2185
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2186
+ regex = new RegExp(processedPattern, "gs");
2187
+ state.info.regex = true;
1954
2188
  }
1955
2189
  else {
1956
2190
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1957
2191
  regex = new RegExp(escapedPattern, "g");
1958
2192
  }
1959
- if (!val.match(regex)) {
1960
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
1961
- state.info.failCause.assertionFailed = true;
1962
- state.info.failCause.lastError = errorMessage;
1963
- throw new Error(errorMessage);
2193
+ if (attribute === "innerText") {
2194
+ if (state.info.regex) {
2195
+ if (!regex.test(val)) {
2196
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2197
+ state.info.failCause.assertionFailed = true;
2198
+ state.info.failCause.lastError = errorMessage;
2199
+ throw new Error(errorMessage);
2200
+ }
2201
+ }
2202
+ else {
2203
+ const valLines = val.split("\n");
2204
+ const expectedLines = expectedValue.split("\n");
2205
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2206
+ if (!isPart) {
2207
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2208
+ state.info.failCause.assertionFailed = true;
2209
+ state.info.failCause.lastError = errorMessage;
2210
+ throw new Error(errorMessage);
2211
+ }
2212
+ }
2213
+ }
2214
+ else {
2215
+ if (!val.match(regex)) {
2216
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2217
+ state.info.failCause.assertionFailed = true;
2218
+ state.info.failCause.lastError = errorMessage;
2219
+ throw new Error(errorMessage);
2220
+ }
1964
2221
  }
1965
2222
  return state.info;
1966
2223
  }
@@ -1968,7 +2225,7 @@ class StableBrowser {
1968
2225
  await _commandError(state, e, this);
1969
2226
  }
1970
2227
  finally {
1971
- _commandFinally(state, this);
2228
+ await _commandFinally(state, this);
1972
2229
  }
1973
2230
  }
1974
2231
  async extractEmailData(emailAddress, options, world) {
@@ -2213,7 +2470,7 @@ class StableBrowser {
2213
2470
  Object.assign(e, { info: info });
2214
2471
  error = e;
2215
2472
  // throw e;
2216
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2473
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
2217
2474
  }
2218
2475
  finally {
2219
2476
  const endTime = Date.now();
@@ -2238,27 +2495,89 @@ class StableBrowser {
2238
2495
  });
2239
2496
  }
2240
2497
  }
2241
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2498
+ async verifyPageTitle(title, options = {}, world = null) {
2499
+ const startTime = Date.now();
2500
+ let error = null;
2501
+ let screenshotId = null;
2502
+ let screenshotPath = null;
2503
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2504
+ const info = {};
2505
+ info.log = "***** verify page title " + title + " *****\n";
2506
+ info.operation = "verifyPageTitle";
2507
+ const newValue = await this._replaceWithLocalData(title, world);
2508
+ if (newValue !== title) {
2509
+ this.logger.info(title + "=" + newValue);
2510
+ title = newValue;
2511
+ }
2512
+ info.title = title;
2513
+ try {
2514
+ for (let i = 0; i < 30; i++) {
2515
+ const foundTitle = await this.page.title();
2516
+ if (!foundTitle.includes(title)) {
2517
+ if (i === 29) {
2518
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2519
+ }
2520
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2521
+ continue;
2522
+ }
2523
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2524
+ return info;
2525
+ }
2526
+ }
2527
+ catch (e) {
2528
+ //await this.closeUnexpectedPopups();
2529
+ this.logger.error("verify page title failed " + info.log);
2530
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2531
+ info.screenshotPath = screenshotPath;
2532
+ Object.assign(e, { info: info });
2533
+ error = e;
2534
+ // throw e;
2535
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2536
+ }
2537
+ finally {
2538
+ const endTime = Date.now();
2539
+ _reportToWorld(world, {
2540
+ type: Types.VERIFY_PAGE_PATH,
2541
+ text: "Verify page title",
2542
+ _text: "Verify the page title contains " + title,
2543
+ screenshotId,
2544
+ result: error
2545
+ ? {
2546
+ status: "FAILED",
2547
+ startTime,
2548
+ endTime,
2549
+ message: error?.message,
2550
+ }
2551
+ : {
2552
+ status: "PASSED",
2553
+ startTime,
2554
+ endTime,
2555
+ },
2556
+ info: info,
2557
+ });
2558
+ }
2559
+ }
2560
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2242
2561
  const frames = this.page.frames();
2243
2562
  let results = [];
2244
- let ignoreCase = false;
2563
+ // let ignoreCase = false;
2245
2564
  for (let i = 0; i < frames.length; i++) {
2246
2565
  if (dateAlternatives.date) {
2247
2566
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2248
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2567
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2249
2568
  result.frame = frames[i];
2250
2569
  results.push(result);
2251
2570
  }
2252
2571
  }
2253
2572
  else if (numberAlternatives.number) {
2254
2573
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2255
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2574
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2256
2575
  result.frame = frames[i];
2257
2576
  results.push(result);
2258
2577
  }
2259
2578
  }
2260
2579
  else {
2261
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2580
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2262
2581
  result.frame = frames[i];
2263
2582
  results.push(result);
2264
2583
  }
@@ -2277,11 +2596,14 @@ class StableBrowser {
2277
2596
  scroll: false,
2278
2597
  highlight: false,
2279
2598
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2280
- text: `Verify text exists in page`,
2599
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2281
2600
  _text: `Verify the text '${text}' exists in page`,
2282
2601
  operation: "verifyTextExistInPage",
2283
2602
  log: "***** verify text " + text + " exists in page *****\n",
2284
2603
  };
2604
+ if (testForRegex(text)) {
2605
+ text = text.replace(/\\"/g, '"');
2606
+ }
2285
2607
  const timeout = this._getFindElementTimeout(options);
2286
2608
  await new Promise((resolve) => setTimeout(resolve, 2000));
2287
2609
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2352,7 +2674,7 @@ class StableBrowser {
2352
2674
  await _commandError(state, e, this);
2353
2675
  }
2354
2676
  finally {
2355
- _commandFinally(state, this);
2677
+ await _commandFinally(state, this);
2356
2678
  }
2357
2679
  }
2358
2680
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2365,11 +2687,14 @@ class StableBrowser {
2365
2687
  scroll: false,
2366
2688
  highlight: false,
2367
2689
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2368
- text: `Verify text does not exist in page`,
2690
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2369
2691
  _text: `Verify the text '${text}' does not exist in page`,
2370
2692
  operation: "verifyTextNotExistInPage",
2371
2693
  log: "***** verify text " + text + " does not exist in page *****\n",
2372
2694
  };
2695
+ if (testForRegex(text)) {
2696
+ text = text.replace(/\\"/g, '"');
2697
+ }
2373
2698
  const timeout = this._getFindElementTimeout(options);
2374
2699
  await new Promise((resolve) => setTimeout(resolve, 2000));
2375
2700
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2406,7 +2731,7 @@ class StableBrowser {
2406
2731
  await _commandError(state, e, this);
2407
2732
  }
2408
2733
  finally {
2409
- _commandFinally(state, this);
2734
+ await _commandFinally(state, this);
2410
2735
  }
2411
2736
  }
2412
2737
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2448,7 +2773,7 @@ class StableBrowser {
2448
2773
  };
2449
2774
  while (true) {
2450
2775
  try {
2451
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2776
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2452
2777
  }
2453
2778
  catch (error) {
2454
2779
  // ignore
@@ -2476,7 +2801,7 @@ class StableBrowser {
2476
2801
  const count = await frame.locator(css).count();
2477
2802
  for (let j = 0; j < count; j++) {
2478
2803
  const continer = await frame.locator(css).nth(j);
2479
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2804
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2480
2805
  if (result.elementCount > 0) {
2481
2806
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2482
2807
  await this._highlightElements(frame, dataAttribute);
@@ -2517,9 +2842,33 @@ class StableBrowser {
2517
2842
  await _commandError(state, e, this);
2518
2843
  }
2519
2844
  finally {
2520
- _commandFinally(state, this);
2845
+ await _commandFinally(state, this);
2521
2846
  }
2522
2847
  }
2848
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2849
+ const frames = this.page.frames();
2850
+ let results = [];
2851
+ let ignoreCase = false;
2852
+ for (let i = 0; i < frames.length; i++) {
2853
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2854
+ result.frame = frames[i];
2855
+ const climbArray = [];
2856
+ for (let i = 0; i < climb; i++) {
2857
+ climbArray.push("..");
2858
+ }
2859
+ let climbXpath = "xpath=" + climbArray.join("/");
2860
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2861
+ const count = await frames[i].locator(newLocator).count();
2862
+ if (count > 0) {
2863
+ result.elementCount = count;
2864
+ result.locator = newLocator;
2865
+ results.push(result);
2866
+ }
2867
+ }
2868
+ // state.info.results = results;
2869
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2870
+ return resultWithElementsFound;
2871
+ }
2523
2872
  async visualVerification(text, options = {}, world = null) {
2524
2873
  const startTime = Date.now();
2525
2874
  let error = null;
@@ -2836,7 +3185,13 @@ class StableBrowser {
2836
3185
  }
2837
3186
  }
2838
3187
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2839
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3188
+ try {
3189
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3190
+ }
3191
+ catch (error) {
3192
+ this.logger.debug(error);
3193
+ throw error;
3194
+ }
2840
3195
  }
2841
3196
  _getLoadTimeout(options) {
2842
3197
  let timeout = 15000;
@@ -2873,6 +3228,9 @@ class StableBrowser {
2873
3228
  this.registerEventListeners(this.context);
2874
3229
  registerNetworkEvents(this.world, this, this.context, this.page);
2875
3230
  registerDownloadEvent(this.page, this.world, this.context);
3231
+ if (this.onRestoreSaveState) {
3232
+ this.onRestoreSaveState(path);
3233
+ }
2876
3234
  }
2877
3235
  async waitForPageLoad(options = {}, world = null) {
2878
3236
  let timeout = this._getLoadTimeout(options);
@@ -2955,11 +3313,98 @@ class StableBrowser {
2955
3313
  await _commandError(state, e, this);
2956
3314
  }
2957
3315
  finally {
2958
- _commandFinally(state, this);
3316
+ await _commandFinally(state, this);
3317
+ }
3318
+ }
3319
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3320
+ let operation = null;
3321
+ if (!options || !options.operation) {
3322
+ throw new Error("operation is not defined");
3323
+ }
3324
+ operation = options.operation;
3325
+ // validate operation is one of the supported operations
3326
+ if (operation != "click" && operation != "hover+click") {
3327
+ throw new Error("operation is not supported");
3328
+ }
3329
+ const state = {
3330
+ options,
3331
+ world,
3332
+ locate: false,
3333
+ scroll: false,
3334
+ highlight: false,
3335
+ type: Types.TABLE_OPERATION,
3336
+ text: `Table operation`,
3337
+ _text: `Table ${operation} operation`,
3338
+ operation: operation,
3339
+ log: "***** Table operation *****\n",
3340
+ };
3341
+ const timeout = this._getFindElementTimeout(options);
3342
+ try {
3343
+ await _preCommand(state, this);
3344
+ const start = Date.now();
3345
+ let cellArea = null;
3346
+ while (true) {
3347
+ try {
3348
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3349
+ if (cellArea) {
3350
+ break;
3351
+ }
3352
+ }
3353
+ catch (e) {
3354
+ // ignore
3355
+ }
3356
+ if (Date.now() - start > timeout) {
3357
+ throw new Error(`Cell not found in table`);
3358
+ }
3359
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3360
+ }
3361
+ switch (operation) {
3362
+ case "click":
3363
+ if (!options.css) {
3364
+ // will click in the center of the cell
3365
+ let xOffset = 0;
3366
+ let yOffset = 0;
3367
+ if (options.xOffset) {
3368
+ xOffset = options.xOffset;
3369
+ }
3370
+ if (options.yOffset) {
3371
+ yOffset = options.yOffset;
3372
+ }
3373
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3374
+ }
3375
+ else {
3376
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3377
+ if (results.length === 0) {
3378
+ throw new Error(`Element not found in cell area`);
3379
+ }
3380
+ state.element = results[0];
3381
+ await performAction("click", state.element, options, this, state, _params);
3382
+ }
3383
+ break;
3384
+ case "hover+click":
3385
+ if (!options.css) {
3386
+ throw new Error("css is not defined");
3387
+ }
3388
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3389
+ if (results.length === 0) {
3390
+ throw new Error(`Element not found in cell area`);
3391
+ }
3392
+ state.element = results[0];
3393
+ await performAction("hover+click", state.element, options, this, state, _params);
3394
+ break;
3395
+ default:
3396
+ throw new Error("operation is not supported");
3397
+ }
3398
+ }
3399
+ catch (e) {
3400
+ await _commandError(state, e, this);
3401
+ }
3402
+ finally {
3403
+ await _commandFinally(state, this);
2959
3404
  }
2960
3405
  }
2961
3406
  saveTestDataAsGlobal(options, world) {
2962
- const dataFile = this._getDataFile(world);
3407
+ const dataFile = _getDataFile(world, this.context, this);
2963
3408
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2964
3409
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2965
3410
  }
@@ -3060,7 +3505,39 @@ class StableBrowser {
3060
3505
  console.log("#-#");
3061
3506
  }
3062
3507
  }
3508
+ async beforeScenario(world, scenario) {
3509
+ this.beforeScenarioCalled = true;
3510
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3511
+ this.scenarioName = scenario.pickle.name;
3512
+ }
3513
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3514
+ this.featureName = scenario.gherkinDocument.feature.name;
3515
+ }
3516
+ if (this.context) {
3517
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3518
+ }
3519
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3520
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3521
+ // check if @global_test_data tag is present
3522
+ if (this.tags.includes("@global_test_data")) {
3523
+ this.saveTestDataAsGlobal({}, world);
3524
+ }
3525
+ }
3526
+ // update test data based on feature/scenario
3527
+ let envName = null;
3528
+ if (this.context && this.context.environment) {
3529
+ envName = this.context.environment.name;
3530
+ }
3531
+ if (!process.env.TEMP_RUN) {
3532
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3533
+ }
3534
+ await loadBrunoParams(this.context, this.context.environment.name);
3535
+ }
3536
+ async afterScenario(world, scenario) { }
3063
3537
  async beforeStep(world, step) {
3538
+ if (!this.beforeScenarioCalled) {
3539
+ this.beforeScenario(world, step);
3540
+ }
3064
3541
  if (this.stepIndex === undefined) {
3065
3542
  this.stepIndex = 0;
3066
3543
  }
@@ -3077,21 +3554,11 @@ class StableBrowser {
3077
3554
  else {
3078
3555
  this.stepName = "step " + this.stepIndex;
3079
3556
  }
3080
- if (this.context) {
3081
- this.context.examplesRow = extractStepExampleParameters(step);
3082
- }
3083
3557
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3084
3558
  if (this.context.browserObject.context) {
3085
3559
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3086
3560
  }
3087
3561
  }
3088
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3089
- this.tags = step.pickle.tags.map((tag) => tag.name);
3090
- // check if @global_test_data tag is present
3091
- if (this.tags.includes("@global_test_data")) {
3092
- this.saveTestDataAsGlobal({}, world);
3093
- }
3094
- }
3095
3562
  if (this.initSnapshotTaken === false) {
3096
3563
  this.initSnapshotTaken = true;
3097
3564
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3116,15 +3583,22 @@ class StableBrowser {
3116
3583
  const content = [`- path: ${path}`, `- title: ${title}`];
3117
3584
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3118
3585
  for (let i = 0; i < frames.length; i++) {
3119
- content.push(`- frame: ${i}`);
3120
3586
  const frame = frames[i];
3121
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3122
- content.push(snapshot);
3587
+ try {
3588
+ // Ensure frame is attached and has body
3589
+ const body = frame.locator("body");
3590
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3591
+ const snapshot = await body.ariaSnapshot({ timeout });
3592
+ content.push(`- frame: ${i}`);
3593
+ content.push(snapshot);
3594
+ }
3595
+ catch (innerErr) { }
3123
3596
  }
3124
3597
  return content.join("\n");
3125
3598
  }
3126
3599
  catch (e) {
3127
- console.error(e);
3600
+ console.log("Error in getAriaSnapshot");
3601
+ //console.debug(e);
3128
3602
  }
3129
3603
  return null;
3130
3604
  }
@@ -3135,6 +3609,13 @@ class StableBrowser {
3135
3609
  await this.context.browserObject.context.tracing.stopChunk({
3136
3610
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3137
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`);
3138
3619
  }
3139
3620
  }
3140
3621
  if (this.context) {