automation_model 1.0.639-dev → 1.0.639-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 (57) 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 +96 -26
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.js +28 -8
  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 +51 -24
  45. package/lib/stable_browser.js +738 -195
  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.js +15 -0
  50. package/lib/table_helper.js.map +1 -1
  51. package/lib/test_context.d.ts +2 -0
  52. package/lib/test_context.js +2 -0
  53. package/lib/test_context.js.map +1 -1
  54. package/lib/utils.d.ts +6 -4
  55. package/lib/utils.js +131 -20
  56. package/lib/utils.js.map +1 -1
  57. package/package.json +11 -6
@@ -15,6 +15,7 @@ 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) => {
@@ -126,6 +136,14 @@ class StableBrowser {
126
136
  }
127
137
  context.pageLoading.status = true;
128
138
  this.page = page;
139
+ try {
140
+ if (this.configuration && this.configuration.acceptDialog) {
141
+ await page.on("dialog", (dialog) => dialog.accept());
142
+ }
143
+ }
144
+ catch (error) {
145
+ console.error("Error on dialog accept registration", error);
146
+ }
129
147
  context.page = page;
130
148
  context.pages.push(page);
131
149
  registerNetworkEvents(this.world, this, context, this.page);
@@ -180,6 +198,30 @@ class StableBrowser {
180
198
  await this.waitForPageLoad();
181
199
  }
182
200
  }
