automation_model 1.0.637-dev → 1.0.637-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 (58) hide show
  1. package/README.md +16 -16
  2. package/lib/analyze_helper.js.map +1 -1
  3. package/lib/api.d.ts +0 -1
  4. package/lib/api.js +35 -21
  5. package/lib/api.js.map +1 -1
  6. package/lib/auto_page.d.ts +1 -1
  7. package/lib/auto_page.js +99 -28
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.js +38 -9
  10. package/lib/browser_manager.js.map +1 -1
  11. package/lib/bruno.d.ts +2 -0
  12. package/lib/bruno.js +381 -0
  13. package/lib/bruno.js.map +1 -0
  14. package/lib/command_common.d.ts +4 -4
  15. package/lib/command_common.js +36 -16
  16. package/lib/command_common.js.map +1 -1
  17. package/lib/date_time.js.map +1 -1
  18. package/lib/drawRect.js.map +1 -1
  19. package/lib/environment.d.ts +1 -0
  20. package/lib/environment.js +1 -0
  21. package/lib/environment.js.map +1 -1
  22. package/lib/error-messages.js.map +1 -1
  23. package/lib/file_checker.d.ts +1 -0
  24. package/lib/file_checker.js +61 -0
  25. package/lib/file_checker.js.map +1 -0
  26. package/lib/find_function.js.map +1 -1
  27. package/lib/index.d.ts +2 -0
  28. package/lib/index.js +2 -0
  29. package/lib/index.js.map +1 -1
  30. package/lib/init_browser.d.ts +2 -2
  31. package/lib/init_browser.js +33 -27
  32. package/lib/init_browser.js.map +1 -1
  33. package/lib/locate_element.js +2 -2
  34. package/lib/locate_element.js.map +1 -1
  35. package/lib/locator.js +1 -1
  36. package/lib/locator.js.map +1 -1
  37. package/lib/locator_log.js.map +1 -1
  38. package/lib/network.d.ts +1 -1
  39. package/lib/network.js +5 -5
  40. package/lib/network.js.map +1 -1
  41. package/lib/snapshot_validation.d.ts +37 -0
  42. package/lib/snapshot_validation.js +357 -0
  43. package/lib/snapshot_validation.js.map +1 -0
  44. package/lib/stable_browser.d.ts +52 -25
  45. package/lib/stable_browser.js +720 -229
  46. package/lib/stable_browser.js.map +1 -1
  47. package/lib/table.js.map +1 -1
  48. package/lib/table_analyze.js.map +1 -1
  49. package/lib/table_helper.d.ts +1 -1
  50. package/lib/table_helper.js +26 -5
  51. package/lib/table_helper.js.map +1 -1
  52. package/lib/test_context.d.ts +2 -0
  53. package/lib/test_context.js +2 -0
  54. package/lib/test_context.js.map +1 -1
  55. package/lib/utils.d.ts +7 -4
  56. package/lib/utils.js +204 -20
  57. package/lib/utils.js.map +1 -1
  58. package/package.json +11 -6
@@ -10,11 +10,12 @@ 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, testForRegex, } 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";
@@ -22,23 +23,26 @@ import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
23
  import { LocatorLog } from "./locator_log.js";
23
24
  import axios from "axios";
24
25
  import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { snapshotValidation } from "./snapshot_validation.js";
