automation_model 1.0.653-dev → 1.0.653-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 +3 -1
  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 +143 -57
  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 +1 -1
  15. package/lib/command_common.js +24 -4
  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.js +4 -4
  31. package/lib/init_browser.js.map +1 -1
  32. package/lib/locate_element.js.map +1 -1
  33. package/lib/locator.d.ts +1 -0
  34. package/lib/locator.js +10 -3
  35. package/lib/locator.js.map +1 -1
  36. package/lib/locator_log.js.map +1 -1
  37. package/lib/network.js.map +1 -1
  38. package/lib/scripts/axe.mini.js +3 -3
  39. package/lib/snapshot_validation.d.ts +37 -0
  40. package/lib/snapshot_validation.js +357 -0
  41. package/lib/snapshot_validation.js.map +1 -0
  42. package/lib/stable_browser.d.ts +58 -24
  43. package/lib/stable_browser.js +916 -185
  44. package/lib/stable_browser.js.map +1 -1
  45. package/lib/table.d.ts +9 -7
  46. package/lib/table.js +82 -12
  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 +1 -0
  52. package/lib/test_context.js +1 -0
  53. package/lib/test_context.js.map +1 -1
  54. package/lib/utils.d.ts +5 -3
  55. package/lib/utils.js +132 -68
  56. package/lib/utils.js.map +1 -1
  57. package/package.json +12 -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,13 @@ 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",
68
+ SLEEP: "sleep",
58
69
  };
59
70
  export const apps = {};
