automation_model 1.0.647-dev → 1.0.647-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 +50 -24
  41. package/lib/stable_browser.js +870 -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 +157 -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,12 @@ export const Types = {
55
59
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
56
60
  VERIFY_ATTRIBUTE: "verify_element_attribute",
57
61
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
62
+ BRUNO: "bruno",
63
+ VERIFY_FILE_EXISTS: "verify_file_exists",
64
+ SET_INPUT_FILES: "set_input_files",
65
+ SNAPSHOT_VALIDATION: "snapshot_validation",
66
+ REPORT_COMMAND: "report_command",
67
+ STEP_COMPLETE: "step_complete",
58
68
  };
59
69
  export const apps = {};
60
70
  const formatElementName = (elementName) => {
@@ -66,6 +76,7 @@ class StableBrowser {
66
76
  logger;
67
77
  context;
68
78
  world;
79
+ fastMode;
69
80
  project_path = null;
70
81
  webLogFile = null;
71
82
  networkLogger = null;
@@ -74,12 +85,13 @@ class StableBrowser {
74
85
  tags = null;
75
86
  isRecording = false;
76
87
  initSnapshotTaken = false;
77
- constructor(browser, page, logger = null, context = null, world = null) {
88
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
78
89
  this.browser = browser;
79
90
  this.page = page;
80
91
  this.logger = logger;
81
92
  this.context = context;
82
93
  this.world = world;
94
+ this.fastMode = fastMode;
83
95
  if (!this.logger) {
84
96
  this.logger = console;
85
97
  }
@@ -108,6 +120,12 @@ class StableBrowser {
108
120
  context.pages = [this.page];
109
121
  const logFolder = path.join(this.project_path, "logs", "web");
110
122
  this.world = world;
123
+ if (process.env.FAST_MODE === "true") {
124
+ this.fastMode = true;
125
+ }
126
+ if (this.context) {
127
+ this.context.fastMode = this.fastMode;
128
+ }
111
129
  this.registerEventListeners(this.context);
112
130
  registerNetworkEvents(this.world, this, this.context, this.page);
113
131
  registerDownloadEvent(this.page, this.world, this.context);
@@ -118,6 +136,9 @@ class StableBrowser {
118
136
  if (!context.pageLoading) {
119
137
  context.pageLoading = { status: false };
120
138
  }
139
+ if (this.configuration && this.configuration.acceptDialog && this.page) {
140
+ this.page.on("dialog", (dialog) => dialog.accept());
141
+ }
121
142
  context.playContext.on("page", async function (page) {
122
143
  if (this.configuration && this.configuration.closePopups === true) {
123
144
  console.log("close unexpected popups");
@@ -126,6 +147,14 @@ class StableBrowser {
126
147
  }
127
148
  context.pageLoading.status = true;
128
149
  this.page = page;
150
+ try {
151
+ if (this.configuration && this.configuration.acceptDialog) {
152
+ await page.on("dialog", (dialog) => dialog.accept());
153
+ }
154
+ }
155
+ catch (error) {
156
+ console.error("Error on dialog accept registration", error);
157
+ }
129
158
  context.page = page;
130
159
  context.pages.push(page);
131
160
  registerNetworkEvents(this.world, this, context, this.page);
@@ -177,9 +206,35 @@ class StableBrowser {
177
206
  if (newContextCreated) {
178
207
  this.registerEventListeners(this.context);
179
208
  await this.goto(this.context.environment.baseUrl);
180
- await this.waitForPageLoad();
209
+ if (!this.fastMode) {
210
+ await this.waitForPageLoad();
211
+ }
181
212
  }
182
213
  }
214
+ async switchTab(tabTitleOrIndex) {
215
+ // first check if the tabNameOrIndex is a number
216
+ let index = parseInt(tabTitleOrIndex);
217
+ if (!isNaN(index)) {
218
+ if (index >= 0 && index < this.context.pages.length) {
219
+ this.page = this.context.pages[index];
220
+ this.context.page = this.page;
221
+ await this.page.bringToFront();
222
+ return;
223
+ }
224
+ }
225
+ // if the tabNameOrIndex is a string, find the tab by name
226
+ for (let i = 0; i < this.context.pages.length; i++) {
227
+ let page = this.context.pages[i];
228
+ let title = await page.title();
229
+ if (title.includes(tabTitleOrIndex)) {
230
+ this.page = page;
231
+ this.context.page = this.page;
232
+ await this.page.bringToFront();
233
+ return;
234
+ }
235
+ }
236
+ throw new Error("Tab not found: " + tabTitleOrIndex);
237
+ }
183
238
  registerConsoleLogListener(page, context) {
184
239
  if (!this.context.webLogger) {
185
240
  this.context.webLogger = [];
@@ -247,6 +302,7 @@ class StableBrowser {
247
302
  if (!url) {
248
303
  throw new Error("url is null, verify that the environment file is correct");
249
304
  }
305
+ url = await this._replaceWithLocalData(url, this.world);
250
306
  if (!url.startsWith("http")) {
251
307
  url = "https://" + url;
252
308
  }
@@ -275,7 +331,7 @@ class StableBrowser {
275
331
  _commandError(state, error, this);
276
332
  }
277
333
  finally {
278
- _commandFinally(state, this);
334
+ await _commandFinally(state, this);
279
335
  }
280
336
  }
281
337
  async _getLocator(locator, scope, _params) {
@@ -391,7 +447,7 @@ class StableBrowser {
391
447
  }
392
448
  return { elementCount: tagCount, randomToken };
393
449
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
450
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
451
  if (!info) {
396
452
  info = {};
397
453
  }
@@ -458,7 +514,7 @@ class StableBrowser {
458
514
  }
459
515
  return;
460
516
  }
461
- if (info.locatorLog && count === 0) {
517
+ if (info.locatorLog && count === 0 && logErrors) {
462
518
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
519
  }
464
520
  for (let j = 0; j < count; j++) {
@@ -473,7 +529,7 @@ class StableBrowser {
473
529
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
530
  }
475
531
  }
476
- else {
532
+ else if (logErrors) {
477
533
  info.failCause.visible = visible;
478
534
  info.failCause.enabled = enabled;
479
535
  if (!info.printMessages) {
@@ -565,15 +621,27 @@ class StableBrowser {
565
621
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
622
  if (!element.rerun) {
567
623
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
624
+ await element.evaluate((el, randomToken) => {
569
625
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
626
  }, 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;
627
+ // if (element._frame) {
628
+ // return element;
629
+ // }
630
+ const scope = element._frame ?? element.page();
631
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
632
+ let prefixSelector = "";
633
+ const frameControlSelector = " >> internal:control=enter-frame";
634
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
635
+ if (frameSelectorIndex !== -1) {
636
+ // remove everything after the >> internal:control=enter-frame
637
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
638
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
639
+ }
640
+ // if (element?._frame?._selector) {
641
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
642
+ // }
643
+ const newSelector = prefixSelector + newElementSelector;
644
+ return scope.locator(newSelector);
577
645
  }
578
646
  }
579
647
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +793,9 @@ class StableBrowser {
725
793
  // info.log += "scanning locators in priority 2" + "\n";
726
794
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
795
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
796
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
797
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
798
  }
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
799
  let foundElements = result.foundElements;
737
800
  if (foundElements.length === 1 && foundElements[0].unique) {
738
801
  info.box = foundElements[0].box;
@@ -787,6 +850,11 @@ class StableBrowser {
787
850
  visibleOnly = false;
788
851
  }
789
852
  await new Promise((resolve) => setTimeout(resolve, 1000));
853
+ // sheck of more of half of the timeout has passed
854
+ if (Date.now() - startTime > timeout / 2) {
855
+ highPriorityOnly = false;
856
+ visibleOnly = false;
857
+ }
790
858
  }
791
859
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
860
  // if (info.locatorLog) {
@@ -802,7 +870,7 @@ class StableBrowser {
802
870
  }
803
871
  throw new Error("failed to locate first element no elements found, " + info.log);
804
872
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
873
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
874
  let foundElements = [];
807
875
  const result = {
808
876
  foundElements: foundElements,
@@ -821,7 +889,9 @@ class StableBrowser {
821
889
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
890
  }
823
891
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
892
+ if (logErrors) {
893
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
894
+ }
825
895
  }
826
896
  }
827
897
  if (foundLocators.length === 1) {
@@ -862,7 +932,7 @@ class StableBrowser {
862
932
  });
863
933
  result.locatorIndex = i;
864
934
  }
865
- else {
935
+ else if (logErrors) {
866
936
  info.failCause.foundMultiple = true;
867
937
  if (info.locatorLog) {
868
938
  info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
@@ -914,7 +984,7 @@ class StableBrowser {
914
984
  await _commandError(state, "timeout looking for " + elementDescription, this);
915
985
  }
916
986
  finally {
917
- _commandFinally(state, this);
987
+ await _commandFinally(state, this);
918
988
  }
919
989
  }
920
990
  }
@@ -963,7 +1033,7 @@ class StableBrowser {
963
1033
  await _commandError(state, "timeout looking for " + elementDescription, this);
964
1034
  }
965
1035
  finally {
966
- _commandFinally(state, this);
1036
+ await _commandFinally(state, this);
967
1037
  }
968
1038
  }
969
1039
  }
@@ -985,14 +1055,16 @@ class StableBrowser {
985
1055
  try {
986
1056
  await _preCommand(state, this);
987
1057
  await performAction("click", state.element, options, this, state, _params);
988
- await this.waitForPageLoad();
1058
+ if (!this.fastMode) {
1059
+ await this.waitForPageLoad();
1060
+ }
989
1061
  return state.info;
990
1062
  }
991
1063
  catch (e) {
992
1064
  await _commandError(state, e, this);
993
1065
  }
994
1066
  finally {
995
- _commandFinally(state, this);
1067
+ await _commandFinally(state, this);
996
1068
  }
997
1069
  }
998
1070
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1023,7 +1095,7 @@ class StableBrowser {
1023
1095
  // await _commandError(state, e, this);
1024
1096
  }
1025
1097
  finally {
1026
- _commandFinally(state, this);
1098
+ await _commandFinally(state, this);
1027
1099
  }
1028
1100
  return found;
1029
1101
  }
@@ -1047,8 +1119,8 @@ class StableBrowser {
1047
1119
  try {
1048
1120
  // if (world && world.screenshot && !world.screenshotPath) {
1049
1121
  // console.log(`Highlighting while running from recorder`);
1050
- await this._highlightElements(element);
1051
- await state.element.setChecked(checked);
1122
+ await this._highlightElements(state.element);
1123
+ await state.element.setChecked(checked, { timeout: 2000 });
1052
1124
  await new Promise((resolve) => setTimeout(resolve, 1000));
1053
1125
  // await this._unHighlightElements(element);
1054
1126
  // }
@@ -1060,11 +1132,28 @@ class StableBrowser {
1060
1132
  this.logger.info("element did not change its state, ignoring...");
1061
1133
  }
1062
1134
  else {
1135
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1063
1136
  //await this.closeUnexpectedPopups();
1064
1137
  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));
1138
+ state.element_found = false;
1139
+ try {
1140
+ state.element = await this._locate(selectors, state.info, _params, 100);
1141
+ state.element_found = true;
1142
+ // check the check state
1143
+ }
1144
+ catch (error) {
1145
+ // element dismissed
1146
+ }
1147
+ if (state.element_found) {
1148
+ const isChecked = await state.element.isChecked();
1149
+ if (isChecked !== checked) {
1150
+ // perform click
1151
+ await state.element.click({ timeout: 2000, force: true });
1152
+ }
1153
+ else {
1154
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1155
+ }
1156
+ }
1068
1157
  }
1069
1158
  }
1070
1159
  await this.waitForPageLoad();
@@ -1074,7 +1163,7 @@ class StableBrowser {
1074
1163
  await _commandError(state, e, this);
1075
1164
  }
1076
1165
  finally {
1077
- _commandFinally(state, this);
1166
+ await _commandFinally(state, this);
1078
1167
  }
1079
1168
  }
1080
1169
  async hover(selectors, _params, options = {}, world = null) {
@@ -1100,7 +1189,7 @@ class StableBrowser {
1100
1189
  await _commandError(state, e, this);
1101
1190
  }
1102
1191
  finally {
1103
- _commandFinally(state, this);
1192
+ await _commandFinally(state, this);
1104
1193
  }
1105
1194
  }
1106
1195
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1136,7 +1225,7 @@ class StableBrowser {
1136
1225
  await _commandError(state, e, this);
1137
1226
  }
1138
1227
  finally {
1139
- _commandFinally(state, this);
1228
+ await _commandFinally(state, this);
1140
1229
  }
1141
1230
  }
1142
1231
  async type(_value, _params = null, options = {}, world = null) {
@@ -1182,7 +1271,7 @@ class StableBrowser {
1182
1271
  await _commandError(state, e, this);
1183
1272
  }
1184
1273
  finally {
1185
- _commandFinally(state, this);
1274
+ await _commandFinally(state, this);
1186
1275
  }
1187
1276
  }
1188
1277
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1218,7 +1307,7 @@ class StableBrowser {
1218
1307
  await _commandError(state, e, this);
1219
1308
  }
1220
1309
  finally {
1221
- _commandFinally(state, this);
1310
+ await _commandFinally(state, this);
1222
1311
  }
1223
1312
  }
1224
1313
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1287,7 +1376,7 @@ class StableBrowser {
1287
1376
  await _commandError(state, e, this);
1288
1377
  }
1289
1378
  finally {
1290
- _commandFinally(state, this);
1379
+ await _commandFinally(state, this);
1291
1380
  }
1292
1381
  }
1293
1382
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1360,7 +1449,9 @@ class StableBrowser {
1360
1449
  await new Promise((resolve) => setTimeout(resolve, 500));
1361
1450
  }
1362
1451
  }
1452
+ //if (!this.fastMode) {
1363
1453
  await _screenshot(state, this);
1454
+ //}
1364
1455
  if (enter === true) {
1365
1456
  await new Promise((resolve) => setTimeout(resolve, 2000));
1366
1457
  await this.page.keyboard.press("Enter");
@@ -1387,7 +1478,7 @@ class StableBrowser {
1387
1478
  await _commandError(state, e, this);
1388
1479
  }
1389
1480
  finally {
1390
- _commandFinally(state, this);
1481
+ await _commandFinally(state, this);
1391
1482
  }
1392
1483
  }
1393
1484
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1417,7 +1508,42 @@ class StableBrowser {
1417
1508
  await _commandError(state, e, this);
1418
1509
  }
1419
1510
  finally {
1420
- _commandFinally(state, this);
1511
+ await _commandFinally(state, this);
1512
+ }
1513
+ }
1514
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1515
+ const state = {
1516
+ selectors,
1517
+ _params,
1518
+ files,
1519
+ value: '"' + files.join('", "') + '"',
1520
+ options,
1521
+ world,
1522
+ type: Types.SET_INPUT_FILES,
1523
+ text: `Set input files`,
1524
+ _text: `Set input files on ${selectors.element_name}`,
1525
+ operation: "setInputFiles",
1526
+ log: "***** set input files " + selectors.element_name + " *****\n",
1527
+ };
1528
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1529
+ try {
1530
+ await _preCommand(state, this);
1531
+ for (let i = 0; i < files.length; i++) {
1532
+ const file = files[i];
1533
+ const filePath = path.join(uploadsFolder, file);
1534
+ if (!fs.existsSync(filePath)) {
1535
+ throw new Error(`File not found: ${filePath}`);
1536
+ }
1537
+ state.files[i] = filePath;
1538
+ }
1539
+ await state.element.setInputFiles(files);
1540
+ return state.info;
1541
+ }
1542
+ catch (e) {
1543
+ await _commandError(state, e, this);
1544
+ }
1545
+ finally {
1546
+ await _commandFinally(state, this);
1421
1547
  }
1422
1548
  }
1423
1549
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1533,7 +1659,7 @@ class StableBrowser {
1533
1659
  await _commandError(state, e, this);
1534
1660
  }
1535
1661
  finally {
1536
- _commandFinally(state, this);
1662
+ await _commandFinally(state, this);
1537
1663
  }
1538
1664
  }
1539
1665
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1568,7 +1694,7 @@ class StableBrowser {
1568
1694
  while (Date.now() - startTime < timeout) {
1569
1695
  try {
1570
1696
  await _preCommand(state, this);
1571
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1697
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1572
1698
  if (foundObj && foundObj.element) {
1573
1699
  await this.scrollIfNeeded(foundObj.element, state.info);
1574
1700
  }
@@ -1610,7 +1736,84 @@ class StableBrowser {
1610
1736
  throw e;
1611
1737
  }
1612
1738
  finally {
1613
- _commandFinally(state, this);
1739
+ await _commandFinally(state, this);
1740
+ }
1741
+ }
1742
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1743
+ const timeout = this._getFindElementTimeout(options);
1744
+ const startTime = Date.now();
1745
+ const state = {
1746
+ _params,
1747
+ value: referanceSnapshot,
1748
+ options,
1749
+ world,
1750
+ locate: false,
1751
+ scroll: false,
1752
+ screenshot: true,
1753
+ highlight: false,
1754
+ type: Types.SNAPSHOT_VALIDATION,
1755
+ text: `verify snapshot: ${referanceSnapshot}`,
1756
+ operation: "snapshotValidation",
1757
+ log: "***** verify snapshot *****\n",
1758
+ };
1759
+ if (!referanceSnapshot) {
1760
+ throw new Error("referanceSnapshot is null");
1761
+ }
1762
+ let text = null;
1763
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1764
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1765
+ }
1766
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1767
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1768
+ }
1769
+ else if (referanceSnapshot.startsWith("yaml:")) {
1770
+ text = referanceSnapshot.substring(5);
1771
+ }
1772
+ else {
1773
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1774
+ }
1775
+ state.text = text;
1776
+ const newValue = await this._replaceWithLocalData(text, world);
1777
+ await _preCommand(state, this);
1778
+ let foundObj = null;
1779
+ try {
1780
+ let matchResult = null;
1781
+ while (Date.now() - startTime < timeout) {
1782
+ try {
1783
+ let scope = null;
1784
+ if (!frameSelectors) {
1785
+ scope = this.page;
1786
+ }
1787
+ else {
1788
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1789
+ }
1790
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1791
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1792
+ if (matchResult.errorLine !== -1) {
1793
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1794
+ }
1795
+ // highlight and screenshot
1796
+ try {
1797
+ await await highlightSnapshot(newValue, scope);
1798
+ await _screenshot(state, this);
1799
+ }
1800
+ catch (e) { }
1801
+ return state.info;
1802
+ }
1803
+ catch (e) {
1804
+ // Log error but continue retrying until timeout is reached
1805
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1806
+ }
1807
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1808
+ }
1809
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1810
+ }
1811
+ catch (e) {
1812
+ await _commandError(state, e, this);
1813
+ throw e;
1814
+ }
1815
+ finally {
1816
+ await _commandFinally(state, this);
1614
1817
  }
1615
1818
  }
1616
1819
  async waitForUserInput(message, world = null) {
@@ -1648,6 +1851,15 @@ class StableBrowser {
1648
1851
  // save the data to the file
1649
1852
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1650
1853
  }
1854
+ overwriteTestData(testData, world = null) {
1855
+ if (!testData) {
1856
+ return;
1857
+ }
1858
+ // if data file exists, load it
1859
+ const dataFile = _getDataFile(world, this.context, this);
1860
+ // save the data to the file
1861
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1862
+ }
1651
1863
  _getDataFilePath(fileName) {
1652
1864
  let dataFile = path.join(this.project_path, "data", fileName);
1653
1865
  if (fs.existsSync(dataFile)) {
@@ -1900,7 +2112,7 @@ class StableBrowser {
1900
2112
  await _commandError(state, e, this);
1901
2113
  }
1902
2114
  finally {
1903
- _commandFinally(state, this);
2115
+ await _commandFinally(state, this);
1904
2116
  }
1905
2117
  }
1906
2118
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1931,10 +2143,31 @@ class StableBrowser {
1931
2143
  case "value":
1932
2144
  state.value = await state.element.inputValue();
1933
2145
  break;
2146
+ case "text":
2147
+ state.value = await state.element.textContent();
2148
+ break;
1934
2149
  default:
1935
2150
  state.value = await state.element.getAttribute(attribute);
1936
2151
  break;
1937
2152
  }
2153
+ if (options !== null) {
2154
+ if (options.regex && options.regex !== "") {
2155
+ // Construct a regex pattern from the provided string
2156
+ const regex = options.regex.slice(1, -1);
2157
+ const regexPattern = new RegExp(regex, "g");
2158
+ const matches = state.value.match(regexPattern);
2159
+ if (matches) {
2160
+ let newValue = "";
2161
+ for (const match of matches) {
2162
+ newValue += match;
2163
+ }
2164
+ state.value = newValue;
2165
+ }
2166
+ }
2167
+ if (options.trimSpaces && options.trimSpaces === true) {
2168
+ state.value = state.value.trim();
2169
+ }
2170
+ }
1938
2171
  state.info.value = state.value;
1939
2172
  this.setTestData({ [variable]: state.value }, world);
1940
2173
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1945,7 +2178,78 @@ class StableBrowser {
1945
2178
  await _commandError(state, e, this);
1946
2179
  }
1947
2180
  finally {
1948
- _commandFinally(state, this);
2181
+ await _commandFinally(state, this);
2182
+ }
2183
+ }
2184
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2185
+ const state = {
2186
+ selectors,
2187
+ _params,
2188
+ property,
2189
+ variable,
2190
+ options,
2191
+ world,
2192
+ type: Types.EXTRACT_PROPERTY,
2193
+ text: `Extract property from element`,
2194
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2195
+ operation: "extractProperty",
2196
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2197
+ allowDisabled: true,
2198
+ };
2199
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2200
+ try {
2201
+ await _preCommand(state, this);
2202
+ switch (property) {
2203
+ case "inner_text":
2204
+ state.value = await state.element.innerText();
2205
+ break;
2206
+ case "href":
2207
+ state.value = await state.element.getAttribute("href");
2208
+ break;
2209
+ case "value":
2210
+ state.value = await state.element.inputValue();
2211
+ break;
2212
+ case "text":
2213
+ state.value = await state.element.textContent();
2214
+ break;
2215
+ default:
2216
+ if (property.startsWith("dataset.")) {
2217
+ const dataAttribute = property.substring(8);
2218
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2219
+ }
2220
+ else {
2221
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2222
+ }
2223
+ }
2224
+ if (options !== null) {
2225
+ if (options.regex && options.regex !== "") {
2226
+ // Construct a regex pattern from the provided string
2227
+ const regex = options.regex.slice(1, -1);
2228
+ const regexPattern = new RegExp(regex, "g");
2229
+ const matches = state.value.match(regexPattern);
2230
+ if (matches) {
2231
+ let newValue = "";
2232
+ for (const match of matches) {
2233
+ newValue += match;
2234
+ }
2235
+ state.value = newValue;
2236
+ }
2237
+ }
2238
+ if (options.trimSpaces && options.trimSpaces === true) {
2239
+ state.value = state.value.trim();
2240
+ }
2241
+ }
2242
+ state.info.value = state.value;
2243
+ this.setTestData({ [variable]: state.value }, world);
2244
+ this.logger.info("set test data: " + variable + "=" + state.value);
2245
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2246
+ return state.info;
2247
+ }
2248
+ catch (e) {
2249
+ await _commandError(state, e, this);
2250
+ }
2251
+ finally {
2252
+ await _commandFinally(state, this);
1949
2253
  }
1950
2254
  }
1951
2255
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1970,12 +2274,15 @@ class StableBrowser {
1970
2274
  let expectedValue;
1971
2275
  try {
1972
2276
  await _preCommand(state, this);
1973
- expectedValue = state.value;
2277
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1974
2278
  state.info.expectedValue = expectedValue;
1975
2279
  switch (attribute) {
1976
2280
  case "innerText":
1977
2281
  val = String(await state.element.innerText());
1978
2282
  break;
2283
+ case "text":
2284
+ val = String(await state.element.textContent());
2285
+ break;
1979
2286
  case "value":
1980
2287
  val = String(await state.element.inputValue());
1981
2288
  break;
@@ -1997,17 +2304,42 @@ class StableBrowser {
1997
2304
  let regex;
1998
2305
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1999
2306
  const patternBody = expectedValue.slice(1, -1);
2000
- regex = new RegExp(patternBody, "g");
2307
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2308
+ regex = new RegExp(processedPattern, "gs");
2309
+ state.info.regex = true;
2001
2310
  }
2002
2311
  else {
2003
2312
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2004
2313
  regex = new RegExp(escapedPattern, "g");
2005
2314
  }
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);
2315
+ if (attribute === "innerText") {
2316
+ if (state.info.regex) {
2317
+ if (!regex.test(val)) {
2318
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2319
+ state.info.failCause.assertionFailed = true;
2320
+ state.info.failCause.lastError = errorMessage;
2321
+ throw new Error(errorMessage);
2322
+ }
2323
+ }
2324
+ else {
2325
+ const valLines = val.split("\n");
2326
+ const expectedLines = expectedValue.split("\n");
2327
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2328
+ if (!isPart) {
2329
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2330
+ state.info.failCause.assertionFailed = true;
2331
+ state.info.failCause.lastError = errorMessage;
2332
+ throw new Error(errorMessage);
2333
+ }
2334
+ }
2335
+ }
2336
+ else {
2337
+ if (!val.match(regex)) {
2338
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2339
+ state.info.failCause.assertionFailed = true;
2340
+ state.info.failCause.lastError = errorMessage;
2341
+ throw new Error(errorMessage);
2342
+ }
2011
2343
  }
2012
2344
  return state.info;
2013
2345
  }
@@ -2015,7 +2347,110 @@ class StableBrowser {
2015
2347
  await _commandError(state, e, this);
2016
2348
  }
2017
2349
  finally {
2018
- _commandFinally(state, this);
2350
+ await _commandFinally(state, this);
2351
+ }
2352
+ }
2353
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2354
+ const state = {
2355
+ selectors,
2356
+ _params,
2357
+ property,
2358
+ value,
2359
+ options,
2360
+ world,
2361
+ type: Types.VERIFY_PROPERTY,
2362
+ highlight: true,
2363
+ screenshot: true,
2364
+ text: `Verify element property`,
2365
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2366
+ operation: "verifyProperty",
2367
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2368
+ allowDisabled: true,
2369
+ };
2370
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2371
+ let val;
2372
+ let expectedValue;
2373
+ try {
2374
+ await _preCommand(state, this);
2375
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2376
+ state.info.expectedValue = expectedValue;
2377
+ switch (property) {
2378
+ case "innerText":
2379
+ val = String(await state.element.innerText());
2380
+ break;
2381
+ case "text":
2382
+ val = String(await state.element.textContent());
2383
+ break;
2384
+ case "value":
2385
+ val = String(await state.element.inputValue());
2386
+ break;
2387
+ case "checked":
2388
+ val = String(await state.element.isChecked());
2389
+ break;
2390
+ case "disabled":
2391
+ val = String(await state.element.isDisabled());
2392
+ break;
2393
+ case "readOnly":
2394
+ const isEditable = await state.element.isEditable();
2395
+ val = String(!isEditable);
2396
+ break;
2397
+ default:
2398
+ if (property.startsWith("dataset.")) {
2399
+ const dataAttribute = property.substring(8);
2400
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2401
+ }
2402
+ else {
2403
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2404
+ }
2405
+ }
2406
+ state.info.value = val;
2407
+ let regex;
2408
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2409
+ const patternBody = expectedValue.slice(1, -1);
2410
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2411
+ regex = new RegExp(processedPattern, "gs");
2412
+ state.info.regex = true;
2413
+ }
2414
+ else {
2415
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2416
+ regex = new RegExp(escapedPattern, "g");
2417
+ }
2418
+ if (property === "innerText") {
2419
+ if (state.info.regex) {
2420
+ if (!regex.test(val)) {
2421
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2422
+ state.info.failCause.assertionFailed = true;
2423
+ state.info.failCause.lastError = errorMessage;
2424
+ throw new Error(errorMessage);
2425
+ }
2426
+ }
2427
+ else {
2428
+ const valLines = val.split("\n");
2429
+ const expectedLines = expectedValue.split("\n");
2430
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2431
+ if (!isPart) {
2432
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2433
+ state.info.failCause.assertionFailed = true;
2434
+ state.info.failCause.lastError = errorMessage;
2435
+ throw new Error(errorMessage);
2436
+ }
2437
+ }
2438
+ }
2439
+ else {
2440
+ if (!val.match(regex)) {
2441
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2442
+ state.info.failCause.assertionFailed = true;
2443
+ state.info.failCause.lastError = errorMessage;
2444
+ throw new Error(errorMessage);
2445
+ }
2446
+ }
2447
+ return state.info;
2448
+ }
2449
+ catch (e) {
2450
+ await _commandError(state, e, this);
2451
+ }
2452
+ finally {
2453
+ await _commandFinally(state, this);
2019
2454
  }
2020
2455
  }
