automation_model 1.0.629-dev → 1.0.629-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 +38 -9
  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 +20 -3
  37. package/lib/stable_browser.js +562 -114
  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,17 +10,21 @@ 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",
26
30
  WAIT_ELEMENT: "wait_element",
@@ -45,6 +49,7 @@ export const Types = {
45
49
  UNCHECK: "uncheck_element",
46
50
  EXTRACT: "extract_attribute",
47
51
  CLOSE_PAGE: "close_page",
52
+ TABLE_OPERATION: "table_operation",
48
53
  SET_DATE_TIME: "set_date_time",
49
54
  SET_VIEWPORT: "set_viewport",
50
55
  VERIFY_VISUAL: "verify_visual",
@@ -53,6 +58,10 @@ export const Types = {
53
58
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
54
59
  VERIFY_ATTRIBUTE: "verify_element_attribute",
55
60
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
61
+ BRUNO: "bruno",
62
+ VERIFY_FILE_EXISTS: "verify_file_exists",
63
+ SET_INPUT_FILES: "set_input_files",
64
+ SNAPSHOT_VALIDATION: "snapshot_validation",
56
65
  };
57
66
  export const apps = {};
58
67
  const formatElementName = (elementName) => {
@@ -178,6 +187,30 @@ class StableBrowser {
178
187
  await this.waitForPageLoad();
179
188
  }
180
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
+ }
181
214
  registerConsoleLogListener(page, context) {
182
215
  if (!this.context.webLogger) {
183
216
  this.context.webLogger = [];
@@ -273,7 +306,7 @@ class StableBrowser {
273
306
  _commandError(state, error, this);
274
307
  }
275
308
  finally {
276
- _commandFinally(state, this);
309
+ await _commandFinally(state, this);
277
310
  }
278
311
  }
279
312
  async _getLocator(locator, scope, _params) {
@@ -354,7 +387,7 @@ class StableBrowser {
354
387
  return resultCss;
355
388
  }
356
389
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
357
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
390
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
358
391
  const locator = scope.locator(query);
359
392
  const count = await locator.count();
360
393
  if (!tag1) {
@@ -374,6 +407,12 @@ class StableBrowser {
374
407
  if (!el.setAttribute) {
375
408
  el = el.parentElement;
376
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
+ // }
377
416
  el.setAttribute("data-blinq-id-" + randomToken, "");
378
417
  return true;
379
418
  }, [tag1, randomToken]))) {
@@ -383,7 +422,7 @@ class StableBrowser {
383
422
  }
384
423
  return { elementCount: tagCount, randomToken };
385
424
  }
386
- 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) {
387
426
  if (!info) {
388
427
  info = {};
389
428
  }
@@ -450,7 +489,7 @@ class StableBrowser {
450
489
  }
451
490
  return;
452
491
  }
453
- if (info.locatorLog && count === 0) {
492
+ if (info.locatorLog && count === 0 && logErrors) {
454
493
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
455
494
  }
456
495
  for (let j = 0; j < count; j++) {
@@ -465,7 +504,7 @@ class StableBrowser {
465
504
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
466
505
  }
467
506
  }
468
- else {
507
+ else if (logErrors) {
469
508
  info.failCause.visible = visible;
470
509
  info.failCause.enabled = enabled;
471
510
  if (!info.printMessages) {
@@ -560,12 +599,24 @@ class StableBrowser {
560
599
  element.evaluate((el, randomToken) => {
561
600
  el.setAttribute("data-blinq-id-" + randomToken, "");
562
601
  }, randomToken);
563
- if (element._frame) {
564
- return element;
565
- }
566
- const scope = element.page();
567
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
568
- return newSelector;
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);
569
620
  }
570
621
  }
571
622
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -717,14 +768,9 @@ class StableBrowser {
717
768
  // info.log += "scanning locators in priority 2" + "\n";
718
769
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
719
770
  }
720
- if (result.foundElements.length === 0 && onlyPriority3) {
771
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
721
772
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
722
773
  }
723
- else {
724
- if (result.foundElements.length === 0 && !highPriorityOnly) {
725
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
726
- }
727
- }
728
774
  let foundElements = result.foundElements;
729
775
  if (foundElements.length === 1 && foundElements[0].unique) {
730
776
  info.box = foundElements[0].box;
@@ -779,6 +825,11 @@ class StableBrowser {
779
825
  visibleOnly = false;
780
826
  }
781
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
+ }
782
833
  }
783
834
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
784
835
  // if (info.locatorLog) {
@@ -794,7 +845,7 @@ class StableBrowser {
794
845
  }
795
846
  throw new Error("failed to locate first element no elements found, " + info.log);
796
847
  }
797
- 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) {
798
849
  let foundElements = [];
799
850
  const result = {
800
851
  foundElements: foundElements,
@@ -813,7 +864,9 @@ class StableBrowser {
813
864
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
814
865
  }
815
866
  catch (e) {
816
- 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
+ }
817
870
  }
818
871
  }
