automation_model 1.0.626-dev → 1.0.626-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 (47) 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/network.d.ts +1 -1
  30. package/lib/network.js +5 -5
  31. package/lib/network.js.map +1 -1
  32. package/lib/snapshot_validation.d.ts +37 -0
  33. package/lib/snapshot_validation.js +280 -0
  34. package/lib/snapshot_validation.js.map +1 -0
  35. package/lib/stable_browser.d.ts +20 -1
  36. package/lib/stable_browser.js +587 -108
  37. package/lib/stable_browser.js.map +1 -1
  38. package/lib/table_helper.d.ts +19 -0
  39. package/lib/table_helper.js +116 -0
  40. package/lib/table_helper.js.map +1 -0
  41. package/lib/test_context.d.ts +2 -0
  42. package/lib/test_context.js +2 -0
  43. package/lib/test_context.js.map +1 -1
  44. package/lib/utils.d.ts +8 -4
  45. package/lib/utils.js +221 -20
  46. package/lib/utils.js.map +1 -1
  47. 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]))) {
@@ -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) {
@@ -824,9 +876,40 @@ class StableBrowser {
824
876
  result.locatorIndex = i;
825
877
  }
826
878
  if (foundLocators.length > 1) {
827
- info.failCause.foundMultiple = true;
828
- if (info.locatorLog) {
829
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
879
+ // remove elements that consume the same space with 10 pixels tolerance
880
+ const boxes = [];
881
+ for (let j = 0; j < foundLocators.length; j++) {
882
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
883
+ }
884
+ for (let j = 0; j < boxes.length; j++) {
885
+ for (let k = 0; k < boxes.length; k++) {
886
+ if (j === k) {
887
+ continue;
888
+ }
889
+ // check if x, y, width, height are the same with 10 pixels tolerance
890
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
891
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
892
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
893
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
894
+ // as the element is not unique, will remove it
895
+ boxes.splice(k, 1);
896
+ k--;
897
+ }
898
+ }
899
+ }
900
+ if (boxes.length === 1) {
901
+ result.foundElements.push({
902
+ locator: boxes[0].locator.first(),
903
+ box: boxes[0].box,
904
+ unique: true,
905
+ });
906
+ result.locatorIndex = i;
907
+ }
908
+ else {
909
+ info.failCause.foundMultiple = true;
910
+ if (info.locatorLog) {
911
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
912
+ }
830
913
  }
831
914
  }
832
915
  }
@@ -874,7 +957,7 @@ class StableBrowser {
874
957
  await _commandError(state, "timeout looking for " + elementDescription, this);
875
958
  }
876
959
  finally {
877
- _commandFinally(state, this);
960
+ await _commandFinally(state, this);
878
961
  }
879
962
  }
880
963
  }
@@ -923,7 +1006,7 @@ class StableBrowser {
923
1006
  await _commandError(state, "timeout looking for " + elementDescription, this);
924
1007
  }
925
1008
  finally {
926
- _commandFinally(state, this);
1009
+ await _commandFinally(state, this);
927
1010
  }
928
1011
  }
929
1012
  }
@@ -944,19 +1027,7 @@ class StableBrowser {
944
1027
  };
945
1028
  try {
946
1029
  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
- }
1030
+ await performAction("click", state.element, options, this, state, _params);
960
1031
  await this.waitForPageLoad();
961
1032
  return state.info;
962
1033
  }
@@ -964,9 +1035,41 @@ class StableBrowser {
964
1035
  await _commandError(state, e, this);
965
1036
  }
966
1037
  finally {
967
- _commandFinally(state, this);
1038
+ await _commandFinally(state, this);
968
1039
  }
969
1040
  }