60
71
  const formatElementName = (elementName) => {
@@ -66,6 +77,7 @@ class StableBrowser {
66
77
  logger;
67
78
  context;
68
79
  world;
80
+ fastMode;
69
81
  project_path = null;
70
82
  webLogFile = null;
71
83
  networkLogger = null;
@@ -74,12 +86,13 @@ class StableBrowser {
74
86
  tags = null;
75
87
  isRecording = false;
76
88
  initSnapshotTaken = false;
77
- constructor(browser, page, logger = null, context = null, world = null) {
89
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
78
90
  this.browser = browser;
79
91
  this.page = page;
80
92
  this.logger = logger;
81
93
  this.context = context;
82
94
  this.world = world;
95
+ this.fastMode = fastMode;
83
96
  if (!this.logger) {
84
97
  this.logger = console;
85
98
  }
@@ -108,6 +121,12 @@ class StableBrowser {
108
121
  context.pages = [this.page];
109
122
  const logFolder = path.join(this.project_path, "logs", "web");
110
123
  this.world = world;
124
+ if (process.env.FAST_MODE === "true") {
125
+ this.fastMode = true;
126
+ }
127
+ if (this.context) {
128
+ this.context.fastMode = this.fastMode;
129
+ }
111
130
  this.registerEventListeners(this.context);
112
131
  registerNetworkEvents(this.world, this, this.context, this.page);
113
132
  registerDownloadEvent(this.page, this.world, this.context);
@@ -118,6 +137,9 @@ class StableBrowser {
118
137
  if (!context.pageLoading) {
119
138
  context.pageLoading = { status: false };
120
139
  }
140
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
141
+ this.page.on("dialog", (dialog) => dialog.accept());
142
+ }
121
143
  context.playContext.on("page", async function (page) {
122
144
  if (this.configuration && this.configuration.closePopups === true) {
123
145
  console.log("close unexpected popups");
@@ -126,6 +148,14 @@ class StableBrowser {
126
148
  }
127
149
  context.pageLoading.status = true;
128
150
  this.page = page;
151
+ try {
152
+ if (this.configuration && this.configuration.acceptDialog) {
153
+ await page.on("dialog", (dialog) => dialog.accept());
154
+ }
155
+ }
156
+ catch (error) {
157
+ console.error("Error on dialog accept registration", error);
158
+ }
129
159
  context.page = page;
130
160
  context.pages.push(page);
131
161
  registerNetworkEvents(this.world, this, context, this.page);
@@ -177,8 +207,34 @@ class StableBrowser {
177
207
  if (newContextCreated) {
178
208
  this.registerEventListeners(this.context);
179
209
  await this.goto(this.context.environment.baseUrl);
180
- await this.waitForPageLoad();
210
+ if (!this.fastMode) {
211
+ await this.waitForPageLoad();
212
+ }
213
+ }
214
+ }
215
+ async switchTab(tabTitleOrIndex) {
216
+ // first check if the tabNameOrIndex is a number
217
+ let index = parseInt(tabTitleOrIndex);
218
+ if (!isNaN(index)) {
219
+ if (index >= 0 && index < this.context.pages.length) {
220
+ this.page = this.context.pages[index];
221
+ this.context.page = this.page;
222
+ await this.page.bringToFront();
223
+ return;
224
+ }
181
225
  }
226
+ // if the tabNameOrIndex is a string, find the tab by name
227
+ for (let i = 0; i < this.context.pages.length; i++) {
228
+ let page = this.context.pages[i];
229
+ let title = await page.title();
230
+ if (title.includes(tabTitleOrIndex)) {
231
+ this.page = page;
232
+ this.context.page = this.page;
233
+ await this.page.bringToFront();
234
+ return;
235
+ }
236
+ }
237
+ throw new Error("Tab not found: " + tabTitleOrIndex);
182
238
  }
183
239
  registerConsoleLogListener(page, context) {
184
240
  if (!this.context.webLogger) {
@@ -247,6 +303,7 @@ class StableBrowser {
247
303
  if (!url) {
248
304
  throw new Error("url is null, verify that the environment file is correct");
249
305
  }
306
+ url = await this._replaceWithLocalData(url, this.world);
250
307
  if (!url.startsWith("http")) {
251
308
  url = "https://" + url;
252
309
  }
@@ -275,7 +332,7 @@ class StableBrowser {
275
332
  _commandError(state, error, this);
276
333
  }
277
334
  finally {
278
- _commandFinally(state, this);
335
+ await _commandFinally(state, this);
279
336
  }
280
337
  }
281
338
  async _getLocator(locator, scope, _params) {
@@ -391,7 +448,7 @@ class StableBrowser {
391
448
  }
392
449
  return { elementCount: tagCount, randomToken };
393
450
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
451
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
452
  if (!info) {
396
453
  info = {};
397
454
  }
@@ -458,7 +515,7 @@ class StableBrowser {
458
515
  }
459
516
  return;
460
517
  }
461
- if (info.locatorLog && count === 0) {
518
+ if (info.locatorLog && count === 0 && logErrors) {
462
519
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
520
  }
464
521
  for (let j = 0; j < count; j++) {
@@ -473,7 +530,7 @@ class StableBrowser {
473
530
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
531
  }
475
532
  }
476
- else {
533
+ else if (logErrors) {
477
534
  info.failCause.visible = visible;
478
535
  info.failCause.enabled = enabled;
479
536
  if (!info.printMessages) {
@@ -565,15 +622,27 @@ class StableBrowser {
565
622
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
623
  if (!element.rerun) {
567
624
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
625
+ await element.evaluate((el, randomToken) => {
569
626
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
627
  }, 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;
628
+ // if (element._frame) {
629
+ // return element;
630
+ // }
631
+ const scope = element._frame ?? element.page();
632
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
633
+ let prefixSelector = "";
634
+ const frameControlSelector = " >> internal:control=enter-frame";
635
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
636
+ if (frameSelectorIndex !== -1) {
637
+ // remove everything after the >> internal:control=enter-frame
638
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
639
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
640
+ }
641
+ // if (element?._frame?._selector) {
642
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
643
+ // }
644
+ const newSelector = prefixSelector + newElementSelector;
645
+ return scope.locator(newSelector);
577
646
  }
578
647
  }
579
648
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +794,9 @@ class StableBrowser {
725
794
  // info.log += "scanning locators in priority 2" + "\n";
726
795
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
796
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
797
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
798
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
799
  }
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
800
  let foundElements = result.foundElements;
737
801
  if (foundElements.length === 1 && foundElements[0].unique) {
738
802
  info.box = foundElements[0].box;
@@ -787,6 +851,11 @@ class StableBrowser {
787
851
  visibleOnly = false;
788
852
  }
789
853
  await new Promise((resolve) => setTimeout(resolve, 1000));
854
+ // sheck of more of half of the timeout has passed
855
+ if (Date.now() - startTime > timeout / 2) {
856
+ highPriorityOnly = false;
857
+ visibleOnly = false;
858
+ }
790
859
  }
791
860
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
861
  // if (info.locatorLog) {
@@ -802,7 +871,7 @@ class StableBrowser {
802
871
  }
803
872
  throw new Error("failed to locate first element no elements found, " + info.log);
804
873
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
874
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
875
  let foundElements = [];
807
876
  const result = {
808
877
  foundElements: foundElements,
@@ -821,7 +890,9 @@ class StableBrowser {
821
890
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
891
  }
823
892
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
893
+ if (logErrors) {
894
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
895
+ }
825
896
  }
826
897
  }
827
898
  if (foundLocators.length === 1) {
@@ -862,7 +933,7 @@ class StableBrowser {
862
933
  });
863
934
  result.locatorIndex = i;
864
935
  }
865
- else {
936
+ else if (logErrors) {
866
937
  info.failCause.foundMultiple = true;
867
938
  if (info.locatorLog) {
868
939
  info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
@@ -914,7 +985,7 @@ class StableBrowser {
914
985
  await _commandError(state, "timeout looking for " + elementDescription, this);
915
986
  }
916
987
  finally {
917
- _commandFinally(state, this);
988
+ await _commandFinally(state, this);
918
989
  }
919
990
  }
920
991
  }
@@ -963,7 +1034,7 @@ class StableBrowser {
963
1034
  await _commandError(state, "timeout looking for " + elementDescription, this);
964
1035
  }
965
1036
  finally {
966
- _commandFinally(state, this);
1037
+ await _commandFinally(state, this);
967
1038
  }
968
1039
  }
969
1040
  }
@@ -985,14 +1056,16 @@ class StableBrowser {
985
1056
  try {
986
1057
  await _preCommand(state, this);
987
1058
  await performAction("click", state.element, options, this, state, _params);
988
- await this.waitForPageLoad();
1059
+ if (!this.fastMode) {
1060
+ await this.waitForPageLoad();
1061
+ }
989
1062
  return state.info;
990
1063
  }
991
1064
  catch (e) {
992
1065
  await _commandError(state, e, this);
993
1066
  }
994
1067
  finally {
995
- _commandFinally(state, this);
1068
+ await _commandFinally(state, this);
996
1069
  }
997
1070
  }
998
1071
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1023,7 +1096,7 @@ class StableBrowser {
1023
1096
  // await _commandError(state, e, this);
1024
1097
  }
1025
1098
  finally {
1026
- _commandFinally(state, this);
1099
+ await _commandFinally(state, this);
1027
1100
  }
1028
1101
  return found;
1029
1102
  }
@@ -1048,7 +1121,7 @@ class StableBrowser {
1048
1121
  // if (world && world.screenshot && !world.screenshotPath) {
1049
1122
  // console.log(`Highlighting while running from recorder`);
1050
1123
  await this._highlightElements(state.element);
1051
- await state.element.setChecked(checked);
1124
+ await state.element.setChecked(checked, { timeout: 2000 });
1052
1125
  await new Promise((resolve) => setTimeout(resolve, 1000));
1053
1126
  // await this._unHighlightElements(element);
1054
1127
  // }
@@ -1060,11 +1133,28 @@ class StableBrowser {
1060
1133
  this.logger.info("element did not change its state, ignoring...");
1061
1134
  }
1062
1135
  else {
1136
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1063
1137
  //await this.closeUnexpectedPopups();
1064
1138
  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));
1139
+ state.element_found = false;
1140
+ try {
1141
+ state.element = await this._locate(selectors, state.info, _params, 100);
1142
+ state.element_found = true;
1143
+ // check the check state
1144
+ }
1145
+ catch (error) {
1146
+ // element dismissed
1147
+ }
1148
+ if (state.element_found) {
1149
+ const isChecked = await state.element.isChecked();
1150
+ if (isChecked !== checked) {
1151
+ // perform click
1152
+ await state.element.click({ timeout: 2000, force: true });
1153
+ }
1154
+ else {
1155
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1156
+ }
1157
+ }
1068
1158
  }
1069
1159
  }
1070
1160
  await this.waitForPageLoad();
@@ -1074,7 +1164,7 @@ class StableBrowser {
1074
1164
  await _commandError(state, e, this);
1075
1165
  }
1076
1166
  finally {
1077
- _commandFinally(state, this);
1167
+ await _commandFinally(state, this);
1078
1168
  }
1079
1169
  }
1080
1170
  async hover(selectors, _params, options = {}, world = null) {
@@ -1100,7 +1190,7 @@ class StableBrowser {
1100
1190
  await _commandError(state, e, this);
1101
1191
  }
1102
1192
  finally {
1103
- _commandFinally(state, this);
1193
+ await _commandFinally(state, this);
1104
1194
  }
1105
1195
  }
1106
1196
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1136,7 +1226,7 @@ class StableBrowser {
1136
1226
  await _commandError(state, e, this);
1137
1227
  }
1138
1228
  finally {
1139
- _commandFinally(state, this);
1229
+ await _commandFinally(state, this);
1140
1230
  }
1141
1231
  }
1142
1232
  async type(_value, _params = null, options = {}, world = null) {
@@ -1182,7 +1272,7 @@ class StableBrowser {
1182
1272
  await _commandError(state, e, this);
1183
1273
  }
1184
1274
  finally {
1185
- _commandFinally(state, this);
1275
+ await _commandFinally(state, this);
1186
1276
  }
1187
1277
  }
1188
1278
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1218,7 +1308,7 @@ class StableBrowser {
1218
1308
  await _commandError(state, e, this);
1219
1309
  }
1220
1310
  finally {
1221
- _commandFinally(state, this);
1311
+ await _commandFinally(state, this);
1222
1312
  }
1223
1313
  }
1224
1314
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1287,7 +1377,7 @@ class StableBrowser {
1287
1377
  await _commandError(state, e, this);
1288
1378
  }
1289
1379
  finally {
1290
- _commandFinally(state, this);
1380
+ await _commandFinally(state, this);
1291
1381
  }
1292
1382
  }
1293
1383
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1360,7 +1450,9 @@ class StableBrowser {
1360
1450
  await new Promise((resolve) => setTimeout(resolve, 500));
1361
1451
  }
1362
1452
  }
1453
+ //if (!this.fastMode) {
1363
1454
  await _screenshot(state, this);
1455
+ //}
1364
1456
  if (enter === true) {
1365
1457
  await new Promise((resolve) => setTimeout(resolve, 2000));
1366
1458
  await this.page.keyboard.press("Enter");
@@ -1387,7 +1479,7 @@ class StableBrowser {
1387
1479
  await _commandError(state, e, this);
1388
1480
  }
1389
1481
  finally {
1390
- _commandFinally(state, this);
1482
+ await _commandFinally(state, this);
1391
1483
  }
1392
1484
  }
1393
1485
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1417,7 +1509,42 @@ class StableBrowser {
1417
1509
  await _commandError(state, e, this);
1418
1510
  }
1419
1511
  finally {
1420
- _commandFinally(state, this);
1512
+ await _commandFinally(state, this);
1513
+ }
1514
+ }
1515
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1516
+ const state = {
1517
+ selectors,
1518
+ _params,
1519
+ files,
1520
+ value: '"' + files.join('", "') + '"',
1521
+ options,
1522
+ world,
1523
+ type: Types.SET_INPUT_FILES,
1524
+ text: `Set input files`,
1525
+ _text: `Set input files on ${selectors.element_name}`,
1526
+ operation: "setInputFiles",
1527
+ log: "***** set input files " + selectors.element_name + " *****\n",
1528
+ };
1529
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1530
+ try {
1531
+ await _preCommand(state, this);
1532
+ for (let i = 0; i < files.length; i++) {
1533
+ const file = files[i];
1534
+ const filePath = path.join(uploadsFolder, file);
1535
+ if (!fs.existsSync(filePath)) {
1536
+ throw new Error(`File not found: ${filePath}`);
1537
+ }
1538
+ state.files[i] = filePath;
1539
+ }
1540
+ await state.element.setInputFiles(files);
1541
+ return state.info;
1542
+ }
1543
+ catch (e) {
1544
+ await _commandError(state, e, this);
1545
+ }
1546
+ finally {
1547
+ await _commandFinally(state, this);
1421
1548
  }
1422
1549
  }
1423
1550
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1533,7 +1660,7 @@ class StableBrowser {
1533
1660
  await _commandError(state, e, this);
1534
1661
  }
1535
1662
  finally {
1536
- _commandFinally(state, this);
1663
+ await _commandFinally(state, this);
1537
1664
  }
1538
1665
  }
1539
1666
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1568,7 +1695,7 @@ class StableBrowser {
1568
1695
  while (Date.now() - startTime < timeout) {
1569
1696
  try {
1570
1697
  await _preCommand(state, this);
1571
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1698
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1572
1699
  if (foundObj && foundObj.element) {
1573
1700
  await this.scrollIfNeeded(foundObj.element, state.info);
1574
1701
  }
@@ -1610,7 +1737,84 @@ class StableBrowser {
1610
1737
  throw e;
1611
1738
  }
1612
1739
  finally {
1613
- _commandFinally(state, this);
1740
+ await _commandFinally(state, this);
1741
+ }
1742
+ }
1743
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1744
+ const timeout = this._getFindElementTimeout(options);
1745
+ const startTime = Date.now();
1746
+ const state = {
1747
+ _params,
1748
+ value: referanceSnapshot,
1749
+ options,
1750
+ world,
1751
+ locate: false,
1752
+ scroll: false,
1753
+ screenshot: true,
1754
+ highlight: false,
1755
+ type: Types.SNAPSHOT_VALIDATION,
1756
+ text: `verify snapshot: ${referanceSnapshot}`,
1757
+ operation: "snapshotValidation",
1758
+ log: "***** verify snapshot *****\n",
1759
+ };
1760
+ if (!referanceSnapshot) {
1761
+ throw new Error("referanceSnapshot is null");
1762
+ }
1763
+ let text = null;
1764
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1765
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1766
+ }
1767
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1768
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1769
+ }
1770
+ else if (referanceSnapshot.startsWith("yaml:")) {
1771
+ text = referanceSnapshot.substring(5);
1772
+ }
1773
+ else {
1774
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1775
+ }
1776
+ state.text = text;
1777
+ const newValue = await this._replaceWithLocalData(text, world);
1778
+ await _preCommand(state, this);
1779
+ let foundObj = null;
1780
+ try {
1781
+ let matchResult = null;
1782
+ while (Date.now() - startTime < timeout) {
1783
+ try {
1784
+ let scope = null;
1785
+ if (!frameSelectors) {
1786
+ scope = this.page;
1787
+ }
1788
+ else {
1789
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1790
+ }
1791
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1792
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1793
+ if (matchResult.errorLine !== -1) {
1794
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1795
+ }
1796
+ // highlight and screenshot
1797
+ try {
1798
+ await await highlightSnapshot(newValue, scope);
1799
+ await _screenshot(state, this);
1800
+ }
1801
+ catch (e) { }
1802
+ return state.info;
1803
+ }
1804
+ catch (e) {
1805
+ // Log error but continue retrying until timeout is reached
1806
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1807
+ }
1808
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1809
+ }
1810
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1811
+ }
1812
+ catch (e) {
1813
+ await _commandError(state, e, this);
1814
+ throw e;
1815
+ }
1816
+ finally {
1817
+ await _commandFinally(state, this);
1614
1818
  }
1615
1819
  }