27
+ import { loadBrunoParams } from "./bruno.js";
25
28
  export const Types = {
26
29
  CLICK: "click_element",
27
30
  WAIT_ELEMENT: "wait_element",
28
- NAVIGATE: "navigate",
31
+ NAVIGATE: "navigate", ///
29
32
  FILL: "fill_element",
30
- EXECUTE: "execute_page_method",
31
- OPEN: "open_environment",
33
+ EXECUTE: "execute_page_method", //
34
+ OPEN: "open_environment", //
32
35
  COMPLETE: "step_complete",
33
36
  ASK: "information_needed",
34
- GET_PAGE_STATUS: "get_page_status",
35
- CLICK_ROW_ACTION: "click_row_action",
37
+ GET_PAGE_STATUS: "get_page_status", ///
38
+ CLICK_ROW_ACTION: "click_row_action", //
36
39
  VERIFY_ELEMENT_CONTAINS_TEXT: "verify_element_contains_text",
37
40
  VERIFY_PAGE_CONTAINS_TEXT: "verify_page_contains_text",
38
41
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
39
42
  ANALYZE_TABLE: "analyze_table",
40
- SELECT: "select_combobox",
43
+ SELECT: "select_combobox", //
41
44
  VERIFY_PAGE_PATH: "verify_page_path",
45
+ VERIFY_PAGE_TITLE: "verify_page_title",
42
46
  TYPE_PRESS: "type_press",
43
47
  PRESS: "press_key",
44
48
  HOVER: "hover_element",
@@ -55,6 +59,12 @@ export const Types = {
55
59
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
56
60
  VERIFY_ATTRIBUTE: "verify_element_attribute",
57
61
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
62
+ BRUNO: "bruno",
63
+ VERIFY_FILE_EXISTS: "verify_file_exists",
64
+ SET_INPUT_FILES: "set_input_files",
65
+ SNAPSHOT_VALIDATION: "snapshot_validation",
66
+ REPORT_COMMAND: "report_command",
67
+ STEP_COMPLETE: "step_complete",
58
68
  };
59
69
  export const apps = {};
60
70
  const formatElementName = (elementName) => {
@@ -180,6 +190,30 @@ class StableBrowser {
180
190
  await this.waitForPageLoad();
181
191
  }
182
192
  }
193
+ async switchTab(tabTitleOrIndex) {
194
+ // first check if the tabNameOrIndex is a number
195
+ let index = parseInt(tabTitleOrIndex);
196
+ if (!isNaN(index)) {
197
+ if (index >= 0 && index < this.context.pages.length) {
198
+ this.page = this.context.pages[index];
199
+ this.context.page = this.page;
200
+ await this.page.bringToFront();
201
+ return;
202
+ }
203
+ }
204
+ // if the tabNameOrIndex is a string, find the tab by name
205
+ for (let i = 0; i < this.context.pages.length; i++) {
206
+ let page = this.context.pages[i];
207
+ let title = await page.title();
208
+ if (title.includes(tabTitleOrIndex)) {
209
+ this.page = page;
210
+ this.context.page = this.page;
211
+ await this.page.bringToFront();
212
+ return;
213
+ }
214
+ }
215
+ throw new Error("Tab not found: " + tabTitleOrIndex);
216
+ }
183
217
  registerConsoleLogListener(page, context) {
184
218
  if (!this.context.webLogger) {
185
219
  this.context.webLogger = [];
@@ -247,6 +281,7 @@ class StableBrowser {
247
281
  if (!url) {
248
282
  throw new Error("url is null, verify that the environment file is correct");
249
283
  }
284
+ url = await this._replaceWithLocalData(url, this.world);
250
285
  if (!url.startsWith("http")) {
251
286
  url = "https://" + url;
252
287
  }
@@ -275,7 +310,7 @@ class StableBrowser {
275
310
  _commandError(state, error, this);
276
311
  }
277
312
  finally {
278
- _commandFinally(state, this);
313
+ await _commandFinally(state, this);
279
314
  }
280
315
  }
281
316
  async _getLocator(locator, scope, _params) {
@@ -391,7 +426,7 @@ class StableBrowser {
391
426
  }
392
427
  return { elementCount: tagCount, randomToken };
393
428
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
429
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
430
  if (!info) {
396
431
  info = {};
397
432
  }
@@ -458,7 +493,7 @@ class StableBrowser {
458
493
  }
459
494
  return;
460
495
  }
461
- if (info.locatorLog && count === 0) {
496
+ if (info.locatorLog && count === 0 && logErrors) {
462
497
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
498
  }
464
499
  for (let j = 0; j < count; j++) {
@@ -473,7 +508,7 @@ class StableBrowser {
473
508
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
509
  }
475
510
  }
476
- else {
511
+ else if (logErrors) {
477
512
  info.failCause.visible = visible;
478
513
  info.failCause.enabled = enabled;
479
514
  if (!info.printMessages) {
@@ -565,15 +600,27 @@ class StableBrowser {
565
600
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
601
  if (!element.rerun) {
567
602
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
603
+ await element.evaluate((el, randomToken) => {
569
604
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
605
  }, randomToken);
571
- if (element._frame) {
572
- return element;
573
- }
574
- const scope = element.page();
575
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
576
- 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);
577
624
  }
578
625
  }
579
626
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +772,9 @@ class StableBrowser {
725
772
  // info.log += "scanning locators in priority 2" + "\n";
726
773
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
774
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
775
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
776
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
777
  }
731
- else {
732
- if (result.foundElements.length === 0 && !highPriorityOnly) {
733
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
734
- }
735
- }
736
778
  let foundElements = result.foundElements;
737
779
  if (foundElements.length === 1 && foundElements[0].unique) {
738
780
  info.box = foundElements[0].box;
@@ -787,6 +829,11 @@ class StableBrowser {
787
829
  visibleOnly = false;
788
830
  }
789
831
  await new Promise((resolve) => setTimeout(resolve, 1000));
832
+ // sheck of more of half of the timeout has passed
833
+ if (Date.now() - startTime > timeout / 2) {
834
+ highPriorityOnly = false;
835
+ visibleOnly = false;
836
+ }
790
837
  }
791
838
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
839
  // if (info.locatorLog) {
@@ -802,7 +849,7 @@ class StableBrowser {
802
849
  }
803
850
  throw new Error("failed to locate first element no elements found, " + info.log);
804
851
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
852
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
853
  let foundElements = [];
807
854
  const result = {
808
855
  foundElements: foundElements,
@@ -821,7 +868,9 @@ class StableBrowser {
821
868
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
869
  }
823
870
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
871
+ if (logErrors) {
872
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
873
+ }
825
874
  }
826
875
  }
827
876
  if (foundLocators.length === 1) {
@@ -833,9 +882,40 @@ class StableBrowser {
833
882
  result.locatorIndex = i;
834
883
  }
835
884
  if (foundLocators.length > 1) {
836
- info.failCause.foundMultiple = true;
837
- if (info.locatorLog) {
838
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
885
+ // remove elements that consume the same space with 10 pixels tolerance
886
+ const boxes = [];
887
+ for (let j = 0; j < foundLocators.length; j++) {
888
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
889
+ }
890
+ for (let j = 0; j < boxes.length; j++) {
891
+ for (let k = 0; k < boxes.length; k++) {
892
+ if (j === k) {
893
+ continue;
894
+ }
895
+ // check if x, y, width, height are the same with 10 pixels tolerance
896
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
897
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
898
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
899
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
900
+ // as the element is not unique, will remove it
901
+ boxes.splice(k, 1);
902
+ k--;
903
+ }
904
+ }
905
+ }
906
+ if (boxes.length === 1) {
907
+ result.foundElements.push({
908
+ locator: boxes[0].locator.first(),
909
+ box: boxes[0].box,
910
+ unique: true,
911
+ });
912
+ result.locatorIndex = i;
913
+ }
914
+ else if (logErrors) {
915
+ info.failCause.foundMultiple = true;
916
+ if (info.locatorLog) {
917
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
918
+ }
839
919
  }
840
920
  }
841
921
  }
@@ -883,7 +963,7 @@ class StableBrowser {
883
963
  await _commandError(state, "timeout looking for " + elementDescription, this);
884
964
  }
885
965
  finally {
886
- _commandFinally(state, this);
966
+ await _commandFinally(state, this);
887
967
  }
888
968
  }
889
969
  }
@@ -932,7 +1012,7 @@ class StableBrowser {
932
1012
  await _commandError(state, "timeout looking for " + elementDescription, this);
933
1013
  }
934
1014
  finally {
935
- _commandFinally(state, this);
1015
+ await _commandFinally(state, this);
936
1016
  }
937
1017
  }
938
1018
  }
@@ -953,19 +1033,7 @@ class StableBrowser {
953
1033
  };
954
1034
  try {
955
1035
  await _preCommand(state, this);
956
- // if (state.options && state.options.context) {
957
- // state.selectors.locators[0].text = state.options.context;
958
- // }
959
- try {
960
- await state.element.click();
961
- // await new Promise((resolve) => setTimeout(resolve, 1000));
962
- }
963
- catch (e) {
964
- // await this.closeUnexpectedPopups();
965
- state.element = await this._locate(selectors, state.info, _params);
966
- await state.element.dispatchEvent("click");
967
- // await new Promise((resolve) => setTimeout(resolve, 1000));
968
- }
1036
+ await performAction("click", state.element, options, this, state, _params);
969
1037
  await this.waitForPageLoad();
970
1038
  return state.info;
971
1039
  }
@@ -973,7 +1041,7 @@ class StableBrowser {
973
1041
  await _commandError(state, e, this);
974
1042
  }
975
1043
  finally {
976
- _commandFinally(state, this);
1044
+ await _commandFinally(state, this);
977
1045
  }
978
1046
  }
979
1047
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1004,7 +1072,7 @@ class StableBrowser {
1004
1072
  // await _commandError(state, e, this);
1005
1073
  }
1006
1074
  finally {
1007
- _commandFinally(state, this);
1075
+ await _commandFinally(state, this);
1008
1076
  }
1009
1077
  return found;
1010
1078
  }
