automation_model 1.0.624-dev → 1.0.624-stage

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +130 -0
  2. package/lib/api.js +35 -21
  3. package/lib/api.js.map +1 -1
  4. package/lib/auto_page.d.ts +1 -1
  5. package/lib/auto_page.js +99 -28
  6. package/lib/auto_page.js.map +1 -1
  7. package/lib/browser_manager.js +68 -23
  8. package/lib/browser_manager.js.map +1 -1
  9. package/lib/bruno.d.ts +2 -0
  10. package/lib/bruno.js +381 -0
  11. package/lib/bruno.js.map +1 -0
  12. package/lib/command_common.d.ts +4 -4
  13. package/lib/command_common.js +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 +37 -0
  33. package/lib/snapshot_validation.js +246 -0
  34. package/lib/snapshot_validation.js.map +1 -0
  35. package/lib/stable_browser.d.ts +21 -1
  36. package/lib/stable_browser.js +654 -139
  37. package/lib/stable_browser.js.map +1 -1
  38. package/lib/table_helper.d.ts +19 -0
  39. package/lib/table_helper.js +116 -0
  40. package/lib/table_helper.js.map +1 -0
  41. package/lib/test_context.d.ts +2 -0
  42. package/lib/test_context.js +2 -0
  43. package/lib/test_context.js.map +1 -1
  44. package/lib/utils.d.ts +8 -4
  45. package/lib/utils.js +221 -20
  46. package/lib/utils.js.map +1 -1
  47. package/package.json +7 -6
@@ -10,19 +10,24 @@ import { getDateTimeValue } from "./date_time.js";
10
10
  import drawRectangle from "./drawRect.js";
11
11
  //import { closeUnexpectedPopups } from "./popups.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
14
14
  import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
17
  import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
18
19
  import { locate_element } from "./locate_element.js";
19
20
  import { randomUUID } from "crypto";
20
21
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
21
22
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
23
  import { LocatorLog } from "./locator_log.js";
23
24
  import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { loadBrunoParams } from "./bruno.js";
27
+ import { snapshotValidation } from "./snapshot_validation.js";
24
28
  export const Types = {
25
29
  CLICK: "click_element",
30
+ WAIT_ELEMENT: "wait_element",
26
31
  NAVIGATE: "navigate",
27
32
  FILL: "fill_element",
28
33
  EXECUTE: "execute_page_method",
@@ -44,6 +49,7 @@ export const Types = {
44
49
  UNCHECK: "uncheck_element",
45
50
  EXTRACT: "extract_attribute",
46
51
  CLOSE_PAGE: "close_page",
52
+ TABLE_OPERATION: "table_operation",
47
53
  SET_DATE_TIME: "set_date_time",
48
54
  SET_VIEWPORT: "set_viewport",
49
55
  VERIFY_VISUAL: "verify_visual",
@@ -52,6 +58,10 @@ export const Types = {
52
58
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
53
59
  VERIFY_ATTRIBUTE: "verify_element_attribute",
54
60
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
61
+ BRUNO: "bruno",
62
+ VERIFY_FILE_EXISTS: "verify_file_exists",
63
+ SET_INPUT_FILES: "set_input_files",
64
+ SNAPSHOT_VALIDATION: "snapshot_validation",
55
65
  };
56
66
  export const apps = {};
57
67
  const formatElementName = (elementName) => {
@@ -177,6 +187,30 @@ class StableBrowser {
177
187
  await this.waitForPageLoad();
178
188
  }
179
189
  }
190
+ async switchTab(tabTitleOrIndex) {
191
+ // first check if the tabNameOrIndex is a number
192
+ let index = parseInt(tabTitleOrIndex);
193
+ if (!isNaN(index)) {
194
+ if (index >= 0 && index < this.context.pages.length) {
195
+ this.page = this.context.pages[index];
196
+ this.context.page = this.page;
197
+ await this.page.bringToFront();
198
+ return;
199
+ }
200
+ }
201
+ // if the tabNameOrIndex is a string, find the tab by name
202
+ for (let i = 0; i < this.context.pages.length; i++) {
203
+ let page = this.context.pages[i];
204
+ let title = await page.title();
205
+ if (title.includes(tabTitleOrIndex)) {
206
+ this.page = page;
207
+ this.context.page = this.page;
208
+ await this.page.bringToFront();
209
+ return;
210
+ }
211
+ }
212
+ throw new Error("Tab not found: " + tabTitleOrIndex);
213
+ }
180
214
  registerConsoleLogListener(page, context) {
181
215
  if (!this.context.webLogger) {
182
216
  this.context.webLogger = [];
@@ -272,7 +306,7 @@ class StableBrowser {
272
306
  _commandError(state, error, this);
273
307
  }
274
308
  finally {
275
- _commandFinally(state, this);
309
+ await _commandFinally(state, this);
276
310
  }
277
311
  }
278
312
  async _getLocator(locator, scope, _params) {
@@ -353,7 +387,7 @@ class StableBrowser {
353
387
  return resultCss;
354
388
  }
355
389
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
356
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
390
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
357
391
  const locator = scope.locator(query);
358
392
  const count = await locator.count();
359
393
  if (!tag1) {
@@ -373,6 +407,12 @@ class StableBrowser {
373
407
  if (!el.setAttribute) {
374
408
  el = el.parentElement;
375
409
  }
410
+ // remove any attributes start with data-blinq-id
411
+ // for (let i = 0; i < el.attributes.length; i++) {
412
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
413
+ // el.removeAttribute(el.attributes[i].name);
414
+ // }
415
+ // }
376
416
  el.setAttribute("data-blinq-id-" + randomToken, "");
377
417
  return true;
378
418
  }, [tag1, randomToken]))) {
@@ -559,12 +599,24 @@ class StableBrowser {
559
599
  element.evaluate((el, randomToken) => {
560
600
  el.setAttribute("data-blinq-id-" + randomToken, "");
561
601
  }, randomToken);
562
- if (element._frame) {
563
- return element;
564
- }
565
- const scope = element.page();
566
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
567
- return newSelector;
602
+ // if (element._frame) {
603
+ // return element;
604
+ // }
605
+ const scope = element._frame ?? element.page();
606
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
607
+ let prefixSelector = "";
608
+ const frameControlSelector = " >> internal:control=enter-frame";
609
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
610
+ if (frameSelectorIndex !== -1) {
611
+ // remove everything after the >> internal:control=enter-frame
612
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
613
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
614
+ }
615
+ // if (element?._frame?._selector) {
616
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
617
+ // }
618
+ const newSelector = prefixSelector + newElementSelector;
619
+ return scope.locator(newSelector);
568
620
  }
569
621
  }
570
622
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -716,14 +768,9 @@ class StableBrowser {
716
768
  // info.log += "scanning locators in priority 2" + "\n";
717
769
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
718
770
  }
719
- if (result.foundElements.length === 0 && onlyPriority3) {
771
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
720
772
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
721
773
  }
722
- else {
723
- if (result.foundElements.length === 0 && !highPriorityOnly) {
724
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
725
- }
726
- }
727
774
  let foundElements = result.foundElements;
728
775
  if (foundElements.length === 1 && foundElements[0].unique) {
729
776
  info.box = foundElements[0].box;
@@ -778,6 +825,11 @@ class StableBrowser {
778
825
  visibleOnly = false;
779
826
  }
780
827
  await new Promise((resolve) => setTimeout(resolve, 1000));
828
+ // sheck of more of half of the timeout has passed
829
+ if (Date.now() - startTime > timeout / 2) {
830
+ highPriorityOnly = false;
831
+ visibleOnly = false;
832
+ }
781
833
  }
782
834
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
783
835
  // if (info.locatorLog) {
@@ -824,9 +876,40 @@ class StableBrowser {
824
876
  result.locatorIndex = i;
825
877
  }
826
878
  if (foundLocators.length > 1) {
827
- info.failCause.foundMultiple = true;
828
- if (info.locatorLog) {
829
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
879
+ // remove elements that consume the same space with 10 pixels tolerance
880
+ const boxes = [];
881
+ for (let j = 0; j < foundLocators.length; j++) {
882
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
883
+ }
884
+ for (let j = 0; j < boxes.length; j++) {
885
+ for (let k = 0; k < boxes.length; k++) {
886
+ if (j === k) {
887
+ continue;
888
+ }
889
+ // check if x, y, width, height are the same with 10 pixels tolerance
890
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
891
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
892
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
893
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
894
+ // as the element is not unique, will remove it
895
+ boxes.splice(k, 1);
896
+ k--;
897
+ }
898
+ }
899
+ }
900
+ if (boxes.length === 1) {
901
+ result.foundElements.push({
902
+ locator: boxes[0].locator.first(),
903
+ box: boxes[0].box,
904
+ unique: true,
905
+ });
906
+ result.locatorIndex = i;
907
+ }
908
+ else {
909
+ info.failCause.foundMultiple = true;
910
+ if (info.locatorLog) {
911
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
912
+ }
830
913
  }
831
914
  }
832
915
  }
@@ -874,7 +957,7 @@ class StableBrowser {
874
957
  await _commandError(state, "timeout looking for " + elementDescription, this);
875
958
  }
876
959
  finally {
877
- _commandFinally(state, this);
960
+ await _commandFinally(state, this);
878
961
  }
879
962
  }