201
+ async switchTab(tabTitleOrIndex) {
202
+ // first check if the tabNameOrIndex is a number
203
+ let index = parseInt(tabTitleOrIndex);
204
+ if (!isNaN(index)) {
205
+ if (index >= 0 && index < this.context.pages.length) {
206
+ this.page = this.context.pages[index];
207
+ this.context.page = this.page;
208
+ await this.page.bringToFront();
209
+ return;
210
+ }
211
+ }
212
+ // if the tabNameOrIndex is a string, find the tab by name
213
+ for (let i = 0; i < this.context.pages.length; i++) {
214
+ let page = this.context.pages[i];
215
+ let title = await page.title();
216
+ if (title.includes(tabTitleOrIndex)) {
217
+ this.page = page;
218
+ this.context.page = this.page;
219
+ await this.page.bringToFront();
220
+ return;
221
+ }
222
+ }
223
+ throw new Error("Tab not found: " + tabTitleOrIndex);
224
+ }
183
225
  registerConsoleLogListener(page, context) {
184
226
  if (!this.context.webLogger) {
185
227
  this.context.webLogger = [];
@@ -247,6 +289,7 @@ class StableBrowser {
247
289
  if (!url) {
248
290
  throw new Error("url is null, verify that the environment file is correct");
249
291
  }
292
+ url = await this._replaceWithLocalData(url, this.world);
250
293
  if (!url.startsWith("http")) {
251
294
  url = "https://" + url;
252
295
  }
@@ -275,7 +318,7 @@ class StableBrowser {
275
318
  _commandError(state, error, this);
276
319
  }
277
320
  finally {
278
- _commandFinally(state, this);
321
+ await _commandFinally(state, this);
279
322
  }
280
323
  }
281
324
  async _getLocator(locator, scope, _params) {
@@ -391,7 +434,7 @@ class StableBrowser {
391
434
  }
392
435
  return { elementCount: tagCount, randomToken };
393
436
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
437
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
438
  if (!info) {
396
439
  info = {};
397
440
  }
@@ -458,7 +501,7 @@ class StableBrowser {
458
501
  }
459
502
  return;
460
503
  }
461
- if (info.locatorLog && count === 0) {
504
+ if (info.locatorLog && count === 0 && logErrors) {
462
505
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
506
  }
464
507
  for (let j = 0; j < count; j++) {
@@ -473,7 +516,7 @@ class StableBrowser {
473
516
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
517
  }
475
518
  }
476
- else {
519
+ else if (logErrors) {
477
520
  info.failCause.visible = visible;
478
521
  info.failCause.enabled = enabled;
479
522
  if (!info.printMessages) {
@@ -565,15 +608,27 @@ class StableBrowser {
565
608
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
609
  if (!element.rerun) {
567
610
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
611
+ await element.evaluate((el, randomToken) => {
569
612
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
613
  }, 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;
614
+ // if (element._frame) {
615
+ // return element;
616
+ // }
617
+ const scope = element._frame ?? element.page();
618
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
619
+ let prefixSelector = "";
620
+ const frameControlSelector = " >> internal:control=enter-frame";
621
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
622
+ if (frameSelectorIndex !== -1) {
623
+ // remove everything after the >> internal:control=enter-frame
624
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
625
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
626
+ }
627
+ // if (element?._frame?._selector) {
628
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
629
+ // }
630
+ const newSelector = prefixSelector + newElementSelector;
631
+ return scope.locator(newSelector);
577
632
  }
578
633
  }
579
634
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +780,9 @@ class StableBrowser {
725
780
  // info.log += "scanning locators in priority 2" + "\n";
726
781
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
782
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
783
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
784
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
785
  }
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
786
  let foundElements = result.foundElements;
737
787
  if (foundElements.length === 1 && foundElements[0].unique) {
738
788
  info.box = foundElements[0].box;
@@ -787,6 +837,11 @@ class StableBrowser {
787
837
  visibleOnly = false;
788
838
  }
789
839
  await new Promise((resolve) => setTimeout(resolve, 1000));
840
+ // sheck of more of half of the timeout has passed
841
+ if (Date.now() - startTime > timeout / 2) {
842
+ highPriorityOnly = false;
843
+ visibleOnly = false;
844
+ }
790
845
  }
791
846
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
847
  // if (info.locatorLog) {
@@ -802,7 +857,7 @@ class StableBrowser {
802
857
  }
803
858
  throw new Error("failed to locate first element no elements found, " + info.log);
804
859
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
860
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
861
  let foundElements = [];
807
862
  const result = {
808
863
  foundElements: foundElements,
@@ -821,7 +876,9 @@ class StableBrowser {
821
876
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
877
  }
823
878
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
879
+ if (logErrors) {
880
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
881
+ }
825
882
  }
826
883
  }
827
884
  if (foundLocators.length === 1) {
@@ -833,9 +890,40 @@ class StableBrowser {
833
890
  result.locatorIndex = i;
834
891
  }
835
892
  if (foundLocators.length > 1) {
836
- info.failCause.foundMultiple = true;
837
- if (info.locatorLog) {
838
- 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 if (logErrors) {
923
+ info.failCause.foundMultiple = true;
924
+ if (info.locatorLog) {
925
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
926
+ }
839
927
  }
840
928
  }
841
929
  }
@@ -883,7 +971,7 @@ class StableBrowser {
883
971
  await _commandError(state, "timeout looking for " + elementDescription, this);
884
972
  }
885
973
  finally {
886
- _commandFinally(state, this);
974
+ await _commandFinally(state, this);
887
975
  }
888
976
  }
889
977
  }
@@ -932,7 +1020,7 @@ class StableBrowser {
932
1020
  await _commandError(state, "timeout looking for " + elementDescription, this);
933
1021
  }
934
1022
  finally {
935
- _commandFinally(state, this);
1023
+ await _commandFinally(state, this);
936
1024
  }
937
1025
  }
938
1026
  }
@@ -961,7 +1049,7 @@ class StableBrowser {
961
1049
  await _commandError(state, e, this);
962
1050
  }
963
1051
  finally {
964
- _commandFinally(state, this);
1052
+ await _commandFinally(state, this);
965
1053
  }
966
1054
  }
967
1055
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -992,7 +1080,7 @@ class StableBrowser {
992
1080
  // await _commandError(state, e, this);
993
1081
  }
994
1082
  finally {
995
- _commandFinally(state, this);
1083
+ await _commandFinally(state, this);
996
1084
  }
997
1085
  return found;
998
1086
  }
@@ -1016,8 +1104,8 @@ class StableBrowser {
1016
1104
  try {
1017
1105
  // if (world && world.screenshot && !world.screenshotPath) {
1018
1106
  // console.log(`Highlighting while running from recorder`);
1019
- await this._highlightElements(element);
1020
- await state.element.setChecked(checked);
1107
+ await this._highlightElements(state.element);
1108
+ await state.element.setChecked(checked, { timeout: 2000 });
1021
1109
  await new Promise((resolve) => setTimeout(resolve, 1000));
1022
1110
  // await this._unHighlightElements(element);
1023
1111
  // }
@@ -1029,11 +1117,28 @@ class StableBrowser {
1029
1117
  this.logger.info("element did not change its state, ignoring...");
1030
1118
  }
1031
1119
  else {
1120
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1032
1121
  //await this.closeUnexpectedPopups();
1033
1122
  state.info.log += "setCheck failed, will try again" + "\n";
1034
- state.element = await this._locate(selectors, state.info, _params);
1035
- await state.element.setChecked(checked, { timeout: 5000, force: true });
1036
- await new Promise((resolve) => setTimeout(resolve, 1000));
1123
+ state.element_found = false;
1124
+ try {
1125
+ state.element = await this._locate(selectors, state.info, _params, 100);
1126
+ state.element_found = true;
1127
+ // check the check state
1128
+ }
1129
+ catch (error) {
1130
+ // element dismissed
1131
+ }
1132
+ if (state.element_found) {
1133
+ const isChecked = await state.element.isChecked();
1134
+ if (isChecked !== checked) {
1135
+ // perform click
1136
+ await state.element.click({ timeout: 2000, force: true });
1137
+ }
1138
+ else {
1139
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1140
+ }
1141
+ }
1037
1142
  }
1038
1143
  }
1039
1144
  await this.waitForPageLoad();
@@ -1043,7 +1148,7 @@ class StableBrowser {
1043
1148
  await _commandError(state, e, this);
1044
1149
  }
1045
1150
  finally {
1046
- _commandFinally(state, this);
1151
+ await _commandFinally(state, this);
1047
1152
  }
1048
1153
  }
1049
1154
  async hover(selectors, _params, options = {}, world = null) {
@@ -1069,7 +1174,7 @@ class StableBrowser {
1069
1174
  await _commandError(state, e, this);
1070
1175
  }
1071
1176
  finally {
1072
- _commandFinally(state, this);
1177
+ await _commandFinally(state, this);
1073
1178
  }
1074
1179
  }
1075
1180
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1105,7 +1210,7 @@ class StableBrowser {
1105
1210
  await _commandError(state, e, this);
1106
1211
  }
1107
1212
  finally {
1108
- _commandFinally(state, this);
1213
+ await _commandFinally(state, this);
1109
1214
  }
1110
1215
  }
1111
1216
  async type(_value, _params = null, options = {}, world = null) {
@@ -1151,7 +1256,7 @@ class StableBrowser {
1151
1256
  await _commandError(state, e, this);
1152
1257
  }
1153
1258
  finally {
1154
- _commandFinally(state, this);
1259
+ await _commandFinally(state, this);
1155
1260
  }
1156
1261
  }
1157
1262
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1187,7 +1292,7 @@ class StableBrowser {
1187
1292
  await _commandError(state, e, this);
1188
1293
  }
1189
1294
  finally {
1190
- _commandFinally(state, this);
1295
+ await _commandFinally(state, this);
1191
1296
  }
1192
1297
  }
1193
1298
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1256,7 +1361,7 @@ class StableBrowser {
1256
1361
  await _commandError(state, e, this);
1257
1362
  }
1258
1363
  finally {
1259
- _commandFinally(state, this);
1364
+ await _commandFinally(state, this);
1260
1365
  }
1261
1366
  }
1262
1367
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1275,6 +1380,9 @@ class StableBrowser {
1275
1380
  operation: "clickType",
1276
1381
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1277
1382
  };