819
872
  if (foundLocators.length === 1) {
@@ -825,9 +878,40 @@ class StableBrowser {
825
878
  result.locatorIndex = i;
826
879
  }
827
880
  if (foundLocators.length > 1) {
828
- info.failCause.foundMultiple = true;
829
- if (info.locatorLog) {
830
- 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
+ }
831
915
  }
832
916
  }
833
917
  }
@@ -875,7 +959,7 @@ class StableBrowser {
875
959
  await _commandError(state, "timeout looking for " + elementDescription, this);
876
960
  }
877
961
  finally {
878
- _commandFinally(state, this);
962
+ await _commandFinally(state, this);
879
963
  }
880
964
  }
881
965
  }
@@ -924,7 +1008,7 @@ class StableBrowser {
924
1008
  await _commandError(state, "timeout looking for " + elementDescription, this);
925
1009
  }
926
1010
  finally {
927
- _commandFinally(state, this);
1011
+ await _commandFinally(state, this);
928
1012
  }
929
1013
  }
930
1014
  }
@@ -945,19 +1029,7 @@ class StableBrowser {
945
1029
  };
946
1030
  try {
947
1031
  await _preCommand(state, this);
948
- // if (state.options && state.options.context) {
949
- // state.selectors.locators[0].text = state.options.context;
950
- // }
951
- try {
952
- await state.element.click();
953
- // await new Promise((resolve) => setTimeout(resolve, 1000));
954
- }
955
- catch (e) {
956
- // await this.closeUnexpectedPopups();
957
- state.element = await this._locate(selectors, state.info, _params);
958
- await state.element.dispatchEvent("click");
959
- // await new Promise((resolve) => setTimeout(resolve, 1000));
960
- }
1032
+ await performAction("click", state.element, options, this, state, _params);
961
1033
  await this.waitForPageLoad();
962
1034
  return state.info;
963
1035
  }
@@ -965,7 +1037,7 @@ class StableBrowser {
965
1037
  await _commandError(state, e, this);
966
1038
  }
967
1039
  finally {
968
- _commandFinally(state, this);
1040
+ await _commandFinally(state, this);
969
1041
  }
970
1042
  }
971
1043
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -996,7 +1068,7 @@ class StableBrowser {
996
1068
  // await _commandError(state, e, this);
997
1069
  }
998
1070
  finally {
999
- _commandFinally(state, this);
1071
+ await _commandFinally(state, this);
1000
1072
  }
1001
1073
  return found;
1002
1074
  }
