automation_model 1.0.638-dev → 1.0.638-stage

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +16 -16
  2. package/lib/analyze_helper.js.map +1 -1
  3. package/lib/api.d.ts +0 -1
  4. package/lib/api.js +35 -21
  5. package/lib/api.js.map +1 -1
  6. package/lib/auto_page.d.ts +1 -1
  7. package/lib/auto_page.js +96 -26
  8. package/lib/auto_page.js.map +1 -1
  9. package/lib/browser_manager.js +33 -9
  10. package/lib/browser_manager.js.map +1 -1
  11. package/lib/bruno.d.ts +2 -0
  12. package/lib/bruno.js +381 -0
  13. package/lib/bruno.js.map +1 -0
  14. package/lib/command_common.d.ts +4 -4
  15. package/lib/command_common.js +36 -16
  16. package/lib/command_common.js.map +1 -1
  17. package/lib/date_time.js.map +1 -1
  18. package/lib/drawRect.js.map +1 -1
  19. package/lib/environment.d.ts +1 -0
  20. package/lib/environment.js +1 -0
  21. package/lib/environment.js.map +1 -1
  22. package/lib/error-messages.js.map +1 -1
  23. package/lib/file_checker.d.ts +1 -0
  24. package/lib/file_checker.js +61 -0
  25. package/lib/file_checker.js.map +1 -0
  26. package/lib/find_function.js.map +1 -1
  27. package/lib/index.d.ts +2 -0
  28. package/lib/index.js +2 -0
  29. package/lib/index.js.map +1 -1
  30. package/lib/init_browser.d.ts +2 -2
  31. package/lib/init_browser.js +33 -27
  32. package/lib/init_browser.js.map +1 -1
  33. package/lib/locate_element.js +2 -2
  34. package/lib/locate_element.js.map +1 -1
  35. package/lib/locator.js +1 -1
  36. package/lib/locator.js.map +1 -1
  37. package/lib/locator_log.js.map +1 -1
  38. package/lib/network.d.ts +1 -1
  39. package/lib/network.js +5 -5
  40. package/lib/network.js.map +1 -1
  41. package/lib/snapshot_validation.d.ts +37 -0
  42. package/lib/snapshot_validation.js +357 -0
  43. package/lib/snapshot_validation.js.map +1 -0
  44. package/lib/stable_browser.d.ts +51 -24
  45. package/lib/stable_browser.js +730 -195
  46. package/lib/stable_browser.js.map +1 -1
  47. package/lib/table.js.map +1 -1
  48. package/lib/table_analyze.js.map +1 -1
  49. package/lib/table_helper.js +15 -0
  50. package/lib/table_helper.js.map +1 -1
  51. package/lib/test_context.d.ts +2 -0
  52. package/lib/test_context.js +2 -0
  53. package/lib/test_context.js.map +1 -1
  54. package/lib/utils.d.ts +6 -4
  55. package/lib/utils.js +131 -20
  56. package/lib/utils.js.map +1 -1
  57. package/package.json +11 -6
@@ -15,6 +15,7 @@ import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
17
  import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
18
19
  import { locate_element } from "./locate_element.js";
19
20
  import { randomUUID } from "crypto";
20
21
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
@@ -22,23 +23,26 @@ import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
23
  import { LocatorLog } from "./locator_log.js";
23
24
  import axios from "axios";
24
25
  import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { 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) => {
@@ -180,6 +190,30 @@ class StableBrowser {
180
190
  await this.waitForPageLoad();
181
191
  }
182
192
  }