2021
2456
  async extractEmailData(emailAddress, options, world) {
@@ -2175,56 +2610,49 @@ class StableBrowser {
2175
2610
  console.debug(error);
2176
2611
  }
2177
2612
  }
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
- // }
2613
+ _matcher(text) {
2614
+ if (!text) {
2615
+ return { matcher: "contains", queryText: "" };
2616
+ }
2617
+ if (text.length < 2) {
2618
+ return { matcher: "contains", queryText: text };
2619
+ }
2620
+ const split = text.split(":");
2621
+ const matcher = split[0].toLowerCase();
2622
+ const queryText = split.slice(1).join(":").trim();
2623
+ return { matcher, queryText };
2624
+ }
2625
+ _getDomain(url) {
2626
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2627
+ return "";
2628
+ }
2629
+ let hostnameFragments = url.split("/")[2].split(".");
2630
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2631
+ return hostnameFragments.join("-").split(":").join("-");
2632
+ }
2633
+ let n = hostnameFragments.length;
2634
+ let fragments = [...hostnameFragments];
2635
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2636
+ hostnameFragments.pop();
2637
+ n = hostnameFragments.length;
2638
+ }
2639
+ if (n == 0) {
2640
+ if (fragments[0] === "www")
2641
+ fragments = fragments.slice(1);
2642
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2643
+ }
2644
+ if (hostnameFragments[0] === "www")
2645
+ hostnameFragments = hostnameFragments.slice(1);
2646
+ return hostnameFragments.join(".");
2647
+ }
2648
+ /**
2649
+ * Verify the page path matches the given path.
2650
+ * @param {string} pathPart - The path to verify.
2651
+ * @param {object} options - Options for verification.
2652
+ * @param {object} world - The world context.
2653
+ * @returns {Promise<object>} - The state info after verification.
2654
+ */
2226
2655
  async verifyPagePath(pathPart, options = {}, world = null) {
2227
- const startTime = Date.now();
2228
2656
  let error = null;
2229
2657
  let screenshotId = null;
2230
2658
  let screenshotPath = null;
@@ -2238,51 +2666,212 @@ class StableBrowser {
2238
2666
  pathPart = newValue;
2239
2667
  }
2240
2668
  info.pathPart = pathPart;
2669
+ const { matcher, queryText } = this._matcher(pathPart);
2670
+ const state = {
2671
+ text_search: queryText,
2672
+ options,
2673
+ world,
2674
+ locate: false,
2675
+ scroll: false,
2676
+ highlight: false,
2677
+ type: Types.VERIFY_PAGE_PATH,
2678
+ text: `Verify the page url is ${queryText}`,
2679
+ _text: `Verify the page url is ${queryText}`,
2680
+ operation: "verifyPagePath",
2681
+ log: "***** verify page url is " + queryText + " *****\n",
2682
+ };
2241
2683
  try {
2684
+ await _preCommand(state, this);
2685
+ state.info.text = queryText;
2242
2686
  for (let i = 0; i < 30; i++) {
2243
2687
  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;
2688
+ switch (matcher) {
2689
+ case "exact":
2690
+ if (url !== queryText) {
2691
+ if (i === 29) {
2692
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2693
+ }
2694
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2695
+ continue;
2696
+ }
2697
+ break;
2698
+ case "contains":
2699
+ if (!url.includes(queryText)) {
2700
+ if (i === 29) {
2701
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2702
+ }
2703
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2704
+ continue;
2705
+ }
2706
+ break;
2707
+ case "starts-with":
2708
+ {
2709
+ const domain = this._getDomain(url);
2710
+ if (domain.length > 0 && domain !== queryText) {
2711
+ if (i === 29) {
2712
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2713
+ }
2714
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2715
+ continue;
2716
+ }
2717
+ }
2718
+ break;
2719
+ case "ends-with":
2720
+ {
2721
+ const urlObj = new URL(url);
2722
+ let route = "/";
2723
+ if (urlObj.pathname !== "/") {
2724
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2725
+ }
2726
+ else {
2727
+ route = "/";
2728
+ }
2729
+ if (route !== queryText) {
2730
+ if (i === 29) {
2731
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2732
+ }
2733
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2734
+ continue;
2735
+ }
2736
+ }
2737
+ break;
2738
+ case "regex":
2739
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2740
+ if (!regex.test(url)) {
2741
+ if (i === 29) {
2742
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2743
+ }
2744
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2745
+ continue;
2746
+ }
2747
+ break;
2748
+ default:
2749
+ console.log("Unknown matching type, defaulting to contains matching");
2750
+ if (!url.includes(pathPart)) {
2751
+ if (i === 29) {
2752
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2753
+ }
2754
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2755
+ continue;
2756
+ }
2250
2757
  }
2251
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2252
- return info;
2758
+ await _screenshot(state, this);
2759
+ return state.info;
2253
2760
  }
2254
2761
  }
2255
2762
  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);
