automation_model 1.0.618-dev → 1.0.618-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 +105 -30
  6. package/lib/auto_page.js.map +1 -1
  7. package/lib/browser_manager.js +70 -22
  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 +34 -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 +35 -0
  33. package/lib/snapshot_validation.js +239 -0
  34. package/lib/snapshot_validation.js.map +1 -0
  35. package/lib/stable_browser.d.ts +23 -1
  36. package/lib/stable_browser.js +657 -123
  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 +220 -19
  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) => {
@@ -70,6 +80,7 @@ class StableBrowser {
70
80
  appName = "main";
71
81
  tags = null;
72
82
  isRecording = false;
83
+ initSnapshotTaken = false;
73
84
  constructor(browser, page, logger = null, context = null, world = null) {
74
85
  this.browser = browser;
75
86
  this.page = page;
@@ -176,6 +187,30 @@ class StableBrowser {
176
187
  await this.waitForPageLoad();
177
188
  }
178
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
+ }
179
214
  registerConsoleLogListener(page, context) {
180
215
  if (!this.context.webLogger) {
181
216
  this.context.webLogger = [];
@@ -271,7 +306,7 @@ class StableBrowser {
271
306
  _commandError(state, error, this);
272
307
  }
273
308
  finally {
274
- _commandFinally(state, this);
309
+ await _commandFinally(state, this);
275
310
  }
276
311
  }
277
312
  async _getLocator(locator, scope, _params) {
@@ -352,7 +387,7 @@ class StableBrowser {
352
387
  return resultCss;
353
388
  }
354
389
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
355
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
390
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
356
391
  const locator = scope.locator(query);
357
392
  const count = await locator.count();
358
393
  if (!tag1) {
@@ -372,6 +407,12 @@ class StableBrowser {
372
407
  if (!el.setAttribute) {
373
408
  el = el.parentElement;
374
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
+ // }
375
416
  el.setAttribute("data-blinq-id-" + randomToken, "");
376
417
  return true;
377
418
  }, [tag1, randomToken]))) {
@@ -558,12 +599,24 @@ class StableBrowser {
558
599
  element.evaluate((el, randomToken) => {
559
600
  el.setAttribute("data-blinq-id-" + randomToken, "");
560
601
  }, randomToken);
561
- if (element._frame) {
562
- return element;
563
- }
564
- const scope = element.page();
565
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
566
- 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);
567
620
  }
568
621
  }
569
622
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -715,14 +768,9 @@ class StableBrowser {
715
768
  // info.log += "scanning locators in priority 2" + "\n";
716
769
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
717
770
  }
718
- if (result.foundElements.length === 0 && onlyPriority3) {
771
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
719
772
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
720
773
  }
721
- else {
722
- if (result.foundElements.length === 0 && !highPriorityOnly) {
723
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
724
- }
725
- }
726
774
  let foundElements = result.foundElements;
727
775
  if (foundElements.length === 1 && foundElements[0].unique) {
728
776
  info.box = foundElements[0].box;
@@ -777,6 +825,11 @@ class StableBrowser {
777
825
  visibleOnly = false;
778
826
  }
779
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
+ }
780
833
  }
781
834
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
782
835
  // if (info.locatorLog) {
@@ -823,9 +876,40 @@ class StableBrowser {
823
876
  result.locatorIndex = i;
824
877
  }
825
878
  if (foundLocators.length > 1) {
826
- info.failCause.foundMultiple = true;
827
- if (info.locatorLog) {
828
- 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
+ }
829
913
  }
830
914
  }
831
915
  }
@@ -873,7 +957,7 @@ class StableBrowser {
873
957
  await _commandError(state, "timeout looking for " + elementDescription, this);
874
958
  }
875
959
  finally {
876
- _commandFinally(state, this);
960
+ await _commandFinally(state, this);
877
961
  }
878
962
  }
879
963
  }
@@ -922,7 +1006,7 @@ class StableBrowser {
922
1006
  await _commandError(state, "timeout looking for " + elementDescription, this);
923
1007
  }
924
1008
  finally {
925
- _commandFinally(state, this);
1009
+ await _commandFinally(state, this);
926
1010
  }
927
1011
  }
928
1012
  }
@@ -936,25 +1020,14 @@ class StableBrowser {
936
1020
  options,
937
1021
  world,
938
1022
  text: "Click element",
1023
+ _text: "Click on " + selectors.element_name,
939
1024
  type: Types.CLICK,
940
1025
  operation: "click",
941
1026
  log: "***** click on " + selectors.element_name + " *****\n",
942
1027
  };
943
1028
  try {
944
1029
  await _preCommand(state, this);
945
- // if (state.options && state.options.context) {
946
- // state.selectors.locators[0].text = state.options.context;
947
- // }
948
- try {
949
- await state.element.click();
950
- // await new Promise((resolve) => setTimeout(resolve, 1000));
951
- }
952
- catch (e) {
953
- // await this.closeUnexpectedPopups();
954
- state.element = await this._locate(selectors, state.info, _params);
955
- await state.element.dispatchEvent("click");
956
- // await new Promise((resolve) => setTimeout(resolve, 1000));
957
- }
1030
+ await performAction("click", state.element, options, this, state, _params);
958
1031
  await this.waitForPageLoad();
959
1032
  return state.info;
960
1033
  }
