automation_model 1.0.648-dev → 1.0.648-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 (53) hide show
  1. package/lib/analyze_helper.js.map +1 -1
  2. package/lib/api.d.ts +0 -1
  3. package/lib/api.js +35 -21
  4. package/lib/api.js.map +1 -1
  5. package/lib/auto_page.d.ts +1 -1
  6. package/lib/auto_page.js +143 -57
  7. package/lib/auto_page.js.map +1 -1
  8. package/lib/browser_manager.js +28 -8
  9. package/lib/browser_manager.js.map +1 -1
  10. package/lib/bruno.d.ts +2 -0
  11. package/lib/bruno.js +381 -0
  12. package/lib/bruno.js.map +1 -0
  13. package/lib/command_common.d.ts +1 -1
  14. package/lib/command_common.js +24 -4
  15. package/lib/command_common.js.map +1 -1
  16. package/lib/date_time.js.map +1 -1
  17. package/lib/drawRect.js.map +1 -1
  18. package/lib/environment.d.ts +1 -0
  19. package/lib/environment.js +1 -0
  20. package/lib/environment.js.map +1 -1
  21. package/lib/error-messages.js.map +1 -1
  22. package/lib/file_checker.d.ts +1 -0
  23. package/lib/file_checker.js +61 -0
  24. package/lib/file_checker.js.map +1 -0
  25. package/lib/find_function.js.map +1 -1
  26. package/lib/index.d.ts +2 -0
  27. package/lib/index.js +2 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/init_browser.js +4 -4
  30. package/lib/init_browser.js.map +1 -1
  31. package/lib/locate_element.js.map +1 -1
  32. package/lib/locator.js +1 -1
  33. package/lib/locator.js.map +1 -1
  34. package/lib/locator_log.js.map +1 -1
  35. package/lib/network.js.map +1 -1
  36. package/lib/scripts/axe.mini.js +3 -3
  37. package/lib/snapshot_validation.d.ts +37 -0
  38. package/lib/snapshot_validation.js +357 -0
  39. package/lib/snapshot_validation.js.map +1 -0
  40. package/lib/stable_browser.d.ts +59 -24
  41. package/lib/stable_browser.js +908 -186
  42. package/lib/stable_browser.js.map +1 -1
  43. package/lib/table.js.map +1 -1
  44. package/lib/table_analyze.js.map +1 -1
  45. package/lib/table_helper.js +15 -0
  46. package/lib/table_helper.js.map +1 -1
  47. package/lib/test_context.d.ts +1 -0
  48. package/lib/test_context.js +1 -0
  49. package/lib/test_context.js.map +1 -1
  50. package/lib/utils.d.ts +5 -3
  51. package/lib/utils.js +154 -65
  52. package/lib/utils.js.map +1 -1
  53. 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,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
  }