@@ -1028,7 +1096,7 @@ class StableBrowser {
1028
1096
  try {
1029
1097
  // if (world && world.screenshot && !world.screenshotPath) {
1030
1098
  // console.log(`Highlighting while running from recorder`);
1031
- await this._highlightElements(element);
1099
+ await this._highlightElements(state.element);
1032
1100
  await state.element.setChecked(checked);
1033
1101
  await new Promise((resolve) => setTimeout(resolve, 1000));
1034
1102
  // await this._unHighlightElements(element);
@@ -1055,7 +1123,7 @@ class StableBrowser {
1055
1123
  await _commandError(state, e, this);
1056
1124
  }
1057
1125
  finally {
1058
- _commandFinally(state, this);
1126
+ await _commandFinally(state, this);
1059
1127
  }
1060
1128
  }
1061
1129
  async hover(selectors, _params, options = {}, world = null) {
@@ -1072,19 +1140,7 @@ class StableBrowser {
1072
1140
  };
1073
1141
  try {
1074
1142
  await _preCommand(state, this);
1075
- try {
1076
- await state.element.hover();
1077
- // await _screenshot(state, this);
1078
- await new Promise((resolve) => setTimeout(resolve, 1000));
1079
- }
1080
- catch (e) {
1081
- //await this.closeUnexpectedPopups();
1082
- state.info.log += "hover failed, will try again" + "\n";
1083
- state.element = await this._locate(selectors, state.info, _params);
1084
- await state.element.hover({ timeout: 10000 });
1085
- // await _screenshot(state, this);
1086
- await new Promise((resolve) => setTimeout(resolve, 1000));
1087
- }
1143
+ await performAction("hover", state.element, options, this, state, _params);
1088
1144
  await _screenshot(state, this);
1089
1145
  await this.waitForPageLoad();
1090
1146
  return state.info;
@@ -1093,7 +1149,7 @@ class StableBrowser {
1093
1149
  await _commandError(state, e, this);
1094
1150
  }
1095
1151
  finally {
1096
- _commandFinally(state, this);
1152
+ await _commandFinally(state, this);
1097
1153
  }
1098
1154
  }
1099
1155
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1129,7 +1185,7 @@ class StableBrowser {
1129
1185
  await _commandError(state, e, this);
1130
1186
  }
1131
1187
  finally {
1132
- _commandFinally(state, this);
1188
+ await _commandFinally(state, this);
1133
1189
  }
1134
1190
  }
1135
1191
  async type(_value, _params = null, options = {}, world = null) {
@@ -1175,7 +1231,7 @@ class StableBrowser {
1175
1231
  await _commandError(state, e, this);
1176
1232
  }
1177
1233
  finally {
1178
- _commandFinally(state, this);
1234
+ await _commandFinally(state, this);
1179
1235
  }
1180
1236
  }
1181
1237
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1211,7 +1267,7 @@ class StableBrowser {
1211
1267
  await _commandError(state, e, this);
1212
1268
  }
1213
1269
  finally {
1214
- _commandFinally(state, this);
1270
+ await _commandFinally(state, this);
1215
1271
  }
1216
1272
  }
