automation_model 1.0.646-dev → 1.0.646-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 +48 -24
  41. package/lib/stable_browser.js +696 -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 +171 -54
  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,8 +206,34 @@ 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
+ }
212
+ }
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
+ }
181
235
  }
236
+ throw new Error("Tab not found: " + tabTitleOrIndex);
182
237
  }
183
238
  registerConsoleLogListener(page, context) {
184
239
  if (!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,7 @@ class StableBrowser {
1945
2178
  await _commandError(state, e, this);
1946
2179
  }
1947
2180
  finally {
1948
- _commandFinally(state, this);
2181
+ await _commandFinally(state, this);
1949
2182
  }
1950
2183
  }
1951
2184
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1970,12 +2203,15 @@ class StableBrowser {
1970
2203
  let expectedValue;
1971
2204
  try {
1972
2205
  await _preCommand(state, this);
1973
- expectedValue = state.value;
2206
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1974
2207
  state.info.expectedValue = expectedValue;
1975
2208
  switch (attribute) {
1976
2209
  case "innerText":
1977
2210
  val = String(await state.element.innerText());
1978
2211
  break;
2212
+ case "text":
2213
+ val = String(await state.element.textContent());
2214
+ break;
1979
2215
  case "value":
1980
2216
  val = String(await state.element.inputValue());
1981
2217
  break;
@@ -1997,17 +2233,42 @@ class StableBrowser {
1997
2233
  let regex;
1998
2234
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1999
2235
  const patternBody = expectedValue.slice(1, -1);
2000
- regex = new RegExp(patternBody, "g");
2236
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2237
+ regex = new RegExp(processedPattern, "gs");
2238
+ state.info.regex = true;
2001
2239
  }
2002
2240
  else {
2003
2241
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2004
2242
  regex = new RegExp(escapedPattern, "g");
2005
2243
  }
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);
2244
+ if (attribute === "innerText") {
2245
+ if (state.info.regex) {
2246
+ if (!regex.test(val)) {
2247
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2248
+ state.info.failCause.assertionFailed = true;
2249
+ state.info.failCause.lastError = errorMessage;
2250
+ throw new Error(errorMessage);
2251
+ }
2252
+ }
2253
+ else {
2254
+ const valLines = val.split("\n");
2255
+ const expectedLines = expectedValue.split("\n");
2256
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2257
+ if (!isPart) {
2258
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2259
+ state.info.failCause.assertionFailed = true;
2260
+ state.info.failCause.lastError = errorMessage;
2261
+ throw new Error(errorMessage);
2262
+ }
2263
+ }
2264
+ }
2265
+ else {
2266
+ if (!val.match(regex)) {
2267
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2268
+ state.info.failCause.assertionFailed = true;
2269
+ state.info.failCause.lastError = errorMessage;
2270
+ throw new Error(errorMessage);
2271
+ }
2011
2272
  }
2012
2273
  return state.info;
2013
2274
  }
@@ -2015,7 +2276,7 @@ class StableBrowser {
2015
2276
  await _commandError(state, e, this);
2016
2277
  }
2017
2278
  finally {
2018
- _commandFinally(state, this);
2279
+ await _commandFinally(state, this);
2019
2280
  }
2020
2281
  }
2021
2282
  async extractEmailData(emailAddress, options, world) {
@@ -2175,56 +2436,49 @@ class StableBrowser {
2175
2436
  console.debug(error);
2176
2437
  }
2177
2438
  }
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
- // }
2439
+ _matcher(text) {
2440
+ if (!text) {
2441
+ return { matcher: "contains", queryText: "" };
2442
+ }
2443
+ if (text.length < 2) {
2444
+ return { matcher: "contains", queryText: text };
2445
+ }
2446
+ const split = text.split(":");
2447
+ const matcher = split[0].toLowerCase();
2448
+ const queryText = split.slice(1).join(":").trim();
2449
+ return { matcher, queryText };
2450
+ }
2451
+ _getDomain(url) {
2452
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2453
+ return "";
2454
+ }
2455
+ let hostnameFragments = url.split("/")[2].split(".");
2456
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2457
+ return hostnameFragments.join("-").split(":").join("-");
2458
+ }
2459
+ let n = hostnameFragments.length;
2460
+ let fragments = [...hostnameFragments];
2461
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2462
+ hostnameFragments.pop();
2463
+ n = hostnameFragments.length;
2464
+ }
2465
+ if (n == 0) {
2466
+ if (fragments[0] === "www")
2467
+ fragments = fragments.slice(1);
2468
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2469
+ }
2470
+ if (hostnameFragments[0] === "www")
2471
+ hostnameFragments = hostnameFragments.slice(1);
2472
+ return hostnameFragments.join(".");
2473
+ }
2474
+ /**
2475
+ * Verify the page path matches the given path.
2476
+ * @param {string} pathPart - The path to verify.
2477
+ * @param {object} options - Options for verification.
2478
+ * @param {object} world - The world context.
2479
+ * @returns {Promise<object>} - The state info after verification.
2480
+ */
2226
2481
  async verifyPagePath(pathPart, options = {}, world = null) {
2227
- const startTime = Date.now();
2228
2482
  let error = null;
2229
2483
  let screenshotId = null;
2230
2484
  let screenshotPath = null;
@@ -2238,51 +2492,212 @@ class StableBrowser {
2238
2492
  pathPart = newValue;
2239
2493
  }
2240
2494
  info.pathPart = pathPart;
2495
+ const { matcher, queryText } = this._matcher(pathPart);
2496
+ const state = {
2497
+ text_search: queryText,
2498
+ options,
2499
+ world,
2500
+ locate: false,
2501
+ scroll: false,
2502
+ highlight: false,
2503
+ type: Types.VERIFY_PAGE_PATH,
2504
+ text: `Verify the page url is ${queryText}`,
2505
+ _text: `Verify the page url is ${queryText}`,
2506
+ operation: "verifyPagePath",
2507
+ log: "***** verify page url is " + queryText + " *****\n",
2508
+ };
2241
2509
  try {
2510
+ await _preCommand(state, this);
2511
+ state.info.text = queryText;
2242
2512
  for (let i = 0; i < 30; i++) {
2243
2513
  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;
2514
+ switch (matcher) {
2515
+ case "exact":
2516
+ if (url !== queryText) {
2517
+ if (i === 29) {
2518
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2519
+ }
2520
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2521
+ continue;
2522
+ }
2523
+ break;
2524
+ case "contains":
2525
+ if (!url.includes(queryText)) {
2526
+ if (i === 29) {
2527
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2528
+ }
2529
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2530
+ continue;
2531
+ }
2532
+ break;
2533
+ case "starts-with":
2534
+ {
2535
+ const domain = this._getDomain(url);
2536
+ if (domain.length > 0 && domain !== queryText) {
2537
+ if (i === 29) {
2538
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2539
+ }
2540
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2541
+ continue;
2542
+ }
2543
+ }
2544
+ break;
2545
+ case "ends-with":
2546
+ {
2547
+ const urlObj = new URL(url);
2548
+ let route = "/";
2549
+ if (urlObj.pathname !== "/") {
2550
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2551
+ }
2552
+ else {
2553
+ route = "/";
2554
+ }
2555
+ if (route !== queryText) {
2556
+ if (i === 29) {
2557
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2558
+ }
2559
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2560
+ continue;
2561
+ }
2562
+ }
2563
+ break;
2564
+ case "regex":
2565
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2566
+ if (!regex.test(url)) {
2567
+ if (i === 29) {
2568
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2569
+ }
2570
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2571
+ continue;
2572
+ }
2573
+ break;
2574
+ default:
2575
+ console.log("Unknown matching type, defaulting to contains matching");
2576
+ if (!url.includes(pathPart)) {
2577
+ if (i === 29) {
2578
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2579
+ }
2580
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2581
+ continue;
2582
+ }
2250
2583
  }
2251
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2252
- return info;
2584
+ await _screenshot(state, this);
2585
+ return state.info;
2253
2586
  }
2254
2587
  }
2255
2588
  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);
