automation_model 1.0.644-dev → 1.0.644-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 +145 -59
  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 +29 -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 +46 -23
  45. package/lib/stable_browser.js +683 -187
  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 +176 -59
  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 { highlightSnapshot, 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) => {
@@ -118,6 +128,9 @@ class StableBrowser {
118
128
  if (!context.pageLoading) {
119
129
  context.pageLoading = { status: false };
120
130
  }
131
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
132
+ this.page.on("dialog", (dialog) => dialog.accept());
133
+ }
121
134
  context.playContext.on("page", async function (page) {
122
135
  if (this.configuration && this.configuration.closePopups === true) {
123
136
  console.log("close unexpected popups");
@@ -126,6 +139,14 @@ class StableBrowser {
126
139
  }
127
140
  context.pageLoading.status = true;
128
141
  this.page = page;
142
+ try {
143
+ if (this.configuration && this.configuration.acceptDialog) {
144
+ await page.on("dialog", (dialog) => dialog.accept());
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.error("Error on dialog accept registration", error);
149
+ }
129
150
  context.page = page;
130
151
  context.pages.push(page);
131
152
  registerNetworkEvents(this.world, this, context, this.page);
@@ -180,6 +201,30 @@ class StableBrowser {
180
201
  await this.waitForPageLoad();
181
202
  }
182
203
  }
204
+ async switchTab(tabTitleOrIndex) {
205
+ // first check if the tabNameOrIndex is a number
206
+ let index = parseInt(tabTitleOrIndex);
207
+ if (!isNaN(index)) {
208
+ if (index >= 0 && index < this.context.pages.length) {
209
+ this.page = this.context.pages[index];
210
+ this.context.page = this.page;
211
+ await this.page.bringToFront();
212
+ return;
213
+ }
214
+ }
215
+ // if the tabNameOrIndex is a string, find the tab by name
216
+ for (let i = 0; i < this.context.pages.length; i++) {
217
+ let page = this.context.pages[i];
218
+ let title = await page.title();
219
+ if (title.includes(tabTitleOrIndex)) {
220
+ this.page = page;
221
+ this.context.page = this.page;
222
+ await this.page.bringToFront();
223
+ return;
224
+ }
225
+ }
226
+ throw new Error("Tab not found: " + tabTitleOrIndex);
227
+ }
183
228
  registerConsoleLogListener(page, context) {
184
229
  if (!this.context.webLogger) {
185
230
  this.context.webLogger = [];
@@ -247,6 +292,7 @@ class StableBrowser {
247
292
  if (!url) {
248
293
  throw new Error("url is null, verify that the environment file is correct");
249
294
  }
295
+ url = await this._replaceWithLocalData(url, this.world);
250
296
  if (!url.startsWith("http")) {
251
297
  url = "https://" + url;
252
298
  }
@@ -275,7 +321,7 @@ class StableBrowser {
275
321
  _commandError(state, error, this);
276
322
  }
277
323
  finally {
278
- _commandFinally(state, this);
324
+ await _commandFinally(state, this);
279
325
  }
280
326
  }
281
327
  async _getLocator(locator, scope, _params) {
@@ -391,7 +437,7 @@ class StableBrowser {
391
437
  }
392
438
  return { elementCount: tagCount, randomToken };
393
439
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
440
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
441
  if (!info) {
396
442
  info = {};
397
443
  }
@@ -458,7 +504,7 @@ class StableBrowser {
458
504
  }
459
505
  return;
460
506
  }
461
- if (info.locatorLog && count === 0) {
507
+ if (info.locatorLog && count === 0 && logErrors) {
462
508
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
509
  }
464
510
  for (let j = 0; j < count; j++) {
@@ -473,7 +519,7 @@ class StableBrowser {
473
519
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
520
  }
475
521
  }
476
- else {
522
+ else if (logErrors) {
477
523
  info.failCause.visible = visible;
478
524
  info.failCause.enabled = enabled;
479
525
  if (!info.printMessages) {
@@ -565,15 +611,27 @@ class StableBrowser {
565
611
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
612
  if (!element.rerun) {
567
613
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
614
+ await element.evaluate((el, randomToken) => {
569
615
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
616
  }, 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;
617
+ // if (element._frame) {
618
+ // return element;
619
+ // }
620
+ const scope = element._frame ?? element.page();
621
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
622
+ let prefixSelector = "";
623
+ const frameControlSelector = " >> internal:control=enter-frame";
624
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
625
+ if (frameSelectorIndex !== -1) {
626
+ // remove everything after the >> internal:control=enter-frame
627
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
628
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
629
+ }
630
+ // if (element?._frame?._selector) {
631
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
632
+ // }
633
+ const newSelector = prefixSelector + newElementSelector;
634
+ return scope.locator(newSelector);
577
635
  }
578
636
  }
579
637
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +783,9 @@ class StableBrowser {
725
783
  // info.log += "scanning locators in priority 2" + "\n";
726
784
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
785
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
786
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
787
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
788
  }
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
789
  let foundElements = result.foundElements;
737
790
  if (foundElements.length === 1 && foundElements[0].unique) {
738
791
  info.box = foundElements[0].box;
@@ -787,6 +840,11 @@ class StableBrowser {
787
840
  visibleOnly = false;
788
841
  }
789
842
  await new Promise((resolve) => setTimeout(resolve, 1000));
843
+ // sheck of more of half of the timeout has passed
844
+ if (Date.now() - startTime > timeout / 2) {
845
+ highPriorityOnly = false;
846
+ visibleOnly = false;
847
+ }
790
848
  }
791
849
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
850
  // if (info.locatorLog) {
@@ -802,7 +860,7 @@ class StableBrowser {
802
860
  }
803
861
  throw new Error("failed to locate first element no elements found, " + info.log);
804
862
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
863
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
864
  let foundElements = [];
807
865
  const result = {
808
866
  foundElements: foundElements,
@@ -821,7 +879,9 @@ class StableBrowser {
821
879
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
880
  }
823
881
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
882
+ if (logErrors) {
883
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
884
+ }
825
885
  }
826
886
  }
827
887
  if (foundLocators.length === 1) {
@@ -862,7 +922,7 @@ class StableBrowser {
862
922
  });
863
923
  result.locatorIndex = i;
864
924
  }
865
- else {
925
+ else if (logErrors) {
866
926
  info.failCause.foundMultiple = true;
867
927
  if (info.locatorLog) {
868
928
  info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
@@ -914,7 +974,7 @@ class StableBrowser {
914
974
  await _commandError(state, "timeout looking for " + elementDescription, this);
915
975
  }
916
976
  finally {
917
- _commandFinally(state, this);
977
+ await _commandFinally(state, this);
918
978
  }
919
979
  }
920
980
  }
@@ -963,7 +1023,7 @@ class StableBrowser {
963
1023
  await _commandError(state, "timeout looking for " + elementDescription, this);
964
1024
  }
965
1025
  finally {
966
- _commandFinally(state, this);
1026
+ await _commandFinally(state, this);
967
1027
  }
968
1028
  }
969
1029
  }
@@ -992,7 +1052,7 @@ class StableBrowser {
992
1052
  await _commandError(state, e, this);
993
1053
  }
994
1054
  finally {
995
- _commandFinally(state, this);
1055
+ await _commandFinally(state, this);
996
1056
  }
997
1057
  }
998
1058
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1023,7 +1083,7 @@ class StableBrowser {
1023
1083
  // await _commandError(state, e, this);
1024
1084
  }
1025
1085
  finally {
1026
- _commandFinally(state, this);
1086
+ await _commandFinally(state, this);
1027
1087
  }
1028
1088
  return found;
1029
1089
  }
@@ -1047,8 +1107,8 @@ class StableBrowser {
1047
1107
  try {
1048
1108
  // if (world && world.screenshot && !world.screenshotPath) {
1049
1109
  // console.log(`Highlighting while running from recorder`);
1050
- await this._highlightElements(element);
1051
- await state.element.setChecked(checked);
1110
+ await this._highlightElements(state.element);
1111
+ await state.element.setChecked(checked, { timeout: 2000 });
1052
1112
  await new Promise((resolve) => setTimeout(resolve, 1000));
1053
1113
  // await this._unHighlightElements(element);
1054
1114
  // }
@@ -1060,11 +1120,28 @@ class StableBrowser {
1060
1120
  this.logger.info("element did not change its state, ignoring...");
1061
1121
  }
1062
1122
  else {
1123
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1063
1124
  //await this.closeUnexpectedPopups();
1064
1125
  state.info.log += "setCheck failed, will try again" + "\n";
1065
- state.element = await this._locate(selectors, state.info, _params);
1066
- await state.element.setChecked(checked, { timeout: 5000, force: true });
1067
- await new Promise((resolve) => setTimeout(resolve, 1000));
1126
+ state.element_found = false;
1127
+ try {
1128
+ state.element = await this._locate(selectors, state.info, _params, 100);
1129
+ state.element_found = true;
1130
+ // check the check state
1131
+ }
1132
+ catch (error) {
1133
+ // element dismissed
1134
+ }
1135
+ if (state.element_found) {
1136
+ const isChecked = await state.element.isChecked();
1137
+ if (isChecked !== checked) {
1138
+ // perform click
1139
+ await state.element.click({ timeout: 2000, force: true });
1140
+ }
1141
+ else {
1142
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1143
+ }
1144
+ }
1068
1145
  }
1069
1146
  }
1070
1147
  await this.waitForPageLoad();
@@ -1074,7 +1151,7 @@ class StableBrowser {
1074
1151
  await _commandError(state, e, this);
1075
1152
  }
1076
1153
  finally {
1077
- _commandFinally(state, this);
1154
+ await _commandFinally(state, this);
1078
1155
  }
1079
1156
  }
1080
1157
  async hover(selectors, _params, options = {}, world = null) {
@@ -1100,7 +1177,7 @@ class StableBrowser {
1100
1177
  await _commandError(state, e, this);
1101
1178
  }
1102
1179
  finally {
1103
- _commandFinally(state, this);
1180
+ await _commandFinally(state, this);
1104
1181
  }
1105
1182
  }
1106
1183
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1136,7 +1213,7 @@ class StableBrowser {
1136
1213
  await _commandError(state, e, this);
1137
1214
  }
1138
1215
  finally {
1139
- _commandFinally(state, this);
1216
+ await _commandFinally(state, this);
1140
1217
  }
1141
1218
  }
1142
1219
  async type(_value, _params = null, options = {}, world = null) {
@@ -1182,7 +1259,7 @@ class StableBrowser {
1182
1259
  await _commandError(state, e, this);
1183
1260
  }
1184
1261
  finally {
1185
- _commandFinally(state, this);
1262
+ await _commandFinally(state, this);
1186
1263
  }
1187
1264
  }
1188
1265
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1218,7 +1295,7 @@ class StableBrowser {
1218
1295
  await _commandError(state, e, this);
1219
1296
  }
1220
1297
  finally {
1221
- _commandFinally(state, this);
1298
+ await _commandFinally(state, this);
1222
1299
  }
1223
1300
  }
1224
1301
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1287,7 +1364,7 @@ class StableBrowser {
1287
1364
  await _commandError(state, e, this);
1288
1365
  }
1289
1366
  finally {
1290
- _commandFinally(state, this);
1367
+ await _commandFinally(state, this);
1291
1368
  }
1292
1369
  }
1293
1370
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1306,6 +1383,9 @@ class StableBrowser {
1306
1383
  operation: "clickType",
1307
1384
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1308
1385
  };
1386
+ if (!options) {
1387
+ options = {};
1388
+ }
1309
1389
  if (newValue !== _value) {
1310
1390
  //this.logger.info(_value + "=" + newValue);
1311
1391
  _value = newValue;
@@ -1313,7 +1393,7 @@ class StableBrowser {
1313
1393
  try {
1314
1394
  await _preCommand(state, this);
1315
1395
  state.info.value = _value;
1316
- if (options === null || options === undefined || !options.press) {
1396
+ if (!options.press) {
1317
1397
  try {
1318
1398
  let currentValue = await state.element.inputValue();
1319
1399
  if (currentValue) {
@@ -1324,10 +1404,7 @@ class StableBrowser {
1324
1404
  this.logger.info("unable to clear input value");
1325
1405
  }
1326
1406
  }
1327
- if (options === null || options === undefined || options.press) {
1328
- if (!options) {
1329
- options = {};
1330
- }
1407
+ if (options.press) {
1331
1408
  options.timeout = 5000;
1332
1409
  await performAction("click", state.element, options, this, state, _params);
1333
1410
  }
@@ -1387,7 +1464,7 @@ class StableBrowser {
1387
1464
  await _commandError(state, e, this);
1388
1465
  }
1389
1466
  finally {
1390
- _commandFinally(state, this);
1467
+ await _commandFinally(state, this);
1391
1468
  }
1392
1469
  }
1393
1470
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1417,7 +1494,42 @@ class StableBrowser {
1417
1494
  await _commandError(state, e, this);
1418
1495
  }
1419
1496
  finally {
1420
- _commandFinally(state, this);
1497
+ await _commandFinally(state, this);
1498
+ }
1499
+ }
1500
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1501
+ const state = {
1502
+ selectors,
1503
+ _params,
1504
+ files,
1505
+ value: '"' + files.join('", "') + '"',
1506
+ options,
1507
+ world,
1508
+ type: Types.SET_INPUT_FILES,
1509
+ text: `Set input files`,
1510
+ _text: `Set input files on ${selectors.element_name}`,
1511
+ operation: "setInputFiles",
1512
+ log: "***** set input files " + selectors.element_name + " *****\n",
1513
+ };
1514
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1515
+ try {
1516
+ await _preCommand(state, this);
1517
+ for (let i = 0; i < files.length; i++) {
1518
+ const file = files[i];
1519
+ const filePath = path.join(uploadsFolder, file);
1520
+ if (!fs.existsSync(filePath)) {
1521
+ throw new Error(`File not found: ${filePath}`);
1522
+ }
1523
+ state.files[i] = filePath;
1524
+ }
1525
+ await state.element.setInputFiles(files);
1526
+ return state.info;
1527
+ }
1528
+ catch (e) {
1529
+ await _commandError(state, e, this);
1530
+ }
1531
+ finally {
1532
+ await _commandFinally(state, this);
1421
1533
  }
1422
1534
  }
1423
1535
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1533,7 +1645,7 @@ class StableBrowser {
1533
1645
  await _commandError(state, e, this);
1534
1646
  }
1535
1647
  finally {
1536
- _commandFinally(state, this);
1648
+ await _commandFinally(state, this);
1537
1649
  }
1538
1650
  }
1539
1651
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1568,7 +1680,7 @@ class StableBrowser {
1568
1680
  while (Date.now() - startTime < timeout) {
1569
1681
  try {
1570
1682
  await _preCommand(state, this);
1571
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1683
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1572
1684
  if (foundObj && foundObj.element) {
1573
1685
  await this.scrollIfNeeded(foundObj.element, state.info);
1574
1686
  }
@@ -1610,7 +1722,84 @@ class StableBrowser {
1610
1722
  throw e;
1611
1723
  }
1612
1724
  finally {
1613
- _commandFinally(state, this);
1725
+ await _commandFinally(state, this);
1726
+ }
1727
+ }
1728
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1729
+ const timeout = this._getFindElementTimeout(options);
1730
+ const startTime = Date.now();
1731
+ const state = {
1732
+ _params,
1733
+ value: referanceSnapshot,
1734
+ options,
1735
+ world,
1736
+ locate: false,
1737
+ scroll: false,
1738
+ screenshot: true,
1739
+ highlight: false,
1740
+ type: Types.SNAPSHOT_VALIDATION,
1741
+ text: `verify snapshot: ${referanceSnapshot}`,
1742
+ operation: "snapshotValidation",
1743
+ log: "***** verify snapshot *****\n",
1744
+ };
1745
+ if (!referanceSnapshot) {
1746
+ throw new Error("referanceSnapshot is null");
1747
+ }
1748
+ let text = null;
1749
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1750
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1751
+ }
1752
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1753
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1754
+ }
1755
+ else if (referanceSnapshot.startsWith("yaml:")) {
1756
+ text = referanceSnapshot.substring(5);
1757
+ }
1758
+ else {
1759
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1760
+ }
1761
+ state.text = text;
1762
+ const newValue = await this._replaceWithLocalData(text, world);
1763
+ await _preCommand(state, this);
1764
+ let foundObj = null;
1765
+ try {
1766
+ let matchResult = null;
1767
+ while (Date.now() - startTime < timeout) {
1768
+ try {
1769
+ let scope = null;
1770
+ if (!frameSelectors) {
1771
+ scope = this.page;
1772
+ }
1773
+ else {
1774
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1775
+ }
1776
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1777
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1778
+ if (matchResult.errorLine !== -1) {
1779
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1780
+ }
1781
+ // highlight and screenshot
1782
+ try {
1783
+ await await highlightSnapshot(newValue, scope);
1784
+ await _screenshot(state, this);
1785
+ }
1786
+ catch (e) { }
1787
+ return state.info;
1788
+ }
1789
+ catch (e) {
1790
+ // Log error but continue retrying until timeout is reached
1791
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1792
+ }
1793
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1794
+ }
1795
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1796
+ }
1797
+ catch (e) {
1798
+ await _commandError(state, e, this);
1799
+ throw e;
1800
+ }
1801
+ finally {
1802
+ await _commandFinally(state, this);
1614
1803
  }
1615
1804
  }
1616
1805
  async waitForUserInput(message, world = null) {
@@ -1648,6 +1837,15 @@ class StableBrowser {
1648
1837
  // save the data to the file
1649
1838
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1650
1839
  }
1840
+ overwriteTestData(testData, world = null) {
1841
+ if (!testData) {
1842
+ return;
1843
+ }
1844
+ // if data file exists, load it
1845
+ const dataFile = _getDataFile(world, this.context, this);
1846
+ // save the data to the file
1847
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1848
+ }
1651
1849
  _getDataFilePath(fileName) {
1652
1850
  let dataFile = path.join(this.project_path, "data", fileName);
1653
1851
  if (fs.existsSync(dataFile)) {
@@ -1900,7 +2098,7 @@ class StableBrowser {
1900
2098
  await _commandError(state, e, this);
1901
2099
  }
1902
2100
  finally {
1903
- _commandFinally(state, this);
2101
+ await _commandFinally(state, this);
1904
2102
  }
1905
2103
  }
1906
2104
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1931,10 +2129,31 @@ class StableBrowser {
1931
2129
  case "value":
1932
2130
  state.value = await state.element.inputValue();
1933
2131
  break;
2132
+ case "text":
2133
+ state.value = await state.element.textContent();
2134
+ break;
1934
2135
  default:
1935
2136
  state.value = await state.element.getAttribute(attribute);
1936
2137
  break;
1937
2138
  }
2139
+ if (options !== null) {
2140
+ if (options.regex && options.regex !== "") {
2141
+ // Construct a regex pattern from the provided string
2142
+ const regex = options.regex.slice(1, -1);
2143
+ const regexPattern = new RegExp(regex, "g");
2144
+ const matches = state.value.match(regexPattern);
2145
+ if (matches) {
2146
+ let newValue = "";
2147
+ for (const match of matches) {
2148
+ newValue += match;
2149
+ }
2150
+ state.value = newValue;
2151
+ }
2152
+ }
2153
+ if (options.trimSpaces && options.trimSpaces === true) {
2154
+ state.value = state.value.trim();
2155
+ }
2156
+ }
1938
2157
  state.info.value = state.value;
1939
2158
  this.setTestData({ [variable]: state.value }, world);
1940
2159
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1945,7 +2164,7 @@ class StableBrowser {
1945
2164
  await _commandError(state, e, this);
1946
2165
  }
1947
2166
  finally {
1948
- _commandFinally(state, this);
2167
+ await _commandFinally(state, this);
1949
2168
  }
1950
2169
  }
1951
2170
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1970,12 +2189,15 @@ class StableBrowser {
1970
2189
  let expectedValue;
1971
2190
  try {
1972
2191
  await _preCommand(state, this);
1973
- expectedValue = state.value;
2192
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1974
2193
  state.info.expectedValue = expectedValue;
1975
2194
  switch (attribute) {
1976
2195
  case "innerText":
1977
2196
  val = String(await state.element.innerText());
1978
2197
  break;
2198
+ case "text":
2199
+ val = String(await state.element.textContent());
2200
+ break;
1979
2201
  case "value":
1980
2202
  val = String(await state.element.inputValue());
1981
2203
  break;
@@ -1997,17 +2219,42 @@ class StableBrowser {
1997
2219
  let regex;
1998
2220
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1999
2221
  const patternBody = expectedValue.slice(1, -1);
2000
- regex = new RegExp(patternBody, "g");
2222
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2223
+ regex = new RegExp(processedPattern, "gs");
2224
+ state.info.regex = true;
2001
2225
  }
2002
2226
  else {
2003
2227
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2004
2228
  regex = new RegExp(escapedPattern, "g");
2005
2229
  }
2006
- if (!val.match(regex)) {
2007
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2008
- state.info.failCause.assertionFailed = true;
2009
- state.info.failCause.lastError = errorMessage;
2010
- throw new Error(errorMessage);
2230
+ if (attribute === "innerText") {
2231
+ if (state.info.regex) {
2232
+ if (!regex.test(val)) {
2233
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2234
+ state.info.failCause.assertionFailed = true;
2235
+ state.info.failCause.lastError = errorMessage;
2236
+ throw new Error(errorMessage);
2237
+ }
2238
+ }
2239
+ else {
2240
+ const valLines = val.split("\n");
2241
+ const expectedLines = expectedValue.split("\n");
2242
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2243
+ if (!isPart) {
2244
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2245
+ state.info.failCause.assertionFailed = true;
2246
+ state.info.failCause.lastError = errorMessage;
2247
+ throw new Error(errorMessage);
2248
+ }
2249
+ }
2250
+ }
2251
+ else {
2252
+ if (!val.match(regex)) {
2253
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2254
+ state.info.failCause.assertionFailed = true;
2255
+ state.info.failCause.lastError = errorMessage;
2256
+ throw new Error(errorMessage);
2257
+ }
2011
2258
  }
2012
2259
  return state.info;
2013
2260
  }
@@ -2015,7 +2262,7 @@ class StableBrowser {
2015
2262
  await _commandError(state, e, this);
2016
2263
  }
2017
2264
  finally {
2018
- _commandFinally(state, this);
2265
+ await _commandFinally(state, this);
2019
2266
  }
2020
2267
  }
2021
2268
  async extractEmailData(emailAddress, options, world) {
@@ -2175,56 +2422,49 @@ class StableBrowser {
2175
2422
  console.debug(error);
2176
2423
  }
2177
2424
  }
2178
- // async _unhighlightElements(scope, css) {
2179
- // try {
2180
- // if (!scope) {
2181
- // return;
2182
- // }
2183
- // if (!css) {
2184
- // scope
2185
- // .evaluate((node) => {
2186
- // if (node && node.style) {
2187
- // if (!node.__previousOutline) {
2188
- // node.style.outline = "";
2189
- // } else {
2190
- // node.style.outline = node.__previousOutline;
2191
- // }
2192
- // }
2193
- // })
2194
- // .then(() => {})
2195
- // .catch((e) => {
2196
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2197
- // });
2198
- // } else {
2199
- // scope
2200
- // .evaluate(([css]) => {
2201
- // if (!css) {
2202
- // return;
2203
- // }
2204
- // let elements = Array.from(document.querySelectorAll(css));
2205
- // for (i = 0; i < elements.length; i++) {
2206
- // let element = elements[i];
2207
- // if (!element.style) {
2208
- // return;
2209
- // }
2210
- // if (!element.__previousOutline) {
2211
- // element.style.outline = "";
2212
- // } else {
2213
- // element.style.outline = element.__previousOutline;
2214
- // }
2215
- // }
2216
- // })
2217
- // .then(() => {})
2218
- // .catch((e) => {
2219
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2220
- // });
2221
- // }
2222
- // } catch (error) {
2223
- // // console.debug(error);
2224
- // }
2225
- // }
2425
+ _matcher(text) {
2426
+ if (!text) {
2427
+ return { matcher: "contains", queryText: "" };
2428
+ }
2429
+ if (text.length < 2) {
2430
+ return { matcher: "contains", queryText: text };
2431
+ }
2432
+ const split = text.split(":");
2433
+ const matcher = split[0].toLowerCase();
2434
+ const queryText = split.slice(1).join(":").trim();
2435
+ return { matcher, queryText };
2436
+ }
2437
+ _getDomain(url) {
2438
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2439
+ return "";
2440
+ }
2441
+ let hostnameFragments = url.split("/")[2].split(".");
2442
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2443
+ return hostnameFragments.join("-").split(":").join("-");
2444
+ }
2445
+ let n = hostnameFragments.length;
2446
+ let fragments = [...hostnameFragments];
2447
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2448
+ hostnameFragments.pop();
2449
+ n = hostnameFragments.length;
2450
+ }
2451
+ if (n == 0) {
2452
+ if (fragments[0] === "www")
2453
+ fragments = fragments.slice(1);
2454
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2455
+ }
2456
+ if (hostnameFragments[0] === "www")
2457
+ hostnameFragments = hostnameFragments.slice(1);
2458
+ return hostnameFragments.join(".");
2459
+ }
2460
+ /**
2461
+ * Verify the page path matches the given path.
2462
+ * @param {string} pathPart - The path to verify.
2463
+ * @param {object} options - Options for verification.
2464
+ * @param {object} world - The world context.
2465
+ * @returns {Promise<object>} - The state info after verification.
2466
+ */
2226
2467
  async verifyPagePath(pathPart, options = {}, world = null) {
2227
- const startTime = Date.now();
2228
2468
  let error = null;
2229
2469
  let screenshotId = null;
2230
2470
  let screenshotPath = null;
@@ -2238,51 +2478,212 @@ class StableBrowser {
2238
2478
  pathPart = newValue;
2239
2479
  }
2240
2480
  info.pathPart = pathPart;
2481
+ const { matcher, queryText } = this._matcher(pathPart);
2482
+ const state = {
2483
+ text_search: queryText,
2484
+ options,
2485
+ world,
2486
+ locate: false,
2487
+ scroll: false,
2488
+ highlight: false,
2489
+ type: Types.VERIFY_PAGE_PATH,
2490
+ text: `Verify the page url is ${queryText}`,
2491
+ _text: `Verify the page url is ${queryText}`,
2492
+ operation: "verifyPagePath",
2493
+ log: "***** verify page url is " + queryText + " *****\n",
2494
+ };
2241
2495
  try {
2496
+ await _preCommand(state, this);
2497
+ state.info.text = queryText;
2242
2498
  for (let i = 0; i < 30; i++) {
2243
2499
  const url = await this.page.url();
2244
- if (!url.includes(pathPart)) {
2245
- if (i === 29) {
2246
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2247
- }
2248
- await new Promise((resolve) => setTimeout(resolve, 1000));
2249
- continue;
2500
+ switch (matcher) {
2501
+ case "exact":
2502
+ if (url !== queryText) {
2503
+ if (i === 29) {
2504
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2505
+ }
2506
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2507
+ continue;
2508
+ }
2509
+ break;
2510
+ case "contains":
2511
+ if (!url.includes(queryText)) {
2512
+ if (i === 29) {
2513
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2514
+ }
2515
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2516
+ continue;
2517
+ }
2518
+ break;
2519
+ case "starts-with":
2520
+ {
2521
+ const domain = this._getDomain(url);
2522
+ if (domain.length > 0 && domain !== queryText) {
2523
+ if (i === 29) {
2524
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2525
+ }
2526
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2527
+ continue;
2528
+ }
2529
+ }
2530
+ break;
2531
+ case "ends-with":
2532
+ {
2533
+ const urlObj = new URL(url);
2534
+ let route = "/";
2535
+ if (urlObj.pathname !== "/") {
2536
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2537
+ }
2538
+ else {
2539
+ route = "/";
2540
+ }
2541
+ if (route !== queryText) {
2542
+ if (i === 29) {
2543
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2544
+ }
2545
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2546
+ continue;
2547
+ }
2548
+ }
2549
+ break;
2550
+ case "regex":
2551
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2552
+ if (!regex.test(url)) {
2553
+ if (i === 29) {
2554
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2555
+ }
2556
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2557
+ continue;
2558
+ }
2559
+ break;
2560
+ default:
2561
+ console.log("Unknown matching type, defaulting to contains matching");
2562
+ if (!url.includes(pathPart)) {
2563
+ if (i === 29) {
2564
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2565
+ }
2566
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2567
+ continue;
2568
+ }
2250
2569
  }
2251
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2252
- return info;
2570
+ await _screenshot(state, this);
2571
+ return state.info;
2253
2572
  }
2254
2573
  }
2255
2574
  catch (e) {
2256
- //await this.closeUnexpectedPopups();
2257
- this.logger.error("verify page path failed " + info.log);
2258
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2259
- info.screenshotPath = screenshotPath;
2260
- Object.assign(e, { info: info });
2261
- error = e;
2262
- // throw e;
2263
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2575
+ state.info.failCause.lastError = e.message;
2576
+ state.info.failCause.assertionFailed = true;
2577
+ await _commandError(state, e, this);
2264
2578
  }
2265
2579
  finally {
2266
- const endTime = Date.now();
2267
- _reportToWorld(world, {
2268
- type: Types.VERIFY_PAGE_PATH,
2269
- text: "Verify page path",
2270
- _text: "Verify the page path contains " + pathPart,
2271
- screenshotId,
2272
- result: error
2273
- ? {
2274
- status: "FAILED",
2275
- startTime,
2276
- endTime,
2277
- message: error?.message,
2278
- }
2279
- : {
2280
- status: "PASSED",
2281
- startTime,
2282
- endTime,
2283
- },
2284
- info: info,
2285
- });
2580
+ await _commandFinally(state, this);
2581
+ }
2582
+ }
2583
+ /**
2584
+ * Verify the page title matches the given title.
2585
+ * @param {string} title - The title to verify.
2586
+ * @param {object} options - Options for verification.
2587
+ * @param {object} world - The world context.
2588
+ * @returns {Promise<object>} - The state info after verification.
2589
+ */
2590
+ async verifyPageTitle(title, options = {}, world = null) {
2591
+ let error = null;
2592
+ let screenshotId = null;
2593
+ let screenshotPath = null;
2594
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2595
+ const newValue = await this._replaceWithLocalData(title, world);
2596
+ if (newValue !== title) {
2597
+ this.logger.info(title + "=" + newValue);
2598
+ title = newValue;
2599
+ }
2600
+ const { matcher, queryText } = this._matcher(title);
2601
+ const state = {
2602
+ text_search: queryText,
2603
+ options,
2604
+ world,
2605
+ locate: false,
2606
+ scroll: false,
2607
+ highlight: false,
2608
+ type: Types.VERIFY_PAGE_TITLE,
2609
+ text: `Verify the page title is ${queryText}`,
2610
+ _text: `Verify the page title is ${queryText}`,
2611
+ operation: "verifyPageTitle",
2612
+ log: "***** verify page title is " + queryText + " *****\n",
2613
+ };
2614
+ try {
2615
+ await _preCommand(state, this);
2616
+ state.info.text = queryText;
2617
+ for (let i = 0; i < 30; i++) {
2618
+ const foundTitle = await this.page.title();
2619
+ switch (matcher) {
2620
+ case "exact":
2621
+ if (foundTitle !== queryText) {
2622
+ if (i === 29) {
2623
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2624
+ }
2625
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2626
+ continue;
2627
+ }
2628
+ break;
2629
+ case "contains":
2630
+ if (!foundTitle.includes(queryText)) {
2631
+ if (i === 29) {
2632
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2633
+ }
2634
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2635
+ continue;
2636
+ }
2637
+ break;
2638
+ case "starts-with":
2639
+ if (!foundTitle.startsWith(queryText)) {
2640
+ if (i === 29) {
2641
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2642
+ }
2643
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2644
+ continue;
2645
+ }
2646
+ break;
2647
+ case "ends-with":
2648
+ if (!foundTitle.endsWith(queryText)) {
2649
+ if (i === 29) {
2650
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2651
+ }
2652
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2653
+ continue;
2654
+ }
2655
+ break;
2656
+ case "regex":
2657
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2658
+ if (!regex.test(foundTitle)) {
2659
+ if (i === 29) {
2660
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2661
+ }
2662
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2663
+ continue;
2664
+ }
2665
+ break;
2666
+ default:
2667
+ console.log("Unknown matching type, defaulting to contains matching");
2668
+ if (!foundTitle.includes(title)) {
2669
+ if (i === 29) {
2670
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2671
+ }
2672
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2673
+ continue;
2674
+ }
2675
+ }
2676
+ await _screenshot(state, this);
2677
+ return state.info;
2678
+ }
2679
+ }
2680
+ catch (e) {
2681
+ state.info.failCause.lastError = e.message;
2682
+ state.info.failCause.assertionFailed = true;
2683
+ await _commandError(state, e, this);
2684
+ }
2685
+ finally {
2686
+ await _commandFinally(state, this);
2286
2687
  }
2287
2688
  }
2288
2689
  async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
@@ -2324,7 +2725,7 @@ class StableBrowser {
2324
2725
  scroll: false,
2325
2726
  highlight: false,
2326
2727
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2327
- text: `Verify the text '${text}' exists in page`,
2728
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2328
2729
  _text: `Verify the text '${text}' exists in page`,
2329
2730
  operation: "verifyTextExistInPage",
2330
2731
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2366,27 +2767,10 @@ class StableBrowser {
2366
2767
  const frame = resultWithElementsFound[0].frame;
2367
2768
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2368
2769
  await this._highlightElements(frame, dataAttribute);
2369
- // if (world && world.screenshot && !world.screenshotPath) {
2370
- // console.log(`Highlighting for verify text is found while running from recorder`);
2371
- // this._highlightElements(frame, dataAttribute).then(async () => {
2372
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2373
- // this._unhighlightElements(frame, dataAttribute)
2374
- // .then(async () => {
2375
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2376
- // })
2377
- // .catch(
2378
- // (e) => {}
2379
- // console.error(e)
2380
- // );
2381
- // });
2382
- // }
2383
2770
  const element = await frame.locator(dataAttribute).first();
2384
- // await new Promise((resolve) => setTimeout(resolve, 100));
2385
- // await this._unhighlightElements(frame, dataAttribute);
2386
2771
  if (element) {
2387
2772
  await this.scrollIfNeeded(element, state.info);
2388
2773
  await element.dispatchEvent("bvt_verify_page_contains_text");
2389
- // await _screenshot(state, this, element);
2390
2774
  }
2391
2775
  }
2392
2776
  await _screenshot(state, this);
@@ -2396,13 +2780,12 @@ class StableBrowser {
2396
2780
  console.error(error);
2397
2781
  }
2398
2782
  }
2399
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2400
2783
  }
2401
2784
  catch (e) {
2402
2785
  await _commandError(state, e, this);
2403
2786
  }
2404
2787
  finally {
2405
- _commandFinally(state, this);
2788
+ await _commandFinally(state, this);
2406
2789
  }
2407
2790
  }
2408
2791
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2415,7 +2798,7 @@ class StableBrowser {
2415
2798
  scroll: false,
2416
2799
  highlight: false,
2417
2800
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2418
- text: `Verify text does not exist in page`,
2801
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2419
2802
  _text: `Verify the text '${text}' does not exist in page`,
2420
2803
  operation: "verifyTextNotExistInPage",
2421
2804
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2459,7 +2842,7 @@ class StableBrowser {
2459
2842
  await _commandError(state, e, this);
2460
2843
  }
2461
2844
  finally {
2462
- _commandFinally(state, this);
2845
+ await _commandFinally(state, this);
2463
2846
  }
2464
2847
  }
2465
2848
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2529,7 +2912,7 @@ class StableBrowser {
2529
2912
  const count = await frame.locator(css).count();
2530
2913
  for (let j = 0; j < count; j++) {
2531
2914
  const continer = await frame.locator(css).nth(j);
2532
- const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
2915
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2533
2916
  if (result.elementCount > 0) {
2534
2917
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2535
2918
  await this._highlightElements(frame, dataAttribute);
@@ -2570,7 +2953,7 @@ class StableBrowser {
2570
2953
  await _commandError(state, e, this);
2571
2954
  }
2572
2955
  finally {
2573
- _commandFinally(state, this);
2956
+ await _commandFinally(state, this);
2574
2957
  }
2575
2958
  }
2576
2959
  async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
@@ -2913,7 +3296,13 @@ class StableBrowser {
2913
3296
  }
2914
3297
  }
2915
3298
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2916
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3299
+ try {
3300
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3301
+ }
3302
+ catch (error) {
3303
+ this.logger.debug(error);
3304
+ throw error;
3305
+ }
2917
3306
  }
2918
3307
  _getLoadTimeout(options) {
2919
3308
  let timeout = 15000;
@@ -2936,6 +3325,7 @@ class StableBrowser {
2936
3325
  }
2937
3326
  async saveStoreState(path = null, world = null) {
2938
3327
  const storageState = await this.page.context().storageState();
3328
+ path = await this._replaceWithLocalData(path, this.world);
2939
3329
  //const testDataFile = _getDataFile(world, this.context, this);
2940
3330
  if (path) {
2941
3331
  // save { storageState: storageState } into the path
@@ -2946,10 +3336,14 @@ class StableBrowser {
2946
3336
  }
2947
3337
  }
2948
3338
  async restoreSaveState(path = null, world = null) {
3339
+ path = await this._replaceWithLocalData(path, this.world);
2949
3340
  await refreshBrowser(this, path, world);
2950
3341
  this.registerEventListeners(this.context);
2951
3342
  registerNetworkEvents(this.world, this, this.context, this.page);
2952
3343
  registerDownloadEvent(this.page, this.world, this.context);
3344
+ if (this.onRestoreSaveState) {
3345
+ this.onRestoreSaveState(path);
3346
+ }
2953
3347
  }
2954
3348
  async waitForPageLoad(options = {}, world = null) {
2955
3349
  let timeout = this._getLoadTimeout(options);
@@ -3032,7 +3426,7 @@ class StableBrowser {
3032
3426
  await _commandError(state, e, this);
3033
3427
  }
3034
3428
  finally {
3035
- _commandFinally(state, this);
3429
+ await _commandFinally(state, this);
3036
3430
  }
3037
3431
  }
3038
3432
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3119,7 +3513,7 @@ class StableBrowser {
3119
3513
  await _commandError(state, e, this);
3120
3514
  }
3121
3515
  finally {
3122
- _commandFinally(state, this);
3516
+ await _commandFinally(state, this);
3123
3517
  }
3124
3518
  }
3125
3519
  saveTestDataAsGlobal(options, world) {
@@ -3224,7 +3618,39 @@ class StableBrowser {
3224
3618
  console.log("#-#");
3225
3619
  }
3226
3620
  }
3621
+ async beforeScenario(world, scenario) {
3622
+ this.beforeScenarioCalled = true;
3623
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3624
+ this.scenarioName = scenario.pickle.name;
3625
+ }
3626
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3627
+ this.featureName = scenario.gherkinDocument.feature.name;
3628
+ }
3629
+ if (this.context) {
3630
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3631
+ }
3632
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3633
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3634
+ // check if @global_test_data tag is present
3635
+ if (this.tags.includes("@global_test_data")) {
3636
+ this.saveTestDataAsGlobal({}, world);
3637
+ }
3638
+ }
3639
+ // update test data based on feature/scenario
3640
+ let envName = null;
3641
+ if (this.context && this.context.environment) {
3642
+ envName = this.context.environment.name;
3643
+ }
3644
+ if (!process.env.TEMP_RUN) {
3645
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3646
+ }
3647
+ await loadBrunoParams(this.context, this.context.environment.name);
3648
+ }
3649
+ async afterScenario(world, scenario) { }
3227
3650
  async beforeStep(world, step) {
3651
+ if (!this.beforeScenarioCalled) {
3652
+ this.beforeScenario(world, step);
3653
+ }
3228
3654
  if (this.stepIndex === undefined) {
3229
3655
  this.stepIndex = 0;
3230
3656
  }
@@ -3241,21 +3667,11 @@ class StableBrowser {
3241
3667
  else {
3242
3668
  this.stepName = "step " + this.stepIndex;
3243
3669
  }
3244
- if (this.context) {
3245
- this.context.examplesRow = extractStepExampleParameters(step);
3246
- }
3247
3670
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3248
3671
  if (this.context.browserObject.context) {
3249
3672
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3250
3673
  }
3251
3674
  }
3252
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3253
- this.tags = step.pickle.tags.map((tag) => tag.name);
3254
- // check if @global_test_data tag is present
3255
- if (this.tags.includes("@global_test_data")) {
3256
- this.saveTestDataAsGlobal({}, world);
3257
- }
3258
- }
3259
3675
  if (this.initSnapshotTaken === false) {
3260
3676
  this.initSnapshotTaken = true;
3261
3677
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3280,18 +3696,68 @@ class StableBrowser {
3280
3696
  const content = [`- path: ${path}`, `- title: ${title}`];
3281
3697
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3282
3698
  for (let i = 0; i < frames.length; i++) {
3283
- content.push(`- frame: ${i}`);
3284
3699
  const frame = frames[i];
3285
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3286
- content.push(snapshot);
3700
+ try {
3701
+ // Ensure frame is attached and has body
3702
+ const body = frame.locator("body");
3703
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3704
+ const snapshot = await body.ariaSnapshot({ timeout });
3705
+ content.push(`- frame: ${i}`);
3706
+ content.push(snapshot);
3707
+ }
3708
+ catch (innerErr) { }
3287
3709
  }
3288
3710
  return content.join("\n");
3289
3711
  }
3290
3712
  catch (e) {
3291
- console.error(e);
3713
+ console.log("Error in getAriaSnapshot");
3714
+ //console.debug(e);
3292
3715
  }
3293
3716
  return null;
3294
3717
  }
3718
+ /**
3719
+ * Sends command with custom payload to report.
3720
+ * @param commandText - Title of the command to be shown in the report.
3721
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3722
+ * @param content - Content of the command to be shown in the report.
3723
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3724
+ * @param world - Optional world context.
3725
+ * @public
3726
+ */
3727
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3728
+ const state = {
3729
+ options,
3730
+ world,
3731
+ locate: false,
3732
+ scroll: false,
3733
+ screenshot: options.screenshot ?? false,
3734
+ highlight: options.highlight ?? false,
3735
+ type: Types.REPORT_COMMAND,
3736
+ text: commandText,
3737
+ _text: commandText,
3738
+ operation: "report_command",
3739
+ log: "***** " + commandText + " *****\n",
3740
+ };
3741
+ try {
3742
+ await _preCommand(state, this);
3743
+ const payload = {
3744
+ type: options.type ?? "text",
3745
+ content: content,
3746
+ screenshotId: null,
3747
+ };
3748
+ state.payload = payload;
3749
+ if (commandStatus === "FAILED") {
3750
+ state.throwError = true;
3751
+ throw new Error("Command failed");
3752
+ }
3753
+ }
3754
+ catch (e) {
3755
+ await _commandError(state, e, this);
3756
+ }
3757
+ finally {
3758
+ await _commandFinally(state, this);
3759
+ }
3760
+ }
3295
3761
  async afterStep(world, step) {
3296
3762
  this.stepName = null;
3297
3763
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3299,6 +3765,13 @@ class StableBrowser {
3299
3765
  await this.context.browserObject.context.tracing.stopChunk({
3300
3766
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3301
3767
  });
3768
+ if (world && world.attach) {
3769
+ await world.attach(JSON.stringify({
3770
+ type: "trace",
3771
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3772
+ }), "application/json+trace");
3773
+ }
3774
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3302
3775
  }
3303
3776
  }
3304
3777
  if (this.context) {
@@ -3311,6 +3784,29 @@ class StableBrowser {
3311
3784
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3312
3785
  }
3313
3786
  }
3787
+ if (!process.env.TEMP_RUN) {
3788
+ const state = {
3789
+ world,
3790
+ locate: false,
3791
+ scroll: false,
3792
+ screenshot: true,
3793
+ highlight: true,
3794
+ type: Types.STEP_COMPLETE,
3795
+ text: "end of scenario",
3796
+ _text: "end of scenario",
3797
+ operation: "step_complete",
3798
+ log: "***** " + "end of scenario" + " *****\n",
3799
+ };
3800
+ try {
3801
+ await _preCommand(state, this);
3802
+ }
3803
+ catch (e) {
3804
+ await _commandError(state, e, this);
3805
+ }
3806
+ finally {
3807
+ await _commandFinally(state, this);
3808
+ }
3809
+ }
3314
3810
  }
3315
3811
  }
3316
3812
  function createTimedPromise(promise, label) {