automation_model 1.0.620-dev → 1.0.620-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 (49) 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 +103 -26
  6. package/lib/auto_page.js.map +1 -1
  7. package/lib/browser_manager.js +70 -22
  8. package/lib/browser_manager.js.map +1 -1
  9. package/lib/bruno.d.ts +2 -0
  10. package/lib/bruno.js +381 -0
  11. package/lib/bruno.js.map +1 -0
  12. package/lib/command_common.d.ts +4 -4
  13. package/lib/command_common.js +34 -16
  14. package/lib/command_common.js.map +1 -1
  15. package/lib/environment.d.ts +1 -0
  16. package/lib/environment.js +1 -0
  17. package/lib/environment.js.map +1 -1
  18. package/lib/file_checker.d.ts +1 -0
  19. package/lib/file_checker.js +61 -0
  20. package/lib/file_checker.js.map +1 -0
  21. package/lib/index.d.ts +2 -0
  22. package/lib/index.js +2 -0
  23. package/lib/index.js.map +1 -1
  24. package/lib/init_browser.d.ts +2 -2
  25. package/lib/init_browser.js +33 -27
  26. package/lib/init_browser.js.map +1 -1
  27. package/lib/locate_element.js +2 -2
  28. package/lib/locate_element.js.map +1 -1
  29. package/lib/locator_log.js +3 -0
  30. package/lib/locator_log.js.map +1 -1
  31. package/lib/network.d.ts +1 -1
  32. package/lib/network.js +5 -5
  33. package/lib/network.js.map +1 -1
  34. package/lib/snapshot_validation.d.ts +37 -0
  35. package/lib/snapshot_validation.js +246 -0
  36. package/lib/snapshot_validation.js.map +1 -0
  37. package/lib/stable_browser.d.ts +23 -1
  38. package/lib/stable_browser.js +675 -127
  39. package/lib/stable_browser.js.map +1 -1
  40. package/lib/table_helper.d.ts +19 -0
  41. package/lib/table_helper.js +116 -0
  42. package/lib/table_helper.js.map +1 -0
  43. package/lib/test_context.d.ts +2 -0
  44. package/lib/test_context.js +2 -0
  45. package/lib/test_context.js.map +1 -1
  46. package/lib/utils.d.ts +8 -4
  47. package/lib/utils.js +220 -19
  48. package/lib/utils.js.map +1 -1
  49. package/package.json +9 -8
@@ -10,19 +10,24 @@ import { getDateTimeValue } from "./date_time.js";
10
10
  import drawRectangle from "./drawRect.js";
11
11
  //import { closeUnexpectedPopups } from "./popups.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
14
14
  import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
17
  import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
18
19
  import { locate_element } from "./locate_element.js";
19
20
  import { randomUUID } from "crypto";
20
21
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
21
22
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
23
  import { LocatorLog } from "./locator_log.js";
23
24
  import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { loadBrunoParams } from "./bruno.js";