@@ -1047,8 +1120,8 @@ class StableBrowser {
1047
1120
  try {
1048
1121
  // if (world && world.screenshot && !world.screenshotPath) {
1049
1122
  // console.log(`Highlighting while running from recorder`);
1050
- await this._highlightElements(element);
1051
- await state.element.setChecked(checked);
1123
+ await this._highlightElements(state.element);
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) {
@@ -1648,6 +1852,15 @@ class StableBrowser {
1648
1852
  // save the data to the file
1649
1853
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1650
1854
  }
1855
+ overwriteTestData(testData, world = null) {
1856
+ if (!testData) {
1857
+ return;
1858
+ }
1859
+ // if data file exists, load it
1860
+ const dataFile = _getDataFile(world, this.context, this);
1861
+ // save the data to the file
1862
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1863
+ }
1651
1864
  _getDataFilePath(fileName) {
1652
1865
  let dataFile = path.join(this.project_path, "data", fileName);
1653
1866
  if (fs.existsSync(dataFile)) {
@@ -1900,7 +2113,7 @@ class StableBrowser {
1900
2113
  await _commandError(state, e, this);
1901
2114
  }
1902
2115
  finally {
1903
- _commandFinally(state, this);
2116
+ await _commandFinally(state, this);
1904
2117
  }
1905
2118
  }
1906
2119
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1931,10 +2144,102 @@ class StableBrowser {
1931
2144
  case "value":
1932
2145
  state.value = await state.element.inputValue();
1933
2146
  break;
2147
+ case "text":
2148
+ state.value = await state.element.textContent();
2149
+ break;
1934
2150
  default:
1935
2151
  state.value = await state.element.getAttribute(attribute);
1936
2152
  break;
1937
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
+ }
1938
2243
  state.info.value = state.value;
1939
2244
  this.setTestData({ [variable]: state.value }, world);
1940
2245
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1945,7 +2250,7 @@ class StableBrowser {
1945
2250
  await _commandError(state, e, this);
1946
2251
  }
1947
2252
  finally {
1948
- _commandFinally(state, this);
2253
+ await _commandFinally(state, this);
1949
2254
  }
1950
2255
  }
1951
2256
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1970,12 +2275,15 @@ class StableBrowser {
1970
2275
  let expectedValue;
1971
2276
  try {
1972
2277
  await _preCommand(state, this);
1973
- expectedValue = state.value;
2278
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1974
2279
  state.info.expectedValue = expectedValue;
1975
2280
  switch (attribute) {
1976
2281
  case "innerText":
1977
2282
  val = String(await state.element.innerText());
1978
2283
  break;
2284
+ case "text":
2285
+ val = String(await state.element.textContent());
2286
+ break;
1979
2287
  case "value":
1980
2288
  val = String(await state.element.inputValue());
1981
2289
  break;
@@ -1997,17 +2305,145 @@ class StableBrowser {
1997
2305
  let regex;
1998
2306
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1999
2307
  const patternBody = expectedValue.slice(1, -1);
2000
- regex = new RegExp(patternBody, "g");
2308
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2309
+ regex = new RegExp(processedPattern, "gs");
2310
+ state.info.regex = true;
2311
+ }
2312
+ else {
2313
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2314
+ regex = new RegExp(escapedPattern, "g");
2315
+ }
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
+ }
2344
+ }
2345
+ return state.info;
2346
+ }
2347
+ catch (e) {
2348
+ await _commandError(state, e, this);
2349
+ }
2350
+ finally {
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
+ default:
2399
+ if (property.startsWith("dataset.")) {
2400
+ const dataAttribute = property.substring(8);
2401
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2402
+ }
2403
+ else {
2404
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2405
+ }
2406
+ }
2407
+ state.info.value = val;
2408
+ let regex;
2409
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2410
+ const patternBody = expectedValue.slice(1, -1);
2411
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2412
+ regex = new RegExp(processedPattern, "gs");
2413
+ state.info.regex = true;
2001
2414
  }
2002
2415
  else {
2003
2416
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2004
2417
  regex = new RegExp(escapedPattern, "g");
2005
2418
  }
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);
2419
+ if (property === "innerText") {
2420
+ if (state.info.regex) {
2421
+ if (!regex.test(val)) {
2422
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2423
+ state.info.failCause.assertionFailed = true;
2424
+ state.info.failCause.lastError = errorMessage;
2425
+ throw new Error(errorMessage);
2426
+ }
2427
+ }
2428
+ else {
2429
+ const valLines = val.split("\n");
2430
+ const expectedLines = expectedValue.split("\n");
2431
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2432
+ if (!isPart) {
2433
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2434
+ state.info.failCause.assertionFailed = true;
2435
+ state.info.failCause.lastError = errorMessage;
2436
+ throw new Error(errorMessage);
2437
+ }
2438
+ }
2439
+ }
2440
+ else {
2441
+ if (!val.match(regex)) {
2442
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2443
+ state.info.failCause.assertionFailed = true;
2444
+ state.info.failCause.lastError = errorMessage;
2445
+ throw new Error(errorMessage);
2446
+ }
2011
2447
  }
2012
2448
  return state.info;
2013
2449
  }
@@ -2015,7 +2451,7 @@ class StableBrowser {
2015
2451
  await _commandError(state, e, this);
2016
2452
  }
2017
2453
  finally {
2018
- _commandFinally(state, this);
2454
+ await _commandFinally(state, this);
2019
2455
  }
2020
2456
  }