1383
+ if (!options) {
1384
+ options = {};
1385
+ }
1278
1386
  if (newValue !== _value) {
1279
1387
  //this.logger.info(_value + "=" + newValue);
1280
1388
  _value = newValue;
@@ -1282,7 +1390,7 @@ class StableBrowser {
1282
1390
  try {
1283
1391
  await _preCommand(state, this);
1284
1392
  state.info.value = _value;
1285
- if (options === null || options === undefined || !options.press) {
1393
+ if (!options.press) {
1286
1394
  try {
1287
1395
  let currentValue = await state.element.inputValue();
1288
1396
  if (currentValue) {
@@ -1293,10 +1401,7 @@ class StableBrowser {
1293
1401
  this.logger.info("unable to clear input value");
1294
1402
  }
1295
1403
  }
1296
- if (options === null || options === undefined || options.press) {
1297
- if (!options) {
1298
- options = {};
1299
- }
1404
+ if (options.press) {
1300
1405
  options.timeout = 5000;
1301
1406
  await performAction("click", state.element, options, this, state, _params);
1302
1407
  }
@@ -1356,7 +1461,7 @@ class StableBrowser {
1356
1461
  await _commandError(state, e, this);
1357
1462
  }
1358
1463
  finally {
1359
- _commandFinally(state, this);
1464
+ await _commandFinally(state, this);
1360
1465
  }
1361
1466
  }
1362
1467
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1386,7 +1491,42 @@ class StableBrowser {
1386
1491
  await _commandError(state, e, this);
1387
1492
  }
1388
1493
  finally {
1389
- _commandFinally(state, this);
1494
+ await _commandFinally(state, this);
1495
+ }
1496
+ }
1497
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1498
+ const state = {
1499
+ selectors,
1500
+ _params,
1501
+ files,
1502
+ value: '"' + files.join('", "') + '"',
1503
+ options,
1504
+ world,
1505
+ type: Types.SET_INPUT_FILES,
1506
+ text: `Set input files`,
1507
+ _text: `Set input files on ${selectors.element_name}`,
1508
+ operation: "setInputFiles",
1509
+ log: "***** set input files " + selectors.element_name + " *****\n",
1510
+ };
1511
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1512
+ try {
1513
+ await _preCommand(state, this);
1514
+ for (let i = 0; i < files.length; i++) {
1515
+ const file = files[i];
1516
+ const filePath = path.join(uploadsFolder, file);
1517
+ if (!fs.existsSync(filePath)) {
1518
+ throw new Error(`File not found: ${filePath}`);
1519
+ }
1520
+ state.files[i] = filePath;
1521
+ }
1522
+ await state.element.setInputFiles(files);
1523
+ return state.info;
1524
+ }
1525
+ catch (e) {
1526
+ await _commandError(state, e, this);
1527
+ }
1528
+ finally {
1529
+ await _commandFinally(state, this);
1390
1530
  }
1391
1531
  }
1392
1532
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1502,7 +1642,7 @@ class StableBrowser {
1502
1642
  await _commandError(state, e, this);
1503
1643
  }
1504
1644
  finally {
1505
- _commandFinally(state, this);
1645
+ await _commandFinally(state, this);
1506
1646
  }
1507
1647
  }
1508
1648
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1537,7 +1677,7 @@ class StableBrowser {
1537
1677
  while (Date.now() - startTime < timeout) {
1538
1678
  try {
1539
1679
  await _preCommand(state, this);
1540
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1680
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1541
1681
  if (foundObj && foundObj.element) {
1542
1682
  await this.scrollIfNeeded(foundObj.element, state.info);
1543
1683
  }
@@ -1579,7 +1719,79 @@ class StableBrowser {
1579
1719
  throw e;
1580
1720
  }
1581
1721
  finally {
1582
- _commandFinally(state, this);
1722
+ await _commandFinally(state, this);
1723
+ }
1724
+ }
1725
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1726
+ const timeout = this._getFindElementTimeout(options);
1727
+ const startTime = Date.now();
1728
+ const state = {
1729
+ _params,
1730
+ value: referanceSnapshot,
1731
+ options,
1732
+ world,
1733
+ locate: false,
1734
+ scroll: false,
1735
+ screenshot: true,
1736
+ highlight: false,
1737
+ type: Types.SNAPSHOT_VALIDATION,
1738
+ text: `verify snapshot: ${referanceSnapshot}`,
1739
+ operation: "snapshotValidation",
1740
+ log: "***** verify snapshot *****\n",
1741
+ };
1742
+ if (!referanceSnapshot) {
1743
+ throw new Error("referanceSnapshot is null");
1744
+ }
1745
+ let text = null;
1746
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1747
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1748
+ }
1749
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1750
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1751
+ }
1752
+ else if (referanceSnapshot.startsWith("yaml:")) {
1753
+ text = referanceSnapshot.substring(5);
1754
+ }
1755
+ else {
1756
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1757
+ }
1758
+ state.text = text;
1759
+ const newValue = await this._replaceWithLocalData(text, world);
1760
+ await _preCommand(state, this);
1761
+ let foundObj = null;
1762
+ try {
1763
+ let matchResult = null;
1764
+ while (Date.now() - startTime < timeout) {
1765
+ try {
1766
+ let scope = null;
1767
+ if (!frameSelectors) {
1768
+ scope = this.page;
1769
+ }
1770
+ else {
1771
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1772
+ }
1773
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1774
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1775
+ if (matchResult.errorLine !== -1) {
1776
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1777
+ }
1778
+ // highlight and screenshot
1779
+ return state.info;
1780
+ }
1781
+ catch (e) {
1782
+ // Log error but continue retrying until timeout is reached
1783
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1784
+ }
1785
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1786
+ }
1787
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1788
+ }
1789
+ catch (e) {
1790
+ await _commandError(state, e, this);
1791
+ throw e;
1792
+ }
1793
+ finally {
1794
+ await _commandFinally(state, this);
1583
1795
  }
1584
1796
  }
1585
1797
  async waitForUserInput(message, world = null) {
@@ -1617,6 +1829,15 @@ class StableBrowser {
1617
1829
  // save the data to the file
1618
1830
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1619
1831
  }
1832
+ overwriteTestData(testData, world = null) {
1833
+ if (!testData) {
1834
+ return;
1835
+ }
1836
+ // if data file exists, load it
1837
+ const dataFile = _getDataFile(world, this.context, this);
1838
+ // save the data to the file
1839
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1840
+ }
1620
1841
  _getDataFilePath(fileName) {
1621
1842
  let dataFile = path.join(this.project_path, "data", fileName);
1622
1843
  if (fs.existsSync(dataFile)) {
@@ -1869,7 +2090,7 @@ class StableBrowser {
1869
2090
  await _commandError(state, e, this);
1870
2091
  }
1871
2092
  finally {
1872
- _commandFinally(state, this);
2093
+ await _commandFinally(state, this);
1873
2094
  }
1874
2095
  }
1875
2096
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1900,10 +2121,31 @@ class StableBrowser {
1900
2121
  case "value":
1901
2122
  state.value = await state.element.inputValue();
1902
2123
  break;
2124
+ case "text":
2125
+ state.value = await state.element.textContent();
2126
+ break;
1903
2127
  default:
1904
2128
  state.value = await state.element.getAttribute(attribute);
1905
2129
  break;
1906
2130
  }
2131
+ if (options !== null) {
2132
+ if (options.regex && options.regex !== "") {
2133
+ // Construct a regex pattern from the provided string
2134
+ const regex = options.regex.slice(1, -1);
2135
+ const regexPattern = new RegExp(regex, "g");
2136
+ const matches = state.value.match(regexPattern);
2137
+ if (matches) {
2138
+ let newValue = "";
2139
+ for (const match of matches) {
2140
+ newValue += match;
2141
+ }
2142
+ state.value = newValue;
2143
+ }
2144
+ }
2145
+ if (options.trimSpaces && options.trimSpaces === true) {
2146
+ state.value = state.value.trim();
2147
+ }
2148
+ }
1907
2149
  state.info.value = state.value;
1908
2150
  this.setTestData({ [variable]: state.value }, world);
1909
2151
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1914,7 +2156,7 @@ class StableBrowser {
1914
2156
  await _commandError(state, e, this);
1915
2157
  }
1916
2158
  finally {
1917
- _commandFinally(state, this);
2159
+ await _commandFinally(state, this);
1918
2160
  }
1919
2161
  }
1920
2162
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1939,12 +2181,15 @@ class StableBrowser {
1939
2181
  let expectedValue;
1940
2182
  try {
1941
2183
  await _preCommand(state, this);
1942
- expectedValue = state.value;
2184
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1943
2185
  state.info.expectedValue = expectedValue;
1944
2186
  switch (attribute) {
1945
2187
  case "innerText":
1946
2188
  val = String(await state.element.innerText());
1947
2189
  break;
2190
+ case "text":
2191
+ val = String(await state.element.textContent());
2192
+ break;
1948
2193
  case "value":
1949
2194
  val = String(await state.element.inputValue());
1950
2195
  break;
@@ -1966,17 +2211,42 @@ class StableBrowser {
1966
2211
  let regex;
1967
2212
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1968
2213
  const patternBody = expectedValue.slice(1, -1);
1969
- regex = new RegExp(patternBody, "g");
2214
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2215
+ regex = new RegExp(processedPattern, "gs");
2216
+ state.info.regex = true;
1970
2217
  }
1971
2218
  else {
1972
2219
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1973
2220
  regex = new RegExp(escapedPattern, "g");
1974
2221
  }
1975
- if (!val.match(regex)) {
1976
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
1977
- state.info.failCause.assertionFailed = true;
1978
- state.info.failCause.lastError = errorMessage;
1979
- throw new Error(errorMessage);
2222
+ if (attribute === "innerText") {
2223
+ if (state.info.regex) {
2224
+ if (!regex.test(val)) {
2225
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2226
+ state.info.failCause.assertionFailed = true;
2227
+ state.info.failCause.lastError = errorMessage;
2228
+ throw new Error(errorMessage);
2229
+ }
2230
+ }
2231
+ else {
2232
+ const valLines = val.split("\n");
2233
+ const expectedLines = expectedValue.split("\n");
2234
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2235
+ if (!isPart) {
2236
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2237
+ state.info.failCause.assertionFailed = true;
2238
+ state.info.failCause.lastError = errorMessage;
2239
+ throw new Error(errorMessage);
2240
+ }
2241
+ }
2242
+ }
2243
+ else {
2244
+ if (!val.match(regex)) {
2245
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2246
+ state.info.failCause.assertionFailed = true;
2247
+ state.info.failCause.lastError = errorMessage;
2248
+ throw new Error(errorMessage);
2249
+ }
1980
2250
  }
1981
2251
  return state.info;
1982
2252
  }
@@ -1984,7 +2254,7 @@ class StableBrowser {
1984
2254
  await _commandError(state, e, this);
1985
2255
  }
1986
2256
  finally {
1987
- _commandFinally(state, this);
2257
+ await _commandFinally(state, this);
1988
2258
  }
1989
2259
  }
1990
2260
  async extractEmailData(emailAddress, options, world) {
@@ -2144,56 +2414,49 @@ class StableBrowser {
2144
2414
  console.debug(error);
2145
2415
  }
2146
2416
  }
2147
- // async _unhighlightElements(scope, css) {
2148
- // try {
2149
- // if (!scope) {
2150
- // return;
2151
- // }
2152
- // if (!css) {
2153
- // scope
2154
- // .evaluate((node) => {
2155
- // if (node && node.style) {
2156
- // if (!node.__previousOutline) {
2157
- // node.style.outline = "";
2158
- // } else {
2159
- // node.style.outline = node.__previousOutline;
2160
- // }
2161
- // }
2162
- // })
2163
- // .then(() => {})
2164
- // .catch((e) => {
2165
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2166
- // });
2167
- // } else {
2168
- // scope
2169
- // .evaluate(([css]) => {
2170
- // if (!css) {
2171
- // return;
2172
- // }
2173
- // let elements = Array.from(document.querySelectorAll(css));
2174
- // for (i = 0; i < elements.length; i++) {
2175
- // let element = elements[i];
2176
- // if (!element.style) {
2177
- // return;
2178
- // }
2179
- // if (!element.__previousOutline) {
2180
- // element.style.outline = "";
2181
- // } else {
2182
- // element.style.outline = element.__previousOutline;
2183
- // }
2184
- // }
2185
- // })
2186
- // .then(() => {})
2187
- // .catch((e) => {
2188
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2189
- // });
2190
- // }
2191
- // } catch (error) {
2192
- // // console.debug(error);
2193
- // }
2194
- // }
2417
+ _matcher(text) {
2418
+ if (!text) {
2419
+ return { matcher: "contains", queryText: "" };
2420
+ }
2421
+ if (text.length < 2) {
2422
+ return { matcher: "contains", queryText: text };
2423
+ }
2424
+ const split = text.split(":");
2425
+ const matcher = split[0].toLowerCase();
2426
+ const queryText = split.slice(1).join(":").trim();
2427
+ return { matcher, queryText };
2428
+ }
2429
+ _getDomain(url) {
2430
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2431
+ return "";
2432
+ }
2433
+ let hostnameFragments = url.split("/")[2].split(".");
2434
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2435
+ return hostnameFragments.join("-").split(":").join("-");
2436
+ }
2437
+ let n = hostnameFragments.length;
2438
+ let fragments = [...hostnameFragments];
2439
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2440
+ hostnameFragments.pop();
2441
+ n = hostnameFragments.length;
2442
+ }
2443
+ if (n == 0) {
2444
+ if (fragments[0] === "www")
2445
+ fragments = fragments.slice(1);
2446
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2447
+ }
2448
+ if (hostnameFragments[0] === "www")
2449
+ hostnameFragments = hostnameFragments.slice(1);
2450
+ return hostnameFragments.join(".");
2451
+ }
2452
+ /**
2453
+ * Verify the page path matches the given path.
2454
+ * @param {string} pathPart - The path to verify.
2455
+ * @param {object} options - Options for verification.
2456
+ * @param {object} world - The world context.
2457
+ * @returns {Promise<object>} - The state info after verification.
2458
+ */
2195
2459
  async verifyPagePath(pathPart, options = {}, world = null) {
2196
- const startTime = Date.now();
2197
2460
  let error = null;
2198
2461
  let screenshotId = null;
2199
2462
  let screenshotPath = null;
@@ -2207,74 +2470,235 @@ class StableBrowser {
2207
2470
  pathPart = newValue;
2208
2471
  }
2209
2472
  info.pathPart = pathPart;
2473
+ const { matcher, queryText } = this._matcher(pathPart);
2474
+ const state = {
2475
+ text_search: queryText,
2476
+ options,
2477
+ world,
2478
+ locate: false,
2479
+ scroll: false,
2480
+ highlight: false,
2481
+ type: Types.VERIFY_PAGE_PATH,
2482
+ text: `Verify the page url is ${queryText}`,
2483
+ _text: `Verify the page url is ${queryText}`,
2484
+ operation: "verifyPagePath",
2485
+ log: "***** verify page url is " + queryText + " *****\n",
2486
+ };
2210
2487
  try {
2488
+ await _preCommand(state, this);
2489
+ state.info.text = queryText;
2211
2490
  for (let i = 0; i < 30; i++) {
2212
2491
  const url = await this.page.url();
2213
- if (!url.includes(pathPart)) {
2214
- if (i === 29) {
2215
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2216
- }
2217
- await new Promise((resolve) => setTimeout(resolve, 1000));
2218
- continue;
2492
+ switch (matcher) {
2493
+ case "exact":
2494
+ if (url !== queryText) {
2495
+ if (i === 29) {
2496
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2497
+ }
2498
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2499
+ continue;
2500
+ }
2501
+ break;
2502
+ case "contains":
2503
+ if (!url.includes(queryText)) {
2504
+ if (i === 29) {
2505
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2506
+ }
2507
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2508
+ continue;
2509
+ }
2510
+ break;
2511
+ case "starts-with":
2512
+ {
2513
+ const domain = this._getDomain(url);
2514
+ if (domain.length > 0 && domain !== queryText) {
2515
+ if (i === 29) {
2516
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2517
+ }
2518
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2519
+ continue;
2520
+ }
2521
+ }
2522
+ break;
2523
+ case "ends-with":
2524
+ {
2525
+ const urlObj = new URL(url);
2526
+ let route = "/";
2527
+ if (urlObj.pathname !== "/") {
2528
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2529
+ }
2530
+ else {
2531
+ route = "/";
2532
+ }
2533
+ if (route !== queryText) {
2534
+ if (i === 29) {
2535
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2536
+ }
2537
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2538
+ continue;
2539
+ }
2540
+ }
2541
+ break;
2542
+ case "regex":
2543
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2544
+ if (!regex.test(url)) {
2545
+ if (i === 29) {
2546
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2547
+ }
2548
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2549
+ continue;
2550
+ }
2551
+ break;
2552
+ default:
2553
+ console.log("Unknown matching type, defaulting to contains matching");
2554
+ if (!url.includes(pathPart)) {
2555
+ if (i === 29) {
2556
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2557
+ }
2558
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2559
+ continue;
2560
+ }
2219
2561
  }
2220
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2221
- return info;
2562
+ await _screenshot(state, this);
2563
+ return state.info;
2222
2564
  }
2223
2565
  }
2224
2566
  catch (e) {
2225
- //await this.closeUnexpectedPopups();
2226
- this.logger.error("verify page path failed " + info.log);
2227
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2228
- info.screenshotPath = screenshotPath;
2229
- Object.assign(e, { info: info });
2230
- error = e;
2231
- // throw e;
2232
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2567
+ state.info.failCause.lastError = e.message;
2568
+ state.info.failCause.assertionFailed = true;
2569
+ await _commandError(state, e, this);
2233
2570
  }
2234
2571
  finally {
2235
- const endTime = Date.now();
2236
- _reportToWorld(world, {
2237
- type: Types.VERIFY_PAGE_PATH,
2238
- text: "Verify page path",
2239
- _text: "Verify the page path contains " + pathPart,
2240
- screenshotId,
2241
- result: error
2242
- ? {
2243
- status: "FAILED",
2244
- startTime,
2245
- endTime,
2246
- message: error?.message,
2247
- }
2248
- : {
2249
- status: "PASSED",
2250
- startTime,
2251
- endTime,
2252
- },
2253
- info: info,
2254
- });
2572
+ await _commandFinally(state, this);
2573
+ }
2574
+ }
2575
+ /**
2576
+ * Verify the page title matches the given title.
2577
+ * @param {string} title - The title to verify.
2578
+ * @param {object} options - Options for verification.
2579
+ * @param {object} world - The world context.
2580
+ * @returns {Promise<object>} - The state info after verification.
2581
+ */
2582
+ async verifyPageTitle(title, options = {}, world = null) {
2583
+ let error = null;
2584
+ let screenshotId = null;
2585
+ let screenshotPath = null;
2586
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2587
+ const newValue = await this._replaceWithLocalData(title, world);
2588
+ if (newValue !== title) {
2589
+ this.logger.info(title + "=" + newValue);
2590
+ title = newValue;
2591
+ }
2592
+ const { matcher, queryText } = this._matcher(title);
2593
+ const state = {
2594
+ text_search: queryText,
2595
+ options,
2596
+ world,
2597
+ locate: false,
2598
+ scroll: false,
2599
+ highlight: false,
2600
+ type: Types.VERIFY_PAGE_TITLE,
2601
+ text: `Verify the page title is ${queryText}`,
2602
+ _text: `Verify the page title is ${queryText}`,
2603
+ operation: "verifyPageTitle",
2604
+ log: "***** verify page title is " + queryText + " *****\n",
2605
+ };
2606
+ try {
2607
+ await _preCommand(state, this);
2608
+ state.info.text = queryText;
2609
+ for (let i = 0; i < 30; i++) {
2610
+ const foundTitle = await this.page.title();
2611
+ switch (matcher) {
2612
+ case "exact":
2613
+ if (foundTitle !== queryText) {
2614
+ if (i === 29) {
2615
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2616
+ }
2617
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2618
+ continue;
2619
+ }
2620
+ break;
2621
+ case "contains":
2622
+ if (!foundTitle.includes(queryText)) {
2623
+ if (i === 29) {
2624
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2625
+ }
2626
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2627
+ continue;
2628
+ }
2629
+ break;
2630
+ case "starts-with":
2631
+ if (!foundTitle.startsWith(queryText)) {
2632
+ if (i === 29) {
2633
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2634
+ }
2635
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2636
+ continue;
2637
+ }
2638
+ break;
2639
+ case "ends-with":
2640
+ if (!foundTitle.endsWith(queryText)) {
2641
+ if (i === 29) {
2642
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2643
+ }
2644
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2645
+ continue;
2646
+ }
2647
+ break;
2648
+ case "regex":
2649
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2650
+ if (!regex.test(foundTitle)) {
2651
+ if (i === 29) {
2652
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2653
+ }
2654
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2655
+ continue;
2656
+ }
2657
+ break;
2658
+ default:
2659
+ console.log("Unknown matching type, defaulting to contains matching");
2660
+ if (!foundTitle.includes(title)) {
2661
+ if (i === 29) {
2662
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2663
+ }
2664
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2665
+ continue;
2666
+ }
2667
+ }
2668
+ await _screenshot(state, this);
2669
+ return state.info;
2670
+ }
2671
+ }
2672
+ catch (e) {
2673
+ state.info.failCause.lastError = e.message;
2674
+ state.info.failCause.assertionFailed = true;
2675
+ await _commandError(state, e, this);
2676
+ }
2677
+ finally {
2678
+ await _commandFinally(state, this);
2255
2679
  }
2256
2680
  }
2257
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2681
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2258
2682
  const frames = this.page.frames();
2259
2683
  let results = [];
2260
- let ignoreCase = false;
2684
+ // let ignoreCase = false;
2261
2685
  for (let i = 0; i < frames.length; i++) {
2262
2686
  if (dateAlternatives.date) {
2263
2687
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2264
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2688
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2265
2689
  result.frame = frames[i];
2266
2690
  results.push(result);
2267
2691
  }
2268
2692
  }
2269
2693
  else if (numberAlternatives.number) {
2270
2694
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2271
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2695
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2272
2696
  result.frame = frames[i];
2273
2697
  results.push(result);
2274
2698
  }
2275
2699
  }
2276
2700
  else {
2277
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2701
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2278
2702
  result.frame = frames[i];
2279
2703
  results.push(result);
2280
2704
  }
@@ -2293,7 +2717,7 @@ class StableBrowser {
2293
2717
  scroll: false,
2294
2718
  highlight: false,
2295
2719
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2296
- text: `Verify text exists in page`,
2720
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2297
2721
  _text: `Verify the text '${text}' exists in page`,
2298
2722
  operation: "verifyTextExistInPage",
2299
2723
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2335,27 +2759,10 @@ class StableBrowser {
2335
2759
  const frame = resultWithElementsFound[0].frame;
2336
2760
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2337
2761
  await this._highlightElements(frame, dataAttribute);
2338
- // if (world && world.screenshot && !world.screenshotPath) {
2339
- // console.log(`Highlighting for verify text is found while running from recorder`);
2340
- // this._highlightElements(frame, dataAttribute).then(async () => {
2341
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2342
- // this._unhighlightElements(frame, dataAttribute)
2343
- // .then(async () => {
2344
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2345
- // })
2346
- // .catch(
2347
- // (e) => {}
2348
- // console.error(e)
2349
- // );
2350
- // });
2351
- // }
2352
2762
  const element = await frame.locator(dataAttribute).first();
2353
- // await new Promise((resolve) => setTimeout(resolve, 100));
2354
- // await this._unhighlightElements(frame, dataAttribute);
2355
2763
  if (element) {
2356
2764
  await this.scrollIfNeeded(element, state.info);
2357
2765
  await element.dispatchEvent("bvt_verify_page_contains_text");
2358
- // await _screenshot(state, this, element);
2359
2766
  }
2360
2767
  }
2361
2768
  await _screenshot(state, this);
@@ -2365,13 +2772,12 @@ class StableBrowser {
2365
2772
  console.error(error);
2366
2773
  }
2367
2774
  }
2368
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2369
2775
  }
2370
2776
  catch (e) {
2371
2777
  await _commandError(state, e, this);
2372
2778
  }
2373
2779
  finally {
2374
- _commandFinally(state, this);
2780
+ await _commandFinally(state, this);
2375
2781
  }
2376
2782
  }