1217
1273
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1231,7 +1287,7 @@ class StableBrowser {
1231
1287
  try {
1232
1288
  await _preCommand(state, this);
1233
1289
  try {
1234
- await state.element.click();
1290
+ await performAction("click", state.element, options, this, state, _params);
1235
1291
  await new Promise((resolve) => setTimeout(resolve, 500));
1236
1292
  if (format) {
1237
1293
  state.value = dayjs(state.value).format(format);
@@ -1280,7 +1336,7 @@ class StableBrowser {
1280
1336
  await _commandError(state, e, this);
1281
1337
  }
1282
1338
  finally {
1283
- _commandFinally(state, this);
1339
+ await _commandFinally(state, this);
1284
1340
  }
1285
1341
  }
1286
1342
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1299,6 +1355,9 @@ class StableBrowser {
1299
1355
  operation: "clickType",
1300
1356
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1301
1357
  };
1358
+ if (!options) {
1359
+ options = {};
1360
+ }
1302
1361
  if (newValue !== _value) {
1303
1362
  //this.logger.info(_value + "=" + newValue);
1304
1363
  _value = newValue;
@@ -1306,7 +1365,7 @@ class StableBrowser {
1306
1365
  try {
1307
1366
  await _preCommand(state, this);
1308
1367
  state.info.value = _value;
1309
- if (options === null || options === undefined || !options.press) {
1368
+ if (!options.press) {
1310
1369
  try {
1311
1370
  let currentValue = await state.element.inputValue();
1312
1371
  if (currentValue) {
@@ -1317,13 +1376,9 @@ class StableBrowser {
1317
1376
  this.logger.info("unable to clear input value");
1318
1377
  }
1319
1378
  }
1320
- if (options === null || options === undefined || options.press) {
1321
- try {
1322
- await state.element.click({ timeout: 5000 });
1323
- }
1324
- catch (e) {
1325
- await state.element.dispatchEvent("click");
1326
- }
1379
+ if (options.press) {
1380
+ options.timeout = 5000;
1381
+ await performAction("click", state.element, options, this, state, _params);
1327
1382
  }
1328
1383
  else {
1329
1384
  try {
@@ -1381,7 +1436,7 @@ class StableBrowser {
1381
1436
  await _commandError(state, e, this);
1382
1437
  }
1383
1438
  finally {
1384
- _commandFinally(state, this);
1439
+ await _commandFinally(state, this);
1385
1440
  }
1386
1441
  }
1387
1442
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1411,7 +1466,42 @@ class StableBrowser {
1411
1466
  await _commandError(state, e, this);
1412
1467
  }
1413
1468
  finally {
1414
- _commandFinally(state, this);
1469
+ await _commandFinally(state, this);
1470
+ }
1471
+ }
1472
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1473
+ const state = {
1474
+ selectors,
1475
+ _params,
1476
+ files,
1477
+ value: '"' + files.join('", "') + '"',
1478
+ options,
1479
+ world,
1480
+ type: Types.SET_INPUT_FILES,
1481
+ text: `Set input files`,
1482
+ _text: `Set input files on ${selectors.element_name}`,
1483
+ operation: "setInputFiles",
1484
+ log: "***** set input files " + selectors.element_name + " *****\n",
1485
+ };
1486
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1487
+ try {
1488
+ await _preCommand(state, this);
1489
+ for (let i = 0; i < files.length; i++) {
1490
+ const file = files[i];
1491
+ const filePath = path.join(uploadsFolder, file);
1492
+ if (!fs.existsSync(filePath)) {
1493
+ throw new Error(`File not found: ${filePath}`);
1494
+ }
1495
+ state.files[i] = filePath;
1496
+ }
1497
+ await state.element.setInputFiles(files);
1498
+ return state.info;
1499
+ }
1500
+ catch (e) {
1501
+ await _commandError(state, e, this);
1502
+ }
1503
+ finally {
1504
+ await _commandFinally(state, this);
1415
1505
  }
1416
1506
  }
1417
1507
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1527,7 +1617,7 @@ class StableBrowser {
1527
1617
  await _commandError(state, e, this);
1528
1618
  }
1529
1619
  finally {
1530
- _commandFinally(state, this);
1620
+ await _commandFinally(state, this);
1531
1621
  }
1532
1622
  }
1533
1623
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1562,7 +1652,7 @@ class StableBrowser {
1562
1652
  while (Date.now() - startTime < timeout) {
1563
1653
  try {
1564
1654
  await _preCommand(state, this);
1565
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1655
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1566
1656
  if (foundObj && foundObj.element) {
1567
1657
  await this.scrollIfNeeded(foundObj.element, state.info);
1568
1658
  }
@@ -1604,7 +1694,79 @@ class StableBrowser {
1604
1694
  throw e;
1605
1695
  }
1606
1696
  finally {
1607
- _commandFinally(state, this);
1697
+ await _commandFinally(state, this);
1698
+ }
1699
+ }
1700
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1701
+ const timeout = this._getFindElementTimeout(options);
1702
+ const startTime = Date.now();
1703
+ const state = {
1704
+ _params,
1705
+ value: referanceSnapshot,
1706
+ options,
1707
+ world,
1708
+ locate: false,
1709
+ scroll: false,
1710
+ screenshot: true,
1711
+ highlight: false,
1712
+ type: Types.SNAPSHOT_VALIDATION,
1713
+ text: `verify snapshot: ${referanceSnapshot}`,
1714
+ operation: "snapshotValidation",
1715
+ log: "***** verify snapshot *****\n",
1716
+ };
1717
+ if (!referanceSnapshot) {
1718
+ throw new Error("referanceSnapshot is null");
1719
+ }
1720
+ let text = null;
1721
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1722
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1723
+ }
1724
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1725
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1726
+ }
1727
+ else if (referanceSnapshot.startsWith("yaml:")) {
1728
+ text = referanceSnapshot.substring(5);
1729
+ }
1730
+ else {
1731
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1732
+ }
1733
+ state.text = text;
1734
+ const newValue = await this._replaceWithLocalData(text, world);
1735
+ await _preCommand(state, this);
1736
+ let foundObj = null;
1737
+ try {
1738
+ let matchResult = null;
1739
+ while (Date.now() - startTime < timeout) {
1740
+ try {
1741
+ let scope = null;
1742
+ if (!frameSelectors) {
1743
+ scope = this.page;
1744
+ }
1745
+ else {
1746
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1747
+ }
1748
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1749
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1750
+ if (matchResult.errorLine !== -1) {
1751
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1752
+ }
1753
+ // highlight and screenshot
1754
+ return state.info;
1755
+ }
1756
+ catch (e) {
1757
+ // Log error but continue retrying until timeout is reached
1758
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1759
+ }
1760
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1761
+ }
1762
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1763
+ }
1764
+ catch (e) {
1765
+ await _commandError(state, e, this);
1766
+ throw e;
1767
+ }
1768
+ finally {
1769
+ await _commandFinally(state, this);
1608
1770
  }
1609
1771
  }
1610
1772
  async waitForUserInput(message, world = null) {
@@ -1642,6 +1804,15 @@ class StableBrowser {
1642
1804
  // save the data to the file
1643
1805
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1644
1806
  }
1807
+ overwriteTestData(testData, world = null) {
1808
+ if (!testData) {
1809
+ return;
1810
+ }
1811
+ // if data file exists, load it
1812
+ const dataFile = _getDataFile(world, this.context, this);
1813
+ // save the data to the file
1814
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1815
+ }
1645
1816
  _getDataFilePath(fileName) {
1646
1817
  let dataFile = path.join(this.project_path, "data", fileName);
1647
1818
  if (fs.existsSync(dataFile)) {
@@ -1894,7 +2065,7 @@ class StableBrowser {
1894
2065
  await _commandError(state, e, this);
1895
2066
  }
1896
2067
  finally {
1897
- _commandFinally(state, this);
2068
+ await _commandFinally(state, this);
1898
2069
  }
1899
2070
  }
1900
2071
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1925,10 +2096,31 @@ class StableBrowser {
1925
2096
  case "value":
1926
2097
  state.value = await state.element.inputValue();
1927
2098
  break;
2099
+ case "text":
2100
+ state.value = await state.element.textContent();
2101
+ break;
1928
2102
  default:
1929
2103
  state.value = await state.element.getAttribute(attribute);
1930
2104
  break;
1931
2105
  }
2106
+ if (options !== null) {
2107
+ if (options.regex && options.regex !== "") {
2108
+ // Construct a regex pattern from the provided string
2109
+ const regex = options.regex.slice(1, -1);
2110
+ const regexPattern = new RegExp(regex, "g");
2111
+ const matches = state.value.match(regexPattern);
2112
+ if (matches) {
2113
+ let newValue = "";
2114
+ for (const match of matches) {
2115
+ newValue += match;
2116
+ }
2117
+ state.value = newValue;
2118
+ }
2119
+ }
2120
+ if (options.trimSpaces && options.trimSpaces === true) {
2121
+ state.value = state.value.trim();
2122
+ }
2123
+ }
1932
2124
  state.info.value = state.value;
1933
2125
  this.setTestData({ [variable]: state.value }, world);
1934
2126
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1939,7 +2131,7 @@ class StableBrowser {
1939
2131
  await _commandError(state, e, this);
1940
2132
  }
1941
2133
  finally {
1942
- _commandFinally(state, this);
2134
+ await _commandFinally(state, this);
1943
2135
  }
1944
2136
  }
1945
2137
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1964,12 +2156,15 @@ class StableBrowser {
1964
2156
  let expectedValue;
1965
2157
  try {
1966
2158
  await _preCommand(state, this);
1967
- expectedValue = state.value;
2159
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1968
2160
  state.info.expectedValue = expectedValue;
1969
2161
  switch (attribute) {
1970
2162
  case "innerText":
1971
2163
  val = String(await state.element.innerText());
1972
2164
  break;
2165
+ case "text":
2166
+ val = String(await state.element.textContent());
2167
+ break;
1973
2168
  case "value":
1974
2169
  val = String(await state.element.inputValue());
1975
2170
  break;
@@ -1991,17 +2186,42 @@ class StableBrowser {
1991
2186
  let regex;
1992
2187
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1993
2188
  const patternBody = expectedValue.slice(1, -1);
1994
- regex = new RegExp(patternBody, "g");
2189
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2190
+ regex = new RegExp(processedPattern, "gs");
2191
+ state.info.regex = true;
1995
2192
  }
1996
2193
  else {
1997
2194
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1998
2195
  regex = new RegExp(escapedPattern, "g");
1999
2196
  }
2000
- if (!val.match(regex)) {
2001
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2002
- state.info.failCause.assertionFailed = true;
2003
- state.info.failCause.lastError = errorMessage;
2004
- throw new Error(errorMessage);
2197
+ if (attribute === "innerText") {
2198
+ if (state.info.regex) {
2199
+ if (!regex.test(val)) {
2200
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2201
+ state.info.failCause.assertionFailed = true;
2202
+ state.info.failCause.lastError = errorMessage;
2203
+ throw new Error(errorMessage);
2204
+ }
2205
+ }
2206
+ else {
2207
+ const valLines = val.split("\n");
2208
+ const expectedLines = expectedValue.split("\n");
2209
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2210
+ if (!isPart) {
2211
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2212
+ state.info.failCause.assertionFailed = true;
2213
+ state.info.failCause.lastError = errorMessage;
2214
+ throw new Error(errorMessage);
2215
+ }
2216
+ }
2217
+ }
2218
+ else {
2219
+ if (!val.match(regex)) {
2220
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2221
+ state.info.failCause.assertionFailed = true;
2222
+ state.info.failCause.lastError = errorMessage;
2223
+ throw new Error(errorMessage);
2224
+ }
2005
2225
  }
2006
2226
  return state.info;
2007
2227
  }
@@ -2009,7 +2229,7 @@ class StableBrowser {
2009
2229
  await _commandError(state, e, this);
2010
2230
  }
2011
2231
  finally {
2012
- _commandFinally(state, this);
2232
+ await _commandFinally(state, this);
2013
2233
  }
2014
2234
  }
2015
2235
  async extractEmailData(emailAddress, options, world) {
@@ -2169,56 +2389,49 @@ class StableBrowser {
2169
2389
  console.debug(error);
2170
2390
  }
2171
2391
  }
2172
- // async _unhighlightElements(scope, css) {
2173
- // try {
2174
- // if (!scope) {
2175
- // return;
2176
- // }
2177
- // if (!css) {
2178
- // scope
2179
- // .evaluate((node) => {
2180
- // if (node && node.style) {
2181
- // if (!node.__previousOutline) {
2182
- // node.style.outline = "";
2183
- // } else {
2184
- // node.style.outline = node.__previousOutline;
2185
- // }
2186
- // }
2187
- // })
2188
- // .then(() => {})
2189
- // .catch((e) => {
2190
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2191
- // });
2192
- // } else {
2193
- // scope
2194
- // .evaluate(([css]) => {
2195
- // if (!css) {
2196
- // return;
2197
- // }
2198
- // let elements = Array.from(document.querySelectorAll(css));
2199
- // for (i = 0; i < elements.length; i++) {
2200
- // let element = elements[i];
2201
- // if (!element.style) {
2202
- // return;
2203
- // }
2204
- // if (!element.__previousOutline) {
2205
- // element.style.outline = "";
2206
- // } else {
2207
- // element.style.outline = element.__previousOutline;
2208
- // }
2209
- // }
2210
- // })
2211
- // .then(() => {})
2212
- // .catch((e) => {
2213
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2214
- // });
2215
- // }
2216
- // } catch (error) {
2217
- // // console.debug(error);
2218
- // }
2219
- // }
2392
+ _matcher(text) {
2393
+ if (!text) {
2394
+ return { matcher: "contains", queryText: "" };
2395
+ }
2396
+ if (text.length < 2) {
2397
+ return { matcher: "contains", queryText: text };
2398
+ }
2399
+ const split = text.split(":");
2400
+ const matcher = split[0].toLowerCase();
2401
+ const queryText = split.slice(1).join(":").trim();
2402
+ return { matcher, queryText };
2403
+ }
2404
+ _getDomain(url) {
2405
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2406
+ return "";
2407
+ }
2408
+ let hostnameFragments = url.split("/")[2].split(".");
2409
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2410
+ return hostnameFragments.join("-").split(":").join("-");
2411
+ }
2412
+ let n = hostnameFragments.length;
2413
+ let fragments = [...hostnameFragments];
2414
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2415
+ hostnameFragments.pop();
2416
+ n = hostnameFragments.length;
2417
+ }
2418
+ if (n == 0) {
2419
+ if (fragments[0] === "www")
2420
+ fragments = fragments.slice(1);
2421
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2422
+ }
2423
+ if (hostnameFragments[0] === "www")
2424
+ hostnameFragments = hostnameFragments.slice(1);
2425
+ return hostnameFragments.join(".");
2426
+ }
2427
+ /**
2428
+ * Verify the page path matches the given path.
2429
+ * @param {string} pathPart - The path to verify.
2430
+ * @param {object} options - Options for verification.
2431
+ * @param {object} world - The world context.
2432
+ * @returns {Promise<object>} - The state info after verification.
2433
+ */
2220
2434
  async verifyPagePath(pathPart, options = {}, world = null) {
2221
- const startTime = Date.now();
2222
2435
  let error = null;
2223
2436
  let screenshotId = null;
2224
2437
  let screenshotPath = null;
@@ -2232,74 +2445,235 @@ class StableBrowser {
2232
2445
  pathPart = newValue;
2233
2446
  }
2234
2447
  info.pathPart = pathPart;
2448
+ const { matcher, queryText } = this._matcher(pathPart);
2449
+ const state = {
2450
+ text_search: queryText,
2451
+ options,
2452
+ world,
2453
+ locate: false,
2454
+ scroll: false,
2455
+ highlight: false,
2456
+ type: Types.VERIFY_PAGE_PATH,
2457
+ text: `Verify the page url is ${queryText}`,
2458
+ _text: `Verify the page url is ${queryText}`,
2459
+ operation: "verifyPagePath",
2460
+ log: "***** verify page url is " + queryText + " *****\n",
2461
+ };
2235
2462
  try {
2463
+ await _preCommand(state, this);
2464
+ state.info.text = queryText;
2236
2465
  for (let i = 0; i < 30; i++) {
2237
2466
  const url = await this.page.url();
2238
- if (!url.includes(pathPart)) {
2239
- if (i === 29) {
2240
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2241
- }
2242
- await new Promise((resolve) => setTimeout(resolve, 1000));
2243
- continue;
2467
+ switch (matcher) {
2468
+ case "exact":
2469
+ if (url !== queryText) {
2470
+ if (i === 29) {
2471
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2472
+ }
2473
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2474
+ continue;
2475
+ }
2476
+ break;
2477
+ case "contains":
2478
+ if (!url.includes(queryText)) {
2479
+ if (i === 29) {
2480
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2481
+ }
2482
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2483
+ continue;
2484
+ }
2485
+ break;
2486
+ case "starts-with":
2487
+ {
2488
+ const domain = this._getDomain(url);
2489
+ if (domain.length > 0 && domain !== queryText) {
2490
+ if (i === 29) {
2491
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2492
+ }
2493
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2494
+ continue;
2495
+ }
2496
+ }
2497
+ break;
2498
+ case "ends-with":
2499
+ {
2500
+ const urlObj = new URL(url);
2501
+ let route = "/";
2502
+ if (urlObj.pathname !== "/") {
2503
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2504
+ }
2505
+ else {
2506
+ route = "/";
2507
+ }
2508
+ if (route !== queryText) {
2509
+ if (i === 29) {
2510
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2511
+ }
2512
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2513
+ continue;
2514
+ }
2515
+ }
2516
+ break;
2517
+ case "regex":
2518
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2519
+ if (!regex.test(url)) {
2520
+ if (i === 29) {
2521
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2522
+ }
2523
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2524
+ continue;
2525
+ }
2526
+ break;
2527
+ default:
2528
+ console.log("Unknown matching type, defaulting to contains matching");
2529
+ if (!url.includes(pathPart)) {
2530
+ if (i === 29) {
2531
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2532
+ }
2533
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2534
+ continue;
2535
+ }
2244
2536
  }
2245
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2246
- return info;
2537
+ await _screenshot(state, this);
2538
+ return state.info;
2247
2539
  }
2248
2540
  }
2249
2541
  catch (e) {
2250
- //await this.closeUnexpectedPopups();
2251
- this.logger.error("verify page path failed " + info.log);
2252
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2253
- info.screenshotPath = screenshotPath;
2254
- Object.assign(e, { info: info });
2255
- error = e;
2256
- // throw e;
2257
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2542
+ state.info.failCause.lastError = e.message;
2543
+ state.info.failCause.assertionFailed = true;
2544
+ await _commandError(state, e, this);
2258
2545
  }
2259
2546
  finally {
2260
- const endTime = Date.now();
2261
- _reportToWorld(world, {
2262
- type: Types.VERIFY_PAGE_PATH,
2263
- text: "Verify page path",
2264
- _text: "Verify the page path contains " + pathPart,
2265
- screenshotId,
2266
- result: error
2267
- ? {
2268
- status: "FAILED",
2269
- startTime,
2270
- endTime,
2271
- message: error?.message,
2272
- }
2273
- : {
2274
- status: "PASSED",
2275
- startTime,
2276
- endTime,
2277
- },
2278
- info: info,
2279
- });
2547
+ await _commandFinally(state, this);
2280
2548
  }
2281
2549
  }
2282
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2550
+ /**
2551
+ * Verify the page title matches the given title.
2552
+ * @param {string} title - The title to verify.
2553
+ * @param {object} options - Options for verification.
2554
+ * @param {object} world - The world context.
2555
+ * @returns {Promise<object>} - The state info after verification.
2556
+ */
2557
+ async verifyPageTitle(title, options = {}, world = null) {
2558
+ let error = null;
2559
+ let screenshotId = null;
2560
+ let screenshotPath = null;
2561
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2562
+ const newValue = await this._replaceWithLocalData(title, world);
2563
+ if (newValue !== title) {
2564
+ this.logger.info(title + "=" + newValue);
2565
+ title = newValue;
2566
+ }
2567
+ const { matcher, queryText } = this._matcher(title);
2568
+ const state = {
2569
+ text_search: queryText,
2570
+ options,
2571
+ world,
2572
+ locate: false,
2573
+ scroll: false,
2574
+ highlight: false,
2575
+ type: Types.VERIFY_PAGE_TITLE,
2576
+ text: `Verify the page title is ${queryText}`,
2577
+ _text: `Verify the page title is ${queryText}`,
2578
+ operation: "verifyPageTitle",
2579
+ log: "***** verify page title is " + queryText + " *****\n",
2580
+ };
2581
+ try {
2582
+ await _preCommand(state, this);
2583
+ state.info.text = queryText;
2584
+ for (let i = 0; i < 30; i++) {
2585
+ const foundTitle = await this.page.title();
2586
+ switch (matcher) {
2587
+ case "exact":
2588
+ if (foundTitle !== queryText) {
2589
+ if (i === 29) {
2590
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2591
+ }
2592
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2593
+ continue;
2594
+ }
2595
+ break;
2596
+ case "contains":
2597
+ if (!foundTitle.includes(queryText)) {
2598
+ if (i === 29) {
2599
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2600
+ }
2601
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2602
+ continue;
2603
+ }
2604
+ break;
2605
+ case "starts-with":
2606
+ if (!foundTitle.startsWith(queryText)) {
2607
+ if (i === 29) {
2608
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2609
+ }
2610
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2611
+ continue;
2612
+ }
2613
+ break;
2614
+ case "ends-with":
2615
+ if (!foundTitle.endsWith(queryText)) {
2616
+ if (i === 29) {
2617
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2618
+ }
2619
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2620
+ continue;
2621
+ }
2622
+ break;
2623
+ case "regex":
2624
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2625
+ if (!regex.test(foundTitle)) {
2626
+ if (i === 29) {
2627
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2628
+ }
2629
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2630
+ continue;
2631
+ }
2632
+ break;
2633
+ default:
2634
+ console.log("Unknown matching type, defaulting to contains matching");
2635
+ if (!foundTitle.includes(title)) {
2636
+ if (i === 29) {
2637
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2638
+ }
2639
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2640
+ continue;
2641
+ }
2642
+ }
2643
+ await _screenshot(state, this);
2644
+ return state.info;
2645
+ }
2646
+ }
2647
+ catch (e) {
2648
+ state.info.failCause.lastError = e.message;
2649
+ state.info.failCause.assertionFailed = true;
2650
+ await _commandError(state, e, this);
2651
+ }
2652
+ finally {
2653
+ await _commandFinally(state, this);
2654
+ }
2655
+ }
2656
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2283
2657
  const frames = this.page.frames();
2284
2658
  let results = [];
2285
- let ignoreCase = false;
2659
+ // let ignoreCase = false;
2286
2660
  for (let i = 0; i < frames.length; i++) {
2287
2661
  if (dateAlternatives.date) {
2288
2662
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2289
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2663
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2290
2664
  result.frame = frames[i];
2291
2665
  results.push(result);
2292
2666
  }
2293
2667
  }
2294
2668
  else if (numberAlternatives.number) {
2295
2669
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2296
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2670
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2297
2671
  result.frame = frames[i];
2298
2672
  results.push(result);
2299
2673
  }
2300
2674
  }
2301
2675
  else {
2302
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2676
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2303
2677
  result.frame = frames[i];
2304
2678
  results.push(result);
2305
2679
  }
@@ -2318,7 +2692,7 @@ class StableBrowser {
2318
2692
  scroll: false,
2319
2693
  highlight: false,
2320
2694
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2321
- text: `Verify text exists in page`,
2695
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2322
2696
  _text: `Verify the text '${text}' exists in page`,
2323
2697
  operation: "verifyTextExistInPage",
2324
2698
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2360,27 +2734,10 @@ class StableBrowser {
2360
2734
  const frame = resultWithElementsFound[0].frame;
2361
2735
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2362
2736
  await this._highlightElements(frame, dataAttribute);
2363
- // if (world && world.screenshot && !world.screenshotPath) {
2364
- // console.log(`Highlighting for verify text is found while running from recorder`);
2365
- // this._highlightElements(frame, dataAttribute).then(async () => {
2366
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2367
- // this._unhighlightElements(frame, dataAttribute)
2368
- // .then(async () => {
2369
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2370
- // })
2371
- // .catch(
2372
- // (e) => {}
2373
- // console.error(e)
2374
- // );
2375
- // });
2376
- // }
2377
2737
  const element = await frame.locator(dataAttribute).first();
2378
- // await new Promise((resolve) => setTimeout(resolve, 100));
2379
- // await this._unhighlightElements(frame, dataAttribute);
2380
2738
  if (element) {
2381
2739
  await this.scrollIfNeeded(element, state.info);
2382
2740
  await element.dispatchEvent("bvt_verify_page_contains_text");
2383
- // await _screenshot(state, this, element);
2384
2741
  }
2385
2742
  }
2386
2743
  await _screenshot(state, this);
@@ -2390,13 +2747,12 @@ class StableBrowser {
2390
2747
  console.error(error);
2391
2748
  }
2392
2749
  }
2393
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2394
2750
  }
2395
2751
  catch (e) {
2396
2752
  await _commandError(state, e, this);
2397
2753
  }
2398
2754
  finally {
2399
- _commandFinally(state, this);
2755
+ await _commandFinally(state, this);
2400
2756
  }
2401
2757
  }
2402
2758
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2409,7 +2765,7 @@ class StableBrowser {
2409
2765
  scroll: false,
2410
2766
  highlight: false,
2411
2767
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2412
- text: `Verify text does not exist in page`,
2768
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2413
2769
  _text: `Verify the text '${text}' does not exist in page`,
2414
2770
  operation: "verifyTextNotExistInPage",
2415
2771
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2453,7 +2809,7 @@ class StableBrowser {
2453
2809
  await _commandError(state, e, this);
2454
2810
  }
2455
2811
  finally {
2456
- _commandFinally(state, this);
2812
+ await _commandFinally(state, this);
2457
2813
  }
2458
2814
  }
2459
2815
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2495,7 +2851,7 @@ class StableBrowser {
2495
2851
  };
2496
2852
  while (true) {
2497
2853
  try {
2498
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2854
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2499
2855
  }
2500
2856
  catch (error) {
2501
2857
  // ignore
@@ -2523,7 +2879,7 @@ class StableBrowser {
2523
2879
  const count = await frame.locator(css).count();
2524
2880
  for (let j = 0; j < count; j++) {
2525
2881
  const continer = await frame.locator(css).nth(j);
2526
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2882
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2527
2883
  if (result.elementCount > 0) {
2528
2884
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2529
2885
  await this._highlightElements(frame, dataAttribute);
@@ -2564,9 +2920,33 @@ class StableBrowser {
2564
2920
  await _commandError(state, e, this);
2565
2921
  }
2566
2922
  finally {
2567
- _commandFinally(state, this);
2923
+ await _commandFinally(state, this);
2568
2924
  }
2569
2925
  }
2926
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2927
+ const frames = this.page.frames();
2928
+ let results = [];
2929
+ let ignoreCase = false;
2930
+ for (let i = 0; i < frames.length; i++) {
2931
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2932
+ result.frame = frames[i];
2933
+ const climbArray = [];
2934
+ for (let i = 0; i < climb; i++) {
2935
+ climbArray.push("..");
2936
+ }
2937
+ let climbXpath = "xpath=" + climbArray.join("/");
2938
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2939
+ const count = await frames[i].locator(newLocator).count();
2940
+ if (count > 0) {
2941
+ result.elementCount = count;
2942
+ result.locator = newLocator;
2943
+ results.push(result);
2944
+ }
2945
+ }
2946
+ // state.info.results = results;
2947
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2948
+ return resultWithElementsFound;
2949
+ }
2570
2950
  async visualVerification(text, options = {}, world = null) {
2571
2951
  const startTime = Date.now();
2572
2952
  let error = null;
@@ -2883,7 +3263,13 @@ class StableBrowser {
2883
3263
  }
2884
3264
  }
2885
3265
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2886
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3266
+ try {
3267
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3268
+ }
3269
+ catch (error) {
3270
+ this.logger.debug(error);
3271
+ throw error;
3272
+ }
2887
3273
  }
2888
3274
  _getLoadTimeout(options) {
2889
3275
  let timeout = 15000;
@@ -2906,6 +3292,7 @@ class StableBrowser {
2906
3292
  }
2907
3293
  async saveStoreState(path = null, world = null) {
2908
3294
  const storageState = await this.page.context().storageState();
3295
+ path = await this._replaceWithLocalData(path, this.world);
2909
3296
  //const testDataFile = _getDataFile(world, this.context, this);
2910
3297
  if (path) {
2911
3298
  // save { storageState: storageState } into the path
@@ -2916,10 +3303,14 @@ class StableBrowser {
2916
3303
  }
2917
3304
  }
2918
3305
  async restoreSaveState(path = null, world = null) {
3306
+ path = await this._replaceWithLocalData(path, this.world);
2919
3307
  await refreshBrowser(this, path, world);
2920
3308
  this.registerEventListeners(this.context);
2921
3309
  registerNetworkEvents(this.world, this, this.context, this.page);
2922
3310
  registerDownloadEvent(this.page, this.world, this.context);
3311
+ if (this.onRestoreSaveState) {
3312
+ this.onRestoreSaveState(path);
3313
+ }
2923
3314
  }
2924
3315
  async waitForPageLoad(options = {}, world = null) {
2925
3316
  let timeout = this._getLoadTimeout(options);
@@ -3002,10 +3393,10 @@ class StableBrowser {
3002
3393
  await _commandError(state, e, this);
3003
3394
  }
3004
3395
  finally {
3005
- _commandFinally(state, this);
3396
+ await _commandFinally(state, this);
3006
3397
  }
3007
3398
  }
3008
- async tableCellOperation(headerText, rowText, options, world = null) {
3399
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3009
3400
  let operation = null;
3010
3401
  if (!options || !options.operation) {
3011
3402
  throw new Error("operation is not defined");
@@ -3062,26 +3453,24 @@ class StableBrowser {
3062
3453
  await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3063
3454
  }
3064
3455
  else {
3065
- const results = await findElementsInArea(options.css, cellArea, this.page, this, options);
3456
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3066
3457
  if (results.length === 0) {
3067
3458
  throw new Error(`Element not found in cell area`);
3068
3459
  }
3069
3460
  state.element = results[0];
3070
- await results[0].click();
3461
+ await performAction("click", state.element, options, this, state, _params);
3071
3462
  }
3072
3463
  break;
3073
3464
  case "hover+click":
3074
3465
  if (!options.css) {
3075
3466
  throw new Error("css is not defined");
3076
3467
  }
3077
- const results = await findElementsInArea(options.css, cellArea, this.page, this, options);
3468
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3078
3469
  if (results.length === 0) {
3079
3470
  throw new Error(`Element not found in cell area`);
3080
3471
  }
3081
3472
  state.element = results[0];
3082
- await results[0].hover();
3083
- await new Promise((resolve) => setTimeout(resolve, 1000));
3084
- await results[0].click();
3473
+ await performAction("hover+click", state.element, options, this, state, _params);
3085
3474
  break;
3086
3475
  default:
3087
3476
  throw new Error("operation is not supported");
@@ -3091,7 +3480,7 @@ class StableBrowser {
3091
3480
  await _commandError(state, e, this);
3092
3481
  }
3093
3482
  finally {
3094
- _commandFinally(state, this);
3483
+ await _commandFinally(state, this);
3095
3484
  }
3096
3485
  }
3097
3486
  saveTestDataAsGlobal(options, world) {
@@ -3196,7 +3585,39 @@ class StableBrowser {
3196
3585
  console.log("#-#");
3197
3586
  }
3198
3587
  }
3588
+ async beforeScenario(world, scenario) {
3589
+ this.beforeScenarioCalled = true;
3590
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3591
+ this.scenarioName = scenario.pickle.name;
3592
+ }
3593
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3594
+ this.featureName = scenario.gherkinDocument.feature.name;
3595
+ }
3596
+ if (this.context) {
3597
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3598
+ }
3599
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3600
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3601
+ // check if @global_test_data tag is present
3602
+ if (this.tags.includes("@global_test_data")) {
3603
+ this.saveTestDataAsGlobal({}, world);
3604
+ }
3605
+ }
3606
+ // update test data based on feature/scenario
3607
+ let envName = null;
3608
+ if (this.context && this.context.environment) {
3609
+ envName = this.context.environment.name;
3610
+ }
3611
+ if (!process.env.TEMP_RUN) {
3612
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3613
+ }
3614
+ await loadBrunoParams(this.context, this.context.environment.name);
3615
+ }
3616
+ async afterScenario(world, scenario) { }
3199
3617
  async beforeStep(world, step) {
3618
+ if (!this.beforeScenarioCalled) {
3619
+ this.beforeScenario(world, step);
3620
+ }
3200
3621
  if (this.stepIndex === undefined) {
3201
3622
  this.stepIndex = 0;
3202
3623
  }
@@ -3213,21 +3634,11 @@ class StableBrowser {
3213
3634
  else {
3214
3635
  this.stepName = "step " + this.stepIndex;
3215
3636
  }
3216
- if (this.context) {
3217
- this.context.examplesRow = extractStepExampleParameters(step);
3218
- }
3219
3637
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3220
3638
  if (this.context.browserObject.context) {
3221
3639
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3222
3640
  }
3223
3641
  }
3224
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3225
- this.tags = step.pickle.tags.map((tag) => tag.name);
3226
- // check if @global_test_data tag is present
3227
- if (this.tags.includes("@global_test_data")) {
3228
- this.saveTestDataAsGlobal({}, world);
3229
- }
3230
- }
3231
3642
  if (this.initSnapshotTaken === false) {
3232
3643
  this.initSnapshotTaken = true;
3233
3644
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3252,18 +3663,68 @@ class StableBrowser {
3252
3663
  const content = [`- path: ${path}`, `- title: ${title}`];
3253
3664
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3254
3665
  for (let i = 0; i < frames.length; i++) {
3255
- content.push(`- frame: ${i}`);
3256
3666
  const frame = frames[i];
3257
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3258
- content.push(snapshot);
3667
+ try {
3668
+ // Ensure frame is attached and has body
3669
+ const body = frame.locator("body");
3670
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3671
+ const snapshot = await body.ariaSnapshot({ timeout });
3672
+ content.push(`- frame: ${i}`);
3673
+ content.push(snapshot);
3674
+ }
3675
+ catch (innerErr) { }
3259
3676
  }
3260
3677
  return content.join("\n");
3261
3678
  }
3262
3679
  catch (e) {
3263
- console.error(e);
3680
+ console.log("Error in getAriaSnapshot");
3681
+ //console.debug(e);
3264
3682
  }
3265
3683
  return null;
3266
3684
  }
3685
+ /**
3686
+ * Sends command with custom payload to report.
3687
+ * @param commandText - Title of the command to be shown in the report.
3688
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3689
+ * @param content - Content of the command to be shown in the report.
3690
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3691
+ * @param world - Optional world context.
3692
+ * @public
3693
+ */
3694
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3695
+ const state = {
3696
+ options,
3697
+ world,
3698
+ locate: false,
3699
+ scroll: false,
3700
+ screenshot: options.screenshot ?? false,
3701
+ highlight: options.highlight ?? false,
3702
+ type: Types.REPORT_COMMAND,
3703
+ text: commandText,
3704
+ _text: commandText,
3705
+ operation: "report_command",
3706
+ log: "***** " + commandText + " *****\n",
3707
+ };
3708
+ try {
3709
+ await _preCommand(state, this);
3710
+ const payload = {
3711
+ type: options.type ?? "text",
3712
+ content: content,
3713
+ screenshotId: null,
3714
+ };
3715
+ state.payload = payload;
3716
+ if (commandStatus === "FAILED") {
3717
+ state.throwError = true;
3718
+ throw new Error("Command failed");
3719
+ }
3720
+ }
3721
+ catch (e) {
3722
+ await _commandError(state, e, this);
3723
+ }
3724
+ finally {
3725
+ await _commandFinally(state, this);
3726
+ }
3727
+ }
3267
3728
  async afterStep(world, step) {
3268
3729
  this.stepName = null;
3269
3730
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3271,6 +3732,13 @@ class StableBrowser {
3271
3732
  await this.context.browserObject.context.tracing.stopChunk({
3272
3733
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3273
3734
  });
3735
+ if (world && world.attach) {
3736
+ await world.attach(JSON.stringify({
3737
+ type: "trace",
3738
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3739
+ }), "application/json+trace");
3740
+ }
3741
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3274
3742
  }
3275
3743
  }
3276
3744
  if (this.context) {
@@ -3283,6 +3751,29 @@ class StableBrowser {
3283
3751
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3284
3752
  }
3285
3753
  }
3754
+ if (!process.env.TEMP_RUN) {
3755
+ const state = {
3756
+ world,
3757
+ locate: false,
3758
+ scroll: false,
3759
+ screenshot: true,
3760
+ highlight: true,
3761
+ type: Types.STEP_COMPLETE,
3762
+ text: "end of scenario",
3763
+ _text: "end of scenario",
3764
+ operation: "step_complete",
3765
+ log: "***** " + "end of scenario" + " *****\n",
3766
+ };
3767
+ try {
3768
+ await _preCommand(state, this);
3769
+ }
3770
+ catch (e) {
3771
+ await _commandError(state, e, this);
3772
+ }
3773
+ finally {
3774
+ await _commandFinally(state, this);
3775
+ }
3776
+ }
3286
3777
  }
3287
3778
  }
3288
3779
  function createTimedPromise(promise, label) {