1616
1820
  async waitForUserInput(message, world = null) {
@@ -1909,7 +2113,7 @@ class StableBrowser {
1909
2113
  await _commandError(state, e, this);
1910
2114
  }
1911
2115
  finally {
1912
- _commandFinally(state, this);
2116
+ await _commandFinally(state, this);
1913
2117
  }
1914
2118
  }
1915
2119
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1940,10 +2144,102 @@ class StableBrowser {
1940
2144
  case "value":
1941
2145
  state.value = await state.element.inputValue();
1942
2146
  break;
2147
+ case "text":
2148
+ state.value = await state.element.textContent();
2149
+ break;
1943
2150
  default:
1944
2151
  state.value = await state.element.getAttribute(attribute);
1945
2152
  break;
1946
2153
  }
2154
+ if (options !== null) {
2155
+ if (options.regex && options.regex !== "") {
2156
+ // Construct a regex pattern from the provided string
2157
+ const regex = options.regex.slice(1, -1);
2158
+ const regexPattern = new RegExp(regex, "g");
2159
+ const matches = state.value.match(regexPattern);
2160
+ if (matches) {
2161
+ let newValue = "";
2162
+ for (const match of matches) {
2163
+ newValue += match;
2164
+ }
2165
+ state.value = newValue;
2166
+ }
2167
+ }
2168
+ if (options.trimSpaces && options.trimSpaces === true) {
2169
+ state.value = state.value.trim();
2170
+ }
2171
+ }
2172
+ state.info.value = state.value;
2173
+ this.setTestData({ [variable]: state.value }, world);
2174
+ this.logger.info("set test data: " + variable + "=" + state.value);
2175
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2176
+ return state.info;
2177
+ }
2178
+ catch (e) {
2179
+ await _commandError(state, e, this);
2180
+ }
2181
+ finally {
2182
+ await _commandFinally(state, this);
2183
+ }
2184
+ }
2185
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2186
+ const state = {
2187
+ selectors,
2188
+ _params,
2189
+ property,
2190
+ variable,
2191
+ options,
2192
+ world,
2193
+ type: Types.EXTRACT_PROPERTY,
2194
+ text: `Extract property from element`,
2195
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2196
+ operation: "extractProperty",
2197
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2198
+ allowDisabled: true,
2199
+ };
2200
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2201
+ try {
2202
+ await _preCommand(state, this);
2203
+ switch (property) {
2204
+ case "inner_text":
2205
+ state.value = await state.element.innerText();
2206
+ break;
2207
+ case "href":
2208
+ state.value = await state.element.getAttribute("href");
2209
+ break;
2210
+ case "value":
2211
+ state.value = await state.element.inputValue();
2212
+ break;
2213
+ case "text":
2214
+ state.value = await state.element.textContent();
2215
+ break;
2216
+ default:
2217
+ if (property.startsWith("dataset.")) {
2218
+ const dataAttribute = property.substring(8);
2219
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2220
+ }
2221
+ else {
2222
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2223
+ }
2224
+ }
2225
+ if (options !== null) {
2226
+ if (options.regex && options.regex !== "") {
2227
+ // Construct a regex pattern from the provided string
2228
+ const regex = options.regex.slice(1, -1);
2229
+ const regexPattern = new RegExp(regex, "g");
2230
+ const matches = state.value.match(regexPattern);
2231
+ if (matches) {
2232
+ let newValue = "";
2233
+ for (const match of matches) {
2234
+ newValue += match;
2235
+ }
2236
+ state.value = newValue;
2237
+ }
2238
+ }
2239
+ if (options.trimSpaces && options.trimSpaces === true) {
2240
+ state.value = state.value.trim();
2241
+ }
2242
+ }
1947
2243
  state.info.value = state.value;
1948
2244
  this.setTestData({ [variable]: state.value }, world);
1949
2245
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1954,7 +2250,7 @@ class StableBrowser {
1954
2250
  await _commandError(state, e, this);
1955
2251
  }
1956
2252
  finally {
1957
- _commandFinally(state, this);
2253
+ await _commandFinally(state, this);
1958
2254
  }
1959
2255
  }