2377
2783
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2384,7 +2790,7 @@ class StableBrowser {
2384
2790
  scroll: false,
2385
2791
  highlight: false,
2386
2792
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2387
- text: `Verify text does not exist in page`,
2793
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2388
2794
  _text: `Verify the text '${text}' does not exist in page`,
2389
2795
  operation: "verifyTextNotExistInPage",
2390
2796
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2428,7 +2834,7 @@ class StableBrowser {
2428
2834
  await _commandError(state, e, this);
2429
2835
  }
2430
2836
  finally {
2431
- _commandFinally(state, this);
2837
+ await _commandFinally(state, this);
2432
2838
  }
2433
2839
  }
2434
2840
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2470,7 +2876,7 @@ class StableBrowser {
2470
2876
  };
2471
2877
  while (true) {
2472
2878
  try {
2473
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2879
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2474
2880
  }
2475
2881
  catch (error) {
2476
2882
  // ignore
@@ -2498,7 +2904,7 @@ class StableBrowser {
2498
2904
  const count = await frame.locator(css).count();
2499
2905
  for (let j = 0; j < count; j++) {
2500
2906
  const continer = await frame.locator(css).nth(j);
2501
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2907
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2502
2908
  if (result.elementCount > 0) {
2503
2909
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2504
2910
  await this._highlightElements(frame, dataAttribute);
@@ -2539,9 +2945,33 @@ class StableBrowser {
2539
2945
  await _commandError(state, e, this);
2540
2946
  }
2541
2947
  finally {
2542
- _commandFinally(state, this);
2948
+ await _commandFinally(state, this);
2543
2949
  }
2544
2950
  }
2951
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2952
+ const frames = this.page.frames();
2953
+ let results = [];
2954
+ let ignoreCase = false;
2955
+ for (let i = 0; i < frames.length; i++) {
2956
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2957
+ result.frame = frames[i];
2958
+ const climbArray = [];
2959
+ for (let i = 0; i < climb; i++) {
2960
+ climbArray.push("..");
2961
+ }
2962
+ let climbXpath = "xpath=" + climbArray.join("/");
2963
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2964
+ const count = await frames[i].locator(newLocator).count();
2965
+ if (count > 0) {
2966
+ result.elementCount = count;
2967
+ result.locator = newLocator;
2968
+ results.push(result);
2969
+ }
2970
+ }
2971
+ // state.info.results = results;
2972
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2973
+ return resultWithElementsFound;
2974
+ }
2545
2975
  async visualVerification(text, options = {}, world = null) {
2546
2976
  const startTime = Date.now();
2547
2977
  let error = null;
@@ -2858,7 +3288,13 @@ class StableBrowser {
2858
3288
  }
2859
3289
  }
2860
3290
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2861
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3291
+ try {
3292
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3293
+ }
3294
+ catch (error) {
3295
+ this.logger.debug(error);
3296
+ throw error;
3297
+ }
2862
3298
  }
2863
3299
  _getLoadTimeout(options) {
2864
3300
  let timeout = 15000;
@@ -2881,6 +3317,7 @@ class StableBrowser {
2881
3317
  }
2882
3318
  async saveStoreState(path = null, world = null) {
2883
3319
  const storageState = await this.page.context().storageState();
3320
+ path = await this._replaceWithLocalData(path, this.world);
2884
3321
  //const testDataFile = _getDataFile(world, this.context, this);
2885
3322
  if (path) {
2886
3323
  // save { storageState: storageState } into the path
@@ -2891,10 +3328,14 @@ class StableBrowser {
2891
3328
  }
2892
3329
  }
2893
3330
  async restoreSaveState(path = null, world = null) {
3331
+ path = await this._replaceWithLocalData(path, this.world);
2894
3332
  await refreshBrowser(this, path, world);
2895
3333
  this.registerEventListeners(this.context);
2896
3334
  registerNetworkEvents(this.world, this, this.context, this.page);
2897
3335
  registerDownloadEvent(this.page, this.world, this.context);
3336
+ if (this.onRestoreSaveState) {
3337
+ this.onRestoreSaveState(path);
3338
+ }
2898
3339
  }
2899
3340
  async waitForPageLoad(options = {}, world = null) {
2900
3341
  let timeout = this._getLoadTimeout(options);
@@ -2977,7 +3418,7 @@ class StableBrowser {
2977
3418
  await _commandError(state, e, this);
2978
3419
  }
2979
3420
  finally {
2980
- _commandFinally(state, this);
3421
+ await _commandFinally(state, this);
2981
3422
  }
2982
3423
  }
2983
3424
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3064,7 +3505,7 @@ class StableBrowser {
3064
3505
  await _commandError(state, e, this);
3065
3506
  }
3066
3507
  finally {
3067
- _commandFinally(state, this);
3508
+ await _commandFinally(state, this);
3068
3509
  }
3069
3510
  }
3070
3511
  saveTestDataAsGlobal(options, world) {
@@ -3169,7 +3610,39 @@ class StableBrowser {
3169
3610
  console.log("#-#");
3170
3611
  }
3171
3612
  }
3613
+ async beforeScenario(world, scenario) {
3614
+ this.beforeScenarioCalled = true;
3615
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3616
+ this.scenarioName = scenario.pickle.name;
3617
+ }
3618
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3619
+ this.featureName = scenario.gherkinDocument.feature.name;
3620
+ }
3621
+ if (this.context) {
3622
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3623
+ }
3624
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3625
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3626
+ // check if @global_test_data tag is present
3627
+ if (this.tags.includes("@global_test_data")) {
3628
+ this.saveTestDataAsGlobal({}, world);
3629
+ }
3630
+ }
3631
+ // update test data based on feature/scenario
3632
+ let envName = null;
3633
+ if (this.context && this.context.environment) {
3634
+ envName = this.context.environment.name;
3635
+ }
3636
+ if (!process.env.TEMP_RUN) {
3637
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3638
+ }
3639
+ await loadBrunoParams(this.context, this.context.environment.name);
3640
+ }
3641
+ async afterScenario(world, scenario) { }
3172
3642
  async beforeStep(world, step) {
3643
+ if (!this.beforeScenarioCalled) {
3644
+ this.beforeScenario(world, step);
3645
+ }
3173
3646
  if (this.stepIndex === undefined) {
3174
3647
  this.stepIndex = 0;
3175
3648
  }
@@ -3186,21 +3659,11 @@ class StableBrowser {
3186
3659
  else {
3187
3660
  this.stepName = "step " + this.stepIndex;
3188
3661
  }
3189
- if (this.context) {
3190
- this.context.examplesRow = extractStepExampleParameters(step);
3191
- }
3192
3662
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3193
3663
  if (this.context.browserObject.context) {
3194
3664
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3195
3665
  }
3196
3666
  }
3197
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3198
- this.tags = step.pickle.tags.map((tag) => tag.name);
3199
- // check if @global_test_data tag is present
3200
- if (this.tags.includes("@global_test_data")) {
3201
- this.saveTestDataAsGlobal({}, world);
3202
- }
3203
- }
3204
3667
  if (this.initSnapshotTaken === false) {
3205
3668
  this.initSnapshotTaken = true;
3206
3669
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3225,18 +3688,68 @@ class StableBrowser {
3225
3688
  const content = [`- path: ${path}`, `- title: ${title}`];
3226
3689
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3227
3690
  for (let i = 0; i < frames.length; i++) {
3228
- content.push(`- frame: ${i}`);
3229
3691
  const frame = frames[i];
3230
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3231
- content.push(snapshot);
3692
+ try {
3693
+ // Ensure frame is attached and has body
3694
+ const body = frame.locator("body");
3695
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3696
+ const snapshot = await body.ariaSnapshot({ timeout });
3697
+ content.push(`- frame: ${i}`);
3698
+ content.push(snapshot);
3699
+ }
3700
+ catch (innerErr) { }
3232
3701
  }
3233
3702
  return content.join("\n");
3234
3703
  }
3235
3704
  catch (e) {
3236
- console.error(e);
3705
+ console.log("Error in getAriaSnapshot");
3706
+ //console.debug(e);
3237
3707
  }
3238
3708
  return null;
3239
3709
  }