@@ -1020,7 +1092,7 @@ class StableBrowser {
1020
1092
  try {
1021
1093
  // if (world && world.screenshot && !world.screenshotPath) {
1022
1094
  // console.log(`Highlighting while running from recorder`);
1023
- await this._highlightElements(element);
1095
+ await this._highlightElements(state.element);
1024
1096
  await state.element.setChecked(checked);
1025
1097
  await new Promise((resolve) => setTimeout(resolve, 1000));
1026
1098
  // await this._unHighlightElements(element);
@@ -1047,7 +1119,7 @@ class StableBrowser {
1047
1119
  await _commandError(state, e, this);
1048
1120
  }
1049
1121
  finally {
1050
- _commandFinally(state, this);
1122
+ await _commandFinally(state, this);
1051
1123
  }
1052
1124
  }
1053
1125
  async hover(selectors, _params, options = {}, world = null) {
@@ -1064,19 +1136,7 @@ class StableBrowser {
1064
1136
  };
1065
1137
  try {
1066
1138
  await _preCommand(state, this);
1067
- try {
1068
- await state.element.hover();
1069
- // await _screenshot(state, this);
1070
- await new Promise((resolve) => setTimeout(resolve, 1000));
1071
- }
1072
- catch (e) {
1073
- //await this.closeUnexpectedPopups();
1074
- state.info.log += "hover failed, will try again" + "\n";
1075
- state.element = await this._locate(selectors, state.info, _params);
1076
- await state.element.hover({ timeout: 10000 });
1077
- // await _screenshot(state, this);
1078
- await new Promise((resolve) => setTimeout(resolve, 1000));
1079
- }
1139
+ await performAction("hover", state.element, options, this, state, _params);
1080
1140
  await _screenshot(state, this);
1081
1141
  await this.waitForPageLoad();
1082
1142
  return state.info;
@@ -1085,7 +1145,7 @@ class StableBrowser {
1085
1145
  await _commandError(state, e, this);
1086
1146
  }
1087
1147
  finally {
1088
- _commandFinally(state, this);
1148
+ await _commandFinally(state, this);
1089
1149
  }
1090
1150
  }
1091
1151
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1121,7 +1181,7 @@ class StableBrowser {
1121
1181
  await _commandError(state, e, this);
1122
1182
  }
1123
1183
  finally {
1124
- _commandFinally(state, this);
1184
+ await _commandFinally(state, this);
1125
1185
  }
1126
1186
  }
1127
1187
  async type(_value, _params = null, options = {}, world = null) {
@@ -1167,7 +1227,7 @@ class StableBrowser {
1167
1227
  await _commandError(state, e, this);
1168
1228
  }
1169
1229
  finally {
1170
- _commandFinally(state, this);
1230
+ await _commandFinally(state, this);
1171
1231
  }
1172
1232
  }
1173
1233
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1203,7 +1263,7 @@ class StableBrowser {
1203
1263
  await _commandError(state, e, this);
1204
1264
  }
1205
1265
  finally {
1206
- _commandFinally(state, this);
1266
+ await _commandFinally(state, this);
1207
1267
  }
1208
1268
  }