2589
+ state.info.failCause.lastError = e.message;
2590
+ state.info.failCause.assertionFailed = true;
2591
+ await _commandError(state, e, this);
2264
2592
  }
2265
2593
  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
- });
2594
+ await _commandFinally(state, this);
2595
+ }
2596
+ }
2597
+ /**
2598
+ * Verify the page title matches the given title.
2599
+ * @param {string} title - The title to verify.
2600
+ * @param {object} options - Options for verification.
2601
+ * @param {object} world - The world context.
2602
+ * @returns {Promise<object>} - The state info after verification.
2603
+ */
2604
+ async verifyPageTitle(title, options = {}, world = null) {
2605
+ let error = null;
2606
+ let screenshotId = null;
2607
+ let screenshotPath = null;
2608
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2609
+ const newValue = await this._replaceWithLocalData(title, world);
2610
+ if (newValue !== title) {
2611
+ this.logger.info(title + "=" + newValue);
2612
+ title = newValue;
2613
+ }
2614
+ const { matcher, queryText } = this._matcher(title);
2615
+ const state = {
2616
+ text_search: queryText,
2617
+ options,
2618
+ world,
2619
+ locate: false,
2620
+ scroll: false,
2621
+ highlight: false,
2622
+ type: Types.VERIFY_PAGE_TITLE,
2623
+ text: `Verify the page title is ${queryText}`,
2624
+ _text: `Verify the page title is ${queryText}`,
2625
+ operation: "verifyPageTitle",
2626
+ log: "***** verify page title is " + queryText + " *****\n",
2627
+ };
2628
+ try {
2629
+ await _preCommand(state, this);
2630
+ state.info.text = queryText;
2631
+ for (let i = 0; i < 30; i++) {
2632
+ const foundTitle = await this.page.title();
2633
+ switch (matcher) {
2634
+ case "exact":
2635
+ if (foundTitle !== queryText) {
2636
+ if (i === 29) {
2637
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2638
+ }
2639
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2640
+ continue;
2641
+ }
2642
+ break;
2643
+ case "contains":
2644
+ if (!foundTitle.includes(queryText)) {
2645
+ if (i === 29) {
2646
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2647
+ }
2648
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2649
+ continue;
2650
+ }
2651
+ break;
2652
+ case "starts-with":
2653
+ if (!foundTitle.startsWith(queryText)) {
2654
+ if (i === 29) {
2655
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2656
+ }
2657
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2658
+ continue;
2659
+ }
2660
+ break;
2661
+ case "ends-with":
2662
+ if (!foundTitle.endsWith(queryText)) {
2663
+ if (i === 29) {
2664
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2665
+ }
2666
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2667
+ continue;
2668
+ }
2669
+ break;
2670
+ case "regex":
2671
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2672
+ if (!regex.test(foundTitle)) {
2673
+ if (i === 29) {
2674
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2675
+ }
2676
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2677
+ continue;
2678
+ }
2679
+ break;
2680
+ default:
2681
+ console.log("Unknown matching type, defaulting to contains matching");
2682
+ if (!foundTitle.includes(title)) {
2683
+ if (i === 29) {
2684
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2685
+ }
2686
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2687
+ continue;
2688
+ }
2689
+ }
2690
+ await _screenshot(state, this);
2691
+ return state.info;
2692
+ }
2693
+ }
2694
+ catch (e) {
2695
+ state.info.failCause.lastError = e.message;
2696
+ state.info.failCause.assertionFailed = true;
2697
+ await _commandError(state, e, this);
2698
+ }
2699
+ finally {
2700
+ await _commandFinally(state, this);
2286
2701
  }
2287
2702
  }