880
963
  }
@@ -923,7 +1006,7 @@ class StableBrowser {
923
1006
  await _commandError(state, "timeout looking for " + elementDescription, this);
924
1007
  }
925
1008
  finally {
926
- _commandFinally(state, this);
1009
+ await _commandFinally(state, this);
927
1010
  }
928
1011
  }
929
1012
  }
@@ -937,25 +1020,14 @@ class StableBrowser {
937
1020
  options,
938
1021
  world,
939
1022
  text: "Click element",
1023
+ _text: "Click on " + selectors.element_name,
940
1024
  type: Types.CLICK,
941
1025
  operation: "click",
942
1026
  log: "***** click on " + selectors.element_name + " *****\n",
943
1027
  };
944
1028
  try {
945
1029
  await _preCommand(state, this);
946
- // if (state.options && state.options.context) {
947
- // state.selectors.locators[0].text = state.options.context;
948
- // }
949
- try {
950
- await state.element.click();
951
- // await new Promise((resolve) => setTimeout(resolve, 1000));
952
- }
953
- catch (e) {
954
- // await this.closeUnexpectedPopups();
955
- state.element = await this._locate(selectors, state.info, _params);
956
- await state.element.dispatchEvent("click");
957
- // await new Promise((resolve) => setTimeout(resolve, 1000));
958
- }
1030
+ await performAction("click", state.element, options, this, state, _params);
959
1031
  await this.waitForPageLoad();
960
1032
  return state.info;
961
1033
  }
@@ -963,8 +1035,40 @@ class StableBrowser {
963
1035
  await _commandError(state, e, this);
964
1036
  }
965
1037
  finally {
966
- _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);
967
1070
  }
1071
+ return found;
968
1072
  }