1209
1269
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1223,7 +1283,7 @@ class StableBrowser {
1223
1283
  try {
1224
1284
  await _preCommand(state, this);
1225
1285
  try {
1226
- await state.element.click();
1286
+ await performAction("click", state.element, options, this, state, _params);
1227
1287
  await new Promise((resolve) => setTimeout(resolve, 500));
1228
1288
  if (format) {
1229
1289
  state.value = dayjs(state.value).format(format);
@@ -1272,7 +1332,7 @@ class StableBrowser {
1272
1332
  await _commandError(state, e, this);
1273
1333
  }
1274
1334
  finally {
1275
- _commandFinally(state, this);
1335
+ await _commandFinally(state, this);
1276
1336
  }
1277
1337
  }
1278
1338
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1291,6 +1351,9 @@ class StableBrowser {
1291
1351
  operation: "clickType",
1292
1352
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1293
1353
  };
1354
+ if (!options) {
1355
+ options = {};
1356
+ }
1294
1357
  if (newValue !== _value) {
1295
1358
  //this.logger.info(_value + "=" + newValue);
1296
1359
  _value = newValue;
@@ -1298,7 +1361,7 @@ class StableBrowser {
1298
1361
  try {
1299
1362
  await _preCommand(state, this);
1300
1363
  state.info.value = _value;
1301
- if (options === null || options === undefined || !options.press) {
1364
+ if (!options.press) {
1302
1365
  try {
1303
1366
  let currentValue = await state.element.inputValue();
1304
1367
  if (currentValue) {
@@ -1309,13 +1372,9 @@ class StableBrowser {
1309
1372
  this.logger.info("unable to clear input value");
1310
1373
  }
1311
1374
  }
1312
- if (options === null || options === undefined || options.press) {
1313
- try {
1314
- await state.element.click({ timeout: 5000 });
1315
- }
1316
- catch (e) {
1317
- await state.element.dispatchEvent("click");
1318
- }
1375
+ if (options.press) {
1376
+ options.timeout = 5000;
1377
+ await performAction("click", state.element, options, this, state, _params);
1319
1378
  }
1320
1379
  else {
1321
1380
  try {
@@ -1373,7 +1432,7 @@ class StableBrowser {
1373
1432
  await _commandError(state, e, this);
1374
1433
  }
1375
1434
  finally {
1376
- _commandFinally(state, this);
1435
+ await _commandFinally(state, this);
1377
1436
  }
1378
1437
  }
1379
1438
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1403,7 +1462,42 @@ class StableBrowser {
1403
1462
  await _commandError(state, e, this);
1404
1463
  }
1405
1464
  finally {
1406
- _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);
1407
1501
  }
1408
1502
  }
1409
1503
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1519,7 +1613,7 @@ class StableBrowser {
1519
1613
  await _commandError(state, e, this);
1520
1614
  }
1521
1615
  finally {
1522
- _commandFinally(state, this);
1616
+ await _commandFinally(state, this);
1523
1617
  }
1524
1618
  }
1525
1619
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1554,7 +1648,7 @@ class StableBrowser {
1554
1648
  while (Date.now() - startTime < timeout) {
1555
1649
  try {
1556
1650
  await _preCommand(state, this);
1557
- 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);
1558
1652
  if (foundObj && foundObj.element) {
1559
1653
  await this.scrollIfNeeded(foundObj.element, state.info);
1560
1654
  }
@@ -1596,7 +1690,79 @@ class StableBrowser {
1596
1690
  throw e;
1597
1691
  }
1598
1692
  finally {
1599
- _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(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);
1600
1766
  }
1601
1767
  }
1602
1768
  async waitForUserInput(message, world = null) {
@@ -1634,6 +1800,15 @@ class StableBrowser {
1634
1800
  // save the data to the file
1635
1801
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1636
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
+ }
1637
1812
  _getDataFilePath(fileName) {
1638
1813
  let dataFile = path.join(this.project_path, "data", fileName);
1639
1814
  if (fs.existsSync(dataFile)) {
@@ -1886,7 +2061,7 @@ class StableBrowser {
1886
2061
  await _commandError(state, e, this);
1887
2062
  }
1888
2063
  finally {
1889
- _commandFinally(state, this);
2064
+ await _commandFinally(state, this);
1890
2065
  }
1891
2066
  }
1892
2067
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1917,10 +2092,31 @@ class StableBrowser {
1917
2092
  case "value":
1918
2093
  state.value = await state.element.inputValue();
1919
2094
  break;
2095
+ case "text":
2096
+ state.value = await state.element.textContent();
2097
+ break;
1920
2098
  default:
1921
2099
  state.value = await state.element.getAttribute(attribute);
1922
2100
  break;
1923
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
+ }
1924
2120
  state.info.value = state.value;
1925
2121
  this.setTestData({ [variable]: state.value }, world);
1926
2122
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1931,7 +2127,7 @@ class StableBrowser {
1931
2127
  await _commandError(state, e, this);
1932
2128
  }
1933
2129
  finally {
1934
- _commandFinally(state, this);
2130
+ await _commandFinally(state, this);
1935
2131
  }
1936
2132
  }
1937
2133
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1956,12 +2152,15 @@ class StableBrowser {
1956
2152
  let expectedValue;
1957
2153
  try {
1958
2154
  await _preCommand(state, this);
1959
- expectedValue = state.value;
2155
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1960
2156
  state.info.expectedValue = expectedValue;
1961
2157
  switch (attribute) {
1962
2158
  case "innerText":
1963
2159
  val = String(await state.element.innerText());
1964
2160
  break;
2161
+ case "text":
2162
+ val = String(await state.element.textContent());
2163
+ break;
1965
2164
  case "value":
1966
2165
  val = String(await state.element.inputValue());
1967
2166
  break;
@@ -1983,17 +2182,42 @@ class StableBrowser {
1983
2182
  let regex;
1984
2183
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1985
2184
  const patternBody = expectedValue.slice(1, -1);
1986
- regex = new RegExp(patternBody, "g");
2185
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2186
+ regex = new RegExp(processedPattern, "gs");
2187
+ state.info.regex = true;
1987
2188
  }
1988
2189
  else {
1989
2190
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1990
2191
  regex = new RegExp(escapedPattern, "g");
1991
2192
  }
1992
- if (!val.match(regex)) {
1993
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
1994
- state.info.failCause.assertionFailed = true;
1995
- state.info.failCause.lastError = errorMessage;
1996
- throw new Error(errorMessage);
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
+ }
1997
2221
  }
1998
2222
  return state.info;
1999
2223
  }
@@ -2001,7 +2225,7 @@ class StableBrowser {
2001
2225
  await _commandError(state, e, this);
2002
2226
  }
2003
2227
  finally {
2004
- _commandFinally(state, this);
2228
+ await _commandFinally(state, this);
2005
2229
  }
2006
2230
  }
2007
2231
  async extractEmailData(emailAddress, options, world) {
@@ -2246,7 +2470,7 @@ class StableBrowser {
2246
2470
  Object.assign(e, { info: info });
2247
2471
  error = e;
2248
2472
  // throw e;
2249
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2473
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
2250
2474
  }
2251
2475
  finally {
2252
2476
  const endTime = Date.now();
@@ -2271,27 +2495,89 @@ class StableBrowser {
2271
2495
  });
2272
2496
  }
2273
2497
  }
2274
- 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) {
2275
2561
  const frames = this.page.frames();
2276
2562
  let results = [];
2277
- let ignoreCase = false;
2563
+ // let ignoreCase = false;
2278
2564
  for (let i = 0; i < frames.length; i++) {
2279
2565
  if (dateAlternatives.date) {
2280
2566
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2281
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2567
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2282
2568
  result.frame = frames[i];
2283
2569
  results.push(result);
2284
2570
  }
2285
2571
  }
2286
2572
  else if (numberAlternatives.number) {
2287
2573
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2288
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2574
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2289
2575
  result.frame = frames[i];
2290
2576
  results.push(result);
2291
2577
  }
2292
2578
  }
2293
2579
  else {
2294
- 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, {});
2295
2581
  result.frame = frames[i];
2296
2582
  results.push(result);
2297
2583
  }
@@ -2310,11 +2596,14 @@ class StableBrowser {
2310
2596
  scroll: false,
2311
2597
  highlight: false,
2312
2598
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2313
- text: `Verify text exists in page`,
2599
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2314
2600
  _text: `Verify the text '${text}' exists in page`,
2315
2601
  operation: "verifyTextExistInPage",
2316
2602
  log: "***** verify text " + text + " exists in page *****\n",
2317
2603
  };
2604
+ if (testForRegex(text)) {
2605
+ text = text.replace(/\\"/g, '"');
2606
+ }
2318
2607
  const timeout = this._getFindElementTimeout(options);
2319
2608
  await new Promise((resolve) => setTimeout(resolve, 2000));
2320
2609
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2385,7 +2674,7 @@ class StableBrowser {
2385
2674
  await _commandError(state, e, this);
2386
2675
  }
2387
2676
  finally {
2388
- _commandFinally(state, this);
2677
+ await _commandFinally(state, this);
2389
2678
  }
2390
2679
  }
2391
2680
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2398,11 +2687,14 @@ class StableBrowser {
2398
2687
  scroll: false,
2399
2688
  highlight: false,
2400
2689
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2401
- text: `Verify text does not exist in page`,
2690
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2402
2691
  _text: `Verify the text '${text}' does not exist in page`,
2403
2692
  operation: "verifyTextNotExistInPage",
2404
2693
  log: "***** verify text " + text + " does not exist in page *****\n",
2405
2694
  };
2695
+ if (testForRegex(text)) {
2696
+ text = text.replace(/\\"/g, '"');
2697
+ }
2406
2698
  const timeout = this._getFindElementTimeout(options);
2407
2699
  await new Promise((resolve) => setTimeout(resolve, 2000));
2408
2700
  const newValue = await this._replaceWithLocalData(text, world);
@@ -2439,7 +2731,7 @@ class StableBrowser {
2439
2731
  await _commandError(state, e, this);
2440
2732
  }
2441
2733
  finally {
2442
- _commandFinally(state, this);
2734
+ await _commandFinally(state, this);
2443
2735
  }
2444
2736
  }
2445
2737
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2481,7 +2773,7 @@ class StableBrowser {
2481
2773
  };
2482
2774
  while (true) {
2483
2775
  try {
2484
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2776
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2485
2777
  }
2486
2778
  catch (error) {
2487
2779
  // ignore
@@ -2509,7 +2801,7 @@ class StableBrowser {
2509
2801
  const count = await frame.locator(css).count();
2510
2802
  for (let j = 0; j < count; j++) {
2511
2803
  const continer = await frame.locator(css).nth(j);
2512
- 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, {});
2513
2805
  if (result.elementCount > 0) {
2514
2806
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2515
2807
  await this._highlightElements(frame, dataAttribute);
@@ -2550,9 +2842,33 @@ class StableBrowser {
2550
2842
  await _commandError(state, e, this);
2551
2843
  }
2552
2844
  finally {
2553
- _commandFinally(state, this);
2845
+ await _commandFinally(state, this);
2554
2846
  }
2555
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
+ }
2556
2872
  async visualVerification(text, options = {}, world = null) {
2557
2873
  const startTime = Date.now();
2558
2874
  let error = null;
@@ -2869,7 +3185,13 @@ class StableBrowser {
2869
3185
  }
2870
3186
  }
2871
3187
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2872
- 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
+ }
2873
3195
  }
2874
3196
  _getLoadTimeout(options) {
2875
3197
  let timeout = 15000;
@@ -2906,6 +3228,9 @@ class StableBrowser {
2906
3228
  this.registerEventListeners(this.context);
2907
3229
  registerNetworkEvents(this.world, this, this.context, this.page);
2908
3230
  registerDownloadEvent(this.page, this.world, this.context);
3231
+ if (this.onRestoreSaveState) {
3232
+ this.onRestoreSaveState(path);
3233
+ }
2909
3234
  }
2910
3235
  async waitForPageLoad(options = {}, world = null) {
2911
3236
  let timeout = this._getLoadTimeout(options);
@@ -2988,11 +3313,98 @@ class StableBrowser {
2988
3313
  await _commandError(state, e, this);
2989
3314
  }
2990
3315
  finally {
2991
- _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);
2992
3404
  }
2993
3405
  }
2994
3406
  saveTestDataAsGlobal(options, world) {
2995
- const dataFile = this._getDataFile(world);
3407
+ const dataFile = _getDataFile(world, this.context, this);
2996
3408
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2997
3409
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2998
3410
  }
@@ -3093,7 +3505,39 @@ class StableBrowser {
3093
3505
  console.log("#-#");
3094
3506
  }
3095
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) { }
3096
3537
  async beforeStep(world, step) {
3538
+ if (!this.beforeScenarioCalled) {
3539
+ this.beforeScenario(world, step);
3540
+ }
3097
3541
  if (this.stepIndex === undefined) {
3098
3542
  this.stepIndex = 0;
3099
3543
  }
@@ -3110,21 +3554,11 @@ class StableBrowser {
3110
3554
  else {
3111
3555
  this.stepName = "step " + this.stepIndex;
3112
3556
  }
3113
- if (this.context) {
3114
- this.context.examplesRow = extractStepExampleParameters(step);
3115
- }
3116
3557
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3117
3558
  if (this.context.browserObject.context) {
3118
3559
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3119
3560
  }
3120
3561
  }
3121
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3122
- this.tags = step.pickle.tags.map((tag) => tag.name);
3123
- // check if @global_test_data tag is present
3124
- if (this.tags.includes("@global_test_data")) {
3125
- this.saveTestDataAsGlobal({}, world);
3126
- }
3127
- }
3128
3562
  if (this.initSnapshotTaken === false) {
3129
3563
  this.initSnapshotTaken = true;
3130
3564
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3149,15 +3583,22 @@ class StableBrowser {
3149
3583
  const content = [`- path: ${path}`, `- title: ${title}`];
3150
3584
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3151
3585
  for (let i = 0; i < frames.length; i++) {
3152
- content.push(`- frame: ${i}`);
3153
3586
  const frame = frames[i];
3154
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3155
- 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) { }
3156
3596
  }
3157
3597
  return content.join("\n");
3158
3598
  }
3159
3599
  catch (e) {
3160
- console.error(e);
3600
+ console.log("Error in getAriaSnapshot");
3601
+ //console.debug(e);
3161
3602
  }
3162
3603
  return null;
3163
3604
  }
@@ -3168,6 +3609,13 @@ class StableBrowser {
3168
3609
  await this.context.browserObject.context.tracing.stopChunk({
3169
3610
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3170
3611
  });
3612
+ if (world && world.attach) {
3613
+ await world.attach(JSON.stringify({
3614
+ type: "trace",
3615
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3616
+ }), "application/json+trace");
3617
+ }
3618
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3171
3619
  }
3172
3620
  }
3173
3621
  if (this.context) {