@@ -962,8 +1035,40 @@ class StableBrowser {
962
1035
  await _commandError(state, e, this);
963
1036
  }
964
1037
  finally {
965
- _commandFinally(state, this);
1038
+ await _commandFinally(state, this);
1039
+ }
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);
966
1070
  }
1071
+ return found;
967
1072
  }
968
1073
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
969
1074
  const state = {
@@ -973,6 +1078,7 @@ class StableBrowser {
973
1078
  world,
974
1079
  type: checked ? Types.CHECK : Types.UNCHECK,
975
1080
  text: checked ? `Check element` : `Uncheck element`,
1081
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
976
1082
  operation: "setCheck",
977
1083
  log: "***** check " + selectors.element_name + " *****\n",
978
1084
  };
@@ -984,7 +1090,7 @@ class StableBrowser {
984
1090
  try {
985
1091
  // if (world && world.screenshot && !world.screenshotPath) {
986
1092
  // console.log(`Highlighting while running from recorder`);
987
- await this._highlightElements(element);
1093
+ await this._highlightElements(state.element);
988
1094
  await state.element.setChecked(checked);
989
1095
  await new Promise((resolve) => setTimeout(resolve, 1000));
990
1096
  // await this._unHighlightElements(element);
@@ -1011,7 +1117,7 @@ class StableBrowser {
1011
1117
  await _commandError(state, e, this);
1012
1118
  }
1013
1119
  finally {
1014
- _commandFinally(state, this);
1120
+ await _commandFinally(state, this);
1015
1121
  }
1016
1122
  }
1017
1123
  async hover(selectors, _params, options = {}, world = null) {
@@ -1022,24 +1128,13 @@ class StableBrowser {
1022
1128
  world,
1023
1129
  type: Types.HOVER,
1024
1130
  text: `Hover element`,
1131
+ _text: `Hover on ${selectors.element_name}`,
1025
1132
  operation: "hover",
1026
1133
  log: "***** hover " + selectors.element_name + " *****\n",
1027
1134
  };
1028
1135
  try {
1029
1136
  await _preCommand(state, this);
1030
- try {
1031
- await state.element.hover();
1032
- // await _screenshot(state, this);
1033
- await new Promise((resolve) => setTimeout(resolve, 1000));
1034
- }
1035
- catch (e) {
1036
- //await this.closeUnexpectedPopups();
1037
- state.info.log += "hover failed, will try again" + "\n";
1038
- state.element = await this._locate(selectors, state.info, _params);
1039
- await state.element.hover({ timeout: 10000 });
1040
- // await _screenshot(state, this);
1041
- await new Promise((resolve) => setTimeout(resolve, 1000));
1042
- }
1137
+ await performAction("hover", state.element, options, this, state, _params);
1043
1138
  await _screenshot(state, this);
1044
1139
  await this.waitForPageLoad();
1045
1140
  return state.info;
@@ -1048,7 +1143,7 @@ class StableBrowser {
1048
1143
  await _commandError(state, e, this);
1049
1144
  }
1050
1145
  finally {
1051
- _commandFinally(state, this);
1146
+ await _commandFinally(state, this);
1052
1147
  }
1053
1148
  }
1054
1149
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1063,6 +1158,7 @@ class StableBrowser {
1063
1158
  value: values.toString(),
1064
1159
  type: Types.SELECT,
1065
1160
  text: `Select option: ${values}`,
1161
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1066
1162
  operation: "selectOption",
1067
1163
  log: "***** select option " + selectors.element_name + " *****\n",
1068
1164
  };
@@ -1083,7 +1179,7 @@ class StableBrowser {
1083
1179
  await _commandError(state, e, this);
1084
1180
  }
1085
1181
  finally {
1086
- _commandFinally(state, this);
1182
+ await _commandFinally(state, this);
1087
1183
  }
1088
1184
  }
1089
1185
  async type(_value, _params = null, options = {}, world = null) {
@@ -1097,6 +1193,7 @@ class StableBrowser {
1097
1193
  highlight: false,
1098
1194
  type: Types.TYPE_PRESS,
1099
1195
  text: `Type value: ${_value}`,
1196
+ _text: `Type value: ${_value}`,
1100
1197
  operation: "type",
1101
1198
  log: "",
1102
1199
  };
@@ -1128,7 +1225,7 @@ class StableBrowser {
1128
1225
  await _commandError(state, e, this);
1129
1226
  }
1130
1227
  finally {
1131
- _commandFinally(state, this);
1228
+ await _commandFinally(state, this);
1132
1229
  }
1133
1230
  }
1134
1231
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1164,7 +1261,7 @@ class StableBrowser {
1164
1261
  await _commandError(state, e, this);
1165
1262
  }
1166
1263
  finally {
1167
- _commandFinally(state, this);
1264
+ await _commandFinally(state, this);
1168
1265
  }
1169
1266
  }
1170
1267
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1176,6 +1273,7 @@ class StableBrowser {
1176
1273
  world,
1177
1274
  type: Types.SET_DATE_TIME,
1178
1275
  text: `Set date time value: ${value}`,
1276
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1179
1277
  operation: "setDateTime",
1180
1278
  log: "***** set date time value " + selectors.element_name + " *****\n",
1181
1279
  throwError: false,
@@ -1183,7 +1281,7 @@ class StableBrowser {
1183
1281
  try {
1184
1282
  await _preCommand(state, this);
1185
1283
  try {
1186
- await state.element.click();
1284
+ await performAction("click", state.element, options, this, state, _params);
1187
1285
  await new Promise((resolve) => setTimeout(resolve, 500));
1188
1286
  if (format) {
1189
1287
  state.value = dayjs(state.value).format(format);
@@ -1232,7 +1330,7 @@ class StableBrowser {
1232
1330
  await _commandError(state, e, this);
1233
1331
  }
1234
1332
  finally {
1235
- _commandFinally(state, this);
1333
+ await _commandFinally(state, this);
1236
1334
  }
1237
1335
  }
1238
1336
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1247,9 +1345,13 @@ class StableBrowser {
1247
1345
  world,
1248
1346
  type: Types.FILL,
1249
1347
  text: `Click type input with value: ${_value}`,
1348
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1250
1349
  operation: "clickType",
1251
1350
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1252
1351
  };
1352
+ if (!options) {
1353
+ options = {};
1354
+ }
1253
1355
  if (newValue !== _value) {
1254
1356
  //this.logger.info(_value + "=" + newValue);
1255
1357
  _value = newValue;
@@ -1257,7 +1359,7 @@ class StableBrowser {
1257
1359
  try {
1258
1360
  await _preCommand(state, this);
1259
1361
  state.info.value = _value;
1260
- if (options === null || options === undefined || !options.press) {
1362
+ if (!options.press) {
1261
1363
  try {
1262
1364
  let currentValue = await state.element.inputValue();
1263
1365
  if (currentValue) {
@@ -1268,13 +1370,9 @@ class StableBrowser {
1268
1370
  this.logger.info("unable to clear input value");
1269
1371
  }
1270
1372
  }
1271
- if (options === null || options === undefined || options.press) {
1272
- try {
1273
- await state.element.click({ timeout: 5000 });
1274
- }
1275
- catch (e) {
1276
- await state.element.dispatchEvent("click");
1277
- }
1373
+ if (options.press) {
1374
+ options.timeout = 5000;
1375
+ await performAction("click", state.element, options, this, state, _params);
1278
1376
  }
1279
1377
  else {
1280
1378
  try {
@@ -1332,7 +1430,7 @@ class StableBrowser {
1332
1430
  await _commandError(state, e, this);
1333
1431
  }
1334
1432
  finally {
1335
- _commandFinally(state, this);
1433
+ await _commandFinally(state, this);
1336
1434
  }
1337
1435
  }
1338
1436
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1362,13 +1460,49 @@ class StableBrowser {
1362
1460
  await _commandError(state, e, this);
1363
1461
  }
1364
1462
  finally {
1365
- _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);
1366
1499
  }
1367
1500
  }
1368
1501
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1369
1502
  return await this._getText(selectors, 0, _params, options, info, world);
1370
1503
  }
1371
1504
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1505
+ const timeout = this._getFindElementTimeout(options);
1372
1506
  _validateSelectors(selectors);
1373
1507
  let screenshotId = null;
1374
1508
  let screenshotPath = null;
@@ -1378,7 +1512,7 @@ class StableBrowser {
1378
1512
  }
1379
1513
  info.operation = "getText";
1380
1514
  info.selectors = selectors;
1381
- let element = await this._locate(selectors, info, _params);
1515
+ let element = await this._locate(selectors, info, _params, timeout);
1382
1516
  if (climb > 0) {
1383
1517
  const climbArray = [];
1384
1518
  for (let i = 0; i < climb; i++) {
@@ -1445,6 +1579,7 @@ class StableBrowser {
1445
1579
  highlight: false,
1446
1580
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1447
1581
  text: `Verify element contains pattern: ${pattern}`,
1582
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1448
1583
  operation: "containsPattern",
1449
1584
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1450
1585
  };
@@ -1476,10 +1611,12 @@ class StableBrowser {
1476
1611
  await _commandError(state, e, this);
1477
1612
  }
1478
1613
  finally {
1479
- _commandFinally(state, this);
1614
+ await _commandFinally(state, this);
1480
1615
  }
1481
1616
  }
1482
1617
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1618
+ const timeout = this._getFindElementTimeout(options);
1619
+ const startTime = Date.now();
1483
1620
  const state = {
1484
1621
  selectors,
1485
1622
  _params,
@@ -1506,44 +1643,124 @@ class StableBrowser {
1506
1643
  }
1507
1644
  let foundObj = null;
1508
1645
  try {
1509
- await _preCommand(state, this);
1510
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1511
- if (foundObj && foundObj.element) {
1512
- await this.scrollIfNeeded(foundObj.element, state.info);
1513
- }
1514
- await _screenshot(state, this);
1515
- const dateAlternatives = findDateAlternatives(text);
1516
- const numberAlternatives = findNumberAlternatives(text);
1517
- if (dateAlternatives.date) {
1518
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1519
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1520
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1646
+ while (Date.now() - startTime < timeout) {
1647
+ try {
1648
+ await _preCommand(state, this);
1649
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1650
+ if (foundObj && foundObj.element) {
1651
+ await this.scrollIfNeeded(foundObj.element, state.info);
1652
+ }
1653
+ await _screenshot(state, this);
1654
+ const dateAlternatives = findDateAlternatives(text);
1655
+ const numberAlternatives = findNumberAlternatives(text);
1656
+ if (dateAlternatives.date) {
1657
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1658
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1659
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1660
+ return state.info;
1661
+ }
1662
+ }
1663
+ }
1664
+ else if (numberAlternatives.number) {
1665
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1666
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1667
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1668
+ return state.info;
1669
+ }
1670
+ }
1671
+ }
1672
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1521
1673
  return state.info;
1522
1674
  }
1523
1675
  }
1524
- throw new Error("element doesn't contain text " + text);
1676
+ catch (e) {
1677
+ // Log error but continue retrying until timeout is reached
1678
+ this.logger.warn("Retrying containsText due to: " + e.message);
1679
+ }
1680
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1525
1681
  }
1526
- else if (numberAlternatives.number) {
1527
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1528
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1529
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1530
- return state.info;
1682
+ state.info.foundText = foundObj?.text;
1683
+ state.info.value = foundObj?.value;
1684
+ throw new Error("element doesn't contain text " + text);
1685
+ }
1686
+ catch (e) {
1687
+ await _commandError(state, e, this);
1688
+ throw e;
1689
+ }
1690
+ finally {
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;
1531
1738
  }
1739
+ else {
1740
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1741
+ }
1742
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1743
+ matchResult = snapshotValidation(snapshot, newValue);
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;
1532
1749
  }
1533
- throw new Error("element doesn't contain text " + text);
1534
- }
1535
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1536
- state.info.foundText = foundObj?.text;
1537
- state.info.value = foundObj?.value;
1538
- throw new Error("element doesn't contain text " + text);
1750
+ catch (e) {
1751
+ // Log error but continue retrying until timeout is reached
1752
+ this.logger.warn("Retrying containsText due to: " + e.message);
1753
+ }
1754
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1539
1755
  }
1540
- return state.info;
1756
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1541
1757
  }
1542
1758
  catch (e) {
1543
1759
  await _commandError(state, e, this);
1760
+ throw e;
1544
1761
  }
1545
1762
  finally {
1546
- _commandFinally(state, this);
1763
+ await _commandFinally(state, this);
1547
1764
  }
1548
1765
  }
1549
1766
  async waitForUserInput(message, world = null) {
@@ -1581,6 +1798,15 @@ class StableBrowser {
1581
1798
  // save the data to the file
1582
1799
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1583
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
+ }
1584
1810
  _getDataFilePath(fileName) {
1585
1811
  let dataFile = path.join(this.project_path, "data", fileName);
1586
1812
  if (fs.existsSync(dataFile)) {
@@ -1833,7 +2059,7 @@ class StableBrowser {
1833
2059
  await _commandError(state, e, this);
1834
2060
  }
1835
2061
  finally {
1836
- _commandFinally(state, this);
2062
+ await _commandFinally(state, this);
1837
2063
  }
1838
2064
  }
1839
2065
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1846,6 +2072,7 @@ class StableBrowser {
1846
2072
  world,
1847
2073
  type: Types.EXTRACT,
1848
2074
  text: `Extract attribute from element`,
2075
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1849
2076
  operation: "extractAttribute",
1850
2077
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1851
2078
  allowDisabled: true,
@@ -1863,10 +2090,31 @@ class StableBrowser {
1863
2090
  case "value":
1864
2091
  state.value = await state.element.inputValue();
1865
2092
  break;
2093
+ case "text":
2094
+ state.value = await state.element.textContent();
2095
+ break;
1866
2096
  default:
1867
2097
  state.value = await state.element.getAttribute(attribute);
1868
2098
  break;
1869
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
+ }
1870
2118
  state.info.value = state.value;
1871
2119
  this.setTestData({ [variable]: state.value }, world);
1872
2120
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1877,7 +2125,7 @@ class StableBrowser {
1877
2125
  await _commandError(state, e, this);
1878
2126
  }
1879
2127
  finally {
1880
- _commandFinally(state, this);
2128
+ await _commandFinally(state, this);
1881
2129
  }
1882
2130
  }
1883
2131
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1892,6 +2140,7 @@ class StableBrowser {
1892
2140
  highlight: true,
1893
2141
  screenshot: true,
1894
2142
  text: `Verify element attribute`,
2143
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1895
2144
  operation: "verifyAttribute",
1896
2145
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1897
2146
  allowDisabled: true,
@@ -1901,12 +2150,15 @@ class StableBrowser {
1901
2150
  let expectedValue;
1902
2151
  try {
1903
2152
  await _preCommand(state, this);
1904
- expectedValue = state.value;
2153
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1905
2154
  state.info.expectedValue = expectedValue;
1906
2155
  switch (attribute) {
1907
2156
  case "innerText":
1908
2157
  val = String(await state.element.innerText());
1909
2158
  break;
2159
+ case "text":
2160
+ val = String(await state.element.textContent());
2161
+ break;
1910
2162
  case "value":
1911
2163
  val = String(await state.element.inputValue());
1912
2164
  break;
@@ -1946,7 +2198,7 @@ class StableBrowser {
1946
2198
  await _commandError(state, e, this);
1947
2199
  }
1948
2200
  finally {
1949
- _commandFinally(state, this);
2201
+ await _commandFinally(state, this);
1950
2202
  }
1951
2203
  }
1952
2204
  async extractEmailData(emailAddress, options, world) {
@@ -2198,6 +2450,69 @@ class StableBrowser {
2198
2450
  _reportToWorld(world, {
2199
2451
  type: Types.VERIFY_PAGE_PATH,
2200
2452
  text: "Verify page path",
2453
+ _text: "Verify the page path contains " + pathPart,
2454
+ screenshotId,
2455
+ result: error
2456
+ ? {
2457
+ status: "FAILED",
2458
+ startTime,
2459
+ endTime,
2460
+ message: error?.message,
2461
+ }
2462
+ : {
2463
+ status: "PASSED",
2464
+ startTime,
2465
+ endTime,
2466
+ },
2467
+ info: info,
2468
+ });
2469
+ }
2470
+ }
2471
+ async verifyPageTitle(title, options = {}, world = null) {
2472
+ const startTime = Date.now();
2473
+ let error = null;
2474
+ let screenshotId = null;
2475
+ let screenshotPath = null;
2476
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2477
+ const info = {};
2478
+ info.log = "***** verify page title " + title + " *****\n";
2479
+ info.operation = "verifyPageTitle";
2480
+ const newValue = await this._replaceWithLocalData(title, world);
2481
+ if (newValue !== title) {
2482
+ this.logger.info(title + "=" + newValue);
2483
+ title = newValue;
2484
+ }
2485
+ info.title = title;
2486
+ try {
2487
+ for (let i = 0; i < 30; i++) {
2488
+ const foundTitle = await this.page.title();
2489
+ if (!foundTitle.includes(title)) {
2490
+ if (i === 29) {
2491
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2492
+ }
2493
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2494
+ continue;
2495
+ }
2496
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2497
+ return info;
2498
+ }
2499
+ }
2500
+ catch (e) {
2501
+ //await this.closeUnexpectedPopups();
2502
+ this.logger.error("verify page title failed " + info.log);
2503
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2504
+ info.screenshotPath = screenshotPath;
2505
+ Object.assign(e, { info: info });
2506
+ error = e;
2507
+ // throw e;
2508
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2509
+ }
2510
+ finally {
2511
+ const endTime = Date.now();
2512
+ _reportToWorld(world, {
2513
+ type: Types.VERIFY_PAGE_PATH,
2514
+ text: "Verify page title",
2515
+ _text: "Verify the page title contains " + title,
2201
2516
  screenshotId,
2202
2517
  result: error
2203
2518
  ? {
@@ -2215,27 +2530,27 @@ class StableBrowser {
2215
2530
  });
2216
2531
  }
2217
2532
  }
2218
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2533
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2219
2534
  const frames = this.page.frames();
2220
2535
  let results = [];
2221
- let ignoreCase = false;
2536
+ // let ignoreCase = false;
2222
2537
  for (let i = 0; i < frames.length; i++) {
2223
2538
  if (dateAlternatives.date) {
2224
2539
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2225
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2540
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2226
2541
  result.frame = frames[i];
2227
2542
  results.push(result);
2228
2543
  }
2229
2544
  }
2230
2545
  else if (numberAlternatives.number) {
2231
2546
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2232
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2547
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2233
2548
  result.frame = frames[i];
2234
2549
  results.push(result);
2235
2550
  }
2236
2551
  }
2237
2552
  else {
2238
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2553
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2239
2554
  result.frame = frames[i];
2240
2555
  results.push(result);
2241
2556
  }
@@ -2254,11 +2569,15 @@ class StableBrowser {
2254
2569
  scroll: false,
2255
2570
  highlight: false,
2256
2571
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2257
- text: `Verify text exists in page`,
2572
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2573
+ _text: `Verify the text '${text}' exists in page`,
2258
2574
  operation: "verifyTextExistInPage",
2259
2575
  log: "***** verify text " + text + " exists in page *****\n",
2260
2576
  };
2261
- const timeout = this._getLoadTimeout(options);
2577
+ if (testForRegex(text)) {
2578
+ text = text.replace(/\\"/g, '"');
2579
+ }
2580
+ const timeout = this._getFindElementTimeout(options);
2262
2581
  await new Promise((resolve) => setTimeout(resolve, 2000));
2263
2582
  const newValue = await this._replaceWithLocalData(text, world);
2264
2583
  if (newValue !== text) {
@@ -2328,7 +2647,7 @@ class StableBrowser {
2328
2647
  await _commandError(state, e, this);
2329
2648
  }
2330
2649
  finally {
2331
- _commandFinally(state, this);
2650
+ await _commandFinally(state, this);
2332
2651
  }
2333
2652
  }
2334
2653
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2341,11 +2660,15 @@ class StableBrowser {
2341
2660
  scroll: false,
2342
2661
  highlight: false,
2343
2662
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2344
- text: `Verify text does not exist in page`,
2663
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2664
+ _text: `Verify the text '${text}' does not exist in page`,
2345
2665
  operation: "verifyTextNotExistInPage",
2346
2666
  log: "***** verify text " + text + " does not exist in page *****\n",
2347
2667
  };
2348
- const timeout = this._getLoadTimeout(options);
2668
+ if (testForRegex(text)) {
2669
+ text = text.replace(/\\"/g, '"');
2670
+ }
2671
+ const timeout = this._getFindElementTimeout(options);
2349
2672
  await new Promise((resolve) => setTimeout(resolve, 2000));
2350
2673
  const newValue = await this._replaceWithLocalData(text, world);
2351
2674
  if (newValue !== text) {
@@ -2381,7 +2704,7 @@ class StableBrowser {
2381
2704
  await _commandError(state, e, this);
2382
2705
  }
2383
2706
  finally {
2384
- _commandFinally(state, this);
2707
+ await _commandFinally(state, this);
2385
2708
  }
2386
2709
  }
2387
2710
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2396,10 +2719,11 @@ class StableBrowser {
2396
2719
  highlight: false,
2397
2720
  type: Types.VERIFY_TEXT_WITH_RELATION,
2398
2721
  text: `Verify text with relation to another text`,
2722
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2399
2723
  operation: "verify_text_with_relation",
2400
2724
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2401
2725
  };
2402
- const timeout = this._getLoadTimeout(options);
2726
+ const timeout = this._getFindElementTimeout(options);
2403
2727
  await new Promise((resolve) => setTimeout(resolve, 2000));
2404
2728
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2405
2729
  if (newValue !== textAnchor) {
@@ -2422,7 +2746,7 @@ class StableBrowser {
2422
2746
  };
2423
2747
  while (true) {
2424
2748
  try {
2425
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2749
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2426
2750
  }
2427
2751
  catch (error) {
2428
2752
  // ignore
@@ -2450,7 +2774,7 @@ class StableBrowser {
2450
2774
  const count = await frame.locator(css).count();
2451
2775
  for (let j = 0; j < count; j++) {
2452
2776
  const continer = await frame.locator(css).nth(j);
2453
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2777
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2454
2778
  if (result.elementCount > 0) {
2455
2779
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2456
2780
  await this._highlightElements(frame, dataAttribute);
@@ -2491,8 +2815,32 @@ class StableBrowser {
2491
2815
  await _commandError(state, e, this);
2492
2816
  }
2493
2817
  finally {
2494
- _commandFinally(state, this);
2818
+ await _commandFinally(state, this);
2819
+ }
2820
+ }
2821
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2822
+ const frames = this.page.frames();
2823
+ let results = [];
2824
+ let ignoreCase = false;
2825
+ for (let i = 0; i < frames.length; i++) {
2826
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2827
+ result.frame = frames[i];
2828
+ const climbArray = [];
2829
+ for (let i = 0; i < climb; i++) {
2830
+ climbArray.push("..");
2831
+ }
2832
+ let climbXpath = "xpath=" + climbArray.join("/");
2833
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2834
+ const count = await frames[i].locator(newLocator).count();
2835
+ if (count > 0) {
2836
+ result.elementCount = count;
2837
+ result.locator = newLocator;
2838
+ results.push(result);
2839
+ }
2495
2840
  }
2841
+ // state.info.results = results;
2842
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2843
+ return resultWithElementsFound;
2496
2844
  }
2497
2845
  async visualVerification(text, options = {}, world = null) {
2498
2846
  const startTime = Date.now();
@@ -2555,6 +2903,7 @@ class StableBrowser {
2555
2903
  _reportToWorld(world, {
2556
2904
  type: Types.VERIFY_VISUAL,
2557
2905
  text: "Visual verification",
2906
+ _text: "Visual verification of " + text,
2558
2907
  screenshotId,
2559
2908
  result: error
2560
2909
  ? {
@@ -2809,7 +3158,13 @@ class StableBrowser {
2809
3158
  }
2810
3159
  }
2811
3160
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2812
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3161
+ try {
3162
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3163
+ }
3164
+ catch (error) {
3165
+ this.logger.debug(error);
3166
+ throw error;
3167
+ }
2813
3168
  }
2814
3169
  _getLoadTimeout(options) {
2815
3170
  let timeout = 15000;
@@ -2821,6 +3176,15 @@ class StableBrowser {
2821
3176
  }
2822
3177
  return timeout;
2823
3178
  }
3179
+ _getFindElementTimeout(options) {
3180
+ if (options && options.timeout) {
3181
+ return options.timeout;
3182
+ }
3183
+ if (this.configuration.find_element_timeout) {
3184
+ return this.configuration.find_element_timeout;
3185
+ }
3186
+ return 30000;
3187
+ }
2824
3188
  async saveStoreState(path = null, world = null) {
2825
3189
  const storageState = await this.page.context().storageState();
2826
3190
  //const testDataFile = _getDataFile(world, this.context, this);
@@ -2837,6 +3201,9 @@ class StableBrowser {
2837
3201
  this.registerEventListeners(this.context);
2838
3202
  registerNetworkEvents(this.world, this, this.context, this.page);
2839
3203
  registerDownloadEvent(this.page, this.world, this.context);
3204
+ if (this.onRestoreSaveState) {
3205
+ this.onRestoreSaveState(path);
3206
+ }
2840
3207
  }
2841
3208
  async waitForPageLoad(options = {}, world = null) {
2842
3209
  let timeout = this._getLoadTimeout(options);
@@ -2905,6 +3272,7 @@ class StableBrowser {
2905
3272
  highlight: false,
2906
3273
  type: Types.CLOSE_PAGE,
2907
3274
  text: `Close page`,
3275
+ _text: `Close the page`,
2908
3276
  operation: "closePage",
2909
3277
  log: "***** close page *****\n",
2910
3278
  throwError: false,
@@ -2918,11 +3286,98 @@ class StableBrowser {
2918
3286
  await _commandError(state, e, this);
2919
3287
  }
2920
3288
  finally {
2921
- _commandFinally(state, this);
3289
+ await _commandFinally(state, this);
3290
+ }
3291
+ }
3292
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3293
+ let operation = null;
3294
+ if (!options || !options.operation) {
3295
+ throw new Error("operation is not defined");
3296
+ }
3297
+ operation = options.operation;
3298
+ // validate operation is one of the supported operations
3299
+ if (operation != "click" && operation != "hover+click") {
3300
+ throw new Error("operation is not supported");
3301
+ }
3302
+ const state = {
3303
+ options,
3304
+ world,
3305
+ locate: false,
3306
+ scroll: false,
3307
+ highlight: false,
3308
+ type: Types.TABLE_OPERATION,
3309
+ text: `Table operation`,
3310
+ _text: `Table ${operation} operation`,
3311
+ operation: operation,
3312
+ log: "***** Table operation *****\n",
3313
+ };
3314
+ const timeout = this._getFindElementTimeout(options);
3315
+ try {
3316
+ await _preCommand(state, this);
3317
+ const start = Date.now();
3318
+ let cellArea = null;
3319
+ while (true) {
3320
+ try {
3321
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3322
+ if (cellArea) {
3323
+ break;
3324
+ }
3325
+ }
3326
+ catch (e) {
3327
+ // ignore
3328
+ }
3329
+ if (Date.now() - start > timeout) {
3330
+ throw new Error(`Cell not found in table`);
3331
+ }
3332
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3333
+ }
3334
+ switch (operation) {
3335
+ case "click":
3336
+ if (!options.css) {
3337
+ // will click in the center of the cell
3338
+ let xOffset = 0;
3339
+ let yOffset = 0;
3340
+ if (options.xOffset) {
3341
+ xOffset = options.xOffset;
3342
+ }
3343
+ if (options.yOffset) {
3344
+ yOffset = options.yOffset;
3345
+ }
3346
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3347
+ }
3348
+ else {
3349
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3350
+ if (results.length === 0) {
3351
+ throw new Error(`Element not found in cell area`);
3352
+ }
3353
+ state.element = results[0];
3354
+ await performAction("click", state.element, options, this, state, _params);
3355
+ }
3356
+ break;
3357
+ case "hover+click":
3358
+ if (!options.css) {
3359
+ throw new Error("css is not defined");
3360
+ }
3361
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3362
+ if (results.length === 0) {
3363
+ throw new Error(`Element not found in cell area`);
3364
+ }
3365
+ state.element = results[0];
3366
+ await performAction("hover+click", state.element, options, this, state, _params);
3367
+ break;
3368
+ default:
3369
+ throw new Error("operation is not supported");
3370
+ }
3371
+ }
3372
+ catch (e) {
3373
+ await _commandError(state, e, this);
3374
+ }
3375
+ finally {
3376
+ await _commandFinally(state, this);
2922
3377
  }
2923
3378
  }
2924
3379
  saveTestDataAsGlobal(options, world) {
2925
- const dataFile = this._getDataFile(world);
3380
+ const dataFile = _getDataFile(world, this.context, this);
2926
3381
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2927
3382
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2928
3383
  }
@@ -2952,6 +3407,7 @@ class StableBrowser {
2952
3407
  _reportToWorld(world, {
2953
3408
  type: Types.SET_VIEWPORT,
2954
3409
  text: "set viewport size to " + width + "x" + hight,
3410
+ _text: "Set the viewport size to " + width + "x" + hight,
2955
3411
  screenshotId,
2956
3412
  result: error
2957
3413
  ? {
@@ -3022,7 +3478,39 @@ class StableBrowser {
3022
3478
  console.log("#-#");
3023
3479
  }
3024
3480
  }
3481
+ async beforeScenario(world, scenario) {
3482
+ this.beforeScenarioCalled = true;
3483
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3484
+ this.scenarioName = scenario.pickle.name;
3485
+ }
3486
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3487
+ this.featureName = scenario.gherkinDocument.feature.name;
3488
+ }
3489
+ if (this.context) {
3490
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3491
+ }
3492
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3493
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3494
+ // check if @global_test_data tag is present
3495
+ if (this.tags.includes("@global_test_data")) {
3496
+ this.saveTestDataAsGlobal({}, world);
3497
+ }
3498
+ }
3499
+ // update test data based on feature/scenario
3500
+ let envName = null;
3501
+ if (this.context && this.context.environment) {
3502
+ envName = this.context.environment.name;
3503
+ }
3504
+ if (!process.env.TEMP_RUN) {
3505
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3506
+ }
3507
+ await loadBrunoParams(this.context, this.context.environment.name);
3508
+ }
3509
+ async afterScenario(world, scenario) { }
3025
3510
  async beforeStep(world, step) {
3511
+ if (!this.beforeScenarioCalled) {
3512
+ this.beforeScenario(world, step);
3513
+ }
3026
3514
  if (this.stepIndex === undefined) {
3027
3515
  this.stepIndex = 0;
3028
3516
  }
@@ -3039,21 +3527,53 @@ class StableBrowser {
3039
3527
  else {
3040
3528
  this.stepName = "step " + this.stepIndex;
3041
3529
  }
3042
- if (this.context) {
3043
- this.context.examplesRow = extractStepExampleParameters(step);
3044
- }
3045
3530
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3046
3531
  if (this.context.browserObject.context) {
3047
3532
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3048
3533
  }
3049
3534
  }
3050
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3051
- this.tags = step.pickle.tags.map((tag) => tag.name);
3052
- // check if @global_test_data tag is present
3053
- if (this.tags.includes("@global_test_data")) {
3054
- this.saveTestDataAsGlobal({}, world);
3535
+ if (this.initSnapshotTaken === false) {
3536
+ this.initSnapshotTaken = true;
3537
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3538
+ const snapshot = await this.getAriaSnapshot();
3539
+ if (snapshot) {
3540
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3541
+ }
3542
+ }
3543
+ }
3544
+ }
3545
+ async getAriaSnapshot() {
3546
+ try {
3547
+ // find the page url
3548
+ const url = await this.page.url();
3549
+ // extract the path from the url
3550
+ const path = new URL(url).pathname;
3551
+ // get the page title
3552
+ const title = await this.page.title();
3553
+ // go over other frams
3554
+ const frames = this.page.frames();
3555
+ const snapshots = [];
3556
+ const content = [`- path: ${path}`, `- title: ${title}`];
3557
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3558
+ for (let i = 0; i < frames.length; i++) {
3559
+ const frame = frames[i];
3560
+ try {
3561
+ // Ensure frame is attached and has body
3562
+ const body = frame.locator("body");
3563
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3564
+ const snapshot = await body.ariaSnapshot({ timeout });
3565
+ content.push(`- frame: ${i}`);
3566
+ content.push(snapshot);
3567
+ }
3568
+ catch (innerErr) { }
3055
3569
  }
3570
+ return content.join("\n");
3056
3571
  }
3572
+ catch (e) {
3573
+ console.log("Error in getAriaSnapshot");
3574
+ //console.debug(e);
3575
+ }
3576
+ return null;
3057
3577
  }
3058
3578
  async afterStep(world, step) {
3059
3579
  this.stepName = null;
@@ -3062,11 +3582,25 @@ class StableBrowser {
3062
3582
  await this.context.browserObject.context.tracing.stopChunk({
3063
3583
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3064
3584
  });
3585
+ if (world && world.attach) {
3586
+ await world.attach(JSON.stringify({
3587
+ type: "trace",
3588
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3589
+ }), "application/json+trace");
3590
+ }
3591
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3065
3592
  }
3066
3593
  }
3067
3594
  if (this.context) {
3068
3595
  this.context.examplesRow = null;
3069
3596
  }
3597
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3598
+ const snapshot = await this.getAriaSnapshot();
3599
+ if (snapshot) {
3600
+ const obj = {};
3601
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3602
+ }
3603
+ }
3070
3604
  }
3071
3605
  }
3072
3606
  function createTimedPromise(promise, label) {