1960
2256
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1979,12 +2275,15 @@ class StableBrowser {
1979
2275
  let expectedValue;
1980
2276
  try {
1981
2277
  await _preCommand(state, this);
1982
- expectedValue = state.value;
2278
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1983
2279
  state.info.expectedValue = expectedValue;
1984
2280
  switch (attribute) {
1985
2281
  case "innerText":
1986
2282
  val = String(await state.element.innerText());
1987
2283
  break;
2284
+ case "text":
2285
+ val = String(await state.element.textContent());
2286
+ break;
1988
2287
  case "value":
1989
2288
  val = String(await state.element.inputValue());
1990
2289
  break;
@@ -2006,17 +2305,42 @@ class StableBrowser {
2006
2305
  let regex;
2007
2306
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2008
2307
  const patternBody = expectedValue.slice(1, -1);
2009
- regex = new RegExp(patternBody, "g");
2308
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2309
+ regex = new RegExp(processedPattern, "gs");
2310
+ state.info.regex = true;
2010
2311
  }
2011
2312
  else {
2012
2313
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2013
2314
  regex = new RegExp(escapedPattern, "g");
2014
2315
  }
2015
- if (!val.match(regex)) {
2016
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2017
- state.info.failCause.assertionFailed = true;
2018
- state.info.failCause.lastError = errorMessage;
2019
- throw new Error(errorMessage);
2316
+ if (attribute === "innerText") {
2317
+ if (state.info.regex) {
2318
+ if (!regex.test(val)) {
2319
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2320
+ state.info.failCause.assertionFailed = true;
2321
+ state.info.failCause.lastError = errorMessage;
2322
+ throw new Error(errorMessage);
2323
+ }
2324
+ }
2325
+ else {
2326
+ const valLines = val.split("\n");
2327
+ const expectedLines = expectedValue.split("\n");
2328
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2329
+ if (!isPart) {
2330
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2331
+ state.info.failCause.assertionFailed = true;
2332
+ state.info.failCause.lastError = errorMessage;
2333
+ throw new Error(errorMessage);
2334
+ }
2335
+ }
2336
+ }
2337
+ else {
2338
+ if (!val.match(regex)) {
2339
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2340
+ state.info.failCause.assertionFailed = true;
2341
+ state.info.failCause.lastError = errorMessage;
2342
+ throw new Error(errorMessage);
2343
+ }
2020
2344
  }
2021
2345
  return state.info;
2022
2346
  }
@@ -2024,7 +2348,128 @@ class StableBrowser {
2024
2348
  await _commandError(state, e, this);
2025
2349
  }
2026
2350
  finally {
2027
- _commandFinally(state, this);
2351
+ await _commandFinally(state, this);
2352
+ }
2353
+ }
2354
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2355
+ const state = {
2356
+ selectors,
2357
+ _params,
2358
+ property,
2359
+ value,
2360
+ options,
2361
+ world,
2362
+ type: Types.VERIFY_PROPERTY,
2363
+ highlight: true,
2364
+ screenshot: true,
2365
+ text: `Verify element property`,
2366
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2367
+ operation: "verifyProperty",
2368
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2369
+ allowDisabled: true,
2370
+ };
2371
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2372
+ let val;
2373
+ let expectedValue;
2374
+ try {
2375
+ await _preCommand(state, this);
2376
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2377
+ state.info.expectedValue = expectedValue;
2378
+ switch (property) {
2379
+ case "innerText":
2380
+ val = String(await state.element.innerText());
2381
+ break;
2382
+ case "text":
2383
+ val = String(await state.element.textContent());
2384
+ break;
2385
+ case "value":
2386
+ val = String(await state.element.inputValue());
2387
+ break;
2388
+ case "checked":
2389
+ val = String(await state.element.isChecked());
2390
+ break;
2391
+ case "disabled":
2392
+ val = String(await state.element.isDisabled());
2393
+ break;
2394
+ case "readOnly":
2395
+ const isEditable = await state.element.isEditable();
2396
+ val = String(!isEditable);
2397
+ break;
2398
+ case "innerHTML":
2399
+ val = String(await state.element.innerHTML());
2400
+ break;
2401
+ case "outerHTML":
2402
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2403
+ break;
2404
+ default:
2405
+ if (property.startsWith("dataset.")) {
2406
+ const dataAttribute = property.substring(8);
2407
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2408
+ }
2409
+ else {
2410
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2411
+ }
2412
+ }
2413
+ // Helper function to remove all style="" attributes
2414
+ const removeStyleAttributes = (htmlString) => {
2415
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, '');
2416
+ };
2417
+ // Remove style attributes for innerHTML and outerHTML properties
2418
+ if (property === "innerHTML" || property === "outerHTML") {
2419
+ val = removeStyleAttributes(val);
2420
+ expectedValue = removeStyleAttributes(expectedValue);
2421
+ }
2422
+ state.info.value = val;
2423
+ let regex;
2424
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2425
+ const patternBody = expectedValue.slice(1, -1);
2426
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2427
+ regex = new RegExp(processedPattern, "gs");
2428
+ state.info.regex = true;
2429
+ }
2430
+ else {
2431
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2432
+ regex = new RegExp(escapedPattern, "g");
2433
+ }
2434
+ if (property === "innerText") {
2435
+ if (state.info.regex) {
2436
+ if (!regex.test(val)) {
2437
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2438
+ state.info.failCause.assertionFailed = true;
2439
+ state.info.failCause.lastError = errorMessage;
2440
+ throw new Error(errorMessage);
2441
+ }
2442
+ }
2443
+ else {
2444
+ // Fix: Replace escaped newlines with actual newlines before splitting
2445
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, '\n');
2446
+ const valLines = val.split("\n");
2447
+ const expectedLines = normalizedExpectedValue.split("\n");
2448
+ // Check if all expected lines are present in the actual lines
2449
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2450
+ if (!isPart) {
2451
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2452
+ state.info.failCause.assertionFailed = true;
2453
+ state.info.failCause.lastError = errorMessage;
2454
+ throw new Error(errorMessage);
2455
+ }
2456
+ }
2457
+ }
2458
+ else {
2459
+ if (!val.match(regex)) {
2460
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2461
+ state.info.failCause.assertionFailed = true;
2462
+ state.info.failCause.lastError = errorMessage;
2463
+ throw new Error(errorMessage);
2464
+ }
2465
+ }
2466
+ return state.info;
2467
+ }
2468
+ catch (e) {
2469
+ await _commandError(state, e, this);
2470
+ }
2471
+ finally {
2472
+ await _commandFinally(state, this);
2028
2473
  }