193
+ async switchTab(tabTitleOrIndex) {
194
+ // first check if the tabNameOrIndex is a number
195
+ let index = parseInt(tabTitleOrIndex);
196
+ if (!isNaN(index)) {
197
+ if (index >= 0 && index < this.context.pages.length) {
198
+ this.page = this.context.pages[index];
199
+ this.context.page = this.page;
200
+ await this.page.bringToFront();
201
+ return;
202
+ }
203
+ }
204
+ // if the tabNameOrIndex is a string, find the tab by name
205
+ for (let i = 0; i < this.context.pages.length; i++) {
206
+ let page = this.context.pages[i];
207
+ let title = await page.title();
208
+ if (title.includes(tabTitleOrIndex)) {
209
+ this.page = page;
210
+ this.context.page = this.page;
211
+ await this.page.bringToFront();
212
+ return;
213
+ }
214
+ }
215
+ throw new Error("Tab not found: " + tabTitleOrIndex);
216
+ }
183
217
  registerConsoleLogListener(page, context) {
184
218
  if (!this.context.webLogger) {
185
219
  this.context.webLogger = [];
@@ -247,6 +281,7 @@ class StableBrowser {
247
281
  if (!url) {
248
282
  throw new Error("url is null, verify that the environment file is correct");
249
283
  }
284
+ url = await this._replaceWithLocalData(url, this.world);
250
285
  if (!url.startsWith("http")) {
251
286
  url = "https://" + url;
252
287
  }
@@ -275,7 +310,7 @@ class StableBrowser {
275
310
  _commandError(state, error, this);
276
311
  }
277
312
  finally {
278
- _commandFinally(state, this);
313
+ await _commandFinally(state, this);
279
314
  }
280
315
  }
281
316
  async _getLocator(locator, scope, _params) {
@@ -391,7 +426,7 @@ class StableBrowser {
391
426
  }
392
427
  return { elementCount: tagCount, randomToken };
393
428
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
429
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null, logErrors = false) {
395
430
  if (!info) {
396
431
  info = {};
397
432
  }
@@ -458,7 +493,7 @@ class StableBrowser {
458
493
  }
459
494
  return;
460
495
  }
461
- if (info.locatorLog && count === 0) {
496
+ if (info.locatorLog && count === 0 && logErrors) {
462
497
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "NOT_FOUND");
463
498
  }
464
499
  for (let j = 0; j < count; j++) {
@@ -473,7 +508,7 @@ class StableBrowser {
473
508
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
474
509
  }
475
510
  }
476
- else {
511
+ else if (logErrors) {
477
512
  info.failCause.visible = visible;
478
513
  info.failCause.enabled = enabled;
479
514
  if (!info.printMessages) {
@@ -565,15 +600,27 @@ class StableBrowser {
565
600
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
566
601
  if (!element.rerun) {
567
602
  const randomToken = Math.random().toString(36).substring(7);
568
- element.evaluate((el, randomToken) => {
603
+ await element.evaluate((el, randomToken) => {
569
604
  el.setAttribute("data-blinq-id-" + randomToken, "");
570
605
  }, 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;
606
+ // if (element._frame) {
607
+ // return element;
608
+ // }
609
+ const scope = element._frame ?? element.page();
610
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
611
+ let prefixSelector = "";
612
+ const frameControlSelector = " >> internal:control=enter-frame";
613
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
614
+ if (frameSelectorIndex !== -1) {
615
+ // remove everything after the >> internal:control=enter-frame
616
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
617
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
618
+ }
619
+ // if (element?._frame?._selector) {
620
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
621
+ // }
622
+ const newSelector = prefixSelector + newElementSelector;
623
+ return scope.locator(newSelector);
577
624
  }
578
625
  }
579
626
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -725,14 +772,9 @@ class StableBrowser {
725
772
  // info.log += "scanning locators in priority 2" + "\n";
726
773
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
727
774
  }
728
- if (result.foundElements.length === 0 && onlyPriority3) {
775
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
729
776
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
730
777
  }
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
778
  let foundElements = result.foundElements;
737
779
  if (foundElements.length === 1 && foundElements[0].unique) {
738
780
  info.box = foundElements[0].box;
@@ -787,6 +829,11 @@ class StableBrowser {
787
829
  visibleOnly = false;
788
830
  }
789
831
  await new Promise((resolve) => setTimeout(resolve, 1000));
832
+ // sheck of more of half of the timeout has passed
833
+ if (Date.now() - startTime > timeout / 2) {
834
+ highPriorityOnly = false;
835
+ visibleOnly = false;
836
+ }
790
837
  }
791
838
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
792
839
  // if (info.locatorLog) {
@@ -802,7 +849,7 @@ class StableBrowser {
802
849
  }
803
850
  throw new Error("failed to locate first element no elements found, " + info.log);
804
851
  }
805
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
852
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name, logErrors = false) {
806
853
  let foundElements = [];
807
854
  const result = {
808
855
  foundElements: foundElements,
@@ -821,7 +868,9 @@ class StableBrowser {
821
868
  await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
822
869
  }
823
870
  catch (e) {
824
- this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
871
+ if (logErrors) {
872
+ this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
873
+ }
825
874
  }
826
875
  }
827
876
  if (foundLocators.length === 1) {
@@ -833,9 +882,40 @@ class StableBrowser {
833
882
  result.locatorIndex = i;
834
883
  }
835
884
  if (foundLocators.length > 1) {
836
- info.failCause.foundMultiple = true;
837
- if (info.locatorLog) {
838
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
885
+ // remove elements that consume the same space with 10 pixels tolerance
886
+ const boxes = [];
887
+ for (let j = 0; j < foundLocators.length; j++) {
888
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
889
+ }
890
+ for (let j = 0; j < boxes.length; j++) {
891
+ for (let k = 0; k < boxes.length; k++) {
892
+ if (j === k) {
893
+ continue;
894
+ }
895
+ // check if x, y, width, height are the same with 10 pixels tolerance
896
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
897
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
898
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
899
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
900
+ // as the element is not unique, will remove it
901
+ boxes.splice(k, 1);
902
+ k--;
903
+ }
904
+ }
905
+ }
906
+ if (boxes.length === 1) {
907
+ result.foundElements.push({
908
+ locator: boxes[0].locator.first(),
909
+ box: boxes[0].box,
910
+ unique: true,
911
+ });
912
+ result.locatorIndex = i;
913
+ }
914
+ else if (logErrors) {
915
+ info.failCause.foundMultiple = true;
916
+ if (info.locatorLog) {
917
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
918
+ }
839
919
  }
840
920
  }
841
921
  }
@@ -883,7 +963,7 @@ class StableBrowser {
883
963
  await _commandError(state, "timeout looking for " + elementDescription, this);
884
964
  }
885
965
  finally {
886
- _commandFinally(state, this);
966
+ await _commandFinally(state, this);
887
967
  }
888
968
  }
889
969
  }
@@ -932,7 +1012,7 @@ class StableBrowser {
932
1012
  await _commandError(state, "timeout looking for " + elementDescription, this);
933
1013
  }
934
1014
  finally {
935
- _commandFinally(state, this);
1015
+ await _commandFinally(state, this);
936
1016
  }
937
1017
  }
938
1018
  }
@@ -961,7 +1041,7 @@ class StableBrowser {
961
1041
  await _commandError(state, e, this);
962
1042
  }
963
1043
  finally {
964
- _commandFinally(state, this);
1044
+ await _commandFinally(state, this);
965
1045
  }
966
1046
  }
967
1047
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -992,7 +1072,7 @@ class StableBrowser {
992
1072
  // await _commandError(state, e, this);
993
1073
  }
994
1074
  finally {
995
- _commandFinally(state, this);
1075
+ await _commandFinally(state, this);
996
1076
  }
997
1077
  return found;
998
1078
  }
@@ -1016,8 +1096,8 @@ class StableBrowser {
1016
1096
  try {
1017
1097
  // if (world && world.screenshot && !world.screenshotPath) {
1018
1098
  // console.log(`Highlighting while running from recorder`);
1019
- await this._highlightElements(element);
1020
- await state.element.setChecked(checked);
1099
+ await this._highlightElements(state.element);
1100
+ await state.element.setChecked(checked, { timeout: 2000 });
1021
1101
  await new Promise((resolve) => setTimeout(resolve, 1000));
1022
1102
  // await this._unHighlightElements(element);
1023
1103
  // }
@@ -1029,11 +1109,28 @@ class StableBrowser {
1029
1109
  this.logger.info("element did not change its state, ignoring...");
1030
1110
  }
1031
1111
  else {
1112
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1032
1113
  //await this.closeUnexpectedPopups();
1033
1114
  state.info.log += "setCheck failed, will try again" + "\n";
1034
- state.element = await this._locate(selectors, state.info, _params);
1035
- await state.element.setChecked(checked, { timeout: 5000, force: true });
1036
- await new Promise((resolve) => setTimeout(resolve, 1000));
1115
+ state.element_found = false;
1116
+ try {
1117
+ state.element = await this._locate(selectors, state.info, _params, 100);
1118
+ state.element_found = true;
1119
+ // check the check state
1120
+ }
1121
+ catch (error) {
1122
+ // element dismissed
1123
+ }
1124
+ if (state.element_found) {
1125
+ const isChecked = await state.element.isChecked();
1126
+ if (isChecked !== checked) {
1127
+ // perform click
1128
+ await state.element.click({ timeout: 2000, force: true });
1129
+ }
1130
+ else {
1131
+ this.logger.info(`Element ${selectors.element_name} is already in the desired state (${checked})`);
1132
+ }
1133
+ }
1037
1134
  }
1038
1135
  }
1039
1136
  await this.waitForPageLoad();
@@ -1043,7 +1140,7 @@ class StableBrowser {
1043
1140
  await _commandError(state, e, this);
1044
1141
  }
1045
1142
  finally {
1046
- _commandFinally(state, this);
1143
+ await _commandFinally(state, this);
1047
1144
  }
1048
1145
  }
1049
1146
  async hover(selectors, _params, options = {}, world = null) {
@@ -1069,7 +1166,7 @@ class StableBrowser {
1069
1166
  await _commandError(state, e, this);
1070
1167
  }
1071
1168
  finally {
1072
- _commandFinally(state, this);
1169
+ await _commandFinally(state, this);
1073
1170
  }
1074
1171
  }
1075
1172
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1105,7 +1202,7 @@ class StableBrowser {
1105
1202
  await _commandError(state, e, this);
1106
1203
  }
1107
1204
  finally {
1108
- _commandFinally(state, this);
1205
+ await _commandFinally(state, this);
1109
1206
  }
1110
1207
  }