2763
+ state.info.failCause.lastError = e.message;
2764
+ state.info.failCause.assertionFailed = true;
2765
+ await _commandError(state, e, this);
2264
2766
  }
2265
2767
  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
- });
2768
+ await _commandFinally(state, this);
2769
+ }
2770
+ }
2771
+ /**
2772
+ * Verify the page title matches the given title.
2773
+ * @param {string} title - The title to verify.
2774
+ * @param {object} options - Options for verification.
2775
+ * @param {object} world - The world context.
2776
+ * @returns {Promise<object>} - The state info after verification.
2777
+ */
2778
+ async verifyPageTitle(title, options = {}, world = null) {
2779
+ let error = null;
2780
+ let screenshotId = null;
2781
+ let screenshotPath = null;
2782
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2783
+ const newValue = await this._replaceWithLocalData(title, world);
2784
+ if (newValue !== title) {
2785
+ this.logger.info(title + "=" + newValue);
2786
+ title = newValue;
2787
+ }
2788
+ const { matcher, queryText } = this._matcher(title);
2789
+ const state = {
2790
+ text_search: queryText,
2791
+ options,
2792
+ world,
2793
+ locate: false,
2794
+ scroll: false,
2795
+ highlight: false,
2796
+ type: Types.VERIFY_PAGE_TITLE,
2797
+ text: `Verify the page title is ${queryText}`,
2798
+ _text: `Verify the page title is ${queryText}`,
2799
+ operation: "verifyPageTitle",
2800
+ log: "***** verify page title is " + queryText + " *****\n",
2801
+ };
2802
+ try {
2803
+ await _preCommand(state, this);
2804
+ state.info.text = queryText;
2805
+ for (let i = 0; i < 30; i++) {
2806
+ const foundTitle = await this.page.title();
2807
+ switch (matcher) {
2808
+ case "exact":
2809
+ if (foundTitle !== queryText) {
2810
+ if (i === 29) {
2811
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2812
+ }
2813
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2814
+ continue;
2815
+ }
2816
+ break;
2817
+ case "contains":
2818
+ if (!foundTitle.includes(queryText)) {
2819
+ if (i === 29) {
2820
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2821
+ }
2822
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2823
+ continue;
2824
+ }
2825
+ break;
2826
+ case "starts-with":
2827
+ if (!foundTitle.startsWith(queryText)) {
2828
+ if (i === 29) {
2829
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2830
+ }
2831
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2832
+ continue;
2833
+ }
2834
+ break;
2835
+ case "ends-with":
2836
+ if (!foundTitle.endsWith(queryText)) {
2837
+ if (i === 29) {
2838
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2839
+ }
2840
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2841
+ continue;
2842
+ }
2843
+ break;
2844
+ case "regex":
2845
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2846
+ if (!regex.test(foundTitle)) {
2847
+ if (i === 29) {
2848
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2849
+ }
2850
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2851
+ continue;
2852
+ }
2853
+ break;
2854
+ default:
2855
+ console.log("Unknown matching type, defaulting to contains matching");
2856
+ if (!foundTitle.includes(title)) {
2857
+ if (i === 29) {
2858
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2859
+ }
2860
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2861
+ continue;
2862
+ }
2863
+ }
2864
+ await _screenshot(state, this);
2865
+ return state.info;
2866
+ }
2867
+ }
2868
+ catch (e) {
2869
+ state.info.failCause.lastError = e.message;
2870
+ state.info.failCause.assertionFailed = true;
2871
+ await _commandError(state, e, this);
2872
+ }
2873
+ finally {
2874
+ await _commandFinally(state, this);
2286
2875
  }
2287
2876
  }