2021
2457
  async extractEmailData(emailAddress, options, world) {
@@ -2175,56 +2611,49 @@ class StableBrowser {
2175
2611
  console.debug(error);
2176
2612
  }
2177
2613
  }
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
- // }
2614
+ _matcher(text) {
2615
+ if (!text) {
2616
+ return { matcher: "contains", queryText: "" };
2617
+ }
2618
+ if (text.length < 2) {
2619
+ return { matcher: "contains", queryText: text };
2620
+ }
2621
+ const split = text.split(":");
2622
+ const matcher = split[0].toLowerCase();
2623
+ const queryText = split.slice(1).join(":").trim();
2624
+ return { matcher, queryText };
2625
+ }
2626
+ _getDomain(url) {
2627
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2628
+ return "";
2629
+ }
2630
+ let hostnameFragments = url.split("/")[2].split(".");
2631
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2632
+ return hostnameFragments.join("-").split(":").join("-");
2633
+ }
2634
+ let n = hostnameFragments.length;
2635
+ let fragments = [...hostnameFragments];
2636
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2637
+ hostnameFragments.pop();
2638
+ n = hostnameFragments.length;
2639
+ }
2640
+ if (n == 0) {
2641
+ if (fragments[0] === "www")
2642
+ fragments = fragments.slice(1);
2643
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2644
+ }
2645
+ if (hostnameFragments[0] === "www")
2646
+ hostnameFragments = hostnameFragments.slice(1);
2647
+ return hostnameFragments.join(".");
2648
+ }
2649
+ /**
2650
+ * Verify the page path matches the given path.
2651
+ * @param {string} pathPart - The path to verify.
2652
+ * @param {object} options - Options for verification.
2653
+ * @param {object} world - The world context.
2654
+ * @returns {Promise<object>} - The state info after verification.
2655
+ */
2226
2656
  async verifyPagePath(pathPart, options = {}, world = null) {
2227
- const startTime = Date.now();
2228
2657
  let error = null;
2229
2658
  let screenshotId = null;
2230
2659
  let screenshotPath = null;
@@ -2238,51 +2667,212 @@ class StableBrowser {
2238
2667
  pathPart = newValue;
2239
2668
  }
2240
2669
  info.pathPart = pathPart;
2670
+ const { matcher, queryText } = this._matcher(pathPart);
2671
+ const state = {
2672
+ text_search: queryText,
2673
+ options,
2674
+ world,
2675
+ locate: false,
2676
+ scroll: false,
2677
+ highlight: false,
2678
+ type: Types.VERIFY_PAGE_PATH,
2679
+ text: `Verify the page url is ${queryText}`,
2680
+ _text: `Verify the page url is ${queryText}`,
2681
+ operation: "verifyPagePath",
2682
+ log: "***** verify page url is " + queryText + " *****\n",
2683
+ };
2241
2684
  try {
2685
+ await _preCommand(state, this);
2686
+ state.info.text = queryText;
2242
2687
  for (let i = 0; i < 30; i++) {
2243
2688
  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;
2689
+ switch (matcher) {
2690
+ case "exact":
2691
+ if (url !== queryText) {
2692
+ if (i === 29) {
2693
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2694
+ }
2695
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2696
+ continue;
2697
+ }
2698
+ break;
2699
+ case "contains":
2700
+ if (!url.includes(queryText)) {
2701
+ if (i === 29) {
2702
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2703
+ }
2704
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2705
+ continue;
2706
+ }
2707
+ break;
2708
+ case "starts-with":
2709
+ {
2710
+ const domain = this._getDomain(url);
2711
+ if (domain.length > 0 && domain !== queryText) {
2712
+ if (i === 29) {
2713
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2714
+ }
2715
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2716
+ continue;
2717
+ }
2718
+ }
2719
+ break;
2720
+ case "ends-with":
2721
+ {
2722
+ const urlObj = new URL(url);
2723
+ let route = "/";
2724
+ if (urlObj.pathname !== "/") {
2725
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2726
+ }
2727
+ else {
2728
+ route = "/";
2729
+ }
2730
+ if (route !== queryText) {
2731
+ if (i === 29) {
2732
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2733
+ }
2734
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2735
+ continue;
2736
+ }
2737
+ }
2738
+ break;
2739
+ case "regex":
2740
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2741
+ if (!regex.test(url)) {
2742
+ if (i === 29) {
2743
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2744
+ }
2745
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2746
+ continue;
2747
+ }
2748
+ break;
2749
+ default:
2750
+ console.log("Unknown matching type, defaulting to contains matching");
2751
+ if (!url.includes(pathPart)) {
2752
+ if (i === 29) {
2753
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2754
+ }
2755
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2756
+ continue;
2757
+ }
2250
2758
  }
2251
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2252
- return info;
2759
+ await _screenshot(state, this);
2760
+ return state.info;
2253
2761
  }
2254
2762
  }
2255
2763
  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);