1111
1208
  async type(_value, _params = null, options = {}, world = null) {
@@ -1151,7 +1248,7 @@ class StableBrowser {
1151
1248
  await _commandError(state, e, this);
1152
1249
  }
1153
1250
  finally {
1154
- _commandFinally(state, this);
1251
+ await _commandFinally(state, this);
1155
1252
  }
1156
1253
  }
1157
1254
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1187,7 +1284,7 @@ class StableBrowser {
1187
1284
  await _commandError(state, e, this);
1188
1285
  }
1189
1286
  finally {
1190
- _commandFinally(state, this);
1287
+ await _commandFinally(state, this);
1191
1288
  }
1192
1289
  }
1193
1290
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1256,7 +1353,7 @@ class StableBrowser {
1256
1353
  await _commandError(state, e, this);
1257
1354
  }
1258
1355
  finally {
1259
- _commandFinally(state, this);
1356
+ await _commandFinally(state, this);
1260
1357
  }
1261
1358
  }
1262
1359
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1275,6 +1372,9 @@ class StableBrowser {
1275
1372
  operation: "clickType",
1276
1373
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1277
1374
  };
1375
+ if (!options) {
1376
+ options = {};
1377
+ }
1278
1378
  if (newValue !== _value) {
1279
1379
  //this.logger.info(_value + "=" + newValue);
1280
1380
  _value = newValue;
@@ -1282,7 +1382,7 @@ class StableBrowser {
1282
1382
  try {
1283
1383
  await _preCommand(state, this);
1284
1384
  state.info.value = _value;
1285
- if (options === null || options === undefined || !options.press) {
1385
+ if (!options.press) {
1286
1386
  try {
1287
1387
  let currentValue = await state.element.inputValue();
1288
1388
  if (currentValue) {
@@ -1293,10 +1393,7 @@ class StableBrowser {
1293
1393
  this.logger.info("unable to clear input value");
1294
1394
  }
1295
1395
  }
1296
- if (options === null || options === undefined || options.press) {
1297
- if (!options) {
1298
- options = {};
1299
- }
1396
+ if (options.press) {
1300
1397
  options.timeout = 5000;
1301
1398
  await performAction("click", state.element, options, this, state, _params);
1302
1399
  }
@@ -1356,7 +1453,7 @@ class StableBrowser {
1356
1453
  await _commandError(state, e, this);
1357
1454
  }
1358
1455
  finally {
1359
- _commandFinally(state, this);
1456
+ await _commandFinally(state, this);
1360
1457
  }
1361
1458
  }
1362
1459
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1386,7 +1483,42 @@ class StableBrowser {
1386
1483
  await _commandError(state, e, this);
1387
1484
  }
1388
1485
  finally {
1389
- _commandFinally(state, this);
1486
+ await _commandFinally(state, this);
1487
+ }
1488
+ }
1489
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1490
+ const state = {
1491
+ selectors,
1492
+ _params,
1493
+ files,
1494
+ value: '"' + files.join('", "') + '"',
1495
+ options,
1496
+ world,
1497
+ type: Types.SET_INPUT_FILES,
1498
+ text: `Set input files`,
1499
+ _text: `Set input files on ${selectors.element_name}`,
1500
+ operation: "setInputFiles",
1501
+ log: "***** set input files " + selectors.element_name + " *****\n",
1502
+ };
1503
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1504
+ try {
1505
+ await _preCommand(state, this);
1506
+ for (let i = 0; i < files.length; i++) {
1507
+ const file = files[i];
1508
+ const filePath = path.join(uploadsFolder, file);
1509
+ if (!fs.existsSync(filePath)) {
1510
+ throw new Error(`File not found: ${filePath}`);
1511
+ }
1512
+ state.files[i] = filePath;
1513
+ }
1514
+ await state.element.setInputFiles(files);
1515
+ return state.info;
1516
+ }
1517
+ catch (e) {
1518
+ await _commandError(state, e, this);
1519
+ }
1520
+ finally {
1521
+ await _commandFinally(state, this);
1390
1522
  }
1391
1523
  }
1392
1524
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
@@ -1502,7 +1634,7 @@ class StableBrowser {
1502
1634
  await _commandError(state, e, this);
1503
1635
  }
1504
1636
  finally {
1505
- _commandFinally(state, this);
1637
+ await _commandFinally(state, this);
1506
1638
  }
1507
1639
  }
1508
1640
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
@@ -1537,7 +1669,7 @@ class StableBrowser {
1537
1669
  while (Date.now() - startTime < timeout) {
1538
1670
  try {
1539
1671
  await _preCommand(state, this);
1540
- foundObj = await this._getText(selectors, climb, _params, { timeout: 2000 }, state.info, world);
1672
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1541
1673
  if (foundObj && foundObj.element) {
1542
1674
  await this.scrollIfNeeded(foundObj.element, state.info);
1543
1675
  }
@@ -1579,7 +1711,79 @@ class StableBrowser {
1579
1711
  throw e;
1580
1712
  }
1581
1713
  finally {
1582
- _commandFinally(state, this);
1714
+ await _commandFinally(state, this);
1715
+ }
1716
+ }
1717
+ async snapshotValidation(frameSelectors, referanceSnapshot, _params = null, options = {}, world = null) {
1718
+ const timeout = this._getFindElementTimeout(options);
1719
+ const startTime = Date.now();
1720
+ const state = {
1721
+ _params,
1722
+ value: referanceSnapshot,
1723
+ options,
1724
+ world,
1725
+ locate: false,
1726
+ scroll: false,
1727
+ screenshot: true,
1728
+ highlight: false,
1729
+ type: Types.SNAPSHOT_VALIDATION,
1730
+ text: `verify snapshot: ${referanceSnapshot}`,
1731
+ operation: "snapshotValidation",
1732
+ log: "***** verify snapshot *****\n",
1733
+ };
1734
+ if (!referanceSnapshot) {
1735
+ throw new Error("referanceSnapshot is null");
1736
+ }
1737
+ let text = null;
1738
+ if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1739
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
1740
+ }
1741
+ else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1742
+ text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
1743
+ }
1744
+ else if (referanceSnapshot.startsWith("yaml:")) {
1745
+ text = referanceSnapshot.substring(5);
1746
+ }
1747
+ else {
1748
+ throw new Error("referenceSnapshot file not found: " + referanceSnapshot);
1749
+ }
1750
+ state.text = text;
1751
+ const newValue = await this._replaceWithLocalData(text, world);
1752
+ await _preCommand(state, this);
1753
+ let foundObj = null;
1754
+ try {
1755
+ let matchResult = null;
1756
+ while (Date.now() - startTime < timeout) {
1757
+ try {
1758
+ let scope = null;
1759
+ if (!frameSelectors) {
1760
+ scope = this.page;
1761
+ }
1762
+ else {
1763
+ scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1764
+ }
1765
+ const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
1766
+ matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
1767
+ if (matchResult.errorLine !== -1) {
1768
+ throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1769
+ }
1770
+ // highlight and screenshot
1771
+ return state.info;
1772
+ }
1773
+ catch (e) {
1774
+ // Log error but continue retrying until timeout is reached
1775
+ //this.logger.warn("Retrying snapshot validation due to: " + e.message);
1776
+ }
1777
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 1 second before retrying
1778
+ }
1779
+ throw new Error("No snapshot match " + matchResult?.errorLineText);
1780
+ }
1781
+ catch (e) {
1782
+ await _commandError(state, e, this);
1783
+ throw e;
1784
+ }
1785
+ finally {
1786
+ await _commandFinally(state, this);
1583
1787
  }
1584
1788
  }
1585
1789
  async waitForUserInput(message, world = null) {
@@ -1617,6 +1821,15 @@ class StableBrowser {
1617
1821
  // save the data to the file
1618
1822
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1619
1823
  }
1824
+ overwriteTestData(testData, world = null) {
1825
+ if (!testData) {
1826
+ return;
1827
+ }
1828
+ // if data file exists, load it
1829
+ const dataFile = _getDataFile(world, this.context, this);
1830
+ // save the data to the file
1831
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1832
+ }
1620
1833
  _getDataFilePath(fileName) {
1621
1834
  let dataFile = path.join(this.project_path, "data", fileName);
1622
1835
  if (fs.existsSync(dataFile)) {
@@ -1869,7 +2082,7 @@ class StableBrowser {
1869
2082
  await _commandError(state, e, this);
1870
2083
  }
1871
2084
  finally {
1872
- _commandFinally(state, this);
2085
+ await _commandFinally(state, this);
1873
2086
  }
1874
2087
  }
1875
2088
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1900,10 +2113,31 @@ class StableBrowser {
1900
2113
  case "value":
1901
2114
  state.value = await state.element.inputValue();
1902
2115
  break;
2116
+ case "text":
2117
+ state.value = await state.element.textContent();
2118
+ break;
1903
2119
  default:
1904
2120
  state.value = await state.element.getAttribute(attribute);
1905
2121
  break;
1906
2122
  }
2123
+ if (options !== null) {
2124
+ if (options.regex && options.regex !== "") {
2125
+ // Construct a regex pattern from the provided string
2126
+ const regex = options.regex.slice(1, -1);
2127
+ const regexPattern = new RegExp(regex, "g");
2128
+ const matches = state.value.match(regexPattern);
2129
+ if (matches) {
2130
+ let newValue = "";
2131
+ for (const match of matches) {
2132
+ newValue += match;
2133
+ }
2134
+ state.value = newValue;
2135
+ }
2136
+ }
2137
+ if (options.trimSpaces && options.trimSpaces === true) {
2138
+ state.value = state.value.trim();
2139
+ }
2140
+ }
1907
2141
  state.info.value = state.value;
1908
2142
  this.setTestData({ [variable]: state.value }, world);
1909
2143
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1914,7 +2148,7 @@ class StableBrowser {
1914
2148
  await _commandError(state, e, this);
1915
2149
  }
1916
2150
  finally {
1917
- _commandFinally(state, this);
2151
+ await _commandFinally(state, this);
1918
2152
  }
1919
2153
  }
1920
2154
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1939,12 +2173,15 @@ class StableBrowser {
1939
2173
  let expectedValue;
1940
2174
  try {
1941
2175
  await _preCommand(state, this);
1942
- expectedValue = state.value;
2176
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1943
2177
  state.info.expectedValue = expectedValue;
1944
2178
  switch (attribute) {
1945
2179
  case "innerText":
1946
2180
  val = String(await state.element.innerText());
1947
2181
  break;
2182
+ case "text":
2183
+ val = String(await state.element.textContent());
2184
+ break;
1948
2185
  case "value":
1949
2186
  val = String(await state.element.inputValue());
1950
2187
  break;
@@ -1966,17 +2203,42 @@ class StableBrowser {
1966
2203
  let regex;
1967
2204
  if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
1968
2205
  const patternBody = expectedValue.slice(1, -1);
1969
- regex = new RegExp(patternBody, "g");
2206
+ const processedPattern = patternBody.replace(/\n/g, ".*");
2207
+ regex = new RegExp(processedPattern, "gs");
2208
+ state.info.regex = true;
1970
2209
  }
1971
2210
  else {
1972
2211
  const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1973
2212
  regex = new RegExp(escapedPattern, "g");
1974
2213
  }
1975
- if (!val.match(regex)) {
1976
- let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
1977
- state.info.failCause.assertionFailed = true;
1978
- state.info.failCause.lastError = errorMessage;
1979
- throw new Error(errorMessage);
2214
+ if (attribute === "innerText") {
2215
+ if (state.info.regex) {
2216
+ if (!regex.test(val)) {
2217
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2218
+ state.info.failCause.assertionFailed = true;
2219
+ state.info.failCause.lastError = errorMessage;
2220
+ throw new Error(errorMessage);
2221
+ }
2222
+ }
2223
+ else {
2224
+ const valLines = val.split("\n");
2225
+ const expectedLines = expectedValue.split("\n");
2226
+ const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine === expectedLine));
2227
+ if (!isPart) {
2228
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2229
+ state.info.failCause.assertionFailed = true;
2230
+ state.info.failCause.lastError = errorMessage;
2231
+ throw new Error(errorMessage);
2232
+ }
2233
+ }
2234
+ }
2235
+ else {
2236
+ if (!val.match(regex)) {
2237
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2238
+ state.info.failCause.assertionFailed = true;
2239
+ state.info.failCause.lastError = errorMessage;
2240
+ throw new Error(errorMessage);
2241
+ }
1980
2242
  }
1981
2243
  return state.info;
1982
2244
  }
@@ -1984,7 +2246,7 @@ class StableBrowser {
1984
2246
  await _commandError(state, e, this);
1985
2247
  }
1986
2248
  finally {
1987
- _commandFinally(state, this);
2249
+ await _commandFinally(state, this);
1988
2250
  }
1989
2251
  }
1990
2252
  async extractEmailData(emailAddress, options, world) {
@@ -2144,56 +2406,49 @@ class StableBrowser {
2144
2406
  console.debug(error);
2145
2407
  }
2146
2408
  }
2147
- // async _unhighlightElements(scope, css) {
2148
- // try {
2149
- // if (!scope) {
2150
- // return;
2151
- // }
2152
- // if (!css) {
2153
- // scope
2154
- // .evaluate((node) => {
2155
- // if (node && node.style) {
2156
- // if (!node.__previousOutline) {
2157
- // node.style.outline = "";
2158
- // } else {
2159
- // node.style.outline = node.__previousOutline;
2160
- // }
2161
- // }
2162
- // })
2163
- // .then(() => {})
2164
- // .catch((e) => {
2165
- // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2166
- // });
2167
- // } else {
2168
- // scope
2169
- // .evaluate(([css]) => {
2170
- // if (!css) {
2171
- // return;
2172
- // }
2173
- // let elements = Array.from(document.querySelectorAll(css));
2174
- // for (i = 0; i < elements.length; i++) {
2175
- // let element = elements[i];
2176
- // if (!element.style) {
2177
- // return;
2178
- // }
2179
- // if (!element.__previousOutline) {
2180
- // element.style.outline = "";
2181
- // } else {
2182
- // element.style.outline = element.__previousOutline;
2183
- // }
2184
- // }
2185
- // })
2186
- // .then(() => {})
2187
- // .catch((e) => {
2188
- // // console.error(`Error while unhighlighting element in css: ${e}`);
2189
- // });
2190
- // }
2191
- // } catch (error) {
2192
- // // console.debug(error);
2193
- // }
2194
- // }
2409
+ _matcher(text) {
2410
+ if (!text) {
2411
+ return { matcher: "contains", queryText: "" };
2412
+ }
2413
+ if (text.length < 2) {
2414
+ return { matcher: "contains", queryText: text };
2415
+ }
2416
+ const split = text.split(":");
2417
+ const matcher = split[0].toLowerCase();
2418
+ const queryText = split.slice(1).join(":").trim();
2419
+ return { matcher, queryText };
2420
+ }
2421
+ _getDomain(url) {
2422
+ if (url.length === 0 || (!url.startsWith("http://") && !url.startsWith("https://"))) {
2423
+ return "";
2424
+ }
2425
+ let hostnameFragments = url.split("/")[2].split(".");
2426
+ if (hostnameFragments.some((fragment) => fragment.includes(":"))) {
2427
+ return hostnameFragments.join("-").split(":").join("-");
2428
+ }
2429
+ let n = hostnameFragments.length;
2430
+ let fragments = [...hostnameFragments];
2431
+ while (n > 0 && hostnameFragments[n - 1].length <= 3) {
2432
+ hostnameFragments.pop();
2433
+ n = hostnameFragments.length;
2434
+ }
2435
+ if (n == 0) {
2436
+ if (fragments[0] === "www")
2437
+ fragments = fragments.slice(1);
2438
+ return fragments.length > 1 ? fragments.slice(0, fragments.length - 1).join("-") : fragments.join("-");
2439
+ }
2440
+ if (hostnameFragments[0] === "www")
2441
+ hostnameFragments = hostnameFragments.slice(1);
2442
+ return hostnameFragments.join(".");
2443
+ }
2444
+ /**
2445
+ * Verify the page path matches the given path.
2446
+ * @param {string} pathPart - The path to verify.
2447
+ * @param {object} options - Options for verification.
2448
+ * @param {object} world - The world context.
2449
+ * @returns {Promise<object>} - The state info after verification.
2450
+ */
2195
2451
  async verifyPagePath(pathPart, options = {}, world = null) {
2196
- const startTime = Date.now();
2197
2452
  let error = null;
2198
2453
  let screenshotId = null;
2199
2454
  let screenshotPath = null;
@@ -2207,74 +2462,235 @@ class StableBrowser {
2207
2462
  pathPart = newValue;
2208
2463
  }
2209
2464
  info.pathPart = pathPart;
2465
+ const { matcher, queryText } = this._matcher(pathPart);
2466
+ const state = {
2467
+ text_search: queryText,
2468
+ options,
2469
+ world,
2470
+ locate: false,
2471
+ scroll: false,
2472
+ highlight: false,
2473
+ type: Types.VERIFY_PAGE_PATH,
2474
+ text: `Verify the page url is ${queryText}`,
2475
+ _text: `Verify the page url is ${queryText}`,
2476
+ operation: "verifyPagePath",
2477
+ log: "***** verify page url is " + queryText + " *****\n",
2478
+ };
2210
2479
  try {
2480
+ await _preCommand(state, this);
2481
+ state.info.text = queryText;
2211
2482
  for (let i = 0; i < 30; i++) {
2212
2483
  const url = await this.page.url();
2213
- if (!url.includes(pathPart)) {
2214
- if (i === 29) {
2215
- throw new Error(`url ${url} doesn't contain ${pathPart}`);
2216
- }
2217
- await new Promise((resolve) => setTimeout(resolve, 1000));
2218
- continue;
2484
+ switch (matcher) {
2485
+ case "exact":
2486
+ if (url !== queryText) {
2487
+ if (i === 29) {
2488
+ throw new Error(`Page URL ${url} is not equal to ${queryText}`);
2489
+ }
2490
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2491
+ continue;
2492
+ }
2493
+ break;
2494
+ case "contains":
2495
+ if (!url.includes(queryText)) {
2496
+ if (i === 29) {
2497
+ throw new Error(`Page URL ${url} doesn't contain ${queryText}`);
2498
+ }
2499
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2500
+ continue;
2501
+ }
2502
+ break;
2503
+ case "starts-with":
2504
+ {
2505
+ const domain = this._getDomain(url);
2506
+ if (domain.length > 0 && domain !== queryText) {
2507
+ if (i === 29) {
2508
+ throw new Error(`Page URL ${url} doesn't start with ${queryText}`);
2509
+ }
2510
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2511
+ continue;
2512
+ }
2513
+ }
2514
+ break;
2515
+ case "ends-with":
2516
+ {
2517
+ const urlObj = new URL(url);
2518
+ let route = "/";
2519
+ if (urlObj.pathname !== "/") {
2520
+ route = urlObj.pathname.split("/").slice(-1)[0].trim();
2521
+ }
2522
+ else {
2523
+ route = "/";
2524
+ }
2525
+ if (route !== queryText) {
2526
+ if (i === 29) {
2527
+ throw new Error(`Page URL ${url} doesn't end with ${queryText}`);
2528
+ }
2529
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2530
+ continue;
2531
+ }
2532
+ }
2533
+ break;
2534
+ case "regex":
2535
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2536
+ if (!regex.test(url)) {
2537
+ if (i === 29) {
2538
+ throw new Error(`Page URL ${url} doesn't match regex ${queryText}`);
2539
+ }
2540
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2541
+ continue;
2542
+ }
2543
+ break;
2544
+ default:
2545
+ console.log("Unknown matching type, defaulting to contains matching");
2546
+ if (!url.includes(pathPart)) {
2547
+ if (i === 29) {
2548
+ throw new Error(`Page URL ${url} does not contain ${pathPart}`);
2549
+ }
2550
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2551
+ continue;
2552
+ }
2219
2553
  }
2220
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2221
- return info;
2554
+ await _screenshot(state, this);
2555
+ return state.info;
2222
2556
  }
2223
2557
  }
2224
2558
  catch (e) {
2225
- //await this.closeUnexpectedPopups();
2226
- this.logger.error("verify page path failed " + info.log);
2227
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2228
- info.screenshotPath = screenshotPath;
2229
- Object.assign(e, { info: info });
2230
- error = e;
2231
- // throw e;
2232
- await _commandError({ text: "verifyPagePath", operation: "verifyPagePath", pathPart, info }, e, this);
2559
+ state.info.failCause.lastError = e.message;
2560
+ state.info.failCause.assertionFailed = true;
2561
+ await _commandError(state, e, this);
2233
2562
  }
2234
2563
  finally {
2235
- const endTime = Date.now();
2236
- _reportToWorld(world, {
2237
- type: Types.VERIFY_PAGE_PATH,
2238
- text: "Verify page path",
2239
- _text: "Verify the page path contains " + pathPart,
2240
- screenshotId,
2241
- result: error
2242
- ? {
2243
- status: "FAILED",
2244
- startTime,
2245
- endTime,
2246
- message: error?.message,
2247
- }
2248
- : {
2249
- status: "PASSED",
2250
- startTime,
2251
- endTime,
2252
- },
2253
- info: info,
2254
- });
2564
+ await _commandFinally(state, this);
2565
+ }
2566
+ }
2567
+ /**
2568
+ * Verify the page title matches the given title.
2569
+ * @param {string} title - The title to verify.
2570
+ * @param {object} options - Options for verification.
2571
+ * @param {object} world - The world context.
2572
+ * @returns {Promise<object>} - The state info after verification.
2573
+ */
2574
+ async verifyPageTitle(title, options = {}, world = null) {
2575
+ let error = null;
2576
+ let screenshotId = null;
2577
+ let screenshotPath = null;
2578
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2579
+ const newValue = await this._replaceWithLocalData(title, world);
2580
+ if (newValue !== title) {
2581
+ this.logger.info(title + "=" + newValue);
2582
+ title = newValue;
2583
+ }
2584
+ const { matcher, queryText } = this._matcher(title);
2585
+ const state = {
2586
+ text_search: queryText,
2587
+ options,
2588
+ world,
2589
+ locate: false,
2590
+ scroll: false,
2591
+ highlight: false,
2592
+ type: Types.VERIFY_PAGE_TITLE,
2593
+ text: `Verify the page title is ${queryText}`,
2594
+ _text: `Verify the page title is ${queryText}`,
2595
+ operation: "verifyPageTitle",
2596
+ log: "***** verify page title is " + queryText + " *****\n",
2597
+ };
2598
+ try {
2599
+ await _preCommand(state, this);
2600
+ state.info.text = queryText;
2601
+ for (let i = 0; i < 30; i++) {
2602
+ const foundTitle = await this.page.title();
2603
+ switch (matcher) {
2604
+ case "exact":
2605
+ if (foundTitle !== queryText) {
2606
+ if (i === 29) {
2607
+ throw new Error(`Page Title ${foundTitle} is not equal to ${queryText}`);
2608
+ }
2609
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2610
+ continue;
2611
+ }
2612
+ break;
2613
+ case "contains":
2614
+ if (!foundTitle.includes(queryText)) {
2615
+ if (i === 29) {
2616
+ throw new Error(`Page Title ${foundTitle} doesn't contain ${queryText}`);
2617
+ }
2618
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2619
+ continue;
2620
+ }
2621
+ break;
2622
+ case "starts-with":
2623
+ if (!foundTitle.startsWith(queryText)) {
2624
+ if (i === 29) {
2625
+ throw new Error(`Page title ${foundTitle} doesn't start with ${queryText}`);
2626
+ }
2627
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2628
+ continue;
2629
+ }
2630
+ break;
2631
+ case "ends-with":
2632
+ if (!foundTitle.endsWith(queryText)) {
2633
+ if (i === 29) {
2634
+ throw new Error(`Page Title ${foundTitle} doesn't end with ${queryText}`);
2635
+ }
2636
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2637
+ continue;
2638
+ }
2639
+ break;
2640
+ case "regex":
2641
+ const regex = new RegExp(queryText.slice(1, -1), "g");
2642
+ if (!regex.test(foundTitle)) {
2643
+ if (i === 29) {
2644
+ throw new Error(`Page Title ${foundTitle} doesn't match regex ${queryText}`);
2645
+ }
2646
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2647
+ continue;
2648
+ }
2649
+ break;
2650
+ default:
2651
+ console.log("Unknown matching type, defaulting to contains matching");
2652
+ if (!foundTitle.includes(title)) {
2653
+ if (i === 29) {
2654
+ throw new Error(`Page Title ${foundTitle} does not contain ${title}`);
2655
+ }
2656
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2657
+ continue;
2658
+ }
2659
+ }
2660
+ await _screenshot(state, this);
2661
+ return state.info;
2662
+ }
2663
+ }
2664
+ catch (e) {
2665
+ state.info.failCause.lastError = e.message;
2666
+ state.info.failCause.assertionFailed = true;
2667
+ await _commandError(state, e, this);
2668
+ }
2669
+ finally {
2670
+ await _commandFinally(state, this);
2255
2671
  }
2256
2672
  }
2257
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2673
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2258
2674
  const frames = this.page.frames();
2259
2675
  let results = [];
2260
- let ignoreCase = false;
2676
+ // let ignoreCase = false;
2261
2677
  for (let i = 0; i < frames.length; i++) {
2262
2678
  if (dateAlternatives.date) {
2263
2679
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2264
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2680
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2265
2681
  result.frame = frames[i];
2266
2682
  results.push(result);
2267
2683
  }
2268
2684
  }
2269
2685
  else if (numberAlternatives.number) {
2270
2686
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2271
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2687
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2272
2688
  result.frame = frames[i];
2273
2689
  results.push(result);
2274
2690
  }
2275
2691
  }
2276
2692
  else {
2277
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2693
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2278
2694
  result.frame = frames[i];
2279
2695
  results.push(result);
2280
2696
  }
@@ -2293,7 +2709,7 @@ class StableBrowser {
2293
2709
  scroll: false,
2294
2710
  highlight: false,
2295
2711
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2296
- text: `Verify text exists in page`,
2712
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2297
2713
  _text: `Verify the text '${text}' exists in page`,
2298
2714
  operation: "verifyTextExistInPage",
2299
2715
  log: "***** verify text " + text + " exists in page *****\n",
@@ -2335,27 +2751,10 @@ class StableBrowser {
2335
2751
  const frame = resultWithElementsFound[0].frame;
2336
2752
  const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2337
2753
  await this._highlightElements(frame, dataAttribute);
2338
- // if (world && world.screenshot && !world.screenshotPath) {
2339
- // console.log(`Highlighting for verify text is found while running from recorder`);
2340
- // this._highlightElements(frame, dataAttribute).then(async () => {
2341
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2342
- // this._unhighlightElements(frame, dataAttribute)
2343
- // .then(async () => {
2344
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2345
- // })
2346
- // .catch(
2347
- // (e) => {}
2348
- // console.error(e)
2349
- // );
2350
- // });
2351
- // }
2352
2754
  const element = await frame.locator(dataAttribute).first();
2353
- // await new Promise((resolve) => setTimeout(resolve, 100));
2354
- // await this._unhighlightElements(frame, dataAttribute);
2355
2755
  if (element) {
2356
2756
  await this.scrollIfNeeded(element, state.info);
2357
2757
  await element.dispatchEvent("bvt_verify_page_contains_text");
2358
- // await _screenshot(state, this, element);
2359
2758
  }
2360
2759
  }
2361
2760
  await _screenshot(state, this);
@@ -2365,13 +2764,12 @@ class StableBrowser {
2365
2764
  console.error(error);
2366
2765
  }
2367
2766
  }
2368
- // await expect(element).toHaveCount(1, { timeout: 10000 });
2369
2767
  }
2370
2768
  catch (e) {
2371
2769
  await _commandError(state, e, this);
2372
2770
  }
2373
2771
  finally {
2374
- _commandFinally(state, this);
2772
+ await _commandFinally(state, this);
2375
2773
  }
2376
2774
  }
2377
2775
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2384,7 +2782,7 @@ class StableBrowser {
2384
2782
  scroll: false,
2385
2783
  highlight: false,
2386
2784
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2387
- text: `Verify text does not exist in page`,
2785
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2388
2786
  _text: `Verify the text '${text}' does not exist in page`,
2389
2787
  operation: "verifyTextNotExistInPage",
2390
2788
  log: "***** verify text " + text + " does not exist in page *****\n",
@@ -2428,7 +2826,7 @@ class StableBrowser {
2428
2826
  await _commandError(state, e, this);
2429
2827
  }
2430
2828
  finally {
2431
- _commandFinally(state, this);
2829
+ await _commandFinally(state, this);
2432
2830
  }
2433
2831
  }
2434
2832
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2470,7 +2868,7 @@ class StableBrowser {
2470
2868
  };
2471
2869
  while (true) {
2472
2870
  try {
2473
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2871
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2474
2872
  }
2475
2873
  catch (error) {
2476
2874
  // ignore
@@ -2498,7 +2896,7 @@ class StableBrowser {
2498
2896
  const count = await frame.locator(css).count();
2499
2897
  for (let j = 0; j < count; j++) {
2500
2898
  const continer = await frame.locator(css).nth(j);
2501
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2899
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2502
2900
  if (result.elementCount > 0) {
2503
2901
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2504
2902
  await this._highlightElements(frame, dataAttribute);
@@ -2539,8 +2937,32 @@ class StableBrowser {
2539
2937
  await _commandError(state, e, this);
2540
2938
  }
2541
2939
  finally {
2542
- _commandFinally(state, this);
2940
+ await _commandFinally(state, this);
2941
+ }
2942
+ }
2943
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2944
+ const frames = this.page.frames();
2945
+ let results = [];
2946
+ let ignoreCase = false;
2947
+ for (let i = 0; i < frames.length; i++) {
2948
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2949
+ result.frame = frames[i];
2950
+ const climbArray = [];
2951
+ for (let i = 0; i < climb; i++) {
2952
+ climbArray.push("..");
2953
+ }
2954
+ let climbXpath = "xpath=" + climbArray.join("/");
2955
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2956
+ const count = await frames[i].locator(newLocator).count();
2957
+ if (count > 0) {
2958
+ result.elementCount = count;
2959
+ result.locator = newLocator;
2960
+ results.push(result);
2961
+ }
2543
2962
  }
2963
+ // state.info.results = results;
2964
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2965
+ return resultWithElementsFound;
2544
2966
  }
2545
2967
  async visualVerification(text, options = {}, world = null) {
2546
2968
  const startTime = Date.now();
@@ -2858,7 +3280,13 @@ class StableBrowser {
2858
3280
  }
2859
3281
  }
2860
3282
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2861
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3283
+ try {
3284
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3285
+ }
3286
+ catch (error) {
3287
+ this.logger.debug(error);
3288
+ throw error;
3289
+ }
2862
3290
  }
2863
3291
  _getLoadTimeout(options) {
2864
3292
  let timeout = 15000;
@@ -2881,6 +3309,7 @@ class StableBrowser {
2881
3309
  }
2882
3310
  async saveStoreState(path = null, world = null) {
2883
3311
  const storageState = await this.page.context().storageState();
3312
+ path = await this._replaceWithLocalData(path, this.world);
2884
3313
  //const testDataFile = _getDataFile(world, this.context, this);
2885
3314
  if (path) {
2886
3315
  // save { storageState: storageState } into the path
@@ -2891,10 +3320,14 @@ class StableBrowser {
2891
3320
  }
2892
3321
  }
2893
3322
  async restoreSaveState(path = null, world = null) {
3323
+ path = await this._replaceWithLocalData(path, this.world);
2894
3324
  await refreshBrowser(this, path, world);
2895
3325
  this.registerEventListeners(this.context);
2896
3326
  registerNetworkEvents(this.world, this, this.context, this.page);
2897
3327
  registerDownloadEvent(this.page, this.world, this.context);
3328
+ if (this.onRestoreSaveState) {
3329
+ this.onRestoreSaveState(path);
3330
+ }
2898
3331
  }
2899
3332
  async waitForPageLoad(options = {}, world = null) {
2900
3333
  let timeout = this._getLoadTimeout(options);
@@ -2977,7 +3410,7 @@ class StableBrowser {
2977
3410
  await _commandError(state, e, this);
2978
3411
  }
2979
3412
  finally {
2980
- _commandFinally(state, this);
3413
+ await _commandFinally(state, this);
2981
3414
  }
2982
3415
  }
2983
3416
  async tableCellOperation(headerText, rowText, options, _params, world = null) {
@@ -3064,7 +3497,7 @@ class StableBrowser {
3064
3497
  await _commandError(state, e, this);
3065
3498
  }
3066
3499
  finally {
3067
- _commandFinally(state, this);
3500
+ await _commandFinally(state, this);
3068
3501
  }
3069
3502
  }
3070
3503
  saveTestDataAsGlobal(options, world) {
@@ -3169,7 +3602,39 @@ class StableBrowser {
3169
3602
  console.log("#-#");
3170
3603
  }
3171
3604
  }
3605
+ async beforeScenario(world, scenario) {
3606
+ this.beforeScenarioCalled = true;
3607
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3608
+ this.scenarioName = scenario.pickle.name;
3609
+ }
3610
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3611
+ this.featureName = scenario.gherkinDocument.feature.name;
3612
+ }
3613
+ if (this.context) {
3614
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3615
+ }
3616
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3617
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3618
+ // check if @global_test_data tag is present
3619
+ if (this.tags.includes("@global_test_data")) {
3620
+ this.saveTestDataAsGlobal({}, world);
3621
+ }
3622
+ }
3623
+ // update test data based on feature/scenario
3624
+ let envName = null;
3625
+ if (this.context && this.context.environment) {
3626
+ envName = this.context.environment.name;
3627
+ }
3628
+ if (!process.env.TEMP_RUN) {
3629
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3630
+ }
3631
+ await loadBrunoParams(this.context, this.context.environment.name);
3632
+ }
3633
+ async afterScenario(world, scenario) { }
3172
3634
  async beforeStep(world, step) {
3635
+ if (!this.beforeScenarioCalled) {
3636
+ this.beforeScenario(world, step);
3637
+ }
3173
3638
  if (this.stepIndex === undefined) {
3174
3639
  this.stepIndex = 0;
3175
3640
  }
@@ -3186,21 +3651,11 @@ class StableBrowser {
3186
3651
  else {
3187
3652
  this.stepName = "step " + this.stepIndex;
3188
3653
  }
3189
- if (this.context) {
3190
- this.context.examplesRow = extractStepExampleParameters(step);
3191
- }
3192
3654
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3193
3655
  if (this.context.browserObject.context) {
3194
3656
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3195
3657
  }
3196
3658
  }
3197
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3198
- this.tags = step.pickle.tags.map((tag) => tag.name);
3199
- // check if @global_test_data tag is present
3200
- if (this.tags.includes("@global_test_data")) {
3201
- this.saveTestDataAsGlobal({}, world);
3202
- }
3203
- }
3204
3659
  if (this.initSnapshotTaken === false) {
3205
3660
  this.initSnapshotTaken = true;
3206
3661
  if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
@@ -3225,18 +3680,68 @@ class StableBrowser {
3225
3680
  const content = [`- path: ${path}`, `- title: ${title}`];
3226
3681
  const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3227
3682
  for (let i = 0; i < frames.length; i++) {
3228
- content.push(`- frame: ${i}`);
3229
3683
  const frame = frames[i];
3230
- const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3231
- content.push(snapshot);
3684
+ try {
3685
+ // Ensure frame is attached and has body
3686
+ const body = frame.locator("body");
3687
+ await body.waitFor({ timeout: 200 }); // wait explicitly
3688
+ const snapshot = await body.ariaSnapshot({ timeout });
3689
+ content.push(`- frame: ${i}`);
3690
+ content.push(snapshot);
3691
+ }
3692
+ catch (innerErr) { }
3232
3693
  }
3233
3694
  return content.join("\n");
3234
3695
  }
3235
3696
  catch (e) {
3236
- console.error(e);
3697
+ console.log("Error in getAriaSnapshot");
3698
+ //console.debug(e);
3237
3699
  }
3238
3700
  return null;
3239
3701
  }
3702
+ /**
3703
+ * Sends command with custom payload to report.
3704
+ * @param commandText - Title of the command to be shown in the report.
3705
+ * @param commandStatus - Status of the command (e.g. "PASSED", "FAILED").
3706
+ * @param content - Content of the command to be shown in the report.
3707
+ * @param options - Options for the command. Example: { type: "json", screenshot: true }
3708
+ * @param world - Optional world context.
3709
+ * @public
3710
+ */
3711
+ async addCommandToReport(commandText, commandStatus, content, options = {}, world = null) {
3712
+ const state = {
3713
+ options,
3714
+ world,
3715
+ locate: false,
3716
+ scroll: false,
3717
+ screenshot: options.screenshot ?? false,
3718
+ highlight: options.highlight ?? false,
3719
+ type: Types.REPORT_COMMAND,
3720
+ text: commandText,
3721
+ _text: commandText,
3722
+ operation: "report_command",
3723
+ log: "***** " + commandText + " *****\n",
3724
+ };
3725
+ try {
3726
+ await _preCommand(state, this);
3727
+ const payload = {
3728
+ type: options.type ?? "text",
3729
+ content: content,
3730
+ screenshotId: null,
3731
+ };
3732
+ state.payload = payload;
3733
+ if (commandStatus === "FAILED") {
3734
+ state.throwError = true;
3735
+ throw new Error("Command failed");
3736
+ }
3737
+ }
3738
+ catch (e) {
3739
+ await _commandError(state, e, this);
3740
+ }
3741
+ finally {
3742
+ await _commandFinally(state, this);
3743
+ }
3744
+ }
3240
3745
  async afterStep(world, step) {
3241
3746
  this.stepName = null;
3242
3747
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3244,6 +3749,13 @@ class StableBrowser {
3244
3749
  await this.context.browserObject.context.tracing.stopChunk({
3245
3750
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3246
3751
  });
3752
+ if (world && world.attach) {
3753
+ await world.attach(JSON.stringify({
3754
+ type: "trace",
3755
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3756
+ }), "application/json+trace");
3757
+ }
3758
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3247
3759
  }
3248
3760
  }
3249
3761
  if (this.context) {
@@ -3256,6 +3768,29 @@ class StableBrowser {
3256
3768
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3257
3769
  }
3258
3770
  }
3771
+ if (!process.env.TEMP_RUN) {
3772
+ const state = {
3773
+ world,
3774
+ locate: false,
3775
+ scroll: false,
3776
+ screenshot: true,
3777
+ highlight: true,
3778
+ type: Types.STEP_COMPLETE,
3779
+ text: "end of scenario",
3780
+ _text: "end of scenario",
3781
+ operation: "step_complete",
3782
+ log: "***** " + "end of scenario" + " *****\n",
3783
+ };
3784
+ try {
3785
+ await _preCommand(state, this);
3786
+ }
3787
+ catch (e) {
3788
+ await _commandError(state, e, this);
3789
+ }
3790
+ finally {
3791
+ await _commandFinally(state, this);
3792
+ }
3793
+ }
3259
3794
  }
3260
3795
  }
3261
3796
  function createTimedPromise(promise, label) {