2288
2877
  async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
@@ -2324,7 +2913,7 @@ class StableBrowser {
2324
2913
  scroll: false,
2325
2914
  highlight: false,
2326
2915
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2327
- text: `Verify the text '${text}' exists in page`,
2916
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2328
2917
  _text: `Verify the text '${text}' exists in page`,
2329
2918
  operation: "verifyTextExistInPage",
2330
2919
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2366,27 +2955,10 @@ class StableBrowser {
2366
2955
  const frame = resultWithElementsFound[0].frame;
2367
2956
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2368
2957
  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
2958
  const element = await frame.locator(dataAttribute).first();
2384
- // await new Promise((resolve) => setTimeout(resolve, 100));
2385
- // await this._unhighlightElements(frame, dataAttribute);
2386
2959
  if (element) {
2387
2960
  await this.scrollIfNeeded(element, state.info);
2388
2961
  await element.dispatchEvent("bvt_verify_page_contains_text");
2389
- // await _screenshot(state, this, element);
2390
2962
  }
2391
2963
  }
2392
2964
  await _screenshot(state, this);
@@ -2396,13 +2968,12 @@ class StableBrowser {
2396
2968
  console.error(error);
2397
2969
  }
2398
2970
  }
2399
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2400
2971
  }
2401
2972
  catch (e) {
2402
2973
  await _commandError(state, e, this);
2403
2974
  }