2029
2474
  }
2030
2475
  async extractEmailData(emailAddress, options, world) {
@@ -2184,56 +2629,49 @@ class StableBrowser {
2184
2629
  console.debug(error);
2185
2630
  }
2186
2631
  }
2187
- // async _unhighlightElements(scope, css) {
2188
- // try {
2189
- // if (!scope) {
2190
- // return;
2191
- // }
2192
- // if (!css) {
2193
- // scope
2194
- // .evaluate((node) => {
2195
- // if (node && node.style) {
2196
- // if (!node.__previousOutline) {
2197
- // node.style.outline = "";
2198
- // } else {
2199
- // node.style.outline = node.__previousOutline;
2200
- // }
2201
- // }
2202
- // })
2203
- // .then(() => {})
2204
- // .catch((e) => {
2205
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2206
- // });
2207
- // } else {
2208
- // scope
2209
- // .evaluate(([css]) => {
2210
- // if (!css) {
2211
- // return;
2212
- // }
2213
- // let elements = Array.from(document.querySelectorAll(css));
2214
- // for (i = 0; i < elements.length; i++) {
2215
- // let element = elements[i];
2216
- // if (!element.style) {
2217
- // return;
2218
- // }
2219
- // if (!element.__previousOutline) {
2220
- // element.style.outline = "";
2221
- // } else {
2222
- // element.style.outline = element.__previousOutline;
2223
- // }
2224
- // }
2225
- // })
2226
- // .then(() => {})
2227
- // .catch((e) => {
2228
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2229
- // });
2230
- // }
2231
- // } catch (error) {
2232
- // // console.debug(error);
2233
- // }
2234
- // }
2632
+ _matcher(text) {
2633
+ if (!text) {
2634
+ return { matcher: "contains", queryText: "" };
2635
+ }
2636
+ if (text.length < 2) {
2637
+ return { matcher: "contains", queryText: text };
2638
+ }
2639
+ const split = text.split(":");
2640
+ const matcher = split[0].toLowerCase();
2641
+ const queryText = split.slice(1).join(":").trim();
2642
+ return { matcher, queryText };
2643
+ }
2644
+ _getDomain(url) {
2645
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2646
+ return "";
2647
+ }
2648
+ let hostnameFragments = url.split("/")[2].split(".");
2649
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2650
+ return hostnameFragments.join("-").split(":").join("-");
2651
+ }
2652
+ let n = hostnameFragments.length;
2653
+ let fragments = [...hostnameFragments];
2654
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2655
+ hostnameFragments.pop();
2656
+ n = hostnameFragments.length;
2657
+ }
2658
+ if (n == 0) {
2659
+ if (fragments[0] === "www")
2660
+ fragments = fragments.slice(1);
2661
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2662
+ }
2663
+ if (hostnameFragments[0] === "www")
2664
+ hostnameFragments = hostnameFragments.slice(1);
2665
+ return hostnameFragments.join(".");
2666
+ }
2667
+ /**
2668
+ * Verify the page path matches the given path.
2669
+ * @param {string} pathPart - The path to verify.
2670
+ * @param {object} options - Options for verification.
2671
+ * @param {object} world - The world context.
2672
+ * @returns {Promise<object>} - The state info after verification.
2673
+ */
2235
2674
  async verifyPagePath(pathPart, options = {}, world = null) {
2236
- const startTime = Date.now();
2237
2675
  let error = null;
2238
2676
  let screenshotId = null;
2239
2677
  let screenshotPath = null;
@@ -2247,51 +2685,212 @@ class StableBrowser {
2247
2685
  pathPart = newValue;
2248
2686
  }
2249
2687
  info.pathPart = pathPart;
2688
+ const { matcher, queryText } = this._matcher(pathPart);
2689
+ const state = {
2690
+ text_search: queryText,
2691
+ options,
2692
+ world,
2693
+ locate: false,
2694
+ scroll: false,
2695
+ highlight: false,
2696
+ type: Types.VERIFY_PAGE_PATH,
2697
+ text: `Verify the page url is ${queryText}`,
2698
+ _text: `Verify the page url is ${queryText}`,
2699
+ operation: "verifyPagePath",
2700
+ log: "***** verify page url is " + queryText + " *****\n",
2701
+ };
2250
2702
  try {
2703
+ await _preCommand(state, this);
2704
+ state.info.text = queryText;
2251
2705
  for (let i = 0; i < 30; i++) {
2252
2706
  const url = await this.page.url();
2253
- if (!url.includes(pathPart)) {
2254
- if (i === 29) {
2255
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2256
- }
2257
- await new Promise((resolve) => setTimeout(resolve, 1000));
2258
- continue;
2707
+ switch (matcher) {
2708
+ case "exact":
2709
+ if (url !== queryText) {
2710
+ if (i === 29) {
2711
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2712
+ }
2713
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2714
+ continue;
2715
+ }
2716
+ break;
2717
+ case "contains":
2718
+ if (!url.includes(queryText)) {
2719
+ if (i === 29) {
2720
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2721
+ }
2722
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2723
+ continue;
2724
+ }
2725
+ break;
2726
+ case "starts-with":
2727
+ {
2728
+ const domain = this._getDomain(url);
2729
+ if (domain.length > 0 && domain !== queryText) {
2730
+ if (i === 29) {
2731
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2732
+ }
2733
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2734
+ continue;
2735
+ }
2736
+ }
2737
+ break;
2738
+ case "ends-with":
2739
+ {
2740
+ const urlObj = new URL(url);
2741
+ let route = "/";
2742
+ if (urlObj.pathname !== "/") {
2743
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2744
+ }
2745
+ else {
2746
+ route = "/";
2747
+ }
2748
+ if (route !== queryText) {
2749
+ if (i === 29) {
2750
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2751
+ }
2752
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2753
+ continue;
2754
+ }
2755
+ }
2756
+ break;
2757
+ case "regex":
2758
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2759
+ if (!regex.test(url)) {
2760
+ if (i === 29) {
2761
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2762
+ }
2763
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2764
+ continue;
2765
+ }
2766
+ break;
2767
+ default:
2768
+ console.log("Unknown matching type, defaulting to contains matching");
2769
+ if (!url.includes(pathPart)) {
2770
+ if (i === 29) {
2771
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2772
+ }
2773
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2774
+ continue;
2775
+ }
2259
2776
  }
2260
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2261
- return info;
2777
+ await _screenshot(state, this);
2778
+ return state.info;
2262
2779
  }
2263
2780
  }
2264
2781
  catch (e) {
2265
- //await this.closeUnexpectedPopups();
2266
- this.logger.error("verify page path failed " + info.log);
2267
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2268
- info.screenshotPath = screenshotPath;
2269
- Object.assign(e, { info: info });
2270
- error = e;
2271
- // throw e;
2272
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2782
+ state.info.failCause.lastError = e.message;
2783
+ state.info.failCause.assertionFailed = true;
2784
+ await _commandError(state, e, this);
2273
2785
  }
2274
2786
  finally {
2275
- const endTime = Date.now();
2276
- _reportToWorld(world, {
2277
- type: Types.VERIFY_PAGE_PATH,
2278
- text: "Verify page path",
2279
- _text: "Verify the page path contains " + pathPart,
2280
- screenshotId,
2281
- result: error
2282
- ? {
2283
- status: "FAILED",
2284
- startTime,
2285
- endTime,
2286
- message: error?.message,
2287
- }
2288
- : {
2289
- status: "PASSED",
2290
- startTime,
2291
- endTime,
2292
- },
2293
- info: info,
2294
- });
2787
+ await _commandFinally(state, this);
2788
+ }
2789
+ }
2790
+ /**
2791
+ * Verify the page title matches the given title.
2792
+ * @param {string} title - The title to verify.
2793
+ * @param {object} options - Options for verification.
2794
+ * @param {object} world - The world context.
2795
+ * @returns {Promise<object>} - The state info after verification.
2796
+ */
2797
+ async verifyPageTitle(title, options = {}, world = null) {
2798
+ let error = null;
2799
+ let screenshotId = null;
2800
+ let screenshotPath = null;
2801
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2802
+ const newValue = await this._replaceWithLocalData(title, world);
2803
+ if (newValue !== title) {
2804
+ this.logger.info(title + "=" + newValue);
2805
+ title = newValue;
2806
+ }
2807
+ const { matcher, queryText } = this._matcher(title);
2808
+ const state = {
2809
+ text_search: queryText,
2810
+ options,
2811
+ world,
2812
+ locate: false,
2813
+ scroll: false,
2814
+ highlight: false,
2815
+ type: Types.VERIFY_PAGE_TITLE,
2816
+ text: `Verify the page title is ${queryText}`,
2817
+ _text: `Verify the page title is ${queryText}`,
2818
+ operation: "verifyPageTitle",
2819
+ log: "***** verify page title is " + queryText + " *****\n",
2820
+ };
2821
+ try {
2822
+ await _preCommand(state, this);
2823
+ state.info.text = queryText;
2824
+ for (let i = 0; i < 30; i++) {
2825
+ const foundTitle = await this.page.title();
2826
+ switch (matcher) {
2827
+ case "exact":
2828
+ if (foundTitle !== queryText) {
2829
+ if (i === 29) {
2830
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2831
+ }
2832
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2833
+ continue;
2834
+ }
2835
+ break;
2836
+ case "contains":
2837
+ if (!foundTitle.includes(queryText)) {
2838
+ if (i === 29) {
2839
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2840
+ }
2841
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2842
+ continue;
2843
+ }
2844
+ break;
2845
+ case "starts-with":
2846
+ if (!foundTitle.startsWith(queryText)) {
2847
+ if (i === 29) {
2848
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2849
+ }
2850
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2851
+ continue;
2852
+ }
2853
+ break;
2854
+ case "ends-with":
2855
+ if (!foundTitle.endsWith(queryText)) {
2856
+ if (i === 29) {
2857
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2858
+ }
2859
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2860
+ continue;
2861
+ }
2862
+ break;
2863
+ case "regex":
2864
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2865
+ if (!regex.test(foundTitle)) {
2866
+ if (i === 29) {
2867
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2868
+ }
2869
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2870
+ continue;
2871
+ }
2872
+ break;
2873
+ default:
2874
+ console.log("Unknown matching type, defaulting to contains matching");
2875
+ if (!foundTitle.includes(title)) {
2876
+ if (i === 29) {
2877
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2878
+ }
2879
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2880
+ continue;
2881
+ }
2882
+ }
2883
+ await _screenshot(state, this);
2884
+ return state.info;
2885
+ }
2886
+ }
2887
+ catch (e) {
2888
+ state.info.failCause.lastError = e.message;
2889
+ state.info.failCause.assertionFailed = true;
2890
+ await _commandError(state, e, this);
2891
+ }
2892
+ finally {
2893
+ await _commandFinally(state, this);
2295
2894
  }
2296
2895
  }
2297
2896
  async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
@@ -2333,7 +2932,7 @@ class StableBrowser {
2333
2932
  scroll: false,
2334
2933
  highlight: false,
2335
2934
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2336
- text: `Verify the text '${text}' exists in page`,
2935
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2337
2936
  _text: `Verify the text '${text}' exists in page`,
2338
2937
  operation: "verifyTextExistInPage",
2339
2938
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2375,27 +2974,10 @@ class StableBrowser {
2375
2974
  const frame = resultWithElementsFound[0].frame;
2376
2975
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2377
2976
  await this._highlightElements(frame, dataAttribute);
2378
- // if (world && world.screenshot && !world.screenshotPath) {
2379
- // console.log(`Highlighting for verify text is found while running from recorder`);
2380
- // this._highlightElements(frame, dataAttribute).then(async () => {
2381
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2382
- // this._unhighlightElements(frame, dataAttribute)
2383
- // .then(async () => {
2384
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2385
- // })
2386
- // .catch(
2387
- // (e) => {}
2388
- // console.error(e)
2389
- // );
2390
- // });
2391
- // }
2392
2977
  const element = await frame.locator(dataAttribute).first();
2393
- // await new Promise((resolve) => setTimeout(resolve, 100));
2394
- // await this._unhighlightElements(frame, dataAttribute);
2395
2978
  if (element) {
2396
2979
  await this.scrollIfNeeded(element, state.info);
2397
2980
  await element.dispatchEvent("bvt_verify_page_contains_text");
2398
- // await _screenshot(state, this, element);
2399
2981
  }
2400
2982
  }
2401
2983
  await _screenshot(state, this);
@@ -2405,13 +2987,12 @@ class StableBrowser {
2405
2987
  console.error(error);
2406
2988
  }
2407
2989
  }
2408
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2409
2990
  }