2764
+ state.info.failCause.lastError = e.message;
2765
+ state.info.failCause.assertionFailed = true;
2766
+ await _commandError(state, e, this);
2264
2767
  }
2265
2768
  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
- });
2769
+ await _commandFinally(state, this);
2770
+ }
2771
+ }
2772
+ /**
2773
+ * Verify the page title matches the given title.
2774
+ * @param {string} title - The title to verify.
2775
+ * @param {object} options - Options for verification.
2776
+ * @param {object} world - The world context.
2777
+ * @returns {Promise<object>} - The state info after verification.
2778
+ */
2779
+ async verifyPageTitle(title, options = {}, world = null) {
2780
+ let error = null;
2781
+ let screenshotId = null;
2782
+ let screenshotPath = null;
2783
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2784
+ const newValue = await this._replaceWithLocalData(title, world);
2785
+ if (newValue !== title) {
2786
+ this.logger.info(title + "=" + newValue);
2787
+ title = newValue;
2788
+ }
2789
+ const { matcher, queryText } = this._matcher(title);
2790
+ const state = {
2791
+ text_search: queryText,
2792
+ options,
2793
+ world,
2794
+ locate: false,
2795
+ scroll: false,
2796
+ highlight: false,
2797
+ type: Types.VERIFY_PAGE_TITLE,
2798
+ text: `Verify the page title is ${queryText}`,
2799
+ _text: `Verify the page title is ${queryText}`,
2800
+ operation: "verifyPageTitle",
2801
+ log: "***** verify page title is " + queryText + " *****\n",
2802
+ };
2803
+ try {
2804
+ await _preCommand(state, this);
2805
+ state.info.text = queryText;
2806
+ for (let i = 0; i < 30; i++) {
2807
+ const foundTitle = await this.page.title();
2808
+ switch (matcher) {
2809
+ case "exact":
2810
+ if (foundTitle !== queryText) {
2811
+ if (i === 29) {
2812
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2813
+ }
2814
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2815
+ continue;
2816
+ }
2817
+ break;
2818
+ case "contains":
2819
+ if (!foundTitle.includes(queryText)) {
2820
+ if (i === 29) {
2821
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2822
+ }
2823
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2824
+ continue;
2825
+ }
2826
+ break;
2827
+ case "starts-with":
2828
+ if (!foundTitle.startsWith(queryText)) {
2829
+ if (i === 29) {
2830
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2831
+ }
2832
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2833
+ continue;
2834
+ }
2835
+ break;
2836
+ case "ends-with":
2837
+ if (!foundTitle.endsWith(queryText)) {
2838
+ if (i === 29) {
2839
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2840
+ }
2841
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2842
+ continue;
2843
+ }
2844
+ break;
2845
+ case "regex":
2846
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2847
+ if (!regex.test(foundTitle)) {
2848
+ if (i === 29) {
2849
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2850
+ }
2851
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2852
+ continue;
2853
+ }
2854
+ break;
2855
+ default:
2856
+ console.log("Unknown matching type, defaulting to contains matching");
2857
+ if (!foundTitle.includes(title)) {
2858
+ if (i === 29) {
2859
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2860
+ }
2861
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2862
+ continue;
2863
+ }
2864
+ }
2865
+ await _screenshot(state, this);
2866
+ return state.info;
2867
+ }
2868
+ }
2869
+ catch (e) {
2870
+ state.info.failCause.lastError = e.message;
2871
+ state.info.failCause.assertionFailed = true;
2872
+ await _commandError(state, e, this);
2873
+ }
2874
+ finally {
2875
+ await _commandFinally(state, this);
2286
2876
  }
2287
2877
  }
2288
2878
  async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