2404
2975
  finally {
2405
- _commandFinally(state, this);
2976
+ await _commandFinally(state, this);
2406
2977
  }
2407
2978
  }
2408
2979
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2415,7 +2986,7 @@ class StableBrowser {
2415
2986
  scroll: false,
2416
2987
  highlight: false,
2417
2988
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2418
- text: `Verify text does not exist in page`,
2989
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2419
2990
  _text: `Verify the text '${text}' does not exist in page`,
2420
2991
  operation: "verifyTextNotExistInPage",
2421
2992
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2459,7 +3030,7 @@ class StableBrowser {
2459
3030
  await _commandError(state, e, this);
2460
3031
  }
2461
3032
  finally {
2462
- _commandFinally(state, this);
3033
+ await _commandFinally(state, this);
2463
3034
  }
2464
3035
  }
2465
3036
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2529,7 +3100,7 @@ class StableBrowser {
2529
3100
  const count = await frame.locator(css).count();
2530
3101
  for (let j = 0; j < count; j++) {
2531
3102
  const continer = await frame.locator(css).nth(j);
2532
- const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
3103
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2533
3104
  if (result.elementCount > 0) {
2534
3105
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2535
3106
  await this._highlightElements(frame, dataAttribute);
@@ -2570,7 +3141,7 @@ class StableBrowser {
2570
3141
  await _commandError(state, e, this);
2571
3142
  }
2572
3143
  finally {
2573
- _commandFinally(state, this);
3144
+ await _commandFinally(state, this);
2574
3145
  }
2575
3146
  }
2576
3147
  async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
@@ -2913,7 +3484,13 @@ class StableBrowser {
2913
3484
  }
2914
3485
  }
2915
3486
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2916
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3487
+ try {
3488
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3489
+ }
3490
+ catch (error) {
3491
+ this.logger.debug(error);
3492
+ throw error;
3493
+ }
2917
3494
  }