2410
2991
  catch (e) {
2411
2992
  await _commandError(state, e, this);
2412
2993
  }
2413
2994
  finally {
2414
- _commandFinally(state, this);
2995
+ await _commandFinally(state, this);
2415
2996
  }
2416
2997
  }
2417
2998
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2424,7 +3005,7 @@ class StableBrowser {
2424
3005
  scroll: false,
2425
3006
  highlight: false,
2426
3007
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2427
- text: `Verify the text '${text}' does not exist in page`,
3008
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2428
3009
  _text: `Verify the text '${text}' does not exist in page`,
2429
3010
  operation: "verifyTextNotExistInPage",
2430
3011
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2468,7 +3049,7 @@ class StableBrowser {
2468
3049
  await _commandError(state, e, this);
2469
3050
  }
2470
3051
  finally {
2471
- _commandFinally(state, this);
3052
+ await _commandFinally(state, this);
2472
3053
  }
2473
3054
  }
2474
3055
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2538,7 +3119,7 @@ class StableBrowser {
2538
3119
  const count = await frame.locator(css).count();
2539
3120
  for (let j = 0; j < count; j++) {
2540
3121
  const continer = await frame.locator(css).nth(j);
2541
- const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
3122
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2542
3123
  if (result.elementCount > 0) {
2543
3124
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2544
3125
  await this._highlightElements(frame, dataAttribute);
@@ -2579,7 +3160,7 @@ class StableBrowser {
2579
3160
  await _commandError(state, e, this);
2580
3161
  }
2581
3162
  finally {
2582
- _commandFinally(state, this);
3163
+ await _commandFinally(state, this);
2583
3164
  }
2584
3165
  }
2585
3166
  async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
@@ -2921,8 +3502,51 @@ class StableBrowser {
2921
3502
  });
2922
3503
  }
