automation_model 1.0.608-dev → 1.0.608-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 (43) 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 +2 -1
  5. package/lib/auto_page.js +64 -10
  6. package/lib/auto_page.js.map +1 -1
  7. package/lib/browser_manager.js +53 -16
  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/error-messages.js +6 -0
  16. package/lib/error-messages.js.map +1 -1
  17. package/lib/file_checker.d.ts +1 -0
  18. package/lib/file_checker.js +61 -0
  19. package/lib/file_checker.js.map +1 -0
  20. package/lib/index.d.ts +2 -0
  21. package/lib/index.js +2 -0
  22. package/lib/index.js.map +1 -1
  23. package/lib/init_browser.d.ts +3 -2
  24. package/lib/init_browser.js +52 -12
  25. package/lib/init_browser.js.map +1 -1
  26. package/lib/locate_element.js +2 -2
  27. package/lib/locate_element.js.map +1 -1
  28. package/lib/network.d.ts +1 -1
  29. package/lib/network.js +5 -5
  30. package/lib/network.js.map +1 -1
  31. package/lib/stable_browser.d.ts +22 -1
  32. package/lib/stable_browser.js +699 -197
  33. package/lib/stable_browser.js.map +1 -1
  34. package/lib/table_helper.d.ts +19 -0
  35. package/lib/table_helper.js +116 -0
  36. package/lib/table_helper.js.map +1 -0
  37. package/lib/test_context.d.ts +3 -0
  38. package/lib/test_context.js +2 -0
  39. package/lib/test_context.js.map +1 -1
  40. package/lib/utils.d.ts +8 -4
  41. package/lib/utils.js +217 -19
  42. package/lib/utils.js.map +1 -1
  43. package/package.json +9 -8
@@ -10,18 +10,23 @@ 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
- import { getContext } from "./init_browser.js";
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";
24
+ import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { loadBrunoParams } from "./bruno.js";
23
27
  export const Types = {
24
28
  CLICK: "click_element",
29
+ WAIT_ELEMENT: "wait_element",
25
30
  NAVIGATE: "navigate",
26
31
  FILL: "fill_element",
27
32
  EXECUTE: "execute_page_method",
@@ -43,6 +48,7 @@ export const Types = {
43
48
  UNCHECK: "uncheck_element",
44
49
  EXTRACT: "extract_attribute",
45
50
  CLOSE_PAGE: "close_page",
51
+ TABLE_OPERATION: "table_operation",
46
52
  SET_DATE_TIME: "set_date_time",
47
53
  SET_VIEWPORT: "set_viewport",
48
54
  VERIFY_VISUAL: "verify_visual",
@@ -51,6 +57,9 @@ export const Types = {
51
57
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
52
58
  VERIFY_ATTRIBUTE: "verify_element_attribute",
53
59
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
60
+ BRUNO: "bruno",
61
+ VERIFY_FILE_EXISTS: "verify_file_exists",
62
+ SET_INPUT_FILES: "set_input_files",
54
63
  };
55
64
  export const apps = {};
56
65
  const formatElementName = (elementName) => {
@@ -69,6 +78,7 @@ class StableBrowser {
69
78
  appName = "main";
70
79
  tags = null;
71
80
  isRecording = false;
81
+ initSnapshotTaken = false;
72
82
  constructor(browser, page, logger = null, context = null, world = null) {
73
83
  this.browser = browser;
74
84
  this.page = page;
@@ -175,6 +185,30 @@ class StableBrowser {
175
185
  await this.waitForPageLoad();
176
186
  }
177
187
  }
188
+ async switchTab(tabTitleOrIndex) {
189
+ // first check if the tabNameOrIndex is a number
190
+ let index = parseInt(tabTitleOrIndex);
191
+ if (!isNaN(index)) {
192
+ if (index >= 0 && index < this.context.pages.length) {
193
+ this.page = this.context.pages[index];
194
+ this.context.page = this.page;
195
+ await this.page.bringToFront();
196
+ return;
197
+ }
198
+ }
199
+ // if the tabNameOrIndex is a string, find the tab by name
200
+ for (let i = 0; i < this.context.pages.length; i++) {
201
+ let page = this.context.pages[i];
202
+ let title = await page.title();
203
+ if (title.includes(tabTitleOrIndex)) {
204
+ this.page = page;
205
+ this.context.page = this.page;
206
+ await this.page.bringToFront();
207
+ return;
208
+ }
209
+ }
210
+ throw new Error("Tab not found: " + tabTitleOrIndex);
211
+ }
178
212
  registerConsoleLogListener(page, context) {
179
213
  if (!this.context.webLogger) {
180
214
  this.context.webLogger = [];
@@ -239,6 +273,9 @@ class StableBrowser {
239
273
  // await closeUnexpectedPopups(this.page);
240
274
  // }
241
275
  async goto(url, world = null) {
276
+ if (!url) {
277
+ throw new Error("url is null, verify that the environment file is correct");
278
+ }
242
279
  if (!url.startsWith("http")) {
243
280
  url = "https://" + url;
244
281
  }
@@ -267,7 +304,7 @@ class StableBrowser {
267
304
  _commandError(state, error, this);
268
305
  }
269
306
  finally {
270
- _commandFinally(state, this);
307
+ await _commandFinally(state, this);
271
308
  }
272
309
  }
273
310
  async _getLocator(locator, scope, _params) {
@@ -348,7 +385,7 @@ class StableBrowser {
348
385
  return resultCss;
349
386
  }
350
387
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
351
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
388
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
352
389
  const locator = scope.locator(query);
353
390
  const count = await locator.count();
354
391
  if (!tag1) {
@@ -368,6 +405,12 @@ class StableBrowser {
368
405
  if (!el.setAttribute) {
369
406
  el = el.parentElement;
370
407
  }
408
+ // remove any attributes start with data-blinq-id
409
+ // for (let i = 0; i < el.attributes.length; i++) {
410
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
411
+ // el.removeAttribute(el.attributes[i].name);
412
+ // }
413
+ // }
371
414
  el.setAttribute("data-blinq-id-" + randomToken, "");
372
415
  return true;
373
416
  }, [tag1, randomToken]))) {
@@ -554,9 +597,24 @@ class StableBrowser {
554
597
  element.evaluate((el, randomToken) => {
555
598
  el.setAttribute("data-blinq-id-" + randomToken, "");
556
599
  }, randomToken);
600
+ // if (element._frame) {
601
+ // return element;
602
+ // }
557
603
  const scope = element._frame ?? element.page();
558
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
559
- return newSelector;
604
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
605
+ let prefixSelector = "";
606
+ const frameControlSelector = " >> internal:control=enter-frame";
607
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
608
+ if (frameSelectorIndex !== -1) {
609
+ // remove everything after the >> internal:control=enter-frame
610
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
611
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
612
+ }
613
+ // if (element?._frame?._selector) {
614
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
615
+ // }
616
+ const newSelector = prefixSelector + newElementSelector;
617
+ return scope.locator(newSelector);
560
618
  }
561
619
  }
562
620
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -708,14 +766,9 @@ class StableBrowser {
708
766
  // info.log += "scanning locators in priority 2" + "\n";
709
767
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
710
768
  }
711
- if (result.foundElements.length === 0 && onlyPriority3) {
769
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
712
770
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
713
771
  }
714
- else {
715
- if (result.foundElements.length === 0 && !highPriorityOnly) {
716
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
717
- }
718
- }
719
772
  let foundElements = result.foundElements;
720
773
  if (foundElements.length === 1 && foundElements[0].unique) {
721
774
  info.box = foundElements[0].box;
@@ -770,6 +823,11 @@ class StableBrowser {
770
823
  visibleOnly = false;
771
824
  }
772
825
  await new Promise((resolve) => setTimeout(resolve, 1000));
826
+ // sheck of more of half of the timeout has passed
827
+ if (Date.now() - startTime > timeout / 2) {
828
+ highPriorityOnly = false;
829
+ visibleOnly = false;
830
+ }
773
831
  }
774
832
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
775
833
  // if (info.locatorLog) {
@@ -816,9 +874,40 @@ class StableBrowser {
816
874
  result.locatorIndex = i;
817
875
  }
818
876
  if (foundLocators.length > 1) {
819
- info.failCause.foundMultiple = true;
820
- if (info.locatorLog) {
821
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
877
+ // remove elements that consume the same space with 10 pixels tolerance
878
+ const boxes = [];
879
+ for (let j = 0; j < foundLocators.length; j++) {
880
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
881
+ }
882
+ for (let j = 0; j < boxes.length; j++) {
883
+ for (let k = 0; k < boxes.length; k++) {
884
+ if (j === k) {
885
+ continue;
886
+ }
887
+ // check if x, y, width, height are the same with 10 pixels tolerance
888
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
889
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
890
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
891
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
892
+ // as the element is not unique, will remove it
893
+ boxes.splice(k, 1);
894
+ k--;
895
+ }
896
+ }
897
+ }
898
+ if (boxes.length === 1) {
899
+ result.foundElements.push({
900
+ locator: boxes[0].locator.first(),
901
+ box: boxes[0].box,
902
+ unique: true,
903
+ });
904
+ result.locatorIndex = i;
905
+ }
906
+ else {
907
+ info.failCause.foundMultiple = true;
908
+ if (info.locatorLog) {
909
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
910
+ }
822
911
  }
823
912
  }
824
913
  }
@@ -866,7 +955,7 @@ class StableBrowser {
866
955
  await _commandError(state, "timeout looking for " + elementDescription, this);
867
956
  }
868
957
  finally {
869
- _commandFinally(state, this);
958
+ await _commandFinally(state, this);
870
959
  }
871
960
  }
872
961
  }
@@ -915,7 +1004,7 @@ class StableBrowser {
915
1004
  await _commandError(state, "timeout looking for " + elementDescription, this);
916
1005
  }
917
1006
  finally {
918
- _commandFinally(state, this);
1007
+ await _commandFinally(state, this);
919
1008
  }
920
1009
  }
921
1010
  }
@@ -929,25 +1018,14 @@ class StableBrowser {
929
1018
  options,
930
1019
  world,
931
1020
  text: "Click element",
1021
+ _text: "Click on " + selectors.element_name,
932
1022
  type: Types.CLICK,
933
1023
  operation: "click",
934
1024
  log: "***** click on " + selectors.element_name + " *****\n",
935
1025
  };
936
1026
  try {
937
1027
  await _preCommand(state, this);
938
- // if (state.options && state.options.context) {
939
- // state.selectors.locators[0].text = state.options.context;
940
- // }
941
- try {
942
- await state.element.click();
943
- // await new Promise((resolve) => setTimeout(resolve, 1000));
944
- }
945
- catch (e) {
946
- // await this.closeUnexpectedPopups();
947
- state.element = await this._locate(selectors, state.info, _params);
948
- await state.element.dispatchEvent("click");
949
- // await new Promise((resolve) => setTimeout(resolve, 1000));
950
- }
1028
+ await performAction("click", state.element, options, this, state, _params);
951
1029
  await this.waitForPageLoad();
952
1030
  return state.info;
953
1031
  }
@@ -955,8 +1033,40 @@ class StableBrowser {
955
1033
  await _commandError(state, e, this);
956
1034
  }
957
1035
  finally {
958
- _commandFinally(state, this);
1036
+ await _commandFinally(state, this);
1037
+ }
1038
+ }
1039
+ async waitForElement(selectors, _params, options = {}, world = null) {
1040
+ const timeout = this._getFindElementTimeout(options);
1041
+ const state = {
1042
+ selectors,
1043
+ _params,
1044
+ options,
1045
+ world,
1046
+ text: "Wait for element",
1047
+ _text: "Wait for " + selectors.element_name,
1048
+ type: Types.WAIT_ELEMENT,
1049
+ operation: "waitForElement",
1050
+ log: "***** wait for " + selectors.element_name + " *****\n",
1051
+ };
1052
+ let found = false;
1053
+ try {
1054
+ await _preCommand(state, this);
1055
+ // if (state.options && state.options.context) {
1056
+ // state.selectors.locators[0].text = state.options.context;
1057
+ // }
1058
+ await state.element.waitFor({ timeout: timeout });
1059
+ found = true;
1060
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1061
+ }
1062
+ catch (e) {
1063
+ console.error("Error on waitForElement", e);
1064
+ // await _commandError(state, e, this);
1065
+ }
1066
+ finally {
1067
+ await _commandFinally(state, this);
959
1068
  }
1069
+ return found;
960
1070
  }