3710
+ /**
3711
+ * Sends command with custom payload to report.
3712
+ * @param commandText - Title of the command to be shown in the report.
3713
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3714
+ * @param content - Content of the command to be shown in the report.
3715
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3716
+ * @param world - Optional world context.
3717
+ * @public
3718
+ */
3719
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3720
+ const state = {
3721
+ options,
3722
+ world,
3723
+ locate: false,
3724
+ scroll: false,
3725
+ screenshot: options.screenshot ?? false,
3726
+ highlight: options.highlight ?? false,
3727
+ type: Types.REPORT_COMMAND,
3728
+ text: commandText,
3729
+ _text: commandText,
3730
+ operation: "report_command",
3731
+ log: "***** " + commandText + " *****\n",
3732
+ };
3733
+ try {
3734
+ await _preCommand(state, this);
3735
+ const payload = {
3736
+ type: options.type ?? "text",
3737
+ content: content,
3738
+ screenshotId: null,
3739
+ };
3740
+ state.payload = payload;
3741
+ if (commandStatus === "FAILED") {
3742
+ state.throwError = true;
3743
+ throw new Error("Command failed");
3744
+ }
3745
+ }
3746
+ catch (e) {
3747
+ await _commandError(state, e, this);
3748
+ }
3749
+ finally {
3750
+ await _commandFinally(state, this);
3751
+ }
3752
+ }
3240
3753
  async afterStep(world, step) {
3241
3754
  this.stepName = null;
3242
3755
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3244,6 +3757,13 @@ class StableBrowser {
3244
3757
  await this.context.browserObject.context.tracing.stopChunk({
3245
3758
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3246
3759
  });
3760
+ if (world && world.attach) {
3761
+ await world.attach(JSON.stringify({
3762
+ type: "trace",
3763
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3764
+ }), "application/json+trace");
3765
+ }
3766
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3247
3767
  }
3248
3768
  }
3249
3769
  if (this.context) {
@@ -3256,6 +3776,29 @@ class StableBrowser {
3256
3776
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3257
3777
  }
3258
3778
  }
3779
+ if (!process.env.TEMP_RUN) {
3780
+ const state = {
3781
+ world,
3782
+ locate: false,
3783
+ scroll: false,
3784
+ screenshot: true,
3785
+ highlight: true,
3786
+ type: Types.STEP_COMPLETE,
3787
+ text: "end of scenario",
3788
+ _text: "end of scenario",
3789
+ operation: "step_complete",
3790
+ log: "***** " + "end of scenario" + " *****\n",
3791
+ };
3792
+ try {
3793
+ await _preCommand(state, this);
3794
+ }
3795
+ catch (e) {
3796
+ await _commandError(state, e, this);
3797
+ }
3798
+ finally {
3799
+ await _commandFinally(state, this);
3800
+ }
3801
+ }
3259
3802
  }
3260
3803
  }
3261
3804
  function createTimedPromise(promise, label) {