2923
3504
  }
3505
+ /**
3506
+ * Explicit wait/sleep function that pauses execution for a specified duration
3507
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3508
+ * @param options - Optional configuration object
3509
+ * @param world - Optional world context
3510
+ * @returns Promise that resolves after the specified duration
3511
+ */
3512
+ async sleep(duration = 1000, options = {}, world = null) {
3513
+ const state = {
3514
+ duration,
3515
+ options,
3516
+ world,
3517
+ locate: false,
3518
+ scroll: false,
3519
+ screenshot: false,
3520
+ highlight: false,
3521
+ type: Types.SLEEP,
3522
+ text: `Sleep for ${duration} ms`,
3523
+ _text: `Sleep for ${duration} ms`,
3524
+ operation: "sleep",
3525
+ log: `***** Sleep for ${duration} ms *****\n`,
3526
+ };
3527
+ try {
3528
+ await _preCommand(state, this);
3529
+ if (duration < 0) {
3530
+ throw new Error("Sleep duration cannot be negative");
3531
+ }
3532
+ await new Promise((resolve) => setTimeout(resolve, duration));
3533
+ return state.info;
3534
+ }
3535
+ catch (e) {
3536
+ await _commandError(state, e, this);
3537
+ }
3538
+ finally {
3539
+ await _commandFinally(state, this);
3540
+ }
3541
+ }
2924
3542
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2925
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3543
+ try {
3544
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3545
+ }
3546
+ catch (error) {
3547
+ this.logger.debug(error);
3548
+ throw error;
3549
+ }
2926
3550
  }