@@ -2324,7 +2914,7 @@ class StableBrowser {
2324
2914
  scroll: false,
2325
2915
  highlight: false,
2326
2916
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2327
- text: `Verify the text '${text}' exists in page`,
2917
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2328
2918
  _text: `Verify the text '${text}' exists in page`,
2329
2919
  operation: "verifyTextExistInPage",
2330
2920
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2366,27 +2956,10 @@ class StableBrowser {
2366
2956
  const frame = resultWithElementsFound[0].frame;
2367
2957
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2368
2958
  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
2959
  const element = await frame.locator(dataAttribute).first();
2384
- // await new Promise((resolve) => setTimeout(resolve, 100));
2385
- // await this._unhighlightElements(frame, dataAttribute);
2386
2960
  if (element) {
2387
2961
  await this.scrollIfNeeded(element, state.info);
2388
2962
  await element.dispatchEvent("bvt_verify_page_contains_text");
2389
- // await _screenshot(state, this, element);
2390
2963
  }
2391
2964
  }
2392
2965
  await _screenshot(state, this);
@@ -2396,13 +2969,12 @@ class StableBrowser {
2396
2969
  console.error(error);
2397
2970
  }
2398
2971
  }
2399
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2400
2972
  }
2401
2973
  catch (e) {
2402
2974
  await _commandError(state, e, this);
2403
2975
  }
2404
2976
  finally {
2405
- _commandFinally(state, this);
2977
+ await _commandFinally(state, this);
2406
2978
  }
2407
2979
  }
2408
2980
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2415,7 +2987,7 @@ class StableBrowser {
2415
2987
  scroll: false,
2416
2988
  highlight: false,
2417
2989
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2418
- text: `Verify text does not exist in page`,
2990
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2419
2991
  _text: `Verify the text '${text}' does not exist in page`,
2420
2992
  operation: "verifyTextNotExistInPage",
2421
2993
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2459,7 +3031,7 @@ class StableBrowser {
2459
3031
  await _commandError(state, e, this);
2460
3032
  }
2461
3033
  finally {
2462
- _commandFinally(state, this);
3034
+ await _commandFinally(state, this);
2463
3035
  }
2464
3036
  }
2465
3037
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2529,7 +3101,7 @@ class StableBrowser {
2529
3101
  const count = await frame.locator(css).count();
2530
3102
  for (let j = 0; j < count; j++) {
2531
3103
  const continer = await frame.locator(css).nth(j);
2532
- const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
3104
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2533
3105
  if (result.elementCount > 0) {
2534
3106
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2535
3107
  await this._highlightElements(frame, dataAttribute);
@@ -2570,7 +3142,7 @@ class StableBrowser {
2570
3142
  await _commandError(state, e, this);
2571
3143
  }
2572
3144
  finally {
2573
- _commandFinally(state, this);
3145
+ await _commandFinally(state, this);
2574
3146
  }
2575
3147
  }
2576
3148
  async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
@@ -2912,8 +3484,51 @@ class StableBrowser {
2912
3484
  });
2913
3485
  }
2914
3486
  }
3487
+ /**
3488
+ * Explicit wait/sleep function that pauses execution for a specified duration
3489
+ * @param duration - Duration to sleep in milliseconds (default: 1000ms)
3490
+ * @param options - Optional configuration object
3491
+ * @param world - Optional world context
3492
+ * @returns Promise that resolves after the specified duration
3493
+ */
3494
+ async sleep(duration = 1000, options = {}, world = null) {
3495
+ const state = {
3496
+ duration,
3497
+ options,
3498
+ world,
3499
+ locate: false,
3500
+ scroll: false,
3501
+ screenshot: false,
3502
+ highlight: false,
3503
+ type: Types.SLEEP,
3504
+ text: `Sleep for ${duration} ms`,
3505
+ _text: `Sleep for ${duration} ms`,
3506
+ operation: "sleep",
3507
+ log: `***** Sleep for ${duration} ms *****\n`,
3508
+ };
3509
+ try {
3510
+ await _preCommand(state, this);
3511
+ if (duration < 0) {
3512
+ throw new Error("Sleep duration cannot be negative");
3513
+ }
3514
+ await new Promise((resolve) => setTimeout(resolve, duration));
3515
+ return state.info;
3516
+ }
3517
+ catch (e) {
3518
+ await _commandError(state, e, this);
3519
+ }
3520
+ finally {
3521
+ await _commandFinally(state, this);
3522
+ }
3523
+ }
2915
3524
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2916
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3525
+ try {
3526
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3527
+ }
3528
+ catch (error) {
3529
+ this.logger.debug(error);
3530
+ throw error;
3531
+ }
2917
3532
  }