961
1071
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
962
1072
  const state = {
@@ -966,6 +1076,7 @@ class StableBrowser {
966
1076
  world,
967
1077
  type: checked ? Types.CHECK : Types.UNCHECK,
968
1078
  text: checked ? `Check element` : `Uncheck element`,
1079
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
969
1080
  operation: "setCheck",
970
1081
  log: "***** check " + selectors.element_name + " *****\n",
971
1082
  };
@@ -977,7 +1088,7 @@ class StableBrowser {
977
1088
  try {
978
1089
  // if (world && world.screenshot && !world.screenshotPath) {
979
1090
  // console.log(`Highlighting while running from recorder`);
980
- await this._highlightElements(element);
1091
+ await this._highlightElements(state.element);
981
1092
  await state.element.setChecked(checked);
982
1093
  await new Promise((resolve) => setTimeout(resolve, 1000));
983
1094
  // await this._unHighlightElements(element);
@@ -1004,7 +1115,7 @@ class StableBrowser {
1004
1115
  await _commandError(state, e, this);
1005
1116
  }
1006
1117
  finally {
1007
- _commandFinally(state, this);
1118
+ await _commandFinally(state, this);
1008
1119
  }
1009
1120
  }
1010
1121
  async hover(selectors, _params, options = {}, world = null) {
@@ -1015,24 +1126,13 @@ class StableBrowser {
1015
1126
  world,
1016
1127
  type: Types.HOVER,
1017
1128
  text: `Hover element`,
1129
+ _text: `Hover on ${selectors.element_name}`,
1018
1130
  operation: "hover",
1019
1131
  log: "***** hover " + selectors.element_name + " *****\n",
1020
1132
  };
1021
1133
  try {
1022
1134
  await _preCommand(state, this);
1023
- try {
1024
- await state.element.hover();
1025
- // await _screenshot(state, this);
1026
- await new Promise((resolve) => setTimeout(resolve, 1000));
1027
- }
1028
- catch (e) {
1029
- //await this.closeUnexpectedPopups();
1030
- state.info.log += "hover failed, will try again" + "\n";
1031
- state.element = await this._locate(selectors, state.info, _params);
1032
- await state.element.hover({ timeout: 10000 });
1033
- // await _screenshot(state, this);
1034
- await new Promise((resolve) => setTimeout(resolve, 1000));
1035
- }
1135
+ await performAction("hover", state.element, options, this, state, _params);
1036
1136
  await _screenshot(state, this);
1037
1137
  await this.waitForPageLoad();
1038
1138
  return state.info;
@@ -1041,7 +1141,7 @@ class StableBrowser {
1041
1141
  await _commandError(state, e, this);
1042
1142
  }
1043
1143
  finally {
1044
- _commandFinally(state, this);
1144
+ await _commandFinally(state, this);
1045
1145
  }
1046
1146
  }
1047
1147
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1056,6 +1156,7 @@ class StableBrowser {
1056
1156
  value: values.toString(),
1057
1157
  type: Types.SELECT,
1058
1158
  text: `Select option: ${values}`,
1159
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1059
1160
  operation: "selectOption",
1060
1161
  log: "***** select option " + selectors.element_name + " *****\n",
1061
1162
  };
@@ -1076,7 +1177,7 @@ class StableBrowser {
1076
1177
  await _commandError(state, e, this);
1077
1178
  }
1078
1179
  finally {
1079
- _commandFinally(state, this);
1180
+ await _commandFinally(state, this);
1080
1181
  }
1081
1182
  }
1082
1183
  async type(_value, _params = null, options = {}, world = null) {
@@ -1090,6 +1191,7 @@ class StableBrowser {
1090
1191
  highlight: false,
1091
1192
  type: Types.TYPE_PRESS,
1092
1193
  text: `Type value: ${_value}`,
1194
+ _text: `Type value: ${_value}`,
1093
1195
  operation: "type",
1094
1196
  log: "",
1095
1197
  };
@@ -1121,7 +1223,7 @@ class StableBrowser {
1121
1223
  await _commandError(state, e, this);
1122
1224
  }
1123
1225
  finally {
1124
- _commandFinally(state, this);
1226
+ await _commandFinally(state, this);
1125
1227
  }
1126
1228
  }
1127
1229
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1157,7 +1259,7 @@ class StableBrowser {
1157
1259
  await _commandError(state, e, this);
1158
1260
  }
1159
1261
  finally {
1160
- _commandFinally(state, this);
1262
+ await _commandFinally(state, this);
1161
1263
  }
1162
1264
  }
1163
1265
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1169,6 +1271,7 @@ class StableBrowser {
1169
1271
  world,
1170
1272
  type: Types.SET_DATE_TIME,
1171
1273
  text: `Set date time value: ${value}`,
1274
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1172
1275
  operation: "setDateTime",
1173
1276
  log: "***** set date time value " + selectors.element_name + " *****\n",
1174
1277
  throwError: false,
@@ -1176,7 +1279,7 @@ class StableBrowser {
1176
1279
  try {
1177
1280
  await _preCommand(state, this);
1178
1281
  try {
1179
- await state.element.click();
1282
+ await performAction("click", state.element, options, this, state, _params);
1180
1283
  await new Promise((resolve) => setTimeout(resolve, 500));
1181
1284
  if (format) {
1182
1285
  state.value = dayjs(state.value).format(format);
@@ -1225,7 +1328,7 @@ class StableBrowser {
1225
1328
  await _commandError(state, e, this);
1226
1329
  }
1227
1330
  finally {
1228
- _commandFinally(state, this);
1331
+ await _commandFinally(state, this);
1229
1332
  }
1230
1333
  }
1231
1334
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1240,9 +1343,13 @@ class StableBrowser {
1240
1343
  world,
1241
1344
  type: Types.FILL,
1242
1345
  text: `Click type input with value: ${_value}`,
1346
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1243
1347
  operation: "clickType",
1244
1348
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1245
1349
  };
1350
+ if (!options) {
1351
+ options = {};
1352
+ }
1246
1353
  if (newValue !== _value) {
1247
1354
  //this.logger.info(_value + "=" + newValue);
1248
1355
  _value = newValue;
@@ -1250,7 +1357,7 @@ class StableBrowser {
1250
1357
  try {
1251
1358
  await _preCommand(state, this);
1252
1359
  state.info.value = _value;
1253
- if (options === null || options === undefined || !options.press) {
1360
+ if (!options.press) {
1254
1361
  try {
1255
1362
  let currentValue = await state.element.inputValue();
1256
1363
  if (currentValue) {
@@ -1261,13 +1368,9 @@ class StableBrowser {
1261
1368
  this.logger.info("unable to clear input value");
1262
1369
  }
1263
1370
  }
1264
- if (options === null || options === undefined || options.press) {
1265
- try {
1266
- await state.element.click({ timeout: 5000 });
1267
- }
1268
- catch (e) {
1269
- await state.element.dispatchEvent("click");
1270
- }
1371
+ if (options.press) {
1372
+ options.timeout = 5000;
1373
+ await performAction("click", state.element, options, this, state, _params);
1271
1374
  }
1272
1375
  else {
1273
1376
  try {
@@ -1325,7 +1428,7 @@ class StableBrowser {
1325
1428
  await _commandError(state, e, this);
1326
1429
  }
1327
1430
  finally {
1328
- _commandFinally(state, this);
1431
+ await _commandFinally(state, this);
1329
1432
  }
1330
1433
  }
1331
1434
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1355,13 +1458,49 @@ class StableBrowser {
1355
1458
  await _commandError(state, e, this);
1356
1459
  }
1357
1460
  finally {
1358
- _commandFinally(state, this);
1461
+ await _commandFinally(state, this);
1462
+ }
1463
+ }
1464
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1465
+ const state = {
1466
+ selectors,
1467
+ _params,
1468
+ files,
1469
+ value: '"' + files.join('", "') + '"',
1470
+ options,
1471
+ world,
1472
+ type: Types.SET_INPUT_FILES,
1473
+ text: `Set input files`,
1474
+ _text: `Set input files on ${selectors.element_name}`,
1475
+ operation: "setInputFiles",
1476
+ log: "***** set input files " + selectors.element_name + " *****\n",
1477
+ };
1478
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1479
+ try {
1480
+ await _preCommand(state, this);
1481
+ for (let i = 0; i < files.length; i++) {
1482
+ const file = files[i];
1483
+ const filePath = path.join(uploadsFolder, file);
1484
+ if (!fs.existsSync(filePath)) {
1485
+ throw new Error(`File not found: ${filePath}`);
1486
+ }
1487
+ state.files[i] = filePath;
1488
+ }
1489
+ await state.element.setInputFiles(files);
1490
+ return state.info;
1491
+ }
1492
+ catch (e) {
1493
+ await _commandError(state, e, this);
1494
+ }
1495
+ finally {
1496
+ await _commandFinally(state, this);
1359
1497
  }
1360
1498
  }
1361
1499
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1362
1500
  return await this._getText(selectors, 0, _params, options, info, world);
1363
1501
  }
1364
1502
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1503
+ const timeout = this._getFindElementTimeout(options);
1365
1504
  _validateSelectors(selectors);
1366
1505
  let screenshotId = null;
1367
1506
  let screenshotPath = null;
@@ -1371,7 +1510,7 @@ class StableBrowser {
1371
1510
  }
1372
1511
  info.operation = "getText";
1373
1512
  info.selectors = selectors;
1374
- let element = await this._locate(selectors, info, _params);
1513
+ let element = await this._locate(selectors, info, _params, timeout);
1375
1514
  if (climb > 0) {
1376
1515
  const climbArray = [];
1377
1516
  for (let i = 0; i < climb; i++) {
@@ -1438,6 +1577,7 @@ class StableBrowser {
1438
1577
  highlight: false,
1439
1578
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1440
1579
  text: `Verify element contains pattern: ${pattern}`,
1580
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1441
1581
  operation: "containsPattern",
1442
1582
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1443
1583
  };
@@ -1469,10 +1609,12 @@ class StableBrowser {
1469
1609
  await _commandError(state, e, this);
1470
1610
  }
1471
1611
  finally {
1472
- _commandFinally(state, this);
1612
+ await _commandFinally(state, this);
1473
1613
  }
1474
1614
  }
1475
1615
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1616
+ const timeout = this._getFindElementTimeout(options);
1617
+ const startTime = Date.now();
1476
1618
  const state = {
1477
1619
  selectors,
1478
1620
  _params,
@@ -1499,44 +1641,52 @@ class StableBrowser {
1499
1641
  }
1500
1642
  let foundObj = null;
1501
1643
  try {
1502
- await _preCommand(state, this);
1503
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1504
- if (foundObj && foundObj.element) {
1505
- await this.scrollIfNeeded(foundObj.element, state.info);
1506
- }
1507
- await _screenshot(state, this);
1508
- const dateAlternatives = findDateAlternatives(text);
1509
- const numberAlternatives = findNumberAlternatives(text);
1510
- if (dateAlternatives.date) {
1511
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1512
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1513
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1514
- return state.info;
1644
+ while (Date.now() - startTime < timeout) {
1645
+ try {
1646
+ await _preCommand(state, this);
1647
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1648
+ if (foundObj && foundObj.element) {
1649
+ await this.scrollIfNeeded(foundObj.element, state.info);
1515
1650
  }
1516
- }
1517
- throw new Error("element doesn't contain text " + text);
1518
- }
1519
- else if (numberAlternatives.number) {
1520
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1521
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1522
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1651
+ await _screenshot(state, this);
1652
+ const dateAlternatives = findDateAlternatives(text);
1653
+ const numberAlternatives = findNumberAlternatives(text);
1654
+ if (dateAlternatives.date) {
1655
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1656
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1657
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1658
+ return state.info;
1659
+ }
1660
+ }
1661
+ }
1662
+ else if (numberAlternatives.number) {
1663
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1664
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1665
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1666
+ return state.info;
1667
+ }
1668
+ }
1669
+ }
1670
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1523
1671
  return state.info;
1524
1672
  }
1525
1673
  }
1526
- throw new Error("element doesn't contain text " + text);
1527
- }
1528
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1529
- state.info.foundText = foundObj?.text;
1530
- state.info.value = foundObj?.value;
1531
- throw new Error("element doesn't contain text " + text);
1674
+ catch (e) {
1675
+ // Log error but continue retrying until timeout is reached
1676
+ this.logger.warn("Retrying containsText due to: " + e.message);
1677
+ }
1678
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1532
1679
  }
1533
- return state.info;
1680
+ state.info.foundText = foundObj?.text;
1681
+ state.info.value = foundObj?.value;
1682
+ throw new Error("element doesn't contain text " + text);
1534
1683
  }
1535
1684
  catch (e) {
1536
1685
  await _commandError(state, e, this);
1686
+ throw e;
1537
1687
  }
1538
1688
  finally {
1539
- _commandFinally(state, this);
1689
+ await _commandFinally(state, this);
1540
1690
  }
1541
1691
  }
1542
1692
  async waitForUserInput(message, world = null) {
@@ -1574,6 +1724,15 @@ class StableBrowser {
1574
1724
  // save the data to the file
1575
1725
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1576
1726
  }
1727
+ overwriteTestData(testData, world = null) {
1728
+ if (!testData) {
1729
+ return;
1730
+ }
1731
+ // if data file exists, load it
1732
+ const dataFile = _getDataFile(world, this.context, this);
1733
+ // save the data to the file
1734
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1735
+ }
1577
1736
  _getDataFilePath(fileName) {
1578
1737
  let dataFile = path.join(this.project_path, "data", fileName);
1579
1738
  if (fs.existsSync(dataFile)) {
@@ -1803,6 +1962,7 @@ class StableBrowser {
1803
1962
  else {
1804
1963
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1805
1964
  }
1965
+ return screenshotBuffer;
1806
1966
  }
1807
1967
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1808
1968
  const state = {
@@ -1825,7 +1985,7 @@ class StableBrowser {
1825
1985
  await _commandError(state, e, this);
1826
1986
  }
1827
1987
  finally {
1828
- _commandFinally(state, this);
1988
+ await _commandFinally(state, this);
1829
1989
  }
1830
1990
  }
1831
1991
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1838,6 +1998,7 @@ class StableBrowser {
1838
1998
  world,
1839
1999
  type: Types.EXTRACT,
1840
2000
  text: `Extract attribute from element`,
2001
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1841
2002
  operation: "extractAttribute",
1842
2003
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1843
2004
  allowDisabled: true,
@@ -1855,10 +2016,31 @@ class StableBrowser {
1855
2016
  case "value":
1856
2017
  state.value = await state.element.inputValue();
1857
2018
  break;
2019
+ case "text":
2020
+ state.value = await state.element.textContent();
2021
+ break;
1858
2022
  default:
1859
2023
  state.value = await state.element.getAttribute(attribute);
1860
2024
  break;
1861
2025
  }
2026
+ if (options !== null) {
2027
+ if (options.regex && options.regex !== "") {
2028
+ // Construct a regex pattern from the provided string
2029
+ const regex = options.regex.slice(1, -1);
2030
+ const regexPattern = new RegExp(regex, "g");
2031
+ const matches = state.value.match(regexPattern);
2032
+ if (matches) {
2033
+ let newValue = "";
2034
+ for (const match of matches) {
2035
+ newValue += match;
2036
+ }
2037
+ state.value = newValue;
2038
+ }
2039
+ }
2040
+ if (options.trimSpaces && options.trimSpaces === true) {
2041
+ state.value = state.value.trim();
2042
+ }
2043
+ }
1862
2044
  state.info.value = state.value;
1863
2045
  this.setTestData({ [variable]: state.value }, world);
1864
2046
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1869,7 +2051,7 @@ class StableBrowser {
1869
2051
  await _commandError(state, e, this);
1870
2052
  }
1871
2053
  finally {
1872
- _commandFinally(state, this);
2054
+ await _commandFinally(state, this);
1873
2055
  }
1874
2056
  }
1875
2057
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1884,18 +2066,25 @@ class StableBrowser {
1884
2066
  highlight: true,
1885
2067
  screenshot: true,
1886
2068
  text: `Verify element attribute`,
2069
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1887
2070
  operation: "verifyAttribute",
1888
2071
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1889
2072
  allowDisabled: true,
1890
2073
  };
1891
2074
  await new Promise((resolve) => setTimeout(resolve, 2000));
1892
2075
  let val;
2076
+ let expectedValue;
1893
2077
  try {
1894
2078
  await _preCommand(state, this);
2079
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2080
+ state.info.expectedValue = expectedValue;
1895
2081
  switch (attribute) {
1896
2082
  case "innerText":
1897
2083
  val = String(await state.element.innerText());
1898
2084
  break;
2085
+ case "text":
2086
+ val = String(await state.element.textContent());
2087
+ break;
1899
2088
  case "value":
1900
2089
  val = String(await state.element.inputValue());
1901
2090
  break;
@@ -1913,26 +2102,29 @@ class StableBrowser {
1913
2102
  val = String(await state.element.getAttribute(attribute));
1914
2103
  break;
1915
2104
  }
2105
+ state.info.value = val;
1916
2106
  let regex;
1917
- if (value.startsWith("/") && value.endsWith("/")) {
1918
- const patternBody = value.slice(1, -1);
2107
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2108
+ const patternBody = expectedValue.slice(1, -1);
1919
2109
  regex = new RegExp(patternBody, "g");
1920
2110
  }
1921
2111
  else {
1922
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2112
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1923
2113
  regex = new RegExp(escapedPattern, "g");
1924
2114
  }
1925
2115
  if (!val.match(regex)) {
1926
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
2116
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2117
+ state.info.failCause.assertionFailed = true;
2118
+ state.info.failCause.lastError = errorMessage;
2119
+ throw new Error(errorMessage);
1927
2120
  }
1928
- state.info.expectedValue = val;
1929
2121
  return state.info;
1930
2122
  }
1931
2123
  catch (e) {
1932
2124
  await _commandError(state, e, this);
1933
2125
  }
1934
2126
  finally {
1935
- _commandFinally(state, this);
2127
+ await _commandFinally(state, this);
1936
2128
  }
1937
2129
  }
1938
2130
  async extractEmailData(emailAddress, options, world) {
@@ -2184,6 +2376,69 @@ class StableBrowser {
2184
2376
  _reportToWorld(world, {
2185
2377
  type: Types.VERIFY_PAGE_PATH,
2186
2378
  text: "Verify page path",
2379
+ _text: "Verify the page path contains " + pathPart,
2380
+ screenshotId,
2381
+ result: error
2382
+ ? {
2383
+ status: "FAILED",
2384
+ startTime,
2385
+ endTime,
2386
+ message: error?.message,
2387
+ }
2388
+ : {
2389
+ status: "PASSED",
2390
+ startTime,
2391
+ endTime,
2392
+ },
2393
+ info: info,
2394
+ });
2395
+ }
2396
+ }
2397
+ async verifyPageTitle(title, options = {}, world = null) {
2398
+ const startTime = Date.now();
2399
+ let error = null;
2400
+ let screenshotId = null;
2401
+ let screenshotPath = null;
2402
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2403
+ const info = {};
2404
+ info.log = "***** verify page title " + title + " *****\n";
2405
+ info.operation = "verifyPageTitle";
2406
+ const newValue = await this._replaceWithLocalData(title, world);
2407
+ if (newValue !== title) {
2408
+ this.logger.info(title + "=" + newValue);
2409
+ title = newValue;
2410
+ }
2411
+ info.title = title;
2412
+ try {
2413
+ for (let i = 0; i < 30; i++) {
2414
+ const foundTitle = await this.page.title();
2415
+ if (!foundTitle.includes(title)) {
2416
+ if (i === 29) {
2417
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2418
+ }
2419
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2420
+ continue;
2421
+ }
2422
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2423
+ return info;
2424
+ }
2425
+ }
2426
+ catch (e) {
2427
+ //await this.closeUnexpectedPopups();
2428
+ this.logger.error("verify page title failed " + info.log);
2429
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2430
+ info.screenshotPath = screenshotPath;
2431
+ Object.assign(e, { info: info });
2432
+ error = e;
2433
+ // throw e;
2434
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2435
+ }
2436
+ finally {
2437
+ const endTime = Date.now();
2438
+ _reportToWorld(world, {
2439
+ type: Types.VERIFY_PAGE_PATH,
2440
+ text: "Verify page title",
2441
+ _text: "Verify the page title contains " + title,
2187
2442
  screenshotId,
2188
2443
  result: error
2189
2444
  ? {
@@ -2201,27 +2456,27 @@ class StableBrowser {
2201
2456
  });
2202
2457
  }
2203
2458
  }
2204
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2459
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2205
2460
  const frames = this.page.frames();
2206
2461
  let results = [];
2207
- let ignoreCase = false;
2462
+ // let ignoreCase = false;
2208
2463
  for (let i = 0; i < frames.length; i++) {
2209
2464
  if (dateAlternatives.date) {
2210
2465
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2211
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2466
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2212
2467
  result.frame = frames[i];
2213
2468
  results.push(result);
2214
2469
  }
2215
2470
  }
2216
2471
  else if (numberAlternatives.number) {
2217
2472
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2218
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2473
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2219
2474
  result.frame = frames[i];
2220
2475
  results.push(result);
2221
2476
  }
2222
2477
  }
2223
2478
  else {
2224
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2479
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2225
2480
  result.frame = frames[i];
2226
2481
  results.push(result);
2227
2482
  }
@@ -2240,11 +2495,15 @@ class StableBrowser {
2240
2495
  scroll: false,
2241
2496
  highlight: false,
2242
2497
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2243
- text: `Verify text exists in page`,
2498
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2499
+ _text: `Verify the text '${text}' exists in page`,
2244
2500
  operation: "verifyTextExistInPage",
2245
2501
  log: "***** verify text " + text + " exists in page *****\n",
2246
2502
  };
2247
- const timeout = this._getLoadTimeout(options);
2503
+ if (testForRegex(text)) {
2504
+ text = text.replace(/\\"/g, '"');
2505
+ }
2506
+ const timeout = this._getFindElementTimeout(options);
2248
2507
  await new Promise((resolve) => setTimeout(resolve, 2000));
2249
2508
  const newValue = await this._replaceWithLocalData(text, world);
2250
2509
  if (newValue !== text) {
@@ -2257,7 +2516,15 @@ class StableBrowser {
2257
2516
  await _preCommand(state, this);
2258
2517
  state.info.text = text;
2259
2518
  while (true) {
2260
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2519
+ let resultWithElementsFound = {
2520
+ length: 0,
2521
+ };
2522
+ try {
2523
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2524
+ }
2525
+ catch (error) {
2526
+ // ignore
2527
+ }
2261
2528
  if (resultWithElementsFound.length === 0) {
2262
2529
  if (Date.now() - state.startTime > timeout) {
2263
2530
  throw new Error(`Text ${text} not found in page`);
@@ -2265,35 +2532,40 @@ class StableBrowser {
2265
2532
  await new Promise((resolve) => setTimeout(resolve, 1000));
2266
2533
  continue;
2267
2534
  }
2268
- if (resultWithElementsFound[0].randomToken) {
2269
- const frame = resultWithElementsFound[0].frame;
2270
- const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2271
- await this._highlightElements(frame, dataAttribute);
2272
- // if (world && world.screenshot && !world.screenshotPath) {
2273
- // console.log(`Highlighting for verify text is found while running from recorder`);
2274
- // this._highlightElements(frame, dataAttribute).then(async () => {
2275
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2276
- // this._unhighlightElements(frame, dataAttribute)
2277
- // .then(async () => {
2278
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2279
- // })
2280
- // .catch(
2281
- // (e) => {}
2282
- // console.error(e)
2283
- // );
2284
- // });
2285
- // }
2286
- const element = await frame.locator(dataAttribute).first();
2287
- // await new Promise((resolve) => setTimeout(resolve, 100));
2288
- // await this._unhighlightElements(frame, dataAttribute);
2289
- if (element) {
2290
- await this.scrollIfNeeded(element, state.info);
2291
- await element.dispatchEvent("bvt_verify_page_contains_text");
2292
- // await _screenshot(state, this, element);
2535
+ try {
2536
+ if (resultWithElementsFound[0].randomToken) {
2537
+ const frame = resultWithElementsFound[0].frame;
2538
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2539
+ await this._highlightElements(frame, dataAttribute);
2540
+ // if (world && world.screenshot && !world.screenshotPath) {
2541
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2542
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2543
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2544
+ // this._unhighlightElements(frame, dataAttribute)
2545
+ // .then(async () => {
2546
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2547
+ // })
2548
+ // .catch(
2549
+ // (e) => {}
2550
+ // console.error(e)
2551
+ // );
2552
+ // });
2553
+ // }
2554
+ const element = await frame.locator(dataAttribute).first();
2555
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2556
+ // await this._unhighlightElements(frame, dataAttribute);
2557
+ if (element) {
2558
+ await this.scrollIfNeeded(element, state.info);
2559
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2560
+ // await _screenshot(state, this, element);
2561
+ }
2293
2562
  }
2563
+ await _screenshot(state, this);
2564
+ return state.info;
2565
+ }
2566
+ catch (error) {
2567
+ console.error(error);
2294
2568
  }
2295
- await _screenshot(state, this);
2296
- return state.info;
2297
2569
  }
2298
2570
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2299
2571
  }
@@ -2301,7 +2573,7 @@ class StableBrowser {
2301
2573
  await _commandError(state, e, this);
2302
2574
  }
2303
2575
  finally {
2304
- _commandFinally(state, this);
2576
+ await _commandFinally(state, this);
2305
2577
  }
2306
2578
  }
2307
2579
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2314,11 +2586,15 @@ class StableBrowser {
2314
2586
  scroll: false,
2315
2587
  highlight: false,
2316
2588
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2317
- text: `Verify text does not exist in page`,
2589
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2590
+ _text: `Verify the text '${text}' does not exist in page`,
2318
2591
  operation: "verifyTextNotExistInPage",
2319
2592
  log: "***** verify text " + text + " does not exist in page *****\n",
2320
2593
  };
2321
- const timeout = this._getLoadTimeout(options);
2594
+ if (testForRegex(text)) {
2595
+ text = text.replace(/\\"/g, '"');
2596
+ }
2597
+ const timeout = this._getFindElementTimeout(options);
2322
2598
  await new Promise((resolve) => setTimeout(resolve, 2000));
2323
2599
  const newValue = await this._replaceWithLocalData(text, world);
2324
2600
  if (newValue !== text) {
@@ -2330,8 +2606,16 @@ class StableBrowser {
2330
2606
  try {
2331
2607
  await _preCommand(state, this);
2332
2608
  state.info.text = text;
2609
+ let resultWithElementsFound = {
2610
+ length: null, // initial cannot be 0
2611
+ };
2333
2612
  while (true) {
2334
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2613
+ try {
2614
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2615
+ }
2616
+ catch (error) {
2617
+ // ignore
2618
+ }
2335
2619
  if (resultWithElementsFound.length === 0) {
2336
2620
  await _screenshot(state, this);
2337
2621
  return state.info;
@@ -2346,7 +2630,7 @@ class StableBrowser {
2346
2630
  await _commandError(state, e, this);
2347
2631
  }
2348
2632
  finally {
2349
- _commandFinally(state, this);
2633
+ await _commandFinally(state, this);
2350
2634
  }
2351
2635
  }
2352
2636
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2361,10 +2645,11 @@ class StableBrowser {
2361
2645
  highlight: false,
2362
2646
  type: Types.VERIFY_TEXT_WITH_RELATION,
2363
2647
  text: `Verify text with relation to another text`,
2648
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2364
2649
  operation: "verify_text_with_relation",
2365
2650
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2366
2651
  };
2367
- const timeout = this._getLoadTimeout(options);
2652
+ const timeout = this._getFindElementTimeout(options);
2368
2653
  await new Promise((resolve) => setTimeout(resolve, 2000));
2369
2654
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2370
2655
  if (newValue !== textAnchor) {
@@ -2382,8 +2667,16 @@ class StableBrowser {
2382
2667
  try {
2383
2668
  await _preCommand(state, this);
2384
2669
  state.info.text = textToVerify;
2670
+ let resultWithElementsFound = {
2671
+ length: 0,
2672
+ };
2385
2673
  while (true) {
2386
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2674
+ try {
2675
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2676
+ }
2677
+ catch (error) {
2678
+ // ignore
2679
+ }
2387
2680
  if (resultWithElementsFound.length === 0) {
2388
2681
  if (Date.now() - state.startTime > timeout) {
2389
2682
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2391,51 +2684,56 @@ class StableBrowser {
2391
2684
  await new Promise((resolve) => setTimeout(resolve, 1000));
2392
2685
  continue;
2393
2686
  }
2394
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2395
- foundAncore = true;
2396
- const result = resultWithElementsFound[i];
2397
- const token = result.randomToken;
2398
- const frame = result.frame;
2399
- let css = `[data-blinq-id-${token}]`;
2400
- const climbArray1 = [];
2401
- for (let i = 0; i < climb; i++) {
2402
- climbArray1.push("..");
2403
- }
2404
- let climbXpath = "xpath=" + climbArray1.join("/");
2405
- css = css + " >> " + climbXpath;
2406
- const count = await frame.locator(css).count();
2407
- for (let j = 0; j < count; j++) {
2408
- const continer = await frame.locator(css).nth(j);
2409
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2410
- if (result.elementCount > 0) {
2411
- const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2412
- await this._highlightElements(frame, dataAttribute);
2413
- //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2414
- // if (world && world.screenshot && !world.screenshotPath) {
2415
- // console.log(`Highlighting for vtrt while running from recorder`);
2416
- // this._highlightElements(frame, dataAttribute)
2417
- // .then(async () => {
2418
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2419
- // this._unhighlightElements(frame, dataAttribute).then(
2420
- // () => {}
2421
- // console.log(`Unhighlighting vrtr in recorder is successful`)
2422
- // );
2423
- // })
2424
- // .catch(e);
2425
- // }
2426
- //await this._highlightElements(frame, cssAnchor);
2427
- const element = await frame.locator(dataAttribute).first();
2428
- // await new Promise((resolve) => setTimeout(resolve, 100));
2429
- // await this._unhighlightElements(frame, dataAttribute);
2430
- if (element) {
2431
- await this.scrollIfNeeded(element, state.info);
2432
- await element.dispatchEvent("bvt_verify_page_contains_text");
2687
+ try {
2688
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2689
+ foundAncore = true;
2690
+ const result = resultWithElementsFound[i];
2691
+ const token = result.randomToken;
2692
+ const frame = result.frame;
2693
+ let css = `[data-blinq-id-${token}]`;
2694
+ const climbArray1 = [];
2695
+ for (let i = 0; i < climb; i++) {
2696
+ climbArray1.push("..");
2697
+ }
2698
+ let climbXpath = "xpath=" + climbArray1.join("/");
2699
+ css = css + " >> " + climbXpath;
2700
+ const count = await frame.locator(css).count();
2701
+ for (let j = 0; j < count; j++) {
2702
+ const continer = await frame.locator(css).nth(j);
2703
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2704
+ if (result.elementCount > 0) {
2705
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2706
+ await this._highlightElements(frame, dataAttribute);
2707
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2708
+ // if (world && world.screenshot && !world.screenshotPath) {
2709
+ // console.log(`Highlighting for vtrt while running from recorder`);
2710
+ // this._highlightElements(frame, dataAttribute)
2711
+ // .then(async () => {
2712
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2713
+ // this._unhighlightElements(frame, dataAttribute).then(
2714
+ // () => {}
2715
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2716
+ // );
2717
+ // })
2718
+ // .catch(e);
2719
+ // }
2720
+ //await this._highlightElements(frame, cssAnchor);
2721
+ const element = await frame.locator(dataAttribute).first();
2722
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2723
+ // await this._unhighlightElements(frame, dataAttribute);
2724
+ if (element) {
2725
+ await this.scrollIfNeeded(element, state.info);
2726
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2727
+ }
2728
+ await _screenshot(state, this);
2729
+ return state.info;
2433
2730
  }
2434
- await _screenshot(state, this);
2435
- return state.info;
2436
2731
  }
2437
2732
  }
2438
2733
  }
2734
+ catch (error) {
2735
+ console.error(error);
2736
+ }
2439
2737
  }
2440
2738
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2441
2739
  }
@@ -2443,8 +2741,32 @@ class StableBrowser {
2443
2741
  await _commandError(state, e, this);
2444
2742
  }
2445
2743
  finally {
2446
- _commandFinally(state, this);
2744
+ await _commandFinally(state, this);
2745
+ }
2746
+ }
2747
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2748
+ const frames = this.page.frames();
2749
+ let results = [];
2750
+ let ignoreCase = false;
2751
+ for (let i = 0; i < frames.length; i++) {
2752
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2753
+ result.frame = frames[i];
2754
+ const climbArray = [];
2755
+ for (let i = 0; i < climb; i++) {
2756
+ climbArray.push("..");
2757
+ }
2758
+ let climbXpath = "xpath=" + climbArray.join("/");
2759
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2760
+ const count = await frames[i].locator(newLocator).count();
2761
+ if (count > 0) {
2762
+ result.elementCount = count;
2763
+ result.locator = newLocator;
2764
+ results.push(result);
2765
+ }
2447
2766
  }
2767
+ // state.info.results = results;
2768
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2769
+ return resultWithElementsFound;
2448
2770
  }
2449
2771
  async visualVerification(text, options = {}, world = null) {
2450
2772
  const startTime = Date.now();
@@ -2464,10 +2786,13 @@ class StableBrowser {
2464
2786
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2465
2787
  info.screenshotPath = screenshotPath;
2466
2788
  const screenshot = await this.takeScreenshot();
2467
- const request = {
2468
- method: "POST",
2789
+ let request = {
2790
+ method: "post",
2791
+ maxBodyLength: Infinity,
2469
2792
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2470
2793
  headers: {
2794
+ "x-bvt-project-id": path.basename(this.project_path),
2795
+ "x-source": "aaa",
2471
2796
  "Content-Type": "application/json",
2472
2797
  Authorization: `Bearer ${process.env.TOKEN}`,
2473
2798
  },
@@ -2476,7 +2801,7 @@ class StableBrowser {
2476
2801
  screenshot: screenshot,
2477
2802
  }),
2478
2803
  };
2479
- let result = await this.context.api.request(request);
2804
+ const result = await axios.request(request);
2480
2805
  if (result.data.status !== true) {
2481
2806
  throw new Error("Visual validation failed");
2482
2807
  }
@@ -2504,6 +2829,7 @@ class StableBrowser {
2504
2829
  _reportToWorld(world, {
2505
2830
  type: Types.VERIFY_VISUAL,
2506
2831
  text: "Visual verification",
2832
+ _text: "Visual verification of " + text,
2507
2833
  screenshotId,
2508
2834
  result: error
2509
2835
  ? {
@@ -2770,6 +3096,15 @@ class StableBrowser {
2770
3096
  }
2771
3097
  return timeout;
2772
3098
  }
3099
+ _getFindElementTimeout(options) {
3100
+ if (options && options.timeout) {
3101
+ return options.timeout;
3102
+ }
3103
+ if (this.configuration.find_element_timeout) {
3104
+ return this.configuration.find_element_timeout;
3105
+ }
3106
+ return 30000;
3107
+ }
2773
3108
  async saveStoreState(path = null, world = null) {
2774
3109
  const storageState = await this.page.context().storageState();
2775
3110
  //const testDataFile = _getDataFile(world, this.context, this);
@@ -2781,6 +3116,12 @@ class StableBrowser {
2781
3116
  await this.setTestData({ storageState: storageState }, world);
2782
3117
  }
2783
3118
  }
3119
+ async restoreSaveState(path = null, world = null) {
3120
+ await refreshBrowser(this, path, world);
3121
+ this.registerEventListeners(this.context);
3122
+ registerNetworkEvents(this.world, this, this.context, this.page);
3123
+ registerDownloadEvent(this.page, this.world, this.context);
3124
+ }
2784
3125
  async waitForPageLoad(options = {}, world = null) {
2785
3126
  let timeout = this._getLoadTimeout(options);
2786
3127
  const promiseArray = [];
@@ -2848,6 +3189,7 @@ class StableBrowser {
2848
3189
  highlight: false,
2849
3190
  type: Types.CLOSE_PAGE,
2850
3191
  text: `Close page`,
3192
+ _text: `Close the page`,
2851
3193
  operation: "closePage",
2852
3194
  log: "***** close page *****\n",
2853
3195
  throwError: false,
@@ -2861,11 +3203,98 @@ class StableBrowser {
2861
3203
  await _commandError(state, e, this);
2862
3204
  }
2863
3205
  finally {
2864
- _commandFinally(state, this);
3206
+ await _commandFinally(state, this);
3207
+ }
3208
+ }
3209
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3210
+ let operation = null;
3211
+ if (!options || !options.operation) {
3212
+ throw new Error("operation is not defined");
3213
+ }
3214
+ operation = options.operation;
3215
+ // validate operation is one of the supported operations
3216
+ if (operation != "click" && operation != "hover+click") {
3217
+ throw new Error("operation is not supported");
3218
+ }
3219
+ const state = {
3220
+ options,
3221
+ world,
3222
+ locate: false,
3223
+ scroll: false,
3224
+ highlight: false,
3225
+ type: Types.TABLE_OPERATION,
3226
+ text: `Table operation`,
3227
+ _text: `Table ${operation} operation`,
3228
+ operation: operation,
3229
+ log: "***** Table operation *****\n",
3230
+ };
3231
+ const timeout = this._getFindElementTimeout(options);
3232
+ try {
3233
+ await _preCommand(state, this);
3234
+ const start = Date.now();
3235
+ let cellArea = null;
3236
+ while (true) {
3237
+ try {
3238
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3239
+ if (cellArea) {
3240
+ break;
3241
+ }
3242
+ }
3243
+ catch (e) {
3244
+ // ignore
3245
+ }
3246
+ if (Date.now() - start > timeout) {
3247
+ throw new Error(`Cell not found in table`);
3248
+ }
3249
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3250
+ }
3251
+ switch (operation) {
3252
+ case "click":
3253
+ if (!options.css) {
3254
+ // will click in the center of the cell
3255
+ let xOffset = 0;
3256
+ let yOffset = 0;
3257
+ if (options.xOffset) {
3258
+ xOffset = options.xOffset;
3259
+ }
3260
+ if (options.yOffset) {
3261
+ yOffset = options.yOffset;
3262
+ }
3263
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3264
+ }
3265
+ else {
3266
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3267
+ if (results.length === 0) {
3268
+ throw new Error(`Element not found in cell area`);
3269
+ }
3270
+ state.element = results[0];
3271
+ await performAction("click", state.element, options, this, state, _params);
3272
+ }
3273
+ break;
3274
+ case "hover+click":
3275
+ if (!options.css) {
3276
+ throw new Error("css is not defined");
3277
+ }
3278
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3279
+ if (results.length === 0) {
3280
+ throw new Error(`Element not found in cell area`);
3281
+ }
3282
+ state.element = results[0];
3283
+ await performAction("hover+click", state.element, options, this, state, _params);
3284
+ break;
3285
+ default:
3286
+ throw new Error("operation is not supported");
3287
+ }
3288
+ }
3289
+ catch (e) {
3290
+ await _commandError(state, e, this);
3291
+ }
3292
+ finally {
3293
+ await _commandFinally(state, this);
2865
3294
  }
2866
3295
  }
2867
3296
  saveTestDataAsGlobal(options, world) {
2868
- const dataFile = this._getDataFile(world);
3297
+ const dataFile = _getDataFile(world, this.context, this);
2869
3298
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2870
3299
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2871
3300
  }
@@ -2895,6 +3324,7 @@ class StableBrowser {
2895
3324
  _reportToWorld(world, {
2896
3325
  type: Types.SET_VIEWPORT,
2897
3326
  text: "set viewport size to " + width + "x" + hight,
3327
+ _text: "Set the viewport size to " + width + "x" + hight,
2898
3328
  screenshotId,
2899
3329
  result: error
2900
3330
  ? {
@@ -2965,7 +3395,39 @@ class StableBrowser {
2965
3395
  console.log("#-#");
2966
3396
  }
2967
3397
  }
3398
+ async beforeScenario(world, scenario) {
3399
+ this.beforeScenarioCalled = true;
3400
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3401
+ this.scenarioName = scenario.pickle.name;
3402
+ }
3403
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3404
+ this.featureName = scenario.gherkinDocument.feature.name;
3405
+ }
3406
+ if (this.context) {
3407
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3408
+ }
3409
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3410
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3411
+ // check if @global_test_data tag is present
3412
+ if (this.tags.includes("@global_test_data")) {
3413
+ this.saveTestDataAsGlobal({}, world);
3414
+ }
3415
+ }
3416
+ // update test data based on feature/scenario
3417
+ let envName = null;
3418
+ if (this.context && this.context.environment) {
3419
+ envName = this.context.environment.name;
3420
+ }
3421
+ if (!process.env.TEMP_RUN) {
3422
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3423
+ }
3424
+ await loadBrunoParams(this.context, this.context.environment.name);
3425
+ }
3426
+ async afterScenario(world, scenario) { }
2968
3427
  async beforeStep(world, step) {
3428
+ if (!this.beforeScenarioCalled) {
3429
+ this.beforeScenario(world, step);
3430
+ }
2969
3431
  if (this.stepIndex === undefined) {
2970
3432
  this.stepIndex = 0;
2971
3433
  }
@@ -2982,22 +3444,48 @@ class StableBrowser {
2982
3444
  else {
2983
3445
  this.stepName = "step " + this.stepIndex;
2984
3446
  }
2985
- if (this.context) {
2986
- this.context.examplesRow = extractStepExampleParameters(step);
2987
- }
2988
3447
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2989
3448
  if (this.context.browserObject.context) {
2990
3449
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
2991
3450
  }
2992
3451
  }
2993
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
2994
- this.tags = step.pickle.tags.map((tag) => tag.name);
2995
- // check if @global_test_data tag is present
2996
- if (this.tags.includes("@global_test_data")) {
2997
- this.saveTestDataAsGlobal({}, world);
3452
+ if (this.initSnapshotTaken === false) {
3453
+ this.initSnapshotTaken = true;
3454
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3455
+ const snapshot = await this.getAriaSnapshot();
3456
+ if (snapshot) {
3457
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3458
+ }
2998
3459
  }
2999
3460
  }
3000
3461
  }
3462
+ async getAriaSnapshot() {
3463
+ try {
3464
+ // find the page url
3465
+ const url = await this.page.url();
3466
+ // extract the path from the url
3467
+ const path = new URL(url).pathname;
3468
+ // get the page title
3469
+ const title = await this.page.title();
3470
+ // go over other frams
3471
+ const frames = this.page.frames();
3472
+ const snapshots = [];
3473
+ const content = [`- path: ${path}`, `- title: ${title}`];
3474
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3475
+ for (let i = 0; i < frames.length; i++) {
3476
+ content.push(`- frame: ${i}`);
3477
+ const frame = frames[i];
3478
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3479
+ content.push(snapshot);
3480
+ }
3481
+ return content.join("\n");
3482
+ }
3483
+ catch (e) {
3484
+ console.log("Error in getAriaSnapshot");
3485
+ console.debug(e);
3486
+ }
3487
+ return null;
3488
+ }
3001
3489
  async afterStep(world, step) {
3002
3490
  this.stepName = null;
3003
3491
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3005,11 +3493,25 @@ class StableBrowser {
3005
3493
  await this.context.browserObject.context.tracing.stopChunk({
3006
3494
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3007
3495
  });
3496
+ if (world && world.attach) {
3497
+ await world.attach(JSON.stringify({
3498
+ type: "trace",
3499
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3500
+ }), "application/json+trace");
3501
+ }
3502
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3008
3503
  }
3009
3504
  }
3010
3505
  if (this.context) {
3011
3506
  this.context.examplesRow = null;
3012
3507
  }
3508
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3509
+ const snapshot = await this.getAriaSnapshot();
3510
+ if (snapshot) {
3511
+ const obj = {};
3512
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3513
+ }
3514
+ }
3013
3515
  }
3014
3516
  }
3015
3517
  function createTimedPromise(promise, label) {