2927
3551
  _getLoadTimeout(options) {
2928
3552
  let timeout = 15000;
@@ -2945,6 +3569,7 @@ class StableBrowser {
2945
3569
  }
2946
3570
  async saveStoreState(path = null, world = null) {
2947
3571
  const storageState = await this.page.context().storageState();
3572
+ path = await this._replaceWithLocalData(path, this.world);
2948
3573
  //const testDataFile = _getDataFile(world, this.context, this);
2949
3574
  if (path) {
2950
3575
  // save { storageState: storageState } into the path
@@ -2955,10 +3580,14 @@ class StableBrowser {
2955
3580
  }
2956
3581
  }
2957
3582
  async restoreSaveState(path = null, world = null) {
3583
+ path = await this._replaceWithLocalData(path, this.world);
2958
3584
  await refreshBrowser(this, path, world);
2959
3585
  this.registerEventListeners(this.context);
2960
3586
  registerNetworkEvents(this.world, this, this.context, this.page);
2961
3587
  registerDownloadEvent(this.page, this.world, this.context);
3588
+ if (this.onRestoreSaveState) {
3589
+ this.onRestoreSaveState(path);
3590
+ }
2962
3591
  }
2963
3592
  async waitForPageLoad(options = {}, world = null) {
2964
3593
  let timeout = this._getLoadTimeout(options);
@@ -3041,7 +3670,7 @@ class StableBrowser {
3041
3670
  await _commandError(state, e, this);
3042
3671
  }
3043
3672
  finally {
3044
- _commandFinally(state, this);
3673
+ await _commandFinally(state, this);
3045
3674
  }
3046
3675
  }
3047
3676
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3128,7 +3757,7 @@ class StableBrowser {
3128
3757
  await _commandError(state, e, this);
3129
3758
  }
3130
3759
  finally {
3131
- _commandFinally(state, this);
3760
+ await _commandFinally(state, this);
3132
3761
  }
3133
3762
  }
3134
3763
  saveTestDataAsGlobal(options, world) {
@@ -3233,7 +3862,39 @@ class StableBrowser {
3233
3862
  console.log("#-#");
3234
3863
  }
3235
3864
  }
3865
+ async beforeScenario(world, scenario) {
3866
+ this.beforeScenarioCalled = true;
3867
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3868
+ this.scenarioName = scenario.pickle.name;
3869
+ }
3870
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3871
+ this.featureName = scenario.gherkinDocument.feature.name;
3872
+ }
3873
+ if (this.context) {
3874
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3875
+ }
3876
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3877
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3878
+ // check if @global_test_data tag is present
3879
+ if (this.tags.includes("@global_test_data")) {
3880
+ this.saveTestDataAsGlobal({}, world);
3881
+ }
3882
+ }
3883
+ // update test data based on feature/scenario
3884
+ let envName = null;
3885
+ if (this.context && this.context.environment) {
3886
+ envName = this.context.environment.name;
3887
+ }
3888
+ if (!process.env.TEMP_RUN) {
3889
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3890
+ }
3891
+ await loadBrunoParams(this.context, this.context.environment.name);
3892
+ }
3893
+ async afterScenario(world, scenario) { }
3236
3894
  async beforeStep(world, step) {
3895
+ if (!this.beforeScenarioCalled) {
3896
+ this.beforeScenario(world, step);
3897
+ }
3237
3898
  if (this.stepIndex === undefined) {
3238
3899
  this.stepIndex = 0;
3239
3900
  }
@@ -3250,24 +3911,14 @@ class StableBrowser {
3250
3911
  else {
3251
3912
  this.stepName = "step " + this.stepIndex;
3252
3913
  }
3253
- if (this.context) {
3254
- this.context.examplesRow = extractStepExampleParameters(step);
3255
- }
3256
3914
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3257
3915
  if (this.context.browserObject.context) {
3258
3916
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3259
3917
  }
3260
3918
  }
3261
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3262
- this.tags = step.pickle.tags.map((tag) => tag.name);
3263
- // check if @global_test_data tag is present
3264
- if (this.tags.includes("@global_test_data")) {
3265
- this.saveTestDataAsGlobal({}, world);
3266
- }
3267
- }
3268
3919
  if (this.initSnapshotTaken === false) {
3269
3920
  this.initSnapshotTaken = true;
3270
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3921
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
3271
3922
  const snapshot = await this.getAriaSnapshot();
3272
3923
  if (snapshot) {
3273
3924
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -3289,18 +3940,68 @@ class StableBrowser {
3289
3940
  const content = [`- path: ${path}`, `- title: ${title}`];
3290
3941
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3291
3942
  for (let i = 0; i < frames.length; i++) {
3292
- content.push(`- frame: ${i}`);
3293
3943
  const frame = frames[i];
3294
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3295
- content.push(snapshot);
3944
+ try {
3945
+ // Ensure frame is attached and has body
3946
+ const body = frame.locator("body");
3947
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3948
+ const snapshot = await body.ariaSnapshot({ timeout });
3949
+ content.push(`- frame: ${i}`);
3950
+ content.push(snapshot);
3951
+ }
3952
+ catch (innerErr) { }
3296
3953
  }
3297
3954
  return content.join("\n");
3298
3955
  }
3299
3956
  catch (e) {
3300
- console.error(e);
3957
+ console.log("Error in getAriaSnapshot");
3958
+ //console.debug(e);
3301
3959
  }
3302
3960
  return null;
3303
3961
  }
3962
+ /**
3963
+ * Sends command with custom payload to report.
3964
+ * @param commandText - Title of the command to be shown in the report.
3965
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3966
+ * @param content - Content of the command to be shown in the report.
3967
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3968
+ * @param world - Optional world context.
3969
+ * @public
3970
+ */
3971
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3972
+ const state = {
3973
+ options,
3974
+ world,
3975
+ locate: false,
3976
+ scroll: false,
3977
+ screenshot: options.screenshot ?? false,
3978
+ highlight: options.highlight ?? false,
3979
+ type: Types.REPORT_COMMAND,
3980
+ text: commandText,
3981
+ _text: commandText,
3982
+ operation: "report_command",
3983
+ log: "***** " + commandText + " *****\n",
3984
+ };
3985
+ try {
3986
+ await _preCommand(state, this);
3987
+ const payload = {
3988
+ type: options.type ?? "text",
3989
+ content: content,
3990
+ screenshotId: null,
3991
+ };
3992
+ state.payload = payload;
3993
+ if (commandStatus === "FAILED") {
3994
+ state.throwError = true;
3995
+ throw new Error("Command failed");
3996
+ }
3997
+ }
3998
+ catch (e) {
3999
+ await _commandError(state, e, this);
4000
+ }
4001
+ finally {
4002
+ await _commandFinally(state, this);
4003
+ }
4004
+ }
3304
4005
  async afterStep(world, step) {
3305
4006
  this.stepName = null;
3306
4007
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3308,6 +4009,13 @@ class StableBrowser {
3308
4009
  await this.context.browserObject.context.tracing.stopChunk({
3309
4010
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3310
4011
  });
4012
+ if (world && world.attach) {
4013
+ await world.attach(JSON.stringify({
4014
+ type: "trace",
4015
+ traceFilePath: `trace-${this.stepIndex}.zip`,
4016
+ }), "application/json+trace");
4017
+ }
4018
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3311
4019
  }
3312
4020
  }
3313
4021
  if (this.context) {
@@ -3320,6 +4028,29 @@ class StableBrowser {
3320
4028
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3321
4029
  }
3322
4030
  }
4031
+ if (!process.env.TEMP_RUN) {
4032
+ const state = {
4033
+ world,
4034
+ locate: false,
4035
+ scroll: false,
4036
+ screenshot: true,
4037
+ highlight: true,
4038
+ type: Types.STEP_COMPLETE,
4039
+ text: "end of scenario",
4040
+ _text: "end of scenario",
4041
+ operation: "step_complete",
4042
+ log: "***** " + "end of scenario" + " *****\n",
4043
+ };
4044
+ try {
4045
+ await _preCommand(state, this);
4046
+ }
4047
+ catch (e) {
4048
+ await _commandError(state, e, this);
4049
+ }
4050
+ finally {
4051
+ await _commandFinally(state, this);
4052
+ }
4053
+ }
3323
4054
  }
3324
4055
  }
3325
4056
  function createTimedPromise(promise, label) {