2918
3495
  _getLoadTimeout(options) {
2919
3496
  let timeout = 15000;
@@ -2936,6 +3513,7 @@ class StableBrowser {
2936
3513
  }
2937
3514
  async saveStoreState(path = null, world = null) {
2938
3515
  const storageState = await this.page.context().storageState();
3516
+ path = await this._replaceWithLocalData(path, this.world);
2939
3517
  //const testDataFile = _getDataFile(world, this.context, this);
2940
3518
  if (path) {
2941
3519
  // save { storageState: storageState } into the path
@@ -2946,10 +3524,14 @@ class StableBrowser {
2946
3524
  }
2947
3525
  }
2948
3526
  async restoreSaveState(path = null, world = null) {
3527
+ path = await this._replaceWithLocalData(path, this.world);
2949
3528
  await refreshBrowser(this, path, world);
2950
3529
  this.registerEventListeners(this.context);
2951
3530
  registerNetworkEvents(this.world, this, this.context, this.page);
2952
3531
  registerDownloadEvent(this.page, this.world, this.context);
3532
+ if (this.onRestoreSaveState) {
3533
+ this.onRestoreSaveState(path);
3534
+ }
2953
3535
  }
2954
3536
  async waitForPageLoad(options = {}, world = null) {
2955
3537
  let timeout = this._getLoadTimeout(options);
@@ -3032,7 +3614,7 @@ class StableBrowser {
3032
3614
  await _commandError(state, e, this);
3033
3615
  }
3034
3616
  finally {
3035
- _commandFinally(state, this);
3617
+ await _commandFinally(state, this);
3036
3618
  }
3037
3619
  }
3038
3620
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3119,7 +3701,7 @@ class StableBrowser {
3119
3701
  await _commandError(state, e, this);
3120
3702
  }
3121
3703
  finally {
3122
- _commandFinally(state, this);
3704
+ await _commandFinally(state, this);
3123
3705
  }
3124
3706
  }
3125
3707
  saveTestDataAsGlobal(options, world) {
@@ -3224,7 +3806,39 @@ class StableBrowser {
3224
3806
  console.log("#-#");
3225
3807
  }
3226
3808
  }
3809
+ async beforeScenario(world, scenario) {
3810
+ this.beforeScenarioCalled = true;
3811
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3812
+ this.scenarioName = scenario.pickle.name;
3813
+ }
3814
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3815
+ this.featureName = scenario.gherkinDocument.feature.name;
3816
+ }
3817
+ if (this.context) {
3818
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3819
+ }
3820
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3821
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3822
+ // check if @global_test_data tag is present
3823
+ if (this.tags.includes("@global_test_data")) {
3824
+ this.saveTestDataAsGlobal({}, world);
3825
+ }
3826
+ }
3827
+ // update test data based on feature/scenario
3828
+ let envName = null;
3829
+ if (this.context && this.context.environment) {
3830
+ envName = this.context.environment.name;
3831
+ }
3832
+ if (!process.env.TEMP_RUN) {
3833
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3834
+ }
3835
+ await loadBrunoParams(this.context, this.context.environment.name);
3836
+ }
3837
+ async afterScenario(world, scenario) { }
3227
3838
  async beforeStep(world, step) {
3839
+ if (!this.beforeScenarioCalled) {
3840
+ this.beforeScenario(world, step);
3841
+ }
3228
3842
  if (this.stepIndex === undefined) {
3229
3843
  this.stepIndex = 0;
3230
3844
  }
@@ -3241,24 +3855,14 @@ class StableBrowser {
3241
3855
  else {
3242
3856
  this.stepName = "step " + this.stepIndex;
3243
3857
  }
3244
- if (this.context) {
3245
- this.context.examplesRow = extractStepExampleParameters(step);
3246
- }
3247
3858
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3248
3859
  if (this.context.browserObject.context) {
3249
3860
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3250
3861
  }
3251
3862
  }
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
3863
  if (this.initSnapshotTaken === false) {
3260
3864
  this.initSnapshotTaken = true;
3261
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3865
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
3262
3866
  const snapshot = await this.getAriaSnapshot();
3263
3867
  if (snapshot) {
3264
3868
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -3280,18 +3884,68 @@ class StableBrowser {
3280
3884
  const content = [`- path: ${path}`, `- title: ${title}`];
3281
3885
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3282
3886
  for (let i = 0; i < frames.length; i++) {
3283
- content.push(`- frame: ${i}`);
3284
3887
  const frame = frames[i];
3285
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3286
- content.push(snapshot);
3888
+ try {
3889
+ // Ensure frame is attached and has body
3890
+ const body = frame.locator("body");
3891
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3892
+ const snapshot = await body.ariaSnapshot({ timeout });
3893
+ content.push(`- frame: ${i}`);
3894
+ content.push(snapshot);
3895
+ }
3896
+ catch (innerErr) { }
3287
3897
  }
3288
3898
  return content.join("\n");
3289
3899
  }
3290
3900
  catch (e) {
3291
- console.error(e);
3901
+ console.log("Error in getAriaSnapshot");
3902
+ //console.debug(e);
3292
3903
  }
3293
3904
  return null;
3294
3905
  }
3906
+ /**
3907
+ * Sends command with custom payload to report.
3908
+ * @param commandText - Title of the command to be shown in the report.
3909
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3910
+ * @param content - Content of the command to be shown in the report.
3911
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3912
+ * @param world - Optional world context.
3913
+ * @public
3914
+ */
3915
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3916
+ const state = {
3917
+ options,
3918
+ world,
3919
+ locate: false,
3920
+ scroll: false,
3921
+ screenshot: options.screenshot ?? false,
3922
+ highlight: options.highlight ?? false,
3923
+ type: Types.REPORT_COMMAND,
3924
+ text: commandText,
3925
+ _text: commandText,
3926
+ operation: "report_command",
3927
+ log: "***** " + commandText + " *****\n",
3928
+ };
3929
+ try {
3930
+ await _preCommand(state, this);
3931
+ const payload = {
3932
+ type: options.type ?? "text",
3933
+ content: content,
3934
+ screenshotId: null,
3935
+ };
3936
+ state.payload = payload;
3937
+ if (commandStatus === "FAILED") {
3938
+ state.throwError = true;
3939
+ throw new Error("Command failed");
3940
+ }
3941
+ }
3942
+ catch (e) {
3943
+ await _commandError(state, e, this);
3944
+ }
3945
+ finally {
3946
+ await _commandFinally(state, this);
3947
+ }
3948
+ }
3295
3949
  async afterStep(world, step) {
3296
3950
  this.stepName = null;
3297
3951
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3299,6 +3953,13 @@ class StableBrowser {
3299
3953
  await this.context.browserObject.context.tracing.stopChunk({
3300
3954
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3301
3955
  });
3956
+ if (world && world.attach) {
3957
+ await world.attach(JSON.stringify({
3958
+ type: "trace",
3959
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3960
+ }), "application/json+trace");
3961
+ }
3962
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3302
3963
  }
3303
3964
  }
3304
3965
  if (this.context) {
@@ -3311,6 +3972,29 @@ class StableBrowser {
3311
3972
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3312
3973
  }
3313
3974
  }
3975
+ if (!process.env.TEMP_RUN) {
3976
+ const state = {
3977
+ world,
3978
+ locate: false,
3979
+ scroll: false,
3980
+ screenshot: true,
3981
+ highlight: true,
3982
+ type: Types.STEP_COMPLETE,
3983
+ text: "end of scenario",
3984
+ _text: "end of scenario",
3985
+ operation: "step_complete",
3986
+ log: "***** " + "end of scenario" + " *****\n",
3987
+ };
3988
+ try {
3989
+ await _preCommand(state, this);
3990
+ }
3991
+ catch (e) {
3992
+ await _commandError(state, e, this);
3993
+ }
3994
+ finally {
3995
+ await _commandFinally(state, this);
3996
+ }
3997
+ }
3314
3998
  }
3315
3999
  }
3316
4000
  function createTimedPromise(promise, label) {