1041
+ async waitForElement(selectors, _params, options = {}, world = null) {
1042
+ const timeout = this._getFindElementTimeout(options);
1043
+ const state = {
1044
+ selectors,
1045
+ _params,
1046
+ options,
1047
+ world,
1048
+ text: "Wait for element",
1049
+ _text: "Wait for " + selectors.element_name,
1050
+ type: Types.WAIT_ELEMENT,
1051
+ operation: "waitForElement",
1052
+ log: "***** wait for " + selectors.element_name + " *****\n",
1053
+ };
1054
+ let found = false;
1055
+ try {
1056
+ await _preCommand(state, this);
1057
+ // if (state.options && state.options.context) {
1058
+ // state.selectors.locators[0].text = state.options.context;
1059
+ // }
1060
+ await state.element.waitFor({ timeout: timeout });
1061
+ found = true;
1062
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1063
+ }
1064
+ catch (e) {
1065
+ console.error("Error on waitForElement", e);
1066
+ // await _commandError(state, e, this);
1067
+ }
1068
+ finally {
1069
+ await _commandFinally(state, this);
1070
+ }
1071
+ return found;
1072
+ }
970
1073
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
971
1074
  const state = {
972
1075
  selectors,
@@ -987,7 +1090,7 @@ class StableBrowser {
987
1090
  try {
988
1091
  // if (world && world.screenshot && !world.screenshotPath) {
989
1092
  // console.log(`Highlighting while running from recorder`);
990
- await this._highlightElements(element);
1093
+ await this._highlightElements(state.element);
991
1094
  await state.element.setChecked(checked);
992
1095
  await new Promise((resolve) => setTimeout(resolve, 1000));
993
1096
  // await this._unHighlightElements(element);
@@ -1014,7 +1117,7 @@ class StableBrowser {
1014
1117
  await _commandError(state, e, this);
1015
1118
  }
1016
1119
  finally {
1017
- _commandFinally(state, this);
1120
+ await _commandFinally(state, this);
1018
1121
  }
1019
1122
  }
1020
1123
  async hover(selectors, _params, options = {}, world = null) {
@@ -1031,19 +1134,7 @@ class StableBrowser {
1031
1134
  };
1032
1135
  try {
1033
1136
  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
- }
1137
+ await performAction("hover", state.element, options, this, state, _params);
1047
1138
  await _screenshot(state, this);
1048
1139
  await this.waitForPageLoad();
1049
1140
  return state.info;
@@ -1052,7 +1143,7 @@ class StableBrowser {
1052
1143
  await _commandError(state, e, this);
1053
1144
  }
1054
1145
  finally {
1055
- _commandFinally(state, this);
1146
+ await _commandFinally(state, this);
1056
1147
  }
1057
1148
  }
1058
1149
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1088,7 +1179,7 @@ class StableBrowser {
1088
1179
  await _commandError(state, e, this);
1089
1180
  }
1090
1181
  finally {
1091
- _commandFinally(state, this);
1182
+ await _commandFinally(state, this);
1092
1183
  }
1093
1184
  }
1094
1185
  async type(_value, _params = null, options = {}, world = null) {
@@ -1134,7 +1225,7 @@ class StableBrowser {
1134
1225
  await _commandError(state, e, this);
1135
1226
  }
1136
1227
  finally {
1137
- _commandFinally(state, this);
1228
+ await _commandFinally(state, this);
1138
1229
  }
1139
1230
  }
1140
1231
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1170,7 +1261,7 @@ class StableBrowser {
1170
1261
  await _commandError(state, e, this);
1171
1262
  }
1172
1263
  finally {
1173
- _commandFinally(state, this);
1264
+ await _commandFinally(state, this);
1174
1265
  }
1175
1266
  }