969
1073
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
970
1074
  const state = {
@@ -974,6 +1078,7 @@ class StableBrowser {
974
1078
  world,
975
1079
  type: checked ? Types.CHECK : Types.UNCHECK,
976
1080
  text: checked ? `Check element` : `Uncheck element`,
1081
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
977
1082
  operation: "setCheck",
978
1083
  log: "***** check " + selectors.element_name + " *****\n",
979
1084
  };
@@ -985,7 +1090,7 @@ class StableBrowser {
985
1090
  try {
986
1091
  // if (world && world.screenshot && !world.screenshotPath) {
987
1092
  // console.log(`Highlighting while running from recorder`);
988
- await this._highlightElements(element);
1093
+ await this._highlightElements(state.element);
989
1094
  await state.element.setChecked(checked);
990
1095
  await new Promise((resolve) => setTimeout(resolve, 1000));
991
1096
  // await this._unHighlightElements(element);
@@ -1012,7 +1117,7 @@ class StableBrowser {
1012
1117
  await _commandError(state, e, this);
1013
1118
  }
1014
1119
  finally {
1015
- _commandFinally(state, this);
1120
+ await _commandFinally(state, this);
1016
1121
  }
1017
1122
  }
1018
1123
  async hover(selectors, _params, options = {}, world = null) {
@@ -1023,24 +1128,13 @@ class StableBrowser {
1023
1128
  world,
1024
1129
  type: Types.HOVER,
1025
1130
  text: `Hover element`,
1131
+ _text: `Hover on ${selectors.element_name}`,
1026
1132
  operation: "hover",
1027
1133
  log: "***** hover " + selectors.element_name + " *****\n",
1028
1134
  };
1029
1135
  try {
1030
1136
  await _preCommand(state, this);
1031
- try {
1032
- await state.element.hover();
1033
- // await _screenshot(state, this);
1034
- await new Promise((resolve) => setTimeout(resolve, 1000));
1035
- }
1036
- catch (e) {
1037
- //await this.closeUnexpectedPopups();
1038
- state.info.log += "hover failed, will try again" + "\n";
1039
- state.element = await this._locate(selectors, state.info, _params);
1040
- await state.element.hover({ timeout: 10000 });
1041
- // await _screenshot(state, this);
1042
- await new Promise((resolve) => setTimeout(resolve, 1000));
1043
- }
1137
+ await performAction("hover", state.element, options, this, state, _params);
1044
1138
  await _screenshot(state, this);
1045
1139
  await this.waitForPageLoad();
1046
1140
  return state.info;
@@ -1049,7 +1143,7 @@ class StableBrowser {
1049
1143
  await _commandError(state, e, this);
1050
1144
  }
1051
1145
  finally {
1052
- _commandFinally(state, this);
1146
+ await _commandFinally(state, this);
1053
1147
  }
1054
1148
  }
1055
1149
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1064,6 +1158,7 @@ class StableBrowser {
1064
1158
  value: values.toString(),
1065
1159
  type: Types.SELECT,
1066
1160
  text: `Select option: ${values}`,
1161
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1067
1162
  operation: "selectOption",
1068
1163
  log: "***** select option " + selectors.element_name + " *****\n",
1069
1164
  };
@@ -1084,7 +1179,7 @@ class StableBrowser {
1084
1179
  await _commandError(state, e, this);
1085
1180
  }
1086
1181
  finally {
1087
- _commandFinally(state, this);
1182
+ await _commandFinally(state, this);
1088
1183
  }
1089
1184
  }
1090
1185
  async type(_value, _params = null, options = {}, world = null) {
@@ -1098,6 +1193,7 @@ class StableBrowser {
1098
1193
  highlight: false,
1099
1194
  type: Types.TYPE_PRESS,
1100
1195
  text: `Type value: ${_value}`,
1196
+ _text: `Type value: ${_value}`,
1101
1197
  operation: "type",
1102
1198
  log: "",
1103
1199
  };
@@ -1129,7 +1225,7 @@ class StableBrowser {
1129
1225
  await _commandError(state, e, this);
1130
1226
  }
1131
1227
  finally {
1132
- _commandFinally(state, this);
1228
+ await _commandFinally(state, this);
1133
1229
  }
1134
1230
  }
1135
1231
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1165,7 +1261,7 @@ class StableBrowser {
1165
1261
  await _commandError(state, e, this);
1166
1262
  }
1167
1263
  finally {
1168
- _commandFinally(state, this);
1264
+ await _commandFinally(state, this);
1169
1265
  }
1170
1266
  }
1171
1267
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1177,6 +1273,7 @@ class StableBrowser {
1177
1273
  world,
1178
1274
  type: Types.SET_DATE_TIME,
1179
1275
  text: `Set date time value: ${value}`,
1276
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1180
1277
  operation: "setDateTime",
1181
1278
  log: "***** set date time value " + selectors.element_name + " *****\n",
1182
1279
  throwError: false,
@@ -1184,7 +1281,7 @@ class StableBrowser {
1184
1281
  try {
1185
1282
  await _preCommand(state, this);
1186
1283
  try {
1187
- await state.element.click();
1284
+ await performAction("click", state.element, options, this, state, _params);
1188
1285
  await new Promise((resolve) => setTimeout(resolve, 500));
1189
1286
  if (format) {
1190
1287
  state.value = dayjs(state.value).format(format);
@@ -1233,7 +1330,7 @@ class StableBrowser {
1233
1330
  await _commandError(state, e, this);
1234
1331
  }
1235
1332
  finally {
1236
- _commandFinally(state, this);
1333
+ await _commandFinally(state, this);
1237
1334
  }
1238
1335
  }
1239
1336
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1248,9 +1345,13 @@ class StableBrowser {
1248
1345
  world,
1249
1346
  type: Types.FILL,
1250
1347
  text: `Click type input with value: ${_value}`,
1348
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1251
1349
  operation: "clickType",
1252
1350
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1253
1351
  };
1352
+ if (!options) {
1353
+ options = {};
1354
+ }
1254
1355
  if (newValue !== _value) {
1255
1356
  //this.logger.info(_value + "=" + newValue);
1256
1357
  _value = newValue;
@@ -1258,7 +1359,7 @@ class StableBrowser {
1258
1359
  try {
1259
1360
  await _preCommand(state, this);
1260
1361
  state.info.value = _value;
1261
- if (options === null || options === undefined || !options.press) {
1362
+ if (!options.press) {
1262
1363
  try {
1263
1364
  let currentValue = await state.element.inputValue();
1264
1365
  if (currentValue) {
@@ -1269,13 +1370,9 @@ class StableBrowser {
1269
1370
  this.logger.info("unable to clear input value");
1270
1371
  }
1271
1372
  }
1272
- if (options === null || options === undefined || options.press) {
1273
- try {
1274
- await state.element.click({ timeout: 5000 });
1275
- }
1276
- catch (e) {
1277
- await state.element.dispatchEvent("click");
1278
- }
1373
+ if (options.press) {
1374
+ options.timeout = 5000;
1375
+ await performAction("click", state.element, options, this, state, _params);
1279
1376
  }
1280
1377
  else {
1281
1378
  try {
@@ -1333,7 +1430,7 @@ class StableBrowser {
1333
1430
  await _commandError(state, e, this);
1334
1431
  }
1335
1432
  finally {
1336
- _commandFinally(state, this);
1433
+ await _commandFinally(state, this);
1337
1434
  }
1338
1435
  }
1339
1436
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1363,13 +1460,49 @@ class StableBrowser {
1363
1460
  await _commandError(state, e, this);
1364
1461
  }
1365
1462
  finally {
1366
- _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);
1367
1499
  }
1368
1500
  }
1369
1501
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1370
1502
  return await this._getText(selectors, 0, _params, options, info, world);
1371
1503
  }
1372
1504
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1505
+ const timeout = this._getFindElementTimeout(options);
1373
1506
  _validateSelectors(selectors);
1374
1507
  let screenshotId = null;
1375
1508
  let screenshotPath = null;
@@ -1379,7 +1512,7 @@ class StableBrowser {
1379
1512
  }
1380
1513
  info.operation = "getText";
1381
1514
  info.selectors = selectors;
1382
- let element = await this._locate(selectors, info, _params);
1515
+ let element = await this._locate(selectors, info, _params, timeout);
1383
1516
  if (climb > 0) {
1384
1517
  const climbArray = [];
1385
1518
  for (let i = 0; i < climb; i++) {
@@ -1446,6 +1579,7 @@ class StableBrowser {
1446
1579
  highlight: false,
1447
1580
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1448
1581
  text: `Verify element contains pattern: ${pattern}`,
1582
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1449
1583
  operation: "containsPattern",
1450
1584
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1451
1585
  };
@@ -1477,10 +1611,12 @@ class StableBrowser {
1477
1611
  await _commandError(state, e, this);
1478
1612
  }
1479
1613
  finally {
1480
- _commandFinally(state, this);
1614
+ await _commandFinally(state, this);
1481
1615
  }
1482
1616
  }
1483
1617
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1618
+ const timeout = this._getFindElementTimeout(options);
1619
+ const startTime = Date.now();
1484
1620
  const state = {
1485
1621
  selectors,
1486
1622
  _params,
@@ -1507,44 +1643,124 @@ class StableBrowser {
1507
1643
  }
1508
1644
  let foundObj = null;
1509
1645
  try {
1510
- await _preCommand(state, this);
1511
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1512
- if (foundObj && foundObj.element) {
1513
- await this.scrollIfNeeded(foundObj.element, state.info);
1514
- }
1515
- await _screenshot(state, this);
1516
- const dateAlternatives = findDateAlternatives(text);
1517
- const numberAlternatives = findNumberAlternatives(text);
1518
- if (dateAlternatives.date) {
1519
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1520
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1521
- 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)) {
1522
1673
  return state.info;
1523
1674
  }
1524
1675
  }
1525
- 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
1526
1681
  }
1527
- else if (numberAlternatives.number) {
1528
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1529
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1530
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1531
- 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;
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, referanceSnapshot);
1744
+ if (matchResult.errorLine !== -1) {
1745
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1532
1746
  }
1747
+ // highlight and screenshot
1748
+ return state.info;
1533
1749
  }
1534
- throw new Error("element doesn't contain text " + text);
1535
- }
1536
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1537
- state.info.foundText = foundObj?.text;
1538
- state.info.value = foundObj?.value;
1539
- 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 snapshot validation due to: " + e.message);
1753
+ }
1754
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1540
1755
  }
1541
- return state.info;
1756
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1542
1757
  }
1543
1758
  catch (e) {
1544
1759
  await _commandError(state, e, this);
1760
+ throw e;
1545
1761
  }
1546
1762
  finally {
1547
- _commandFinally(state, this);
1763
+ await _commandFinally(state, this);
1548
1764
  }
1549
1765
  }
1550
1766
  async waitForUserInput(message, world = null) {
@@ -1582,6 +1798,15 @@ class StableBrowser {
1582
1798
  // save the data to the file
1583
1799
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1584
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
+ }
1585
1810
  _getDataFilePath(fileName) {
1586
1811
  let dataFile = path.join(this.project_path, "data", fileName);
1587
1812
  if (fs.existsSync(dataFile)) {
@@ -1834,7 +2059,7 @@ class StableBrowser {
1834
2059
  await _commandError(state, e, this);
1835
2060
  }
1836
2061
  finally {
1837
- _commandFinally(state, this);
2062
+ await _commandFinally(state, this);
1838
2063
  }
1839
2064
  }
1840
2065
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1847,6 +2072,7 @@ class StableBrowser {
1847
2072
  world,
1848
2073
  type: Types.EXTRACT,
1849
2074
  text: `Extract attribute from element`,
2075
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1850
2076
  operation: "extractAttribute",
1851
2077
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1852
2078
  allowDisabled: true,
@@ -1864,10 +2090,31 @@ class StableBrowser {
1864
2090
  case "value":
1865
2091
  state.value = await state.element.inputValue();
1866
2092
  break;
2093
+ case "text":
2094
+ state.value = await state.element.textContent();
2095
+ break;
1867
2096
  default:
1868
2097
  state.value = await state.element.getAttribute(attribute);
1869
2098
  break;
1870
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
+ }
1871
2118
  state.info.value = state.value;
1872
2119
  this.setTestData({ [variable]: state.value }, world);
1873
2120
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1878,7 +2125,7 @@ class StableBrowser {
1878
2125
  await _commandError(state, e, this);
1879
2126
  }
1880
2127
  finally {
1881
- _commandFinally(state, this);
2128
+ await _commandFinally(state, this);
1882
2129
  }
1883
2130
  }
1884
2131
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1893,6 +2140,7 @@ class StableBrowser {
1893
2140
  highlight: true,
1894
2141
  screenshot: true,
1895
2142
  text: `Verify element attribute`,
2143
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1896
2144
  operation: "verifyAttribute",
1897
2145
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1898
2146
  allowDisabled: true,
@@ -1902,12 +2150,15 @@ class StableBrowser {
1902
2150
  let expectedValue;
1903
2151
  try {
1904
2152
  await _preCommand(state, this);
1905
- expectedValue = state.value;
2153
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1906
2154
  state.info.expectedValue = expectedValue;
1907
2155
  switch (attribute) {
1908
2156
  case "innerText":
1909
2157
  val = String(await state.element.innerText());
1910
2158
  break;
2159
+ case "text":
2160
+ val = String(await state.element.textContent());
2161
+ break;
1911
2162
  case "value":
1912
2163
  val = String(await state.element.inputValue());
1913
2164
  break;
@@ -1929,17 +2180,42 @@ class StableBrowser {
1929
2180
  let regex;
1930
2181
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1931
2182
  const patternBody = expectedValue.slice(1, -1);
1932
- regex = new RegExp(patternBody, "g");
2183
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2184
+ regex = new RegExp(processedPattern, "gs");
2185
+ state.info.regex = true;
1933
2186
  }
1934
2187
  else {
1935
2188
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1936
2189
  regex = new RegExp(escapedPattern, "g");
1937
2190
  }
1938
- if (!val.match(regex)) {
1939
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
1940
- state.info.failCause.assertionFailed = true;
1941
- state.info.failCause.lastError = errorMessage;
1942
- throw new Error(errorMessage);
2191
+ if (attribute === "innerText") {
2192
+ if (state.info.regex) {
2193
+ if (!regex.test(val)) {
2194
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2195
+ state.info.failCause.assertionFailed = true;
2196
+ state.info.failCause.lastError = errorMessage;
2197
+ throw new Error(errorMessage);
2198
+ }
2199
+ }
2200
+ else {
2201
+ const valLines = val.split("\n");
2202
+ const expectedLines = expectedValue.split("\n");
2203
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2204
+ if (!isPart) {
2205
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2206
+ state.info.failCause.assertionFailed = true;
2207
+ state.info.failCause.lastError = errorMessage;
2208
+ throw new Error(errorMessage);
2209
+ }
2210
+ }
2211
+ }
2212
+ else {
2213
+ if (!val.match(regex)) {
2214
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2215
+ state.info.failCause.assertionFailed = true;
2216
+ state.info.failCause.lastError = errorMessage;
2217
+ throw new Error(errorMessage);
2218
+ }
1943
2219
  }
1944
2220
  return state.info;
1945
2221
  }
@@ -1947,7 +2223,7 @@ class StableBrowser {
1947
2223
  await _commandError(state, e, this);
1948
2224
  }
1949
2225
  finally {
1950
- _commandFinally(state, this);
2226
+ await _commandFinally(state, this);
1951
2227
  }
1952
2228
  }
1953
2229
  async extractEmailData(emailAddress, options, world) {
@@ -2192,13 +2468,76 @@ class StableBrowser {
2192
2468
  Object.assign(e, { info: info });
2193
2469
  error = e;
2194
2470
  // throw e;
2195
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2471
+ await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info, throwError: true }, e, this);
2196
2472
  }
2197
2473
  finally {
2198
2474
  const endTime = Date.now();
2199
2475
  _reportToWorld(world, {
2200
2476
  type: Types.VERIFY_PAGE_PATH,
2201
2477
  text: "Verify page path",
2478
+ _text: "Verify the page path contains " + pathPart,
2479
+ screenshotId,
2480
+ result: error
2481
+ ? {
2482
+ status: "FAILED",
2483
+ startTime,
2484
+ endTime,
2485
+ message: error?.message,
2486
+ }
2487
+ : {
2488
+ status: "PASSED",
2489
+ startTime,
2490
+ endTime,
2491
+ },
2492
+ info: info,
2493
+ });
2494
+ }
2495
+ }
2496
+ async verifyPageTitle(title, options = {}, world = null) {
2497
+ const startTime = Date.now();
2498
+ let error = null;
2499
+ let screenshotId = null;
2500
+ let screenshotPath = null;
2501
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2502
+ const info = {};
2503
+ info.log = "***** verify page title " + title + " *****\n";
2504
+ info.operation = "verifyPageTitle";
2505
+ const newValue = await this._replaceWithLocalData(title, world);
2506
+ if (newValue !== title) {
2507
+ this.logger.info(title + "=" + newValue);
2508
+ title = newValue;
2509
+ }
2510
+ info.title = title;
2511
+ try {
2512
+ for (let i = 0; i < 30; i++) {
2513
+ const foundTitle = await this.page.title();
2514
+ if (!foundTitle.includes(title)) {
2515
+ if (i === 29) {
2516
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2517
+ }
2518
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2519
+ continue;
2520
+ }
2521
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2522
+ return info;
2523
+ }
2524
+ }
2525
+ catch (e) {
2526
+ //await this.closeUnexpectedPopups();
2527
+ this.logger.error("verify page title failed " + info.log);
2528
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2529
+ info.screenshotPath = screenshotPath;
2530
+ Object.assign(e, { info: info });
2531
+ error = e;
2532
+ // throw e;
2533
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2534
+ }
2535
+ finally {
2536
+ const endTime = Date.now();
2537
+ _reportToWorld(world, {
2538
+ type: Types.VERIFY_PAGE_PATH,
2539
+ text: "Verify page title",
2540
+ _text: "Verify the page title contains " + title,
2202
2541
  screenshotId,
2203
2542
  result: error
2204
2543
  ? {
@@ -2216,27 +2555,27 @@ class StableBrowser {
2216
2555
  });
2217
2556
  }
2218
2557
  }
2219
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2558
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2220
2559
  const frames = this.page.frames();
2221
2560
  let results = [];
2222
- let ignoreCase = false;
2561
+ // let ignoreCase = false;
2223
2562
  for (let i = 0; i < frames.length; i++) {
2224
2563
  if (dateAlternatives.date) {
2225
2564
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2226
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2565
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2227
2566
  result.frame = frames[i];
2228
2567
  results.push(result);
2229
2568
  }
2230
2569
  }
2231
2570
  else if (numberAlternatives.number) {
2232
2571
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2233
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2572
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2234
2573
  result.frame = frames[i];
2235
2574
  results.push(result);
2236
2575
  }
2237
2576
  }
2238
2577
  else {
2239
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2578
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2240
2579
  result.frame = frames[i];
2241
2580
  results.push(result);
2242
2581
  }
@@ -2255,11 +2594,15 @@ class StableBrowser {
2255
2594
  scroll: false,
2256
2595
  highlight: false,
2257
2596
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2258
- text: `Verify text exists in page`,
2597
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2598
+ _text: `Verify the text '${text}' exists in page`,
2259
2599
  operation: "verifyTextExistInPage",
2260
2600
  log: "***** verify text " + text + " exists in page *****\n",
2261
2601
  };
2262
- const timeout = this._getLoadTimeout(options);
2602
+ if (testForRegex(text)) {
2603
+ text = text.replace(/\\"/g, '"');
2604
+ }
2605
+ const timeout = this._getFindElementTimeout(options);
2263
2606
  await new Promise((resolve) => setTimeout(resolve, 2000));
2264
2607
  const newValue = await this._replaceWithLocalData(text, world);
2265
2608
  if (newValue !== text) {
@@ -2329,7 +2672,7 @@ class StableBrowser {
2329
2672
  await _commandError(state, e, this);
2330
2673
  }
2331
2674
  finally {
2332
- _commandFinally(state, this);
2675
+ await _commandFinally(state, this);
2333
2676
  }
2334
2677
  }
2335
2678
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2342,11 +2685,15 @@ class StableBrowser {
2342
2685
  scroll: false,
2343
2686
  highlight: false,
2344
2687
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2345
- text: `Verify text does not exist in page`,
2688
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2689
+ _text: `Verify the text '${text}' does not exist in page`,
2346
2690
  operation: "verifyTextNotExistInPage",
2347
2691
  log: "***** verify text " + text + " does not exist in page *****\n",
2348
2692
  };
2349
- const timeout = this._getLoadTimeout(options);
2693
+ if (testForRegex(text)) {
2694
+ text = text.replace(/\\"/g, '"');
2695
+ }
2696
+ const timeout = this._getFindElementTimeout(options);
2350
2697
  await new Promise((resolve) => setTimeout(resolve, 2000));
2351
2698
  const newValue = await this._replaceWithLocalData(text, world);
2352
2699
  if (newValue !== text) {
@@ -2382,7 +2729,7 @@ class StableBrowser {
2382
2729
  await _commandError(state, e, this);
2383
2730
  }
2384
2731
  finally {
2385
- _commandFinally(state, this);
2732
+ await _commandFinally(state, this);
2386
2733
  }
2387
2734
  }
2388
2735
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2397,10 +2744,11 @@ class StableBrowser {
2397
2744
  highlight: false,
2398
2745
  type: Types.VERIFY_TEXT_WITH_RELATION,
2399
2746
  text: `Verify text with relation to another text`,
2747
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2400
2748
  operation: "verify_text_with_relation",
2401
2749
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2402
2750
  };
2403
- const timeout = this._getLoadTimeout(options);
2751
+ const timeout = this._getFindElementTimeout(options);
2404
2752
  await new Promise((resolve) => setTimeout(resolve, 2000));
2405
2753
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2406
2754
  if (newValue !== textAnchor) {
@@ -2423,7 +2771,7 @@ class StableBrowser {
2423
2771
  };
2424
2772
  while (true) {
2425
2773
  try {
2426
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2774
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2427
2775
  }
2428
2776
  catch (error) {
2429
2777
  // ignore
@@ -2451,7 +2799,7 @@ class StableBrowser {
2451
2799
  const count = await frame.locator(css).count();
2452
2800
  for (let j = 0; j < count; j++) {
2453
2801
  const continer = await frame.locator(css).nth(j);
2454
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2802
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2455
2803
  if (result.elementCount > 0) {
2456
2804
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2457
2805
  await this._highlightElements(frame, dataAttribute);
@@ -2492,9 +2840,33 @@ class StableBrowser {
2492
2840
  await _commandError(state, e, this);
2493
2841
  }
2494
2842
  finally {
2495
- _commandFinally(state, this);
2843
+ await _commandFinally(state, this);
2496
2844
  }
2497
2845
  }
2846
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2847
+ const frames = this.page.frames();
2848
+ let results = [];
2849
+ let ignoreCase = false;
2850
+ for (let i = 0; i < frames.length; i++) {
2851
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2852
+ result.frame = frames[i];
2853
+ const climbArray = [];
2854
+ for (let i = 0; i < climb; i++) {
2855
+ climbArray.push("..");
2856
+ }
2857
+ let climbXpath = "xpath=" + climbArray.join("/");
2858
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2859
+ const count = await frames[i].locator(newLocator).count();
2860
+ if (count > 0) {
2861
+ result.elementCount = count;
2862
+ result.locator = newLocator;
2863
+ results.push(result);
2864
+ }
2865
+ }
2866
+ // state.info.results = results;
2867
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2868
+ return resultWithElementsFound;
2869
+ }
2498
2870
  async visualVerification(text, options = {}, world = null) {
2499
2871
  const startTime = Date.now();
2500
2872
  let error = null;
@@ -2556,6 +2928,7 @@ class StableBrowser {
2556
2928
  _reportToWorld(world, {
2557
2929
  type: Types.VERIFY_VISUAL,
2558
2930
  text: "Visual verification",
2931
+ _text: "Visual verification of " + text,
2559
2932
  screenshotId,
2560
2933
  result: error
2561
2934
  ? {
@@ -2810,7 +3183,13 @@ class StableBrowser {
2810
3183
  }
2811
3184
  }
2812
3185
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2813
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3186
+ try {
3187
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3188
+ }
3189
+ catch (error) {
3190
+ this.logger.debug(error);
3191
+ throw error;
3192
+ }
2814
3193
  }
2815
3194
  _getLoadTimeout(options) {
2816
3195
  let timeout = 15000;
@@ -2822,6 +3201,15 @@ class StableBrowser {
2822
3201
  }
2823
3202
  return timeout;
2824
3203
  }
3204
+ _getFindElementTimeout(options) {
3205
+ if (options && options.timeout) {
3206
+ return options.timeout;
3207
+ }
3208
+ if (this.configuration.find_element_timeout) {
3209
+ return this.configuration.find_element_timeout;
3210
+ }
3211
+ return 30000;
3212
+ }
2825
3213
  async saveStoreState(path = null, world = null) {
2826
3214
  const storageState = await this.page.context().storageState();
2827
3215
  //const testDataFile = _getDataFile(world, this.context, this);
@@ -2838,6 +3226,9 @@ class StableBrowser {
2838
3226
  this.registerEventListeners(this.context);
2839
3227
  registerNetworkEvents(this.world, this, this.context, this.page);
2840
3228
  registerDownloadEvent(this.page, this.world, this.context);
3229
+ if (this.onRestoreSaveState) {
3230
+ this.onRestoreSaveState(path);
3231
+ }
2841
3232
  }
2842
3233
  async waitForPageLoad(options = {}, world = null) {
2843
3234
  let timeout = this._getLoadTimeout(options);
@@ -2906,6 +3297,7 @@ class StableBrowser {
2906
3297
  highlight: false,
2907
3298
  type: Types.CLOSE_PAGE,
2908
3299
  text: `Close page`,
3300
+ _text: `Close the page`,
2909
3301
  operation: "closePage",
2910
3302
  log: "***** close page *****\n",
2911
3303
  throwError: false,
@@ -2919,11 +3311,98 @@ class StableBrowser {
2919
3311
  await _commandError(state, e, this);
2920
3312
  }
2921
3313
  finally {
2922
- _commandFinally(state, this);
3314
+ await _commandFinally(state, this);
3315
+ }
3316
+ }
3317
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3318
+ let operation = null;
3319
+ if (!options || !options.operation) {
3320
+ throw new Error("operation is not defined");
3321
+ }
3322
+ operation = options.operation;
3323
+ // validate operation is one of the supported operations
3324
+ if (operation != "click" && operation != "hover+click") {
3325
+ throw new Error("operation is not supported");
3326
+ }
3327
+ const state = {
3328
+ options,
3329
+ world,
3330
+ locate: false,
3331
+ scroll: false,
3332
+ highlight: false,
3333
+ type: Types.TABLE_OPERATION,
3334
+ text: `Table operation`,
3335
+ _text: `Table ${operation} operation`,
3336
+ operation: operation,
3337
+ log: "***** Table operation *****\n",
3338
+ };
3339
+ const timeout = this._getFindElementTimeout(options);
3340
+ try {
3341
+ await _preCommand(state, this);
3342
+ const start = Date.now();
3343
+ let cellArea = null;
3344
+ while (true) {
3345
+ try {
3346
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3347
+ if (cellArea) {
3348
+ break;
3349
+ }
3350
+ }
3351
+ catch (e) {
3352
+ // ignore
3353
+ }
3354
+ if (Date.now() - start > timeout) {
3355
+ throw new Error(`Cell not found in table`);
3356
+ }
3357
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3358
+ }
3359
+ switch (operation) {
3360
+ case "click":
3361
+ if (!options.css) {
3362
+ // will click in the center of the cell
3363
+ let xOffset = 0;
3364
+ let yOffset = 0;
3365
+ if (options.xOffset) {
3366
+ xOffset = options.xOffset;
3367
+ }
3368
+ if (options.yOffset) {
3369
+ yOffset = options.yOffset;
3370
+ }
3371
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3372
+ }
3373
+ else {
3374
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3375
+ if (results.length === 0) {
3376
+ throw new Error(`Element not found in cell area`);
3377
+ }
3378
+ state.element = results[0];
3379
+ await performAction("click", state.element, options, this, state, _params);
3380
+ }
3381
+ break;
3382
+ case "hover+click":
3383
+ if (!options.css) {
3384
+ throw new Error("css is not defined");
3385
+ }
3386
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3387
+ if (results.length === 0) {
3388
+ throw new Error(`Element not found in cell area`);
3389
+ }
3390
+ state.element = results[0];
3391
+ await performAction("hover+click", state.element, options, this, state, _params);
3392
+ break;
3393
+ default:
3394
+ throw new Error("operation is not supported");
3395
+ }
3396
+ }
3397
+ catch (e) {
3398
+ await _commandError(state, e, this);
3399
+ }
3400
+ finally {
3401
+ await _commandFinally(state, this);
2923
3402
  }
2924
3403
  }
2925
3404
  saveTestDataAsGlobal(options, world) {
2926
- const dataFile = this._getDataFile(world);
3405
+ const dataFile = _getDataFile(world, this.context, this);
2927
3406
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2928
3407
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2929
3408
  }
@@ -2953,6 +3432,7 @@ class StableBrowser {
2953
3432
  _reportToWorld(world, {
2954
3433
  type: Types.SET_VIEWPORT,
2955
3434
  text: "set viewport size to " + width + "x" + hight,
3435
+ _text: "Set the viewport size to " + width + "x" + hight,
2956
3436
  screenshotId,
2957
3437
  result: error
2958
3438
  ? {
@@ -3023,7 +3503,39 @@ class StableBrowser {
3023
3503
  console.log("#-#");
3024
3504
  }
3025
3505
  }
3506
+ async beforeScenario(world, scenario) {
3507
+ this.beforeScenarioCalled = true;
3508
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3509
+ this.scenarioName = scenario.pickle.name;
3510
+ }
3511
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3512
+ this.featureName = scenario.gherkinDocument.feature.name;
3513
+ }
3514
+ if (this.context) {
3515
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3516
+ }
3517
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3518
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3519
+ // check if @global_test_data tag is present
3520
+ if (this.tags.includes("@global_test_data")) {
3521
+ this.saveTestDataAsGlobal({}, world);
3522
+ }
3523
+ }
3524
+ // update test data based on feature/scenario
3525
+ let envName = null;
3526
+ if (this.context && this.context.environment) {
3527
+ envName = this.context.environment.name;
3528
+ }
3529
+ if (!process.env.TEMP_RUN) {
3530
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3531
+ }
3532
+ await loadBrunoParams(this.context, this.context.environment.name);
3533
+ }
3534
+ async afterScenario(world, scenario) { }
3026
3535
  async beforeStep(world, step) {
3536
+ if (!this.beforeScenarioCalled) {
3537
+ this.beforeScenario(world, step);
3538
+ }
3027
3539
  if (this.stepIndex === undefined) {
3028
3540
  this.stepIndex = 0;
3029
3541
  }
@@ -3040,27 +3552,17 @@ class StableBrowser {
3040
3552
  else {
3041
3553
  this.stepName = "step " + this.stepIndex;
3042
3554
  }
3043
- if (this.context) {
3044
- this.context.examplesRow = extractStepExampleParameters(step);
3045
- }
3046
3555
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3047
3556
  if (this.context.browserObject.context) {
3048
3557
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3049
3558
  }
3050
3559
  }
3051
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3052
- this.tags = step.pickle.tags.map((tag) => tag.name);
3053
- // check if @global_test_data tag is present
3054
- if (this.tags.includes("@global_test_data")) {
3055
- this.saveTestDataAsGlobal({}, world);
3056
- }
3057
- }
3058
3560
  if (this.initSnapshotTaken === false) {
3059
3561
  this.initSnapshotTaken = true;
3060
3562
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3061
3563
  const snapshot = await this.getAriaSnapshot();
3062
3564
  if (snapshot) {
3063
- await world.attach({ snapshot_init: snapshot }, "application/json+snapshot");
3565
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3064
3566
  }
3065
3567
  }
3066
3568
  }
@@ -3079,15 +3581,22 @@ class StableBrowser {
3079
3581
  const content = [`- path: ${path}`, `- title: ${title}`];
3080
3582
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3081
3583
  for (let i = 0; i < frames.length; i++) {
3082
- content.push(`- frame: ${i}`);
3083
3584
  const frame = frames[i];
3084
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3085
- content.push(snapshot);
3585
+ try {
3586
+ // Ensure frame is attached and has body
3587
+ const body = frame.locator("body");
3588
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3589
+ const snapshot = await body.ariaSnapshot({ timeout });
3590
+ content.push(`- frame: ${i}`);
3591
+ content.push(snapshot);
3592
+ }
3593
+ catch (innerErr) { }
3086
3594
  }
3087
3595
  return content.join("\n");
3088
3596
  }
3089
3597
  catch (e) {
3090
- console.error(e);
3598
+ console.log("Error in getAriaSnapshot");
3599
+ //console.debug(e);
3091
3600
  }
3092
3601
  return null;
3093
3602
  }
@@ -3098,6 +3607,13 @@ class StableBrowser {
3098
3607
  await this.context.browserObject.context.tracing.stopChunk({
3099
3608
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3100
3609
  });
3610
+ if (world && world.attach) {
3611
+ await world.attach(JSON.stringify({
3612
+ type: "trace",
3613
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3614
+ }), "application/json+trace");
3615
+ }
3616
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3101
3617
  }
3102
3618
  }
3103
3619
  if (this.context) {
@@ -3107,8 +3623,7 @@ class StableBrowser {
3107
3623
  const snapshot = await this.getAriaSnapshot();
3108
3624
  if (snapshot) {
3109
3625
  const obj = {};
3110
- obj[`snapshot_${this.stepIndex}`] = snapshot;
3111
- await world.attach(obj, "application/json+snapshot");
3626
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3112
3627
  }
3113
3628
  }
3114
3629
  }