2918
3533
  _getLoadTimeout(options) {
2919
3534
  let timeout = 15000;
@@ -2936,6 +3551,7 @@ class StableBrowser {
2936
3551
  }
2937
3552
  async saveStoreState(path = null, world = null) {
2938
3553
  const storageState = await this.page.context().storageState();
3554
+ path = await this._replaceWithLocalData(path, this.world);
2939
3555
  //const testDataFile = _getDataFile(world, this.context, this);
2940
3556
  if (path) {
2941
3557
  // save { storageState: storageState } into the path
@@ -2946,10 +3562,14 @@ class StableBrowser {
2946
3562
  }
2947
3563
  }
2948
3564
  async restoreSaveState(path = null, world = null) {
3565
+ path = await this._replaceWithLocalData(path, this.world);
2949
3566
  await refreshBrowser(this, path, world);
2950
3567
  this.registerEventListeners(this.context);
2951
3568
  registerNetworkEvents(this.world, this, this.context, this.page);
2952
3569
  registerDownloadEvent(this.page, this.world, this.context);
3570
+ if (this.onRestoreSaveState) {
3571
+ this.onRestoreSaveState(path);
3572
+ }
2953
3573
  }
2954
3574
  async waitForPageLoad(options = {}, world = null) {
2955
3575
  let timeout = this._getLoadTimeout(options);
@@ -3032,7 +3652,7 @@ class StableBrowser {
3032
3652
  await _commandError(state, e, this);
3033
3653
  }
3034
3654
  finally {
3035
- _commandFinally(state, this);
3655
+ await _commandFinally(state, this);
3036
3656
  }
3037
3657
  }
3038
3658
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3119,7 +3739,7 @@ class StableBrowser {
3119
3739
  await _commandError(state, e, this);
3120
3740
  }
3121
3741
  finally {
3122
- _commandFinally(state, this);
3742
+ await _commandFinally(state, this);
3123
3743
  }
3124
3744
  }
3125
3745
  saveTestDataAsGlobal(options, world) {
@@ -3224,7 +3844,39 @@ class StableBrowser {
3224
3844
  console.log("#-#");
3225
3845
  }
3226
3846
  }
3847
+ async beforeScenario(world, scenario) {
3848
+ this.beforeScenarioCalled = true;
3849
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3850
+ this.scenarioName = scenario.pickle.name;
3851
+ }
3852
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3853
+ this.featureName = scenario.gherkinDocument.feature.name;
3854
+ }
3855
+ if (this.context) {
3856
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3857
+ }
3858
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3859
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3860
+ // check if @global_test_data tag is present
3861
+ if (this.tags.includes("@global_test_data")) {
3862
+ this.saveTestDataAsGlobal({}, world);
3863
+ }
3864
+ }
3865
+ // update test data based on feature/scenario
3866
+ let envName = null;
3867
+ if (this.context && this.context.environment) {
3868
+ envName = this.context.environment.name;
3869
+ }
3870
+ if (!process.env.TEMP_RUN) {
3871
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3872
+ }
3873
+ await loadBrunoParams(this.context, this.context.environment.name);
3874
+ }
3875
+ async afterScenario(world, scenario) { }
3227
3876
  async beforeStep(world, step) {
3877
+ if (!this.beforeScenarioCalled) {
3878
+ this.beforeScenario(world, step);
3879
+ }
3228
3880
  if (this.stepIndex === undefined) {
3229
3881
  this.stepIndex = 0;
3230
3882
  }
@@ -3241,24 +3893,14 @@ class StableBrowser {
3241
3893
  else {
3242
3894
  this.stepName = "step " + this.stepIndex;
3243
3895
  }
3244
- if (this.context) {
3245
- this.context.examplesRow = extractStepExampleParameters(step);
3246
- }
3247
3896
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3248
3897
  if (this.context.browserObject.context) {
3249
3898
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3250
3899
  }
3251
3900
  }
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
3901
  if (this.initSnapshotTaken === false) {
3260
3902
  this.initSnapshotTaken = true;
3261
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3903
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
3262
3904
  const snapshot = await this.getAriaSnapshot();
3263
3905
  if (snapshot) {
3264
3906
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -3280,18 +3922,68 @@ class StableBrowser {
3280
3922
  const content = [`- path: ${path}`, `- title: ${title}`];
3281
3923
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3282
3924
  for (let i = 0; i < frames.length; i++) {
3283
- content.push(`- frame: ${i}`);
3284
3925
  const frame = frames[i];
3285
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3286
- content.push(snapshot);
3926
+ try {
3927
+ // Ensure frame is attached and has body
3928
+ const body = frame.locator("body");
3929
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3930
+ const snapshot = await body.ariaSnapshot({ timeout });
3931
+ content.push(`- frame: ${i}`);
3932
+ content.push(snapshot);
3933
+ }
3934
+ catch (innerErr) { }
3287
3935
  }
3288
3936
  return content.join("\n");
3289
3937
  }
3290
3938
  catch (e) {
3291
- console.error(e);
3939
+ console.log("Error in getAriaSnapshot");
3940
+ //console.debug(e);
3292
3941
  }
3293
3942
  return null;
3294
3943
  }
3944
+ /**
3945
+ * Sends command with custom payload to report.
3946
+ * @param commandText - Title of the command to be shown in the report.
3947
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3948
+ * @param content - Content of the command to be shown in the report.
3949
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3950
+ * @param world - Optional world context.
3951
+ * @public
3952
+ */
3953
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3954
+ const state = {
3955
+ options,
3956
+ world,
3957
+ locate: false,
3958
+ scroll: false,
3959
+ screenshot: options.screenshot ?? false,
3960
+ highlight: options.highlight ?? false,
3961
+ type: Types.REPORT_COMMAND,
3962
+ text: commandText,
3963
+ _text: commandText,
3964
+ operation: "report_command",
3965
+ log: "***** " + commandText + " *****\n",
3966
+ };
3967
+ try {
3968
+ await _preCommand(state, this);
3969
+ const payload = {
3970
+ type: options.type ?? "text",
3971
+ content: content,
3972
+ screenshotId: null,
3973
+ };
3974
+ state.payload = payload;
3975
+ if (commandStatus === "FAILED") {
3976
+ state.throwError = true;
3977
+ throw new Error("Command failed");
3978
+ }
3979
+ }
3980
+ catch (e) {
3981
+ await _commandError(state, e, this);
3982
+ }
3983
+ finally {
3984
+ await _commandFinally(state, this);
3985
+ }
3986
+ }
3295
3987
  async afterStep(world, step) {
3296
3988
  this.stepName = null;
3297
3989
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3299,6 +3991,13 @@ class StableBrowser {
3299
3991
  await this.context.browserObject.context.tracing.stopChunk({
3300
3992
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3301
3993
  });
3994
+ if (world && world.attach) {
3995
+ await world.attach(JSON.stringify({
3996
+ type: "trace",
3997
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3998
+ }), "application/json+trace");
3999
+ }
4000
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3302
4001
  }
3303
4002
  }
3304
4003
  if (this.context) {
@@ -3311,6 +4010,29 @@ class StableBrowser {
3311
4010
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3312
4011
  }
3313
4012
  }
4013
+ if (!process.env.TEMP_RUN) {
4014
+ const state = {
4015
+ world,
4016
+ locate: false,
4017
+ scroll: false,
4018
+ screenshot: true,
4019
+ highlight: true,
4020
+ type: Types.STEP_COMPLETE,
4021
+ text: "end of scenario",
4022
+ _text: "end of scenario",
4023
+ operation: "step_complete",
4024
+ log: "***** " + "end of scenario" + " *****\n",
4025
+ };
4026
+ try {
4027
+ await _preCommand(state, this);
4028
+ }
4029
+ catch (e) {
4030
+ await _commandError(state, e, this);
4031
+ }
4032
+ finally {
4033
+ await _commandFinally(state, this);
4034
+ }
4035
+ }
3314
4036
  }
3315
4037
  }
3316
4038
  function createTimedPromise(promise, label) {