1176
1267
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1190,7 +1281,7 @@ class StableBrowser {
1190
1281
  try {
1191
1282
  await _preCommand(state, this);
1192
1283
  try {
1193
- await state.element.click();
1284
+ await performAction("click", state.element, options, this, state, _params);
1194
1285
  await new Promise((resolve) => setTimeout(resolve, 500));
1195
1286
  if (format) {
1196
1287
  state.value = dayjs(state.value).format(format);
@@ -1239,7 +1330,7 @@ class StableBrowser {
1239
1330
  await _commandError(state, e, this);
1240
1331
  }
1241
1332
  finally {
1242
- _commandFinally(state, this);
1333
+ await _commandFinally(state, this);
1243
1334
  }
1244
1335
  }
1245
1336
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1258,6 +1349,9 @@ class StableBrowser {
1258
1349
  operation: "clickType",
1259
1350
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1260
1351
  };
1352
+ if (!options) {
1353
+ options = {};
1354
+ }
1261
1355
  if (newValue !== _value) {
1262
1356
  //this.logger.info(_value + "=" + newValue);
1263
1357
  _value = newValue;
@@ -1265,7 +1359,7 @@ class StableBrowser {
1265
1359
  try {
1266
1360
  await _preCommand(state, this);
1267
1361
  state.info.value = _value;
1268
- if (options === null || options === undefined || !options.press) {
1362
+ if (!options.press) {
1269
1363
  try {
1270
1364
  let currentValue = await state.element.inputValue();
1271
1365
  if (currentValue) {
@@ -1276,13 +1370,9 @@ class StableBrowser {
1276
1370
  this.logger.info("unable to clear input value");
1277
1371
  }
1278
1372
  }
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
- }
1373
+ if (options.press) {
1374
+ options.timeout = 5000;
1375
+ await performAction("click", state.element, options, this, state, _params);
1286
1376
  }
1287
1377
  else {
1288
1378
  try {
@@ -1340,7 +1430,7 @@ class StableBrowser {
1340
1430
  await _commandError(state, e, this);
1341
1431
  }
1342
1432
  finally {
1343
- _commandFinally(state, this);
1433
+ await _commandFinally(state, this);
1344
1434
  }
1345
1435
  }
1346
1436
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1370,7 +1460,42 @@ class StableBrowser {
1370
1460
  await _commandError(state, e, this);
1371
1461
  }
1372
1462
  finally {
1373
- _commandFinally(state, this);
1463
+ await _commandFinally(state, this);
1464
+ }
1465
+ }
1466
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1467
+ const state = {
1468
+ selectors,
1469
+ _params,
1470
+ files,
1471
+ value: '"' + files.join('", "') + '"',
1472
+ options,
1473
+ world,
1474
+ type: Types.SET_INPUT_FILES,
1475
+ text: `Set input files`,
1476
+ _text: `Set input files on ${selectors.element_name}`,
1477
+ operation: "setInputFiles",
1478
+ log: "***** set input files " + selectors.element_name + " *****\n",
1479
+ };
1480
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1481
+ try {
1482
+ await _preCommand(state, this);
1483
+ for (let i = 0; i < files.length; i++) {
1484
+ const file = files[i];
1485
+ const filePath = path.join(uploadsFolder, file);
1486
+ if (!fs.existsSync(filePath)) {
1487
+ throw new Error(`File not found: ${filePath}`);
1488
+ }
1489
+ state.files[i] = filePath;
1490
+ }
1491
+ await state.element.setInputFiles(files);
1492
+ return state.info;
1493
+ }
1494
+ catch (e) {
1495
+ await _commandError(state, e, this);
1496
+ }
1497
+ finally {
1498
+ await _commandFinally(state, this);
1374
1499
  }
1375
1500
  }
1376
1501
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1486,7 +1611,7 @@ class StableBrowser {
1486
1611
  await _commandError(state, e, this);
1487
1612
  }
1488
1613
  finally {
1489
- _commandFinally(state, this);
1614
+ await _commandFinally(state, this);
1490
1615
  }
1491
1616
  }
1492
1617
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1521,7 +1646,7 @@ class StableBrowser {
1521
1646
  while (Date.now() - startTime < timeout) {
1522
1647
  try {
1523
1648
  await _preCommand(state, this);
1524
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1649
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1525
1650
  if (foundObj && foundObj.element) {
1526
1651
  await this.scrollIfNeeded(foundObj.element, state.info);
1527
1652
  }
@@ -1563,7 +1688,79 @@ class StableBrowser {
1563
1688
  throw e;
1564
1689
  }
1565
1690
  finally {
1566
- _commandFinally(state, this);
1691
+ await _commandFinally(state, this);
1692
+ }
1693
+ }
1694
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1695
+ const timeout = this._getFindElementTimeout(options);
1696
+ const startTime = Date.now();
1697
+ const state = {
1698
+ _params,
1699
+ value: referanceSnapshot,
1700
+ options,
1701
+ world,
1702
+ locate: false,
1703
+ scroll: false,
1704
+ screenshot: true,
1705
+ highlight: false,
1706
+ type: Types.SNAPSHOT_VALIDATION,
1707
+ text: `verify snapshot: ${referanceSnapshot}`,
1708
+ operation: "snapshotValidation",
1709
+ log: "***** verify snapshot *****\n",
1710
+ };
1711
+ if (!referanceSnapshot) {
1712
+ throw new Error("referanceSnapshot is null");
1713
+ }
1714
+ let text = null;
1715
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1716
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1717
+ }
1718
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1719
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1720
+ }
1721
+ else if (referanceSnapshot.startsWith("yaml:")) {
1722
+ text = referanceSnapshot.substring(5);
1723
+ }
1724
+ else {
1725
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1726
+ }
1727
+ state.text = text;
1728
+ const newValue = await this._replaceWithLocalData(text, world);
1729
+ await _preCommand(state, this);
1730
+ let foundObj = null;
1731
+ try {
1732
+ let matchResult = null;
1733
+ while (Date.now() - startTime < timeout) {
1734
+ try {
1735
+ let scope = null;
1736
+ if (!frameSelectors) {
1737
+ scope = this.page;
1738
+ }
1739
+ else {
1740
+ scope = await this._findFrameScope({ nestFrmLoc: frameSelectors }, timeout, state.info);
1741
+ }
1742
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1743
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1744
+ if (matchResult.errorLine !== -1) {
1745
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1746
+ }
1747
+ // highlight and screenshot
1748
+ return state.info;
1749
+ }
1750
+ catch (e) {
1751
+ // Log error but continue retrying until timeout is reached
1752
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1753
+ }
1754
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1755
+ }
1756
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1757
+ }
1758
+ catch (e) {
1759
+ await _commandError(state, e, this);
1760
+ throw e;
1761
+ }
1762
+ finally {
1763
+ await _commandFinally(state, this);
1567
1764
  }
1568
1765
  }
1569
1766
  async waitForUserInput(message, world = null) {
@@ -1601,6 +1798,15 @@ class StableBrowser {
1601
1798
  // save the data to the file
1602
1799
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1603
1800
  }
1801
+ overwriteTestData(testData, world = null) {
1802
+ if (!testData) {
1803
+ return;
1804
+ }
1805
+ // if data file exists, load it
1806
+ const dataFile = _getDataFile(world, this.context, this);
1807
+ // save the data to the file
1808
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1809
+ }
1604
1810
  _getDataFilePath(fileName) {
1605
1811
  let dataFile = path.join(this.project_path, "data", fileName);
1606
1812
  if (fs.existsSync(dataFile)) {
@@ -1853,7 +2059,7 @@ class StableBrowser {
1853
2059
  await _commandError(state, e, this);
1854
2060
  }
1855
2061
  finally {
1856
- _commandFinally(state, this);
2062
+ await _commandFinally(state, this);
1857
2063
  }
1858
2064
  }
1859
2065
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1884,10 +2090,31 @@ class StableBrowser {
1884
2090
  case "value":
1885
2091
  state.value = await state.element.inputValue();
1886
2092
  break;
2093
+ case "text":
2094
+ state.value = await state.element.textContent();
2095
+ break;
1887
2096
  default:
1888
2097
  state.value = await state.element.getAttribute(attribute);
1889
2098
  break;
1890
2099
  }
2100
+ if (options !== null) {
2101
+ if (options.regex && options.regex !== "") {
2102
+ // Construct a regex pattern from the provided string
2103
+ const regex = options.regex.slice(1, -1);
2104
+ const regexPattern = new RegExp(regex, "g");
2105
+ const matches = state.value.match(regexPattern);
2106
+ if (matches) {
2107
+ let newValue = "";
2108
+ for (const match of matches) {
2109
+ newValue += match;
2110
+ }
2111
+ state.value = newValue;
2112
+ }
2113
+ }
2114
+ if (options.trimSpaces && options.trimSpaces === true) {
2115
+ state.value = state.value.trim();
2116
+ }
2117
+ }
1891
2118
  state.info.value = state.value;
1892
2119
  this.setTestData({ [variable]: state.value }, world);
1893
2120
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1898,7 +2125,7 @@ class StableBrowser {
1898
2125
  await _commandError(state, e, this);
1899
2126
  }
1900
2127
  finally {
1901
- _commandFinally(state, this);
2128
+ await _commandFinally(state, this);
1902
2129
  }
1903
2130
  }
1904
2131
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1923,12 +2150,15 @@ class StableBrowser {
1923
2150
  let expectedValue;
1924
2151
  try {
1925
2152
  await _preCommand(state, this);
1926
- expectedValue = state.value;
2153
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1927
2154
  state.info.expectedValue = expectedValue;
1928
2155
  switch (attribute) {
1929
2156
  case "innerText":
1930
2157
  val = String(await state.element.innerText());
1931
2158
  break;
2159
+ case "text":
2160
+ val = String(await state.element.textContent());
2161
+ break;
1932
2162
  case "value":
1933
2163
  val = String(await state.element.inputValue());
1934
2164
  break;
@@ -1950,17 +2180,42 @@ class StableBrowser {
1950
2180
  let regex;
1951
2181
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1952
2182
  const patternBody = expectedValue.slice(1, -1);
1953
- regex = new RegExp(patternBody, "g");
2183
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2184
+ regex = new RegExp(processedPattern, "gs");
2185
+ state.info.regex = true;
1954
2186
  }
1955
2187
  else {
1956
2188
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1957
2189
  regex = new RegExp(escapedPattern, "g");
1958
2190
  }
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);
2191
+ if (attribute === "innerText") {
2192
+ if (state.info.regex) {
2193
+ if (!regex.test(val)) {
2194
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2195
+ state.info.failCause.assertionFailed = true;
2196
+ state.info.failCause.lastError = errorMessage;
2197
+ throw new Error(errorMessage);
2198
+ }
2199
+ }
2200
+ else {
2201
+ const valLines = val.split("\n");
2202
+ const expectedLines = expectedValue.split("\n");
2203
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2204
+ if (!isPart) {
2205
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2206
+ state.info.failCause.assertionFailed = true;
2207
+ state.info.failCause.lastError = errorMessage;
2208
+ throw new Error(errorMessage);
2209
+ }
2210
+ }
2211
+ }
2212
+ else {
2213
+ if (!val.match(regex)) {
2214
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2215
+ state.info.failCause.assertionFailed = true;
2216
+ state.info.failCause.lastError = errorMessage;
2217
+ throw new Error(errorMessage);
2218
+ }
1964
2219
  }
1965
2220
  return state.info;
1966
2221
  }
@@ -1968,7 +2223,7 @@ class StableBrowser {
1968
2223
  await _commandError(state, e, this);
1969
2224
  }
1970
2225
  finally {
1971
- _commandFinally(state, this);
2226
+ await _commandFinally(state, this);
1972
2227
  }
1973
2228
  }
1974
2229
  async extractEmailData(emailAddress, options, world) {
@@ -2213,7 +2468,7 @@ class StableBrowser {
2213
2468
  Object.assign(e, { info: info });
2214
2469
  error = e;
2215
2470
  // throw e;
2216
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2471
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
2217
2472
  }
2218
2473
  finally {
2219
2474
  const endTime = Date.now();
@@ -2238,27 +2493,89 @@ class StableBrowser {
2238
2493
  });
2239
2494
  }
2240
2495
  }
2241
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2496
+ async verifyPageTitle(title, options = {}, world = null) {
2497
+ const startTime = Date.now();
2498
+ let error = null;
2499
+ let screenshotId = null;
2500
+ let screenshotPath = null;
2501
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2502
+ const info = {};
2503
+ info.log = "***** verify page title " + title + " *****\n";
2504
+ info.operation = "verifyPageTitle";
2505
+ const newValue = await this._replaceWithLocalData(title, world);
2506
+ if (newValue !== title) {
2507
+ this.logger.info(title + "=" + newValue);
2508
+ title = newValue;
2509
+ }
2510
+ info.title = title;
2511
+ try {
2512
+ for (let i = 0; i < 30; i++) {
2513
+ const foundTitle = await this.page.title();
2514
+ if (!foundTitle.includes(title)) {
2515
+ if (i === 29) {
2516
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2517
+ }
2518
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2519
+ continue;
2520
+ }
2521
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2522
+ return info;
2523
+ }
2524
+ }
2525
+ catch (e) {
2526
+ //await this.closeUnexpectedPopups();
2527
+ this.logger.error("verify page title failed " + info.log);
2528
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2529
+ info.screenshotPath = screenshotPath;
2530
+ Object.assign(e, { info: info });
2531
+ error = e;
2532
+ // throw e;
2533
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2534
+ }
2535
+ finally {
2536
+ const endTime = Date.now();
2537
+ _reportToWorld(world, {
2538
+ type: Types.VERIFY_PAGE_PATH,
2539
+ text: "Verify page title",
2540
+ _text: "Verify the page title contains " + title,
2541
+ screenshotId,
2542
+ result: error
2543
+ ? {
2544
+ status: "FAILED",
2545
+ startTime,
2546
+ endTime,
2547
+ message: error?.message,
2548
+ }
2549
+ : {
2550
+ status: "PASSED",
2551
+ startTime,
2552
+ endTime,
2553
+ },
2554
+ info: info,
2555
+ });
2556
+ }
2557
+ }
2558
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2242
2559
  const frames = this.page.frames();
2243
2560
  let results = [];
2244
- let ignoreCase = false;
2561
+ // let ignoreCase = false;
2245
2562
  for (let i = 0; i < frames.length; i++) {
2246
2563
  if (dateAlternatives.date) {
2247
2564
  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, {});
2565
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2249
2566
  result.frame = frames[i];
2250
2567
  results.push(result);
2251
2568
  }
2252
2569
  }
2253
2570
  else if (numberAlternatives.number) {
2254
2571
  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, {});
2572
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2256
2573
  result.frame = frames[i];
2257
2574
  results.push(result);
2258
2575
  }
2259
2576
  }
2260
2577
  else {
2261
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2578
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2262
2579
  result.frame = frames[i];
2263
2580
  results.push(result);
2264
2581
  }
@@ -2277,11 +2594,14 @@ class StableBrowser {
2277
2594
  scroll: false,
2278
2595
  highlight: false,
2279
2596
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2280
- text: `Verify text exists in page`,
2597
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2281
2598
  _text: `Verify the text '${text}' exists in page`,
2282
2599
  operation: "verifyTextExistInPage",
2283
2600
  log: "***** verify text " + text + " exists in page *****\n",
2284
2601
  };
2602
+ if (testForRegex(text)) {
2603
+ text = text.replace(/\\"/g, '"');
2604
+ }
2285
2605
  const timeout = this._getFindElementTimeout(options);
2286
2606
  await new Promise((resolve) => setTimeout(resolve, 2000));
2287
2607
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2352,7 +2672,7 @@ class StableBrowser {
2352
2672
  await _commandError(state, e, this);
2353
2673
  }
2354
2674
  finally {
2355
- _commandFinally(state, this);
2675
+ await _commandFinally(state, this);
2356
2676
  }
2357
2677
  }
2358
2678
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2365,11 +2685,14 @@ class StableBrowser {
2365
2685
  scroll: false,
2366
2686
  highlight: false,
2367
2687
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2368
- text: `Verify text does not exist in page`,
2688
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2369
2689
  _text: `Verify the text '${text}' does not exist in page`,
2370
2690
  operation: "verifyTextNotExistInPage",
2371
2691
  log: "***** verify text " + text + " does not exist in page *****\n",
2372
2692
  };
2693
+ if (testForRegex(text)) {
2694
+ text = text.replace(/\\"/g, '"');
2695
+ }
2373
2696
  const timeout = this._getFindElementTimeout(options);
2374
2697
  await new Promise((resolve) => setTimeout(resolve, 2000));
2375
2698
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2406,7 +2729,7 @@ class StableBrowser {
2406
2729
  await _commandError(state, e, this);
2407
2730
  }
2408
2731
  finally {
2409
- _commandFinally(state, this);
2732
+ await _commandFinally(state, this);
2410
2733
  }
2411
2734
  }
2412
2735
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2448,7 +2771,7 @@ class StableBrowser {
2448
2771
  };
2449
2772
  while (true) {
2450
2773
  try {
2451
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2774
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2452
2775
  }
2453
2776
  catch (error) {
2454
2777
  // ignore
@@ -2476,7 +2799,7 @@ class StableBrowser {
2476
2799
  const count = await frame.locator(css).count();
2477
2800
  for (let j = 0; j < count; j++) {
2478
2801
  const continer = await frame.locator(css).nth(j);
2479
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2802
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2480
2803
  if (result.elementCount > 0) {
2481
2804
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2482
2805
  await this._highlightElements(frame, dataAttribute);
@@ -2517,9 +2840,33 @@ class StableBrowser {
2517
2840
  await _commandError(state, e, this);
2518
2841
  }
2519
2842
  finally {
2520
- _commandFinally(state, this);
2843
+ await _commandFinally(state, this);
2521
2844
  }
2522
2845
  }
2846
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2847
+ const frames = this.page.frames();
2848
+ let results = [];
2849
+ let ignoreCase = false;
2850
+ for (let i = 0; i < frames.length; i++) {
2851
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2852
+ result.frame = frames[i];
2853
+ const climbArray = [];
2854
+ for (let i = 0; i < climb; i++) {
2855
+ climbArray.push("..");
2856
+ }
2857
+ let climbXpath = "xpath=" + climbArray.join("/");
2858
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2859
+ const count = await frames[i].locator(newLocator).count();
2860
+ if (count > 0) {
2861
+ result.elementCount = count;
2862
+ result.locator = newLocator;
2863
+ results.push(result);
2864
+ }
2865
+ }
2866
+ // state.info.results = results;
2867
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2868
+ return resultWithElementsFound;
2869
+ }
2523
2870
  async visualVerification(text, options = {}, world = null) {
2524
2871
  const startTime = Date.now();
2525
2872
  let error = null;
@@ -2836,7 +3183,13 @@ class StableBrowser {
2836
3183
  }
2837
3184
  }
2838
3185
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2839
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3186
+ try {
3187
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3188
+ }
3189
+ catch (error) {
3190
+ this.logger.debug(error);
3191
+ throw error;
3192
+ }
2840
3193
  }
2841
3194
  _getLoadTimeout(options) {
2842
3195
  let timeout = 15000;
@@ -2873,6 +3226,9 @@ class StableBrowser {
2873
3226
  this.registerEventListeners(this.context);
2874
3227
  registerNetworkEvents(this.world, this, this.context, this.page);
2875
3228
  registerDownloadEvent(this.page, this.world, this.context);
3229
+ if (this.onRestoreSaveState) {
3230
+ this.onRestoreSaveState(path);
3231
+ }
2876
3232
  }
2877
3233
  async waitForPageLoad(options = {}, world = null) {
2878
3234
  let timeout = this._getLoadTimeout(options);
@@ -2955,11 +3311,98 @@ class StableBrowser {
2955
3311
  await _commandError(state, e, this);
2956
3312
  }
2957
3313
  finally {
2958
- _commandFinally(state, this);
3314
+ await _commandFinally(state, this);
3315
+ }
3316
+ }
3317
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3318
+ let operation = null;
3319
+ if (!options || !options.operation) {
3320
+ throw new Error("operation is not defined");
3321
+ }
3322
+ operation = options.operation;
3323
+ // validate operation is one of the supported operations
3324
+ if (operation != "click" && operation != "hover+click") {
3325
+ throw new Error("operation is not supported");
3326
+ }
3327
+ const state = {
3328
+ options,
3329
+ world,
3330
+ locate: false,
3331
+ scroll: false,
3332
+ highlight: false,
3333
+ type: Types.TABLE_OPERATION,
3334
+ text: `Table operation`,
3335
+ _text: `Table ${operation} operation`,
3336
+ operation: operation,
3337
+ log: "***** Table operation *****\n",
3338
+ };
3339
+ const timeout = this._getFindElementTimeout(options);
3340
+ try {
3341
+ await _preCommand(state, this);
3342
+ const start = Date.now();
3343
+ let cellArea = null;
3344
+ while (true) {
3345
+ try {
3346
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3347
+ if (cellArea) {
3348
+ break;
3349
+ }
3350
+ }
3351
+ catch (e) {
3352
+ // ignore
3353
+ }
3354
+ if (Date.now() - start > timeout) {
3355
+ throw new Error(`Cell not found in table`);
3356
+ }
3357
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3358
+ }
3359
+ switch (operation) {
3360
+ case "click":
3361
+ if (!options.css) {
3362
+ // will click in the center of the cell
3363
+ let xOffset = 0;
3364
+ let yOffset = 0;
3365
+ if (options.xOffset) {
3366
+ xOffset = options.xOffset;
3367
+ }
3368
+ if (options.yOffset) {
3369
+ yOffset = options.yOffset;
3370
+ }
3371
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3372
+ }
3373
+ else {
3374
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3375
+ if (results.length === 0) {
3376
+ throw new Error(`Element not found in cell area`);
3377
+ }
3378
+ state.element = results[0];
3379
+ await performAction("click", state.element, options, this, state, _params);
3380
+ }
3381
+ break;
3382
+ case "hover+click":
3383
+ if (!options.css) {
3384
+ throw new Error("css is not defined");
3385
+ }
3386
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3387
+ if (results.length === 0) {
3388
+ throw new Error(`Element not found in cell area`);
3389
+ }
3390
+ state.element = results[0];
3391
+ await performAction("hover+click", state.element, options, this, state, _params);
3392
+ break;
3393
+ default:
3394
+ throw new Error("operation is not supported");
3395
+ }
3396
+ }
3397
+ catch (e) {
3398
+ await _commandError(state, e, this);
3399
+ }
3400
+ finally {
3401
+ await _commandFinally(state, this);
2959
3402
  }
2960
3403
  }
2961
3404
  saveTestDataAsGlobal(options, world) {
2962
- const dataFile = this._getDataFile(world);
3405
+ const dataFile = _getDataFile(world, this.context, this);
2963
3406
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2964
3407
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2965
3408
  }
@@ -3060,7 +3503,39 @@ class StableBrowser {
3060
3503
  console.log("#-#");
3061
3504
  }
3062
3505
  }
3506
+ async beforeScenario(world, scenario) {
3507
+ this.beforeScenarioCalled = true;
3508
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3509
+ this.scenarioName = scenario.pickle.name;
3510
+ }
3511
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3512
+ this.featureName = scenario.gherkinDocument.feature.name;
3513
+ }
3514
+ if (this.context) {
3515
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3516
+ }
3517
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3518
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3519
+ // check if @global_test_data tag is present
3520
+ if (this.tags.includes("@global_test_data")) {
3521
+ this.saveTestDataAsGlobal({}, world);
3522
+ }
3523
+ }
3524
+ // update test data based on feature/scenario
3525
+ let envName = null;
3526
+ if (this.context && this.context.environment) {
3527
+ envName = this.context.environment.name;
3528
+ }
3529
+ if (!process.env.TEMP_RUN) {
3530
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3531
+ }
3532
+ await loadBrunoParams(this.context, this.context.environment.name);
3533
+ }
3534
+ async afterScenario(world, scenario) { }
3063
3535
  async beforeStep(world, step) {
3536
+ if (!this.beforeScenarioCalled) {
3537
+ this.beforeScenario(world, step);
3538
+ }
3064
3539
  if (this.stepIndex === undefined) {
3065
3540
  this.stepIndex = 0;
3066
3541
  }
@@ -3077,21 +3552,11 @@ class StableBrowser {
3077
3552
  else {
3078
3553
  this.stepName = "step " + this.stepIndex;
3079
3554
  }
3080
- if (this.context) {
3081
- this.context.examplesRow = extractStepExampleParameters(step);
3082
- }
3083
3555
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3084
3556
  if (this.context.browserObject.context) {
3085
3557
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3086
3558
  }
3087
3559
  }
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
3560
  if (this.initSnapshotTaken === false) {
3096
3561
  this.initSnapshotTaken = true;
3097
3562
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3116,15 +3581,22 @@ class StableBrowser {
3116
3581
  const content = [`- path: ${path}`, `- title: ${title}`];
3117
3582
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3118
3583
  for (let i = 0; i < frames.length; i++) {
3119
- content.push(`- frame: ${i}`);
3120
3584
  const frame = frames[i];
3121
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3122
- content.push(snapshot);
3585
+ try {
3586
+ // Ensure frame is attached and has body
3587
+ const body = frame.locator("body");
3588
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3589
+ const snapshot = await body.ariaSnapshot({ timeout });
3590
+ content.push(`- frame: ${i}`);
3591
+ content.push(snapshot);
3592
+ }
3593
+ catch (innerErr) { }
3123
3594
  }
3124
3595
  return content.join("\n");
3125
3596
  }
3126
3597
  catch (e) {
3127
- console.error(e);
3598
+ console.log("Error in getAriaSnapshot");
3599
+ //console.debug(e);
3128
3600
  }
3129
3601
  return null;
3130
3602
  }
@@ -3135,6 +3607,13 @@ class StableBrowser {
3135
3607
  await this.context.browserObject.context.tracing.stopChunk({
3136
3608
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3137
3609
  });
3610
+ if (world && world.attach) {
3611
+ await world.attach(JSON.stringify({
3612
+ type: "trace",
3613
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3614
+ }), "application/json+trace");
3615
+ }
3616
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3138
3617
  }
3139
3618
  }
3140
3619
  if (this.context) {