27
+ import { snapshotValidation } from "./snapshot_validation.js";
24
28
  export const Types = {
25
29
  CLICK: "click_element",
30
+ WAIT_ELEMENT: "wait_element",
26
31
  NAVIGATE: "navigate",
27
32
  FILL: "fill_element",
28
33
  EXECUTE: "execute_page_method",
@@ -44,6 +49,7 @@ export const Types = {
44
49
  UNCHECK: "uncheck_element",
45
50
  EXTRACT: "extract_attribute",
46
51
  CLOSE_PAGE: "close_page",
52
+ TABLE_OPERATION: "table_operation",
47
53
  SET_DATE_TIME: "set_date_time",
48
54
  SET_VIEWPORT: "set_viewport",
49
55
  VERIFY_VISUAL: "verify_visual",
@@ -52,6 +58,10 @@ export const Types = {
52
58
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
53
59
  VERIFY_ATTRIBUTE: "verify_element_attribute",
54
60
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
61
+ BRUNO: "bruno",
62
+ VERIFY_FILE_EXISTS: "verify_file_exists",
63
+ SET_INPUT_FILES: "set_input_files",
64
+ SNAPSHOT_VALIDATION: "snapshot_validation",
55
65
  };
56
66
  export const apps = {};
57
67
  const formatElementName = (elementName) => {
@@ -70,6 +80,7 @@ class StableBrowser {
70
80
  appName = "main";
71
81
  tags = null;
72
82
  isRecording = false;
83
+ initSnapshotTaken = false;
73
84
  constructor(browser, page, logger = null, context = null, world = null) {
74
85
  this.browser = browser;
75
86
  this.page = page;
@@ -176,6 +187,30 @@ class StableBrowser {
176
187
  await this.waitForPageLoad();
177
188
  }
178
189
  }
190
+ async switchTab(tabTitleOrIndex) {
191
+ // first check if the tabNameOrIndex is a number
192
+ let index = parseInt(tabTitleOrIndex);
193
+ if (!isNaN(index)) {
194
+ if (index >= 0 && index < this.context.pages.length) {
195
+ this.page = this.context.pages[index];
196
+ this.context.page = this.page;
197
+ await this.page.bringToFront();
198
+ return;
199
+ }
200
+ }
201
+ // if the tabNameOrIndex is a string, find the tab by name
202
+ for (let i = 0; i < this.context.pages.length; i++) {
203
+ let page = this.context.pages[i];
204
+ let title = await page.title();
205
+ if (title.includes(tabTitleOrIndex)) {
206
+ this.page = page;
207
+ this.context.page = this.page;
208
+ await this.page.bringToFront();
209
+ return;
210
+ }
211
+ }
212
+ throw new Error("Tab not found: " + tabTitleOrIndex);
213
+ }
179
214
  registerConsoleLogListener(page, context) {
180
215
  if (!this.context.webLogger) {
181
216
  this.context.webLogger = [];
@@ -271,7 +306,7 @@ class StableBrowser {
271
306
  _commandError(state, error, this);
272
307
  }
273
308
  finally {
274
- _commandFinally(state, this);
309
+ await _commandFinally(state, this);
275
310
  }
276
311
  }
277
312
  async _getLocator(locator, scope, _params) {
@@ -352,7 +387,7 @@ class StableBrowser {
352
387
  return resultCss;
353
388
  }
354
389
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
355
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
390
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
356
391
  const locator = scope.locator(query);
357
392
  const count = await locator.count();
358
393
  if (!tag1) {
@@ -372,6 +407,12 @@ class StableBrowser {
372
407
  if (!el.setAttribute) {
373
408
  el = el.parentElement;
374
409
  }
410
+ // remove any attributes start with data-blinq-id
411
+ // for (let i = 0; i < el.attributes.length; i++) {
412
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
413
+ // el.removeAttribute(el.attributes[i].name);
414
+ // }
415
+ // }
375
416
  el.setAttribute("data-blinq-id-" + randomToken, "");
376
417
  return true;
377
418
  }, [tag1, randomToken]))) {
@@ -547,10 +588,14 @@ class StableBrowser {
547
588
  timeout = 30000;
548
589
  }
549
590
  for (let i = 0; i < 3; i++) {
550
- info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
591
+ if (process.env.SUPRESS_ERRORS === "true") {
592
+ info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
593
+ }
551
594
  for (let j = 0; j < selectors.locators.length; j++) {
552
595
  let selector = selectors.locators[j];
553
- info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
596
+ if (process.env.SUPRESS_ERRORS === "true") {
597
+ info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
598
+ }
554
599
  }
555
600
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
556
601
  if (!element.rerun) {
@@ -558,15 +603,32 @@ class StableBrowser {
558
603
  element.evaluate((el, randomToken) => {
559
604
  el.setAttribute("data-blinq-id-" + randomToken, "");
560
605
  }, randomToken);
561
- if (element._frame) {
562
- return element;
563
- }
564
- const scope = element.page();
565
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
566
- return newSelector;
606
+ // if (element._frame) {
607
+ // return element;
608
+ // }
609
+ const scope = element._frame ?? element.page();
610
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
611
+ let prefixSelector = "";
612
+ const frameControlSelector = " >> internal:control=enter-frame";
613
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
614
+ if (frameSelectorIndex !== -1) {
615
+ // remove everything after the >> internal:control=enter-frame
616
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
617
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
618
+ }
619
+ // if (element?._frame?._selector) {
620
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
621
+ // }
622
+ const newSelector = prefixSelector + newElementSelector;
623
+ return scope.locator(newSelector);
567
624
  }
568
625
  }
569
- throw new Error("unable to locate element " + JSON.stringify(selectors));
626
+ if (process.env.SUPRESS_ERRORS === "true") {
627
+ throw new Error("unable to locate the element");
628
+ }
629
+ else {
630
+ throw new Error("unable to locate element " + JSON.stringify(selectors));
631
+ }
570
632
  }
571
633
  async _findFrameScope(selectors, timeout = 30000, info) {
572
634
  if (!info) {
@@ -715,14 +777,9 @@ class StableBrowser {
715
777
  // info.log += "scanning locators in priority 2" + "\n";
716
778
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
717
779
  }
718
- if (result.foundElements.length === 0 && onlyPriority3) {
780
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
719
781
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
720
782
  }
721
- else {
722
- if (result.foundElements.length === 0 && !highPriorityOnly) {
723
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
724
- }
725
- }
726
783
  let foundElements = result.foundElements;
727
784
  if (foundElements.length === 1 && foundElements[0].unique) {
728
785
  info.box = foundElements[0].box;
@@ -777,6 +834,11 @@ class StableBrowser {
777
834
  visibleOnly = false;
778
835
  }
779
836
  await new Promise((resolve) => setTimeout(resolve, 1000));
837
+ // sheck of more of half of the timeout has passed
838
+ if (Date.now() - startTime > timeout / 2) {
839
+ highPriorityOnly = false;
840
+ visibleOnly = false;
841
+ }
780
842
  }
781
843
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
782
844
  // if (info.locatorLog) {
@@ -790,7 +852,12 @@ class StableBrowser {
790
852
  if (!info?.failCause?.lastError) {
791
853
  info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
792
854
  }
793
- throw new Error("failed to locate first element no elements found, " + info.log);
855
+ if (process.env.SUPRESS_ERRORS === "true") {
856
+ throw new Error("failed to locate first element no elements found");
857
+ }
858
+ else {
859
+ throw new Error("failed to locate first element no elements found, " + info.log);
860
+ }
794
861
  }
795
862
  async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
796
863
  let foundElements = [];
@@ -823,9 +890,40 @@ class StableBrowser {
823
890
  result.locatorIndex = i;
824
891
  }
825
892
  if (foundLocators.length > 1) {
826
- info.failCause.foundMultiple = true;
827
- if (info.locatorLog) {
828
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
893
+ // remove elements that consume the same space with 10 pixels tolerance
894
+ const boxes = [];
895
+ for (let j = 0; j < foundLocators.length; j++) {
896
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
897
+ }
898
+ for (let j = 0; j < boxes.length; j++) {
899
+ for (let k = 0; k < boxes.length; k++) {
900
+ if (j === k) {
901
+ continue;
902
+ }
903
+ // check if x, y, width, height are the same with 10 pixels tolerance
904
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
905
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
906
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
907
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
908
+ // as the element is not unique, will remove it
909
+ boxes.splice(k, 1);
910
+ k--;
911
+ }
912
+ }
913
+ }
914
+ if (boxes.length === 1) {
915
+ result.foundElements.push({
916
+ locator: boxes[0].locator.first(),
917
+ box: boxes[0].box,
918
+ unique: true,
919
+ });
920
+ result.locatorIndex = i;
921
+ }
922
+ else {
923
+ info.failCause.foundMultiple = true;
924
+ if (info.locatorLog) {
925
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
926
+ }
829
927
  }
830
928
  }
831
929
  }
@@ -873,7 +971,7 @@ class StableBrowser {
873
971
  await _commandError(state, "timeout looking for " + elementDescription, this);
874
972
  }
875
973
  finally {
876
- _commandFinally(state, this);
974
+ await _commandFinally(state, this);
877
975
  }
878
976
  }
879
977
  }
@@ -922,7 +1020,7 @@ class StableBrowser {
922
1020
  await _commandError(state, "timeout looking for " + elementDescription, this);
923
1021
  }
924
1022
  finally {
925
- _commandFinally(state, this);
1023
+ await _commandFinally(state, this);
926
1024
  }
927
1025
  }
928
1026
  }
@@ -936,25 +1034,14 @@ class StableBrowser {
936
1034
  options,
937
1035
  world,
938
1036
  text: "Click element",
1037
+ _text: "Click on " + selectors.element_name,
939
1038
  type: Types.CLICK,
940
1039
  operation: "click",
941
1040
  log: "***** click on " + selectors.element_name + " *****\n",
942
1041
  };
943
1042
  try {
944
1043
  await _preCommand(state, this);
945
- // if (state.options && state.options.context) {
946
- // state.selectors.locators[0].text = state.options.context;
947
- // }
948
- try {
949
- await state.element.click();
950
- // await new Promise((resolve) => setTimeout(resolve, 1000));
951
- }
952
- catch (e) {
953
- // await this.closeUnexpectedPopups();
954
- state.element = await this._locate(selectors, state.info, _params);
955
- await state.element.dispatchEvent("click");
956
- // await new Promise((resolve) => setTimeout(resolve, 1000));
957
- }
1044
+ await performAction("click", state.element, options, this, state, _params);
958
1045
  await this.waitForPageLoad();
959
1046
  return state.info;
960
1047
  }
@@ -962,9 +1049,41 @@ class StableBrowser {
962
1049
  await _commandError(state, e, this);
963
1050
  }
964
1051
  finally {
965
- _commandFinally(state, this);
1052
+ await _commandFinally(state, this);
966
1053
  }
967
1054
  }
1055
+ async waitForElement(selectors, _params, options = {}, world = null) {
1056
+ const timeout = this._getFindElementTimeout(options);
1057
+ const state = {
1058
+ selectors,
1059
+ _params,
1060
+ options,
1061
+ world,
1062
+ text: "Wait for element",
1063
+ _text: "Wait for " + selectors.element_name,
1064
+ type: Types.WAIT_ELEMENT,
1065
+ operation: "waitForElement",
1066
+ log: "***** wait for " + selectors.element_name + " *****\n",
1067
+ };
1068
+ let found = false;
1069
+ try {
1070
+ await _preCommand(state, this);
1071
+ // if (state.options && state.options.context) {
1072
+ // state.selectors.locators[0].text = state.options.context;
1073
+ // }
1074
+ await state.element.waitFor({ timeout: timeout });
1075
+ found = true;
1076
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1077
+ }
1078
+ catch (e) {
1079
+ console.error("Error on waitForElement", e);
1080
+ // await _commandError(state, e, this);
1081
+ }
1082
+ finally {
1083
+ await _commandFinally(state, this);
1084
+ }
1085
+ return found;
1086
+ }
968
1087
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
969
1088
  const state = {
970
1089
  selectors,
@@ -973,6 +1092,7 @@ class StableBrowser {
973
1092
  world,
974
1093
  type: checked ? Types.CHECK : Types.UNCHECK,
975
1094
  text: checked ? `Check element` : `Uncheck element`,
1095
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
976
1096
  operation: "setCheck",
977
1097
  log: "***** check " + selectors.element_name + " *****\n",
978
1098
  };
@@ -984,7 +1104,7 @@ class StableBrowser {
984
1104
  try {
985
1105
  // if (world && world.screenshot && !world.screenshotPath) {
986
1106
  // console.log(`Highlighting while running from recorder`);
987
- await this._highlightElements(element);
1107
+ await this._highlightElements(state.element);
988
1108
  await state.element.setChecked(checked);
989
1109
  await new Promise((resolve) => setTimeout(resolve, 1000));
990
1110
  // await this._unHighlightElements(element);
@@ -1011,7 +1131,7 @@ class StableBrowser {
1011
1131
  await _commandError(state, e, this);
1012
1132
  }
1013
1133
  finally {
1014
- _commandFinally(state, this);
1134
+ await _commandFinally(state, this);
1015
1135
  }
1016
1136
  }
1017
1137
  async hover(selectors, _params, options = {}, world = null) {
@@ -1022,24 +1142,13 @@ class StableBrowser {
1022
1142
  world,
1023
1143
  type: Types.HOVER,
1024
1144
  text: `Hover element`,
1145
+ _text: `Hover on ${selectors.element_name}`,
1025
1146
  operation: "hover",
1026
1147
  log: "***** hover " + selectors.element_name + " *****\n",
1027
1148
  };
1028
1149
  try {
1029
1150
  await _preCommand(state, this);
1030
- try {
1031
- await state.element.hover();
1032
- // await _screenshot(state, this);
1033
- await new Promise((resolve) => setTimeout(resolve, 1000));
1034
- }
1035
- catch (e) {
1036
- //await this.closeUnexpectedPopups();
1037
- state.info.log += "hover failed, will try again" + "\n";
1038
- state.element = await this._locate(selectors, state.info, _params);
1039
- await state.element.hover({ timeout: 10000 });
1040
- // await _screenshot(state, this);
1041
- await new Promise((resolve) => setTimeout(resolve, 1000));
1042
- }
1151
+ await performAction("hover", state.element, options, this, state, _params);
1043
1152
  await _screenshot(state, this);
1044
1153
  await this.waitForPageLoad();
1045
1154
  return state.info;
@@ -1048,7 +1157,7 @@ class StableBrowser {
1048
1157
  await _commandError(state, e, this);
1049
1158
  }
1050
1159
  finally {
1051
- _commandFinally(state, this);
1160
+ await _commandFinally(state, this);
1052
1161
  }
1053
1162
  }
1054
1163
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1063,6 +1172,7 @@ class StableBrowser {
1063
1172
  value: values.toString(),
1064
1173
  type: Types.SELECT,
1065
1174
  text: `Select option: ${values}`,
1175
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1066
1176
  operation: "selectOption",
1067
1177
  log: "***** select option " + selectors.element_name + " *****\n",
1068
1178
  };
@@ -1083,7 +1193,7 @@ class StableBrowser {
1083
1193
  await _commandError(state, e, this);
1084
1194
  }
1085
1195
  finally {
1086
- _commandFinally(state, this);
1196
+ await _commandFinally(state, this);
1087
1197
  }
1088
1198
  }
1089
1199
  async type(_value, _params = null, options = {}, world = null) {
@@ -1097,6 +1207,7 @@ class StableBrowser {
1097
1207
  highlight: false,
1098
1208
  type: Types.TYPE_PRESS,
1099
1209
  text: `Type value: ${_value}`,
1210
+ _text: `Type value: ${_value}`,
1100
1211
  operation: "type",
1101
1212
  log: "",
1102
1213
  };
@@ -1128,7 +1239,7 @@ class StableBrowser {
1128
1239
  await _commandError(state, e, this);
1129
1240
  }
1130
1241
  finally {
1131
- _commandFinally(state, this);
1242
+ await _commandFinally(state, this);
1132
1243
  }
1133
1244
  }
1134
1245
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1164,7 +1275,7 @@ class StableBrowser {
1164
1275
  await _commandError(state, e, this);
1165
1276
  }
1166
1277
  finally {
1167
- _commandFinally(state, this);
1278
+ await _commandFinally(state, this);
1168
1279
  }
1169
1280
  }
1170
1281
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1176,6 +1287,7 @@ class StableBrowser {
1176
1287
  world,
1177
1288
  type: Types.SET_DATE_TIME,
1178
1289
  text: `Set date time value: ${value}`,
1290
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1179
1291
  operation: "setDateTime",
1180
1292
  log: "***** set date time value " + selectors.element_name + " *****\n",
1181
1293
  throwError: false,
@@ -1183,7 +1295,7 @@ class StableBrowser {
1183
1295
  try {
1184
1296
  await _preCommand(state, this);
1185
1297
  try {
1186
- await state.element.click();
1298
+ await performAction("click", state.element, options, this, state, _params);
1187
1299
  await new Promise((resolve) => setTimeout(resolve, 500));
1188
1300
  if (format) {
1189
1301
  state.value = dayjs(state.value).format(format);
@@ -1232,7 +1344,7 @@ class StableBrowser {
1232
1344
  await _commandError(state, e, this);
1233
1345
  }
1234
1346
  finally {
1235
- _commandFinally(state, this);
1347
+ await _commandFinally(state, this);
1236
1348
  }
1237
1349
  }
1238
1350
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1247,9 +1359,13 @@ class StableBrowser {
1247
1359
  world,
1248
1360
  type: Types.FILL,
1249
1361
  text: `Click type input with value: ${_value}`,
1362
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1250
1363
  operation: "clickType",
1251
1364
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1252
1365
  };
1366
+ if (!options) {
1367
+ options = {};
1368
+ }
1253
1369
  if (newValue !== _value) {
1254
1370
  //this.logger.info(_value + "=" + newValue);
1255
1371
  _value = newValue;
@@ -1257,7 +1373,7 @@ class StableBrowser {
1257
1373
  try {
1258
1374
  await _preCommand(state, this);
1259
1375
  state.info.value = _value;
1260
- if (options === null || options === undefined || !options.press) {
1376
+ if (!options.press) {
1261
1377
  try {
1262
1378
  let currentValue = await state.element.inputValue();
1263
1379
  if (currentValue) {
@@ -1268,13 +1384,9 @@ class StableBrowser {
1268
1384
  this.logger.info("unable to clear input value");
1269
1385
  }
1270
1386
  }
1271
- if (options === null || options === undefined || options.press) {
1272
- try {
1273
- await state.element.click({ timeout: 5000 });
1274
- }
1275
- catch (e) {
1276
- await state.element.dispatchEvent("click");
1277
- }
1387
+ if (options.press) {
1388
+ options.timeout = 5000;
1389
+ await performAction("click", state.element, options, this, state, _params);
1278
1390
  }
1279
1391
  else {
1280
1392
  try {
@@ -1332,7 +1444,7 @@ class StableBrowser {
1332
1444
  await _commandError(state, e, this);
1333
1445
  }
1334
1446
  finally {
1335
- _commandFinally(state, this);
1447
+ await _commandFinally(state, this);
1336
1448
  }
1337
1449
  }
1338
1450
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1362,13 +1474,49 @@ class StableBrowser {
1362
1474
  await _commandError(state, e, this);
1363
1475
  }
1364
1476
  finally {
1365
- _commandFinally(state, this);
1477
+ await _commandFinally(state, this);
1478
+ }
1479
+ }
1480
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1481
+ const state = {
1482
+ selectors,
1483
+ _params,
1484
+ files,
1485
+ value: '"' + files.join('", "') + '"',
1486
+ options,
1487
+ world,
1488
+ type: Types.SET_INPUT_FILES,
1489
+ text: `Set input files`,
1490
+ _text: `Set input files on ${selectors.element_name}`,
1491
+ operation: "setInputFiles",
1492
+ log: "***** set input files " + selectors.element_name + " *****\n",
1493
+ };
1494
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1495
+ try {
1496
+ await _preCommand(state, this);
1497
+ for (let i = 0; i < files.length; i++) {
1498
+ const file = files[i];
1499
+ const filePath = path.join(uploadsFolder, file);
1500
+ if (!fs.existsSync(filePath)) {
1501
+ throw new Error(`File not found: ${filePath}`);
1502
+ }
1503
+ state.files[i] = filePath;
1504
+ }
1505
+ await state.element.setInputFiles(files);
1506
+ return state.info;
1507
+ }
1508
+ catch (e) {
1509
+ await _commandError(state, e, this);
1510
+ }
1511
+ finally {
1512
+ await _commandFinally(state, this);
1366
1513
  }
1367
1514
  }
1368
1515
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1369
1516
  return await this._getText(selectors, 0, _params, options, info, world);
1370
1517
  }
1371
1518
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1519
+ const timeout = this._getFindElementTimeout(options);
1372
1520
  _validateSelectors(selectors);
1373
1521
  let screenshotId = null;
1374
1522
  let screenshotPath = null;
@@ -1378,7 +1526,7 @@ class StableBrowser {
1378
1526
  }
1379
1527
  info.operation = "getText";
1380
1528
  info.selectors = selectors;
1381
- let element = await this._locate(selectors, info, _params);
1529
+ let element = await this._locate(selectors, info, _params, timeout);
1382
1530
  if (climb > 0) {
1383
1531
  const climbArray = [];
1384
1532
  for (let i = 0; i < climb; i++) {
@@ -1445,6 +1593,7 @@ class StableBrowser {
1445
1593
  highlight: false,
1446
1594
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1447
1595
  text: `Verify element contains pattern: ${pattern}`,
1596
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1448
1597
  operation: "containsPattern",
1449
1598
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1450
1599
  };
@@ -1476,10 +1625,12 @@ class StableBrowser {
1476
1625
  await _commandError(state, e, this);
1477
1626
  }
1478
1627
  finally {
1479
- _commandFinally(state, this);
1628
+ await _commandFinally(state, this);
1480
1629
  }
1481
1630
  }
1482
1631
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1632
+ const timeout = this._getFindElementTimeout(options);
1633
+ const startTime = Date.now();
1483
1634
  const state = {
1484
1635
  selectors,
1485
1636
  _params,
@@ -1506,44 +1657,124 @@ class StableBrowser {
1506
1657
  }
1507
1658
  let foundObj = null;
1508
1659
  try {
1509
- await _preCommand(state, this);
1510
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1511
- if (foundObj && foundObj.element) {
1512
- await this.scrollIfNeeded(foundObj.element, state.info);
1513
- }
1514
- await _screenshot(state, this);
1515
- const dateAlternatives = findDateAlternatives(text);
1516
- const numberAlternatives = findNumberAlternatives(text);
1517
- if (dateAlternatives.date) {
1518
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1519
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1520
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1660
+ while (Date.now() - startTime < timeout) {
1661
+ try {
1662
+ await _preCommand(state, this);
1663
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1664
+ if (foundObj && foundObj.element) {
1665
+ await this.scrollIfNeeded(foundObj.element, state.info);
1666
+ }
1667
+ await _screenshot(state, this);
1668
+ const dateAlternatives = findDateAlternatives(text);
1669
+ const numberAlternatives = findNumberAlternatives(text);
1670
+ if (dateAlternatives.date) {
1671
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1672
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1673
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1674
+ return state.info;
1675
+ }
1676
+ }
1677
+ }
1678
+ else if (numberAlternatives.number) {
1679
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1680
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1681
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1682
+ return state.info;
1683
+ }
1684
+ }
1685
+ }
1686
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1521
1687
  return state.info;
1522
1688
  }
1523
1689
  }
1524
- throw new Error("element doesn't contain text " + text);
1690
+ catch (e) {
1691
+ // Log error but continue retrying until timeout is reached
1692
+ this.logger.warn("Retrying containsText due to: " + e.message);
1693
+ }
1694
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1525
1695
  }
1526
- else if (numberAlternatives.number) {
1527
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1528
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1529
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1530
- return state.info;
1696
+ state.info.foundText = foundObj?.text;
1697
+ state.info.value = foundObj?.value;
1698
+ throw new Error("element doesn't contain text " + text);
1699
+ }
1700
+ catch (e) {
1701
+ await _commandError(state, e, this);
1702
+ throw e;
1703
+ }
1704
+ finally {
1705
+ await _commandFinally(state, this);
1706
+ }
1707
+ }
1708
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1709
+ const timeout = this._getFindElementTimeout(options);
1710
+ const startTime = Date.now();
1711
+ const state = {
1712
+ _params,
1713
+ value: referanceSnapshot,
1714
+ options,
1715
+ world,
1716
+ locate: false,
1717
+ scroll: false,
1718
+ screenshot: true,
1719
+ highlight: false,
1720
+ type: Types.SNAPSHOT_VALIDATION,
1721
+ text: `verify snapshot: ${referanceSnapshot}`,
1722
+ operation: "snapshotValidation",
1723
+ log: "***** verify snapshot *****\n",
1724
+ };
1725
+ if (!referanceSnapshot) {
1726
+ throw new Error("referanceSnapshot is null");
1727
+ }
1728
+ let text = null;
1729
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1730
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1731
+ }
1732
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1733
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1734
+ }
1735
+ else if (referanceSnapshot.startsWith("yaml:")) {
1736
+ text = referanceSnapshot.substring(5);
1737
+ }
1738
+ else {
1739
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1740
+ }
1741
+ state.text = text;
1742
+ const newValue = await this._replaceWithLocalData(text, world);
1743
+ await _preCommand(state, this);
1744
+ let foundObj = null;
1745
+ try {
1746
+ let matchResult = null;
1747
+ while (Date.now() - startTime < timeout) {
1748
+ try {
1749
+ let scope = null;
1750
+ if (!frameSelectors) {
1751
+ scope = this.page;
1752
+ }
1753
+ else {
1754
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1755
+ }
1756
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1757
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1758
+ if (matchResult.errorLine !== -1) {
1759
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1531
1760
  }
1761
+ // highlight and screenshot
1762
+ return state.info;
1532
1763
  }
1533
- throw new Error("element doesn't contain text " + text);
1534
- }
1535
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1536
- state.info.foundText = foundObj?.text;
1537
- state.info.value = foundObj?.value;
1538
- throw new Error("element doesn't contain text " + text);
1764
+ catch (e) {
1765
+ // Log error but continue retrying until timeout is reached
1766
+ this.logger.warn("Retrying snapshot validation due to: " + e.message);
1767
+ }
1768
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1539
1769
  }
1540
- return state.info;
1770
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1541
1771
  }
1542
1772
  catch (e) {
1543
1773
  await _commandError(state, e, this);
1774
+ throw e;
1544
1775
  }
1545
1776
  finally {
1546
- _commandFinally(state, this);
1777
+ await _commandFinally(state, this);
1547
1778
  }
1548
1779
  }
1549
1780
  async waitForUserInput(message, world = null) {
@@ -1581,6 +1812,15 @@ class StableBrowser {
1581
1812
  // save the data to the file
1582
1813
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1583
1814
  }
1815
+ overwriteTestData(testData, world = null) {
1816
+ if (!testData) {
1817
+ return;
1818
+ }
1819
+ // if data file exists, load it
1820
+ const dataFile = _getDataFile(world, this.context, this);
1821
+ // save the data to the file
1822
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1823
+ }
1584
1824
  _getDataFilePath(fileName) {
1585
1825
  let dataFile = path.join(this.project_path, "data", fileName);
1586
1826
  if (fs.existsSync(dataFile)) {
@@ -1833,7 +2073,7 @@ class StableBrowser {
1833
2073
  await _commandError(state, e, this);
1834
2074
  }
1835
2075
  finally {
1836
- _commandFinally(state, this);
2076
+ await _commandFinally(state, this);
1837
2077
  }
1838
2078
  }
1839
2079
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1846,6 +2086,7 @@ class StableBrowser {
1846
2086
  world,
1847
2087
  type: Types.EXTRACT,
1848
2088
  text: `Extract attribute from element`,
2089
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1849
2090
  operation: "extractAttribute",
1850
2091
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1851
2092
  allowDisabled: true,
@@ -1863,10 +2104,31 @@ class StableBrowser {
1863
2104
  case "value":
1864
2105
  state.value = await state.element.inputValue();
1865
2106
  break;
2107
+ case "text":
2108
+ state.value = await state.element.textContent();
2109
+ break;
1866
2110
  default:
1867
2111
  state.value = await state.element.getAttribute(attribute);
1868
2112
  break;
1869
2113
  }
2114
+ if (options !== null) {
2115
+ if (options.regex && options.regex !== "") {
2116
+ // Construct a regex pattern from the provided string
2117
+ const regex = options.regex.slice(1, -1);
2118
+ const regexPattern = new RegExp(regex, "g");
2119
+ const matches = state.value.match(regexPattern);
2120
+ if (matches) {
2121
+ let newValue = "";
2122
+ for (const match of matches) {
2123
+ newValue += match;
2124
+ }
2125
+ state.value = newValue;
2126
+ }
2127
+ }
2128
+ if (options.trimSpaces && options.trimSpaces === true) {
2129
+ state.value = state.value.trim();
2130
+ }
2131
+ }
1870
2132
  state.info.value = state.value;
1871
2133
  this.setTestData({ [variable]: state.value }, world);
1872
2134
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1877,7 +2139,7 @@ class StableBrowser {
1877
2139
  await _commandError(state, e, this);
1878
2140
  }
1879
2141
  finally {
1880
- _commandFinally(state, this);
2142
+ await _commandFinally(state, this);
1881
2143
  }
1882
2144
  }
1883
2145
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1892,6 +2154,7 @@ class StableBrowser {
1892
2154
  highlight: true,
1893
2155
  screenshot: true,
1894
2156
  text: `Verify element attribute`,
2157
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1895
2158
  operation: "verifyAttribute",
1896
2159
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1897
2160
  allowDisabled: true,
@@ -1901,12 +2164,15 @@ class StableBrowser {
1901
2164
  let expectedValue;
1902
2165
  try {
1903
2166
  await _preCommand(state, this);
1904
- expectedValue = state.value;
2167
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1905
2168
  state.info.expectedValue = expectedValue;
1906
2169
  switch (attribute) {
1907
2170
  case "innerText":
1908
2171
  val = String(await state.element.innerText());
1909
2172
  break;
2173
+ case "text":
2174
+ val = String(await state.element.textContent());
2175
+ break;
1910
2176
  case "value":
1911
2177
  val = String(await state.element.inputValue());
1912
2178
  break;
@@ -1946,7 +2212,7 @@ class StableBrowser {
1946
2212
  await _commandError(state, e, this);
1947
2213
  }
1948
2214
  finally {
1949
- _commandFinally(state, this);
2215
+ await _commandFinally(state, this);
1950
2216
  }
1951
2217
  }
1952
2218
  async extractEmailData(emailAddress, options, world) {
@@ -2198,6 +2464,69 @@ class StableBrowser {
2198
2464
  _reportToWorld(world, {
2199
2465
  type: Types.VERIFY_PAGE_PATH,
2200
2466
  text: "Verify page path",
2467
+ _text: "Verify the page path contains " + pathPart,
2468
+ screenshotId,
2469
+ result: error
2470
+ ? {
2471
+ status: "FAILED",
2472
+ startTime,
2473
+ endTime,
2474
+ message: error?.message,
2475
+ }
2476
+ : {
2477
+ status: "PASSED",
2478
+ startTime,
2479
+ endTime,
2480
+ },
2481
+ info: info,
2482
+ });
2483
+ }
2484
+ }
2485
+ async verifyPageTitle(title, options = {}, world = null) {
2486
+ const startTime = Date.now();
2487
+ let error = null;
2488
+ let screenshotId = null;
2489
+ let screenshotPath = null;
2490
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2491
+ const info = {};
2492
+ info.log = "***** verify page title " + title + " *****\n";
2493
+ info.operation = "verifyPageTitle";
2494
+ const newValue = await this._replaceWithLocalData(title, world);
2495
+ if (newValue !== title) {
2496
+ this.logger.info(title + "=" + newValue);
2497
+ title = newValue;
2498
+ }
2499
+ info.title = title;
2500
+ try {
2501
+ for (let i = 0; i < 30; i++) {
2502
+ const foundTitle = await this.page.title();
2503
+ if (!foundTitle.includes(title)) {
2504
+ if (i === 29) {
2505
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2506
+ }
2507
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2508
+ continue;
2509
+ }
2510
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2511
+ return info;
2512
+ }
2513
+ }
2514
+ catch (e) {
2515
+ //await this.closeUnexpectedPopups();
2516
+ this.logger.error("verify page title failed " + info.log);
2517
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2518
+ info.screenshotPath = screenshotPath;
2519
+ Object.assign(e, { info: info });
2520
+ error = e;
2521
+ // throw e;
2522
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2523
+ }
2524
+ finally {
2525
+ const endTime = Date.now();
2526
+ _reportToWorld(world, {
2527
+ type: Types.VERIFY_PAGE_PATH,
2528
+ text: "Verify page title",
2529
+ _text: "Verify the page title contains " + title,
2201
2530
  screenshotId,
2202
2531
  result: error
2203
2532
  ? {
@@ -2215,27 +2544,27 @@ class StableBrowser {
2215
2544
  });
2216
2545
  }
2217
2546
  }
2218
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2547
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2219
2548
  const frames = this.page.frames();
2220
2549
  let results = [];
2221
- let ignoreCase = false;
2550
+ // let ignoreCase = false;
2222
2551
  for (let i = 0; i < frames.length; i++) {
2223
2552
  if (dateAlternatives.date) {
2224
2553
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2225
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2554
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2226
2555
  result.frame = frames[i];
2227
2556
  results.push(result);
2228
2557
  }
2229
2558
  }
2230
2559
  else if (numberAlternatives.number) {
2231
2560
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2232
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2561
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2233
2562
  result.frame = frames[i];
2234
2563
  results.push(result);
2235
2564
  }
2236
2565
  }
2237
2566
  else {
2238
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2567
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2239
2568
  result.frame = frames[i];
2240
2569
  results.push(result);
2241
2570
  }
@@ -2254,11 +2583,15 @@ class StableBrowser {
2254
2583
  scroll: false,
2255
2584
  highlight: false,
2256
2585
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2257
- text: `Verify text exists in page`,
2586
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2587
+ _text: `Verify the text '${text}' exists in page`,
2258
2588
  operation: "verifyTextExistInPage",
2259
2589
  log: "***** verify text " + text + " exists in page *****\n",
2260
2590
  };
2261
- const timeout = this._getLoadTimeout(options);
2591
+ if (testForRegex(text)) {
2592
+ text = text.replace(/\\"/g, '"');
2593
+ }
2594
+ const timeout = this._getFindElementTimeout(options);
2262
2595
  await new Promise((resolve) => setTimeout(resolve, 2000));
2263
2596
  const newValue = await this._replaceWithLocalData(text, world);
2264
2597
  if (newValue !== text) {
@@ -2328,7 +2661,7 @@ class StableBrowser {
2328
2661
  await _commandError(state, e, this);
2329
2662
  }
2330
2663
  finally {
2331
- _commandFinally(state, this);
2664
+ await _commandFinally(state, this);
2332
2665
  }
2333
2666
  }
2334
2667
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2341,11 +2674,15 @@ class StableBrowser {
2341
2674
  scroll: false,
2342
2675
  highlight: false,
2343
2676
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2344
- text: `Verify text does not exist in page`,
2677
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2678
+ _text: `Verify the text '${text}' does not exist in page`,
2345
2679
  operation: "verifyTextNotExistInPage",
2346
2680
  log: "***** verify text " + text + " does not exist in page *****\n",
2347
2681
  };
2348
- const timeout = this._getLoadTimeout(options);
2682
+ if (testForRegex(text)) {
2683
+ text = text.replace(/\\"/g, '"');
2684
+ }
2685
+ const timeout = this._getFindElementTimeout(options);
2349
2686
  await new Promise((resolve) => setTimeout(resolve, 2000));
2350
2687
  const newValue = await this._replaceWithLocalData(text, world);
2351
2688
  if (newValue !== text) {
@@ -2381,7 +2718,7 @@ class StableBrowser {
2381
2718
  await _commandError(state, e, this);
2382
2719
  }
2383
2720
  finally {
2384
- _commandFinally(state, this);
2721
+ await _commandFinally(state, this);
2385
2722
  }
2386
2723
  }
2387
2724
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2396,10 +2733,11 @@ class StableBrowser {
2396
2733
  highlight: false,
2397
2734
  type: Types.VERIFY_TEXT_WITH_RELATION,
2398
2735
  text: `Verify text with relation to another text`,
2736
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2399
2737
  operation: "verify_text_with_relation",
2400
2738
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2401
2739
  };
2402
- const timeout = this._getLoadTimeout(options);
2740
+ const timeout = this._getFindElementTimeout(options);
2403
2741
  await new Promise((resolve) => setTimeout(resolve, 2000));
2404
2742
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2405
2743
  if (newValue !== textAnchor) {
@@ -2422,7 +2760,7 @@ class StableBrowser {
2422
2760
  };
2423
2761
  while (true) {
2424
2762
  try {
2425
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2763
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2426
2764
  }
2427
2765
  catch (error) {
2428
2766
  // ignore
@@ -2450,7 +2788,7 @@ class StableBrowser {
2450
2788
  const count = await frame.locator(css).count();
2451
2789
  for (let j = 0; j < count; j++) {
2452
2790
  const continer = await frame.locator(css).nth(j);
2453
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2791
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2454
2792
  if (result.elementCount > 0) {
2455
2793
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2456
2794
  await this._highlightElements(frame, dataAttribute);
@@ -2491,9 +2829,33 @@ class StableBrowser {
2491
2829
  await _commandError(state, e, this);
2492
2830
  }
2493
2831
  finally {
2494
- _commandFinally(state, this);
2832
+ await _commandFinally(state, this);
2495
2833
  }
2496
2834
  }
2835
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2836
+ const frames = this.page.frames();
2837
+ let results = [];
2838
+ let ignoreCase = false;
2839
+ for (let i = 0; i < frames.length; i++) {
2840
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2841
+ result.frame = frames[i];
2842
+ const climbArray = [];
2843
+ for (let i = 0; i < climb; i++) {
2844
+ climbArray.push("..");
2845
+ }
2846
+ let climbXpath = "xpath=" + climbArray.join("/");
2847
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2848
+ const count = await frames[i].locator(newLocator).count();
2849
+ if (count > 0) {
2850
+ result.elementCount = count;
2851
+ result.locator = newLocator;
2852
+ results.push(result);
2853
+ }
2854
+ }
2855
+ // state.info.results = results;
2856
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2857
+ return resultWithElementsFound;
2858
+ }
2497
2859
  async visualVerification(text, options = {}, world = null) {
2498
2860
  const startTime = Date.now();
2499
2861
  let error = null;
@@ -2555,6 +2917,7 @@ class StableBrowser {
2555
2917
  _reportToWorld(world, {
2556
2918
  type: Types.VERIFY_VISUAL,
2557
2919
  text: "Visual verification",
2920
+ _text: "Visual verification of " + text,
2558
2921
  screenshotId,
2559
2922
  result: error
2560
2923
  ? {
@@ -2809,7 +3172,13 @@ class StableBrowser {
2809
3172
  }
2810
3173
  }
2811
3174
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2812
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3175
+ try {
3176
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3177
+ }
3178
+ catch (error) {
3179
+ this.logger.debug(error);
3180
+ throw error;
3181
+ }
2813
3182
  }
2814
3183
  _getLoadTimeout(options) {
2815
3184
  let timeout = 15000;
@@ -2821,6 +3190,15 @@ class StableBrowser {
2821
3190
  }
2822
3191
  return timeout;
2823
3192
  }
3193
+ _getFindElementTimeout(options) {
3194
+ if (options && options.timeout) {
3195
+ return options.timeout;
3196
+ }
3197
+ if (this.configuration.find_element_timeout) {
3198
+ return this.configuration.find_element_timeout;
3199
+ }
3200
+ return 30000;
3201
+ }
2824
3202
  async saveStoreState(path = null, world = null) {
2825
3203
  const storageState = await this.page.context().storageState();
2826
3204
  //const testDataFile = _getDataFile(world, this.context, this);
@@ -2837,6 +3215,9 @@ class StableBrowser {
2837
3215
  this.registerEventListeners(this.context);
2838
3216
  registerNetworkEvents(this.world, this, this.context, this.page);
2839
3217
  registerDownloadEvent(this.page, this.world, this.context);
3218
+ if (this.onRestoreSaveState) {
3219
+ this.onRestoreSaveState(path);
3220
+ }
2840
3221
  }
2841
3222
  async waitForPageLoad(options = {}, world = null) {
2842
3223
  let timeout = this._getLoadTimeout(options);
@@ -2905,6 +3286,7 @@ class StableBrowser {
2905
3286
  highlight: false,
2906
3287
  type: Types.CLOSE_PAGE,
2907
3288
  text: `Close page`,
3289
+ _text: `Close the page`,
2908
3290
  operation: "closePage",
2909
3291
  log: "***** close page *****\n",
2910
3292
  throwError: false,
@@ -2918,11 +3300,98 @@ class StableBrowser {
2918
3300
  await _commandError(state, e, this);
2919
3301
  }
2920
3302
  finally {
2921
- _commandFinally(state, this);
3303
+ await _commandFinally(state, this);
3304
+ }
3305
+ }
3306
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3307
+ let operation = null;
3308
+ if (!options || !options.operation) {
3309
+ throw new Error("operation is not defined");
3310
+ }
3311
+ operation = options.operation;
3312
+ // validate operation is one of the supported operations
3313
+ if (operation != "click" && operation != "hover+click") {
3314
+ throw new Error("operation is not supported");
3315
+ }
3316
+ const state = {
3317
+ options,
3318
+ world,
3319
+ locate: false,
3320
+ scroll: false,
3321
+ highlight: false,
3322
+ type: Types.TABLE_OPERATION,
3323
+ text: `Table operation`,
3324
+ _text: `Table ${operation} operation`,
3325
+ operation: operation,
3326
+ log: "***** Table operation *****\n",
3327
+ };
3328
+ const timeout = this._getFindElementTimeout(options);
3329
+ try {
3330
+ await _preCommand(state, this);
3331
+ const start = Date.now();
3332
+ let cellArea = null;
3333
+ while (true) {
3334
+ try {
3335
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3336
+ if (cellArea) {
3337
+ break;
3338
+ }
3339
+ }
3340
+ catch (e) {
3341
+ // ignore
3342
+ }
3343
+ if (Date.now() - start > timeout) {
3344
+ throw new Error(`Cell not found in table`);
3345
+ }
3346
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3347
+ }
3348
+ switch (operation) {
3349
+ case "click":
3350
+ if (!options.css) {
3351
+ // will click in the center of the cell
3352
+ let xOffset = 0;
3353
+ let yOffset = 0;
3354
+ if (options.xOffset) {
3355
+ xOffset = options.xOffset;
3356
+ }
3357
+ if (options.yOffset) {
3358
+ yOffset = options.yOffset;
3359
+ }
3360
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3361
+ }
3362
+ else {
3363
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3364
+ if (results.length === 0) {
3365
+ throw new Error(`Element not found in cell area`);
3366
+ }
3367
+ state.element = results[0];
3368
+ await performAction("click", state.element, options, this, state, _params);
3369
+ }
3370
+ break;
3371
+ case "hover+click":
3372
+ if (!options.css) {
3373
+ throw new Error("css is not defined");
3374
+ }
3375
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3376
+ if (results.length === 0) {
3377
+ throw new Error(`Element not found in cell area`);
3378
+ }
3379
+ state.element = results[0];
3380
+ await performAction("hover+click", state.element, options, this, state, _params);
3381
+ break;
3382
+ default:
3383
+ throw new Error("operation is not supported");
3384
+ }
3385
+ }
3386
+ catch (e) {
3387
+ await _commandError(state, e, this);
3388
+ }
3389
+ finally {
3390
+ await _commandFinally(state, this);
2922
3391
  }
2923
3392
  }
2924
3393
  saveTestDataAsGlobal(options, world) {
2925
- const dataFile = this._getDataFile(world);
3394
+ const dataFile = _getDataFile(world, this.context, this);
2926
3395
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2927
3396
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2928
3397
  }
@@ -2952,6 +3421,7 @@ class StableBrowser {
2952
3421
  _reportToWorld(world, {
2953
3422
  type: Types.SET_VIEWPORT,
2954
3423
  text: "set viewport size to " + width + "x" + hight,
3424
+ _text: "Set the viewport size to " + width + "x" + hight,
2955
3425
  screenshotId,
2956
3426
  result: error
2957
3427
  ? {
@@ -3022,7 +3492,39 @@ class StableBrowser {
3022
3492
  console.log("#-#");
3023
3493
  }
3024
3494
  }
3495
+ async beforeScenario(world, scenario) {
3496
+ this.beforeScenarioCalled = true;
3497
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3498
+ this.scenarioName = scenario.pickle.name;
3499
+ }
3500
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3501
+ this.featureName = scenario.gherkinDocument.feature.name;
3502
+ }
3503
+ if (this.context) {
3504
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3505
+ }
3506
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3507
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3508
+ // check if @global_test_data tag is present
3509
+ if (this.tags.includes("@global_test_data")) {
3510
+ this.saveTestDataAsGlobal({}, world);
3511
+ }
3512
+ }
3513
+ // update test data based on feature/scenario
3514
+ let envName = null;
3515
+ if (this.context && this.context.environment) {
3516
+ envName = this.context.environment.name;
3517
+ }
3518
+ if (!process.env.TEMP_RUN) {
3519
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3520
+ }
3521
+ await loadBrunoParams(this.context, this.context.environment.name);
3522
+ }
3523
+ async afterScenario(world, scenario) { }
3025
3524
  async beforeStep(world, step) {
3525
+ if (!this.beforeScenarioCalled) {
3526
+ this.beforeScenario(world, step);
3527
+ }
3026
3528
  if (this.stepIndex === undefined) {
3027
3529
  this.stepIndex = 0;
3028
3530
  }
@@ -3039,22 +3541,54 @@ class StableBrowser {
3039
3541
  else {
3040
3542
  this.stepName = "step " + this.stepIndex;
3041
3543
  }
3042
- if (this.context) {
3043
- this.context.examplesRow = extractStepExampleParameters(step);
3044
- }
3045
3544
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3046
3545
  if (this.context.browserObject.context) {
3047
3546
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3048
3547
  }
3049
3548
  }
3050
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3051
- this.tags = step.pickle.tags.map((tag) => tag.name);
3052
- // check if @global_test_data tag is present
3053
- if (this.tags.includes("@global_test_data")) {
3054
- this.saveTestDataAsGlobal({}, world);
3549
+ if (this.initSnapshotTaken === false) {
3550
+ this.initSnapshotTaken = true;
3551
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3552
+ const snapshot = await this.getAriaSnapshot();
3553
+ if (snapshot) {
3554
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3555
+ }
3055
3556
  }
3056
3557
  }
3057
3558
  }
3559
+ async getAriaSnapshot() {
3560
+ try {
3561
+ // find the page url
3562
+ const url = await this.page.url();
3563
+ // extract the path from the url
3564
+ const path = new URL(url).pathname;
3565
+ // get the page title
3566
+ const title = await this.page.title();
3567
+ // go over other frams
3568
+ const frames = this.page.frames();
3569
+ const snapshots = [];
3570
+ const content = [`- path: ${path}`, `- title: ${title}`];
3571
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3572
+ for (let i = 0; i < frames.length; i++) {
3573
+ const frame = frames[i];
3574
+ try {
3575
+ // Ensure frame is attached and has body
3576
+ const body = frame.locator("body");
3577
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3578
+ const snapshot = await body.ariaSnapshot({ timeout });
3579
+ content.push(`- frame: ${i}`);
3580
+ content.push(snapshot);
3581
+ }
3582
+ catch (innerErr) { }
3583
+ }
3584
+ return content.join("\n");
3585
+ }
3586
+ catch (e) {
3587
+ console.log("Error in getAriaSnapshot");
3588
+ //console.debug(e);
3589
+ }
3590
+ return null;
3591
+ }
3058
3592
  async afterStep(world, step) {
3059
3593
  this.stepName = null;
3060
3594
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3062,11 +3596,25 @@ class StableBrowser {
3062
3596
  await this.context.browserObject.context.tracing.stopChunk({
3063
3597
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3064
3598
  });
3599
+ if (world && world.attach) {
3600
+ await world.attach(JSON.stringify({
3601
+ type: "trace",
3602
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3603
+ }), "application/json+trace");
3604
+ }
3605
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3065
3606
  }
3066
3607
  }
3067
3608
  if (this.context) {
3068
3609
  this.context.examplesRow = null;
3069
3610
  }
3611
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3612
+ const snapshot = await this.getAriaSnapshot();
3613
+ if (snapshot) {
3614
+ const obj = {};
3615
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3616
+ }
3617
+ }
3070
3618
  }
3071
3619
  }
3072
3620
  function createTimedPromise(promise, label) {