2288
2703
  async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
@@ -2324,7 +2739,7 @@ class StableBrowser {
2324
2739
  scroll: false,
2325
2740
  highlight: false,
2326
2741
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2327
- text: `Verify the text '${text}' exists in page`,
2742
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2328
2743
  _text: `Verify the text '${text}' exists in page`,
2329
2744
  operation: "verifyTextExistInPage",
2330
2745
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2366,27 +2781,10 @@ class StableBrowser {
2366
2781
  const frame = resultWithElementsFound[0].frame;
2367
2782
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2368
2783
  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
2784
  const element = await frame.locator(dataAttribute).first();
2384
- // await new Promise((resolve) => setTimeout(resolve, 100));
2385
- // await this._unhighlightElements(frame, dataAttribute);
2386
2785
  if (element) {
2387
2786
  await this.scrollIfNeeded(element, state.info);
2388
2787
  await element.dispatchEvent("bvt_verify_page_contains_text");
2389
- // await _screenshot(state, this, element);
2390
2788
  }
2391
2789
  }
2392
2790
  await _screenshot(state, this);
@@ -2396,13 +2794,12 @@ class StableBrowser {
2396
2794
  console.error(error);
2397
2795
  }
2398
2796
  }
2399
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2400
2797
  }
2401
2798
  catch (e) {
2402
2799
  await _commandError(state, e, this);
2403
2800
  }
2404
2801
  finally {
2405
- _commandFinally(state, this);
2802
+ await _commandFinally(state, this);
2406
2803
  }
2407
2804
  }
2408
2805
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2415,7 +2812,7 @@ class StableBrowser {
2415
2812
  scroll: false,
2416
2813
  highlight: false,
2417
2814
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2418
- text: `Verify text does not exist in page`,
2815
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2419
2816
  _text: `Verify the text '${text}' does not exist in page`,
2420
2817
  operation: "verifyTextNotExistInPage",
2421
2818
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2459,7 +2856,7 @@ class StableBrowser {
2459
2856
  await _commandError(state, e, this);
2460
2857
  }
2461
2858
  finally {
2462
- _commandFinally(state, this);
2859
+ await _commandFinally(state, this);
2463
2860
  }
2464
2861
  }
2465
2862
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2529,7 +2926,7 @@ class StableBrowser {
2529
2926
  const count = await frame.locator(css).count();
2530
2927
  for (let j = 0; j < count; j++) {
2531
2928
  const continer = await frame.locator(css).nth(j);
2532
- const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
2929
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2533
2930
  if (result.elementCount > 0) {
2534
2931
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2535
2932
  await this._highlightElements(frame, dataAttribute);
@@ -2570,7 +2967,7 @@ class StableBrowser {
2570
2967
  await _commandError(state, e, this);
2571
2968
  }
2572
2969
  finally {
2573
- _commandFinally(state, this);
2970
+ await _commandFinally(state, this);
2574
2971
  }
2575
2972
  }
2576
2973
  async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
@@ -2913,7 +3310,13 @@ class StableBrowser {
2913
3310
  }
2914
3311
  }
2915
3312
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2916
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3313
+ try {
3314
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3315
+ }
3316
+ catch (error) {
3317
+ this.logger.debug(error);
3318
+ throw error;
3319
+ }
2917
3320
  }
2918
3321
  _getLoadTimeout(options) {
2919
3322
  let timeout = 15000;
@@ -2936,6 +3339,7 @@ class StableBrowser {
2936
3339
  }
2937
3340
  async saveStoreState(path = null, world = null) {
2938
3341
  const storageState = await this.page.context().storageState();
3342
+ path = await this._replaceWithLocalData(path, this.world);
2939
3343
  //const testDataFile = _getDataFile(world, this.context, this);
2940
3344
  if (path) {
2941
3345
  // save { storageState: storageState } into the path
@@ -2946,10 +3350,14 @@ class StableBrowser {
2946
3350
  }
2947
3351
  }
2948
3352
  async restoreSaveState(path = null, world = null) {
3353
+ path = await this._replaceWithLocalData(path, this.world);
2949
3354
  await refreshBrowser(this, path, world);
2950
3355
  this.registerEventListeners(this.context);
2951
3356
  registerNetworkEvents(this.world, this, this.context, this.page);
2952
3357
  registerDownloadEvent(this.page, this.world, this.context);
3358
+ if (this.onRestoreSaveState) {
3359
+ this.onRestoreSaveState(path);
3360
+ }
2953
3361
  }
2954
3362
  async waitForPageLoad(options = {}, world = null) {
2955
3363
  let timeout = this._getLoadTimeout(options);
@@ -3032,7 +3440,7 @@ class StableBrowser {
3032
3440
  await _commandError(state, e, this);
3033
3441
  }
3034
3442
  finally {
3035
- _commandFinally(state, this);
3443
+ await _commandFinally(state, this);
3036
3444
  }
3037
3445
  }
3038
3446
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3119,7 +3527,7 @@ class StableBrowser {
3119
3527
  await _commandError(state, e, this);
3120
3528
  }
3121
3529
  finally {
3122
- _commandFinally(state, this);
3530
+ await _commandFinally(state, this);
3123
3531
  }
3124
3532
  }
3125
3533
  saveTestDataAsGlobal(options, world) {
@@ -3224,7 +3632,39 @@ class StableBrowser {
3224
3632
  console.log("#-#");
3225
3633
  }
3226
3634
  }
3635
+ async beforeScenario(world, scenario) {
3636
+ this.beforeScenarioCalled = true;
3637
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3638
+ this.scenarioName = scenario.pickle.name;
3639
+ }
3640
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3641
+ this.featureName = scenario.gherkinDocument.feature.name;
3642
+ }
3643
+ if (this.context) {
3644
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3645
+ }
3646
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3647
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3648
+ // check if @global_test_data tag is present
3649
+ if (this.tags.includes("@global_test_data")) {
3650
+ this.saveTestDataAsGlobal({}, world);
3651
+ }
3652
+ }
3653
+ // update test data based on feature/scenario
3654
+ let envName = null;
3655
+ if (this.context && this.context.environment) {
3656
+ envName = this.context.environment.name;
3657
+ }
3658
+ if (!process.env.TEMP_RUN) {
3659
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
3660
+ }
3661
+ await loadBrunoParams(this.context, this.context.environment.name);
3662
+ }
3663
+ async afterScenario(world, scenario) { }
3227
3664
  async beforeStep(world, step) {
3665
+ if (!this.beforeScenarioCalled) {
3666
+ this.beforeScenario(world, step);
3667
+ }
3228
3668
  if (this.stepIndex === undefined) {
3229
3669
  this.stepIndex = 0;
3230
3670
  }
@@ -3241,24 +3681,14 @@ class StableBrowser {
3241
3681
  else {
3242
3682
  this.stepName = "step " + this.stepIndex;
3243
3683
  }
3244
- if (this.context) {
3245
- this.context.examplesRow = extractStepExampleParameters(step);
3246
- }
3247
3684
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3248
3685
  if (this.context.browserObject.context) {
3249
3686
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3250
3687
  }
3251
3688
  }
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
3689
  if (this.initSnapshotTaken === false) {
3260
3690
  this.initSnapshotTaken = true;
3261
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3691
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
3262
3692
  const snapshot = await this.getAriaSnapshot();
3263
3693
  if (snapshot) {
3264
3694
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -3280,18 +3710,68 @@ class StableBrowser {
3280
3710
  const content = [`- path: ${path}`, `- title: ${title}`];
3281
3711
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3282
3712
  for (let i = 0; i < frames.length; i++) {
3283
- content.push(`- frame: ${i}`);
3284
3713
  const frame = frames[i];
3285
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3286
- content.push(snapshot);
3714
+ try {
3715
+ // Ensure frame is attached and has body
3716
+ const body = frame.locator("body");
3717
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3718
+ const snapshot = await body.ariaSnapshot({ timeout });
3719
+ content.push(`- frame: ${i}`);
3720
+ content.push(snapshot);
3721
+ }
3722
+ catch (innerErr) { }
3287
3723
  }
3288
3724
  return content.join("\n");
3289
3725
  }
3290
3726
  catch (e) {
3291
- console.error(e);
3727
+ console.log("Error in getAriaSnapshot");
3728
+ //console.debug(e);
3292
3729
  }
3293
3730
  return null;
3294
3731
  }
3732
+ /**
3733
+ * Sends command with custom payload to report.
3734
+ * @param commandText - Title of the command to be shown in the report.
3735
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3736
+ * @param content - Content of the command to be shown in the report.
3737
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3738
+ * @param world - Optional world context.
3739
+ * @public
3740
+ */
3741
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3742
+ const state = {
3743
+ options,
3744
+ world,
3745
+ locate: false,
3746
+ scroll: false,
3747
+ screenshot: options.screenshot ?? false,
3748
+ highlight: options.highlight ?? false,
3749
+ type: Types.REPORT_COMMAND,
3750
+ text: commandText,
3751
+ _text: commandText,
3752
+ operation: "report_command",
3753
+ log: "***** " + commandText + " *****\n",
3754
+ };
3755
+ try {
3756
+ await _preCommand(state, this);
3757
+ const payload = {
3758
+ type: options.type ?? "text",
3759
+ content: content,
3760
+ screenshotId: null,
3761
+ };
3762
+ state.payload = payload;
3763
+ if (commandStatus === "FAILED") {
3764
+ state.throwError = true;
3765
+ throw new Error("Command failed");
3766
+ }
3767
+ }
3768
+ catch (e) {
3769
+ await _commandError(state, e, this);
3770
+ }
3771
+ finally {
3772
+ await _commandFinally(state, this);
3773
+ }
3774
+ }
3295
3775
  async afterStep(world, step) {
3296
3776
  this.stepName = null;
3297
3777
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3299,6 +3779,13 @@ class StableBrowser {
3299
3779
  await this.context.browserObject.context.tracing.stopChunk({
3300
3780
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3301
3781
  });
3782
+ if (world && world.attach) {
3783
+ await world.attach(JSON.stringify({
3784
+ type: "trace",
3785
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3786
+ }), "application/json+trace");
3787
+ }
3788
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3302
3789
  }
3303
3790
  }
3304
3791
  if (this.context) {
@@ -3311,6 +3798,29 @@ class StableBrowser {
3311
3798
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3312
3799
  }
3313
3800
  }
3801
+ if (!process.env.TEMP_RUN) {
3802
+ const state = {
3803
+ world,
3804
+ locate: false,
3805
+ scroll: false,
3806
+ screenshot: true,
3807
+ highlight: true,
3808
+ type: Types.STEP_COMPLETE,
3809
+ text: "end of scenario",
3810
+ _text: "end of scenario",
3811
+ operation: "step_complete",
3812
+ log: "***** " + "end of scenario" + " *****\n",
3813
+ };
3814
+ try {
3815
+ await _preCommand(state, this);
3816
+ }
3817
+ catch (e) {
3818
+ await _commandError(state, e, this);
3819
+ }
3820
+ finally {
3821
+ await _commandFinally(state, this);
3822
+ }
3823
+ }
3314
3824
  }
3315
3825
  }
3316
3826
  function createTimedPromise(promise, label) {