automation_model 1.0.612-dev → 1.0.612-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.
@@ -10,18 +10,23 @@ import { getDateTimeValue } from "./date_time.js";
10
10
  import drawRectangle from "./drawRect.js";
11
11
  //import { closeUnexpectedPopups } from "./popups.js";
12
12
  import { getTableCells, getTableData } from "./table_analyze.js";
13
- import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, } from "./utils.js";
13
+ import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
14
14
  import csv from "csv-parser";
15
15
  import { Readable } from "node:stream";
16
16
  import readline from "readline";
17
- import { getContext } from "./init_browser.js";
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";
21
22
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
23
  import { LocatorLog } from "./locator_log.js";
24
+ import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { loadBrunoParams } from "./bruno.js";
23
27
  export const Types = {
24
28
  CLICK: "click_element",
29
+ WAIT_ELEMENT: "wait_element",
25
30
  NAVIGATE: "navigate",
26
31
  FILL: "fill_element",
27
32
  EXECUTE: "execute_page_method",
@@ -43,6 +48,7 @@ export const Types = {
43
48
  UNCHECK: "uncheck_element",
44
49
  EXTRACT: "extract_attribute",
45
50
  CLOSE_PAGE: "close_page",
51
+ TABLE_OPERATION: "table_operation",
46
52
  SET_DATE_TIME: "set_date_time",
47
53
  SET_VIEWPORT: "set_viewport",
48
54
  VERIFY_VISUAL: "verify_visual",
@@ -51,6 +57,9 @@ export const Types = {
51
57
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
52
58
  VERIFY_ATTRIBUTE: "verify_element_attribute",
53
59
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
60
+ BRUNO: "bruno",
61
+ VERIFY_FILE_EXISTS: "verify_file_exists",
62
+ SET_INPUT_FILES: "set_input_files",
54
63
  };
55
64
  export const apps = {};
56
65
  const formatElementName = (elementName) => {
@@ -69,6 +78,7 @@ class StableBrowser {
69
78
  appName = "main";
70
79
  tags = null;
71
80
  isRecording = false;
81
+ initSnapshotTaken = false;
72
82
  constructor(browser, page, logger = null, context = null, world = null) {
73
83
  this.browser = browser;
74
84
  this.page = page;
@@ -175,6 +185,30 @@ class StableBrowser {
175
185
  await this.waitForPageLoad();
176
186
  }
177
187
  }
188
+ async switchTab(tabTitleOrIndex) {
189
+ // first check if the tabNameOrIndex is a number
190
+ let index = parseInt(tabTitleOrIndex);
191
+ if (!isNaN(index)) {
192
+ if (index >= 0 && index < this.context.pages.length) {
193
+ this.page = this.context.pages[index];
194
+ this.context.page = this.page;
195
+ await this.page.bringToFront();
196
+ return;
197
+ }
198
+ }
199
+ // if the tabNameOrIndex is a string, find the tab by name
200
+ for (let i = 0; i < this.context.pages.length; i++) {
201
+ let page = this.context.pages[i];
202
+ let title = await page.title();
203
+ if (title.includes(tabTitleOrIndex)) {
204
+ this.page = page;
205
+ this.context.page = this.page;
206
+ await this.page.bringToFront();
207
+ return;
208
+ }
209
+ }
210
+ throw new Error("Tab not found: " + tabTitleOrIndex);
211
+ }
178
212
  registerConsoleLogListener(page, context) {
179
213
  if (!this.context.webLogger) {
180
214
  this.context.webLogger = [];
@@ -270,7 +304,7 @@ class StableBrowser {
270
304
  _commandError(state, error, this);
271
305
  }
272
306
  finally {
273
- _commandFinally(state, this);
307
+ await _commandFinally(state, this);
274
308
  }
275
309
  }
276
310
  async _getLocator(locator, scope, _params) {
@@ -351,7 +385,7 @@ class StableBrowser {
351
385
  return resultCss;
352
386
  }
353
387
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
354
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
388
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
355
389
  const locator = scope.locator(query);
356
390
  const count = await locator.count();
357
391
  if (!tag1) {
@@ -371,6 +405,12 @@ class StableBrowser {
371
405
  if (!el.setAttribute) {
372
406
  el = el.parentElement;
373
407
  }
408
+ // remove any attributes start with data-blinq-id
409
+ // for (let i = 0; i < el.attributes.length; i++) {
410
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
411
+ // el.removeAttribute(el.attributes[i].name);
412
+ // }
413
+ // }
374
414
  el.setAttribute("data-blinq-id-" + randomToken, "");
375
415
  return true;
376
416
  }, [tag1, randomToken]))) {
@@ -557,9 +597,24 @@ class StableBrowser {
557
597
  element.evaluate((el, randomToken) => {
558
598
  el.setAttribute("data-blinq-id-" + randomToken, "");
559
599
  }, randomToken);
600
+ // if (element._frame) {
601
+ // return element;
602
+ // }
560
603
  const scope = element._frame ?? element.page();
561
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
562
- return newSelector;
604
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
605
+ let prefixSelector = "";
606
+ const frameControlSelector = " >> internal:control=enter-frame";
607
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
608
+ if (frameSelectorIndex !== -1) {
609
+ // remove everything after the >> internal:control=enter-frame
610
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
611
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
612
+ }
613
+ // if (element?._frame?._selector) {
614
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
615
+ // }
616
+ const newSelector = prefixSelector + newElementSelector;
617
+ return scope.locator(newSelector);
563
618
  }
564
619
  }
565
620
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -711,14 +766,9 @@ class StableBrowser {
711
766
  // info.log += "scanning locators in priority 2" + "\n";
712
767
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
713
768
  }
714
- if (result.foundElements.length === 0 && onlyPriority3) {
769
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
715
770
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
716
771
  }
717
- else {
718
- if (result.foundElements.length === 0 && !highPriorityOnly) {
719
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
720
- }
721
- }
722
772
  let foundElements = result.foundElements;
723
773
  if (foundElements.length === 1 && foundElements[0].unique) {
724
774
  info.box = foundElements[0].box;
@@ -773,6 +823,11 @@ class StableBrowser {
773
823
  visibleOnly = false;
774
824
  }
775
825
  await new Promise((resolve) => setTimeout(resolve, 1000));
826
+ // sheck of more of half of the timeout has passed
827
+ if (Date.now() - startTime > timeout / 2) {
828
+ highPriorityOnly = false;
829
+ visibleOnly = false;
830
+ }
776
831
  }
777
832
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
778
833
  // if (info.locatorLog) {
@@ -819,9 +874,40 @@ class StableBrowser {
819
874
  result.locatorIndex = i;
820
875
  }
821
876
  if (foundLocators.length > 1) {
822
- info.failCause.foundMultiple = true;
823
- if (info.locatorLog) {
824
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
877
+ // remove elements that consume the same space with 10 pixels tolerance
878
+ const boxes = [];
879
+ for (let j = 0; j < foundLocators.length; j++) {
880
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
881
+ }
882
+ for (let j = 0; j < boxes.length; j++) {
883
+ for (let k = 0; k < boxes.length; k++) {
884
+ if (j === k) {
885
+ continue;
886
+ }
887
+ // check if x, y, width, height are the same with 10 pixels tolerance
888
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
889
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
890
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
891
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
892
+ // as the element is not unique, will remove it
893
+ boxes.splice(k, 1);
894
+ k--;
895
+ }
896
+ }
897
+ }
898
+ if (boxes.length === 1) {
899
+ result.foundElements.push({
900
+ locator: boxes[0].locator.first(),
901
+ box: boxes[0].box,
902
+ unique: true,
903
+ });
904
+ result.locatorIndex = i;
905
+ }
906
+ else {
907
+ info.failCause.foundMultiple = true;
908
+ if (info.locatorLog) {
909
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
910
+ }
825
911
  }
826
912
  }
827
913
  }
@@ -869,7 +955,7 @@ class StableBrowser {
869
955
  await _commandError(state, "timeout looking for " + elementDescription, this);
870
956
  }
871
957
  finally {
872
- _commandFinally(state, this);
958
+ await _commandFinally(state, this);
873
959
  }
874
960
  }
875
961
  }
@@ -918,7 +1004,7 @@ class StableBrowser {
918
1004
  await _commandError(state, "timeout looking for " + elementDescription, this);
919
1005
  }
920
1006
  finally {
921
- _commandFinally(state, this);
1007
+ await _commandFinally(state, this);
922
1008
  }
923
1009
  }
924
1010
  }
@@ -932,25 +1018,14 @@ class StableBrowser {
932
1018
  options,
933
1019
  world,
934
1020
  text: "Click element",
1021
+ _text: "Click on " + selectors.element_name,
935
1022
  type: Types.CLICK,
936
1023
  operation: "click",
937
1024
  log: "***** click on " + selectors.element_name + " *****\n",
938
1025
  };
939
1026
  try {
940
1027
  await _preCommand(state, this);
941
- // if (state.options && state.options.context) {
942
- // state.selectors.locators[0].text = state.options.context;
943
- // }
944
- try {
945
- await state.element.click();
946
- // await new Promise((resolve) => setTimeout(resolve, 1000));
947
- }
948
- catch (e) {
949
- // await this.closeUnexpectedPopups();
950
- state.element = await this._locate(selectors, state.info, _params);
951
- await state.element.dispatchEvent("click");
952
- // await new Promise((resolve) => setTimeout(resolve, 1000));
953
- }
1028
+ await performAction("click", state.element, options, this, state, _params);
954
1029
  await this.waitForPageLoad();
955
1030
  return state.info;
956
1031
  }
@@ -958,8 +1033,40 @@ class StableBrowser {
958
1033
  await _commandError(state, e, this);
959
1034
  }
960
1035
  finally {
961
- _commandFinally(state, this);
1036
+ await _commandFinally(state, this);
1037
+ }
1038
+ }
1039
+ async waitForElement(selectors, _params, options = {}, world = null) {
1040
+ const timeout = this._getFindElementTimeout(options);
1041
+ const state = {
1042
+ selectors,
1043
+ _params,
1044
+ options,
1045
+ world,
1046
+ text: "Wait for element",
1047
+ _text: "Wait for " + selectors.element_name,
1048
+ type: Types.WAIT_ELEMENT,
1049
+ operation: "waitForElement",
1050
+ log: "***** wait for " + selectors.element_name + " *****\n",
1051
+ };
1052
+ let found = false;
1053
+ try {
1054
+ await _preCommand(state, this);
1055
+ // if (state.options && state.options.context) {
1056
+ // state.selectors.locators[0].text = state.options.context;
1057
+ // }
1058
+ await state.element.waitFor({ timeout: timeout });
1059
+ found = true;
1060
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1061
+ }
1062
+ catch (e) {
1063
+ console.error("Error on waitForElement", e);
1064
+ // await _commandError(state, e, this);
1065
+ }
1066
+ finally {
1067
+ await _commandFinally(state, this);
962
1068
  }
1069
+ return found;
963
1070
  }
964
1071
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
965
1072
  const state = {
@@ -969,6 +1076,7 @@ class StableBrowser {
969
1076
  world,
970
1077
  type: checked ? Types.CHECK : Types.UNCHECK,
971
1078
  text: checked ? `Check element` : `Uncheck element`,
1079
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
972
1080
  operation: "setCheck",
973
1081
  log: "***** check " + selectors.element_name + " *****\n",
974
1082
  };
@@ -980,7 +1088,7 @@ class StableBrowser {
980
1088
  try {
981
1089
  // if (world && world.screenshot && !world.screenshotPath) {
982
1090
  // console.log(`Highlighting while running from recorder`);
983
- await this._highlightElements(element);
1091
+ await this._highlightElements(state.element);
984
1092
  await state.element.setChecked(checked);
985
1093
  await new Promise((resolve) => setTimeout(resolve, 1000));
986
1094
  // await this._unHighlightElements(element);
@@ -1007,7 +1115,7 @@ class StableBrowser {
1007
1115
  await _commandError(state, e, this);
1008
1116
  }
1009
1117
  finally {
1010
- _commandFinally(state, this);
1118
+ await _commandFinally(state, this);
1011
1119
  }
1012
1120
  }
1013
1121
  async hover(selectors, _params, options = {}, world = null) {
@@ -1018,24 +1126,13 @@ class StableBrowser {
1018
1126
  world,
1019
1127
  type: Types.HOVER,
1020
1128
  text: `Hover element`,
1129
+ _text: `Hover on ${selectors.element_name}`,
1021
1130
  operation: "hover",
1022
1131
  log: "***** hover " + selectors.element_name + " *****\n",
1023
1132
  };
1024
1133
  try {
1025
1134
  await _preCommand(state, this);
1026
- try {
1027
- await state.element.hover();
1028
- // await _screenshot(state, this);
1029
- await new Promise((resolve) => setTimeout(resolve, 1000));
1030
- }
1031
- catch (e) {
1032
- //await this.closeUnexpectedPopups();
1033
- state.info.log += "hover failed, will try again" + "\n";
1034
- state.element = await this._locate(selectors, state.info, _params);
1035
- await state.element.hover({ timeout: 10000 });
1036
- // await _screenshot(state, this);
1037
- await new Promise((resolve) => setTimeout(resolve, 1000));
1038
- }
1135
+ await performAction("hover", state.element, options, this, state, _params);
1039
1136
  await _screenshot(state, this);
1040
1137
  await this.waitForPageLoad();
1041
1138
  return state.info;
@@ -1044,7 +1141,7 @@ class StableBrowser {
1044
1141
  await _commandError(state, e, this);
1045
1142
  }
1046
1143
  finally {
1047
- _commandFinally(state, this);
1144
+ await _commandFinally(state, this);
1048
1145
  }
1049
1146
  }
1050
1147
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1059,6 +1156,7 @@ class StableBrowser {
1059
1156
  value: values.toString(),
1060
1157
  type: Types.SELECT,
1061
1158
  text: `Select option: ${values}`,
1159
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1062
1160
  operation: "selectOption",
1063
1161
  log: "***** select option " + selectors.element_name + " *****\n",
1064
1162
  };
@@ -1079,7 +1177,7 @@ class StableBrowser {
1079
1177
  await _commandError(state, e, this);
1080
1178
  }
1081
1179
  finally {
1082
- _commandFinally(state, this);
1180
+ await _commandFinally(state, this);
1083
1181
  }
1084
1182
  }
1085
1183
  async type(_value, _params = null, options = {}, world = null) {
@@ -1093,6 +1191,7 @@ class StableBrowser {
1093
1191
  highlight: false,
1094
1192
  type: Types.TYPE_PRESS,
1095
1193
  text: `Type value: ${_value}`,
1194
+ _text: `Type value: ${_value}`,
1096
1195
  operation: "type",
1097
1196
  log: "",
1098
1197
  };
@@ -1124,7 +1223,7 @@ class StableBrowser {
1124
1223
  await _commandError(state, e, this);
1125
1224
  }
1126
1225
  finally {
1127
- _commandFinally(state, this);
1226
+ await _commandFinally(state, this);
1128
1227
  }
1129
1228
  }
1130
1229
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1160,7 +1259,7 @@ class StableBrowser {
1160
1259
  await _commandError(state, e, this);
1161
1260
  }
1162
1261
  finally {
1163
- _commandFinally(state, this);
1262
+ await _commandFinally(state, this);
1164
1263
  }
1165
1264
  }
1166
1265
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1172,6 +1271,7 @@ class StableBrowser {
1172
1271
  world,
1173
1272
  type: Types.SET_DATE_TIME,
1174
1273
  text: `Set date time value: ${value}`,
1274
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1175
1275
  operation: "setDateTime",
1176
1276
  log: "***** set date time value " + selectors.element_name + " *****\n",
1177
1277
  throwError: false,
@@ -1179,7 +1279,7 @@ class StableBrowser {
1179
1279
  try {
1180
1280
  await _preCommand(state, this);
1181
1281
  try {
1182
- await state.element.click();
1282
+ await performAction("click", state.element, options, this, state, _params);
1183
1283
  await new Promise((resolve) => setTimeout(resolve, 500));
1184
1284
  if (format) {
1185
1285
  state.value = dayjs(state.value).format(format);
@@ -1228,7 +1328,7 @@ class StableBrowser {
1228
1328
  await _commandError(state, e, this);
1229
1329
  }
1230
1330
  finally {
1231
- _commandFinally(state, this);
1331
+ await _commandFinally(state, this);
1232
1332
  }
1233
1333
  }
1234
1334
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1243,9 +1343,13 @@ class StableBrowser {
1243
1343
  world,
1244
1344
  type: Types.FILL,
1245
1345
  text: `Click type input with value: ${_value}`,
1346
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1246
1347
  operation: "clickType",
1247
1348
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1248
1349
  };
1350
+ if (!options) {
1351
+ options = {};
1352
+ }
1249
1353
  if (newValue !== _value) {
1250
1354
  //this.logger.info(_value + "=" + newValue);
1251
1355
  _value = newValue;
@@ -1253,7 +1357,7 @@ class StableBrowser {
1253
1357
  try {
1254
1358
  await _preCommand(state, this);
1255
1359
  state.info.value = _value;
1256
- if (options === null || options === undefined || !options.press) {
1360
+ if (!options.press) {
1257
1361
  try {
1258
1362
  let currentValue = await state.element.inputValue();
1259
1363
  if (currentValue) {
@@ -1264,13 +1368,9 @@ class StableBrowser {
1264
1368
  this.logger.info("unable to clear input value");
1265
1369
  }
1266
1370
  }
1267
- if (options === null || options === undefined || options.press) {
1268
- try {
1269
- await state.element.click({ timeout: 5000 });
1270
- }
1271
- catch (e) {
1272
- await state.element.dispatchEvent("click");
1273
- }
1371
+ if (options.press) {
1372
+ options.timeout = 5000;
1373
+ await performAction("click", state.element, options, this, state, _params);
1274
1374
  }
1275
1375
  else {
1276
1376
  try {
@@ -1328,7 +1428,7 @@ class StableBrowser {
1328
1428
  await _commandError(state, e, this);
1329
1429
  }
1330
1430
  finally {
1331
- _commandFinally(state, this);
1431
+ await _commandFinally(state, this);
1332
1432
  }
1333
1433
  }
1334
1434
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1358,13 +1458,49 @@ class StableBrowser {
1358
1458
  await _commandError(state, e, this);
1359
1459
  }
1360
1460
  finally {
1361
- _commandFinally(state, this);
1461
+ await _commandFinally(state, this);
1462
+ }
1463
+ }
1464
+ async setInputFiles(selectors, files, _params = null, options = {}, world = null) {
1465
+ const state = {
1466
+ selectors,
1467
+ _params,
1468
+ files,
1469
+ value: '"' + files.join('", "') + '"',
1470
+ options,
1471
+ world,
1472
+ type: Types.SET_INPUT_FILES,
1473
+ text: `Set input files`,
1474
+ _text: `Set input files on ${selectors.element_name}`,
1475
+ operation: "setInputFiles",
1476
+ log: "***** set input files " + selectors.element_name + " *****\n",
1477
+ };
1478
+ const uploadsFolder = this.configuration.uploadsFolder ?? "data/uploads";
1479
+ try {
1480
+ await _preCommand(state, this);
1481
+ for (let i = 0; i < files.length; i++) {
1482
+ const file = files[i];
1483
+ const filePath = path.join(uploadsFolder, file);
1484
+ if (!fs.existsSync(filePath)) {
1485
+ throw new Error(`File not found: ${filePath}`);
1486
+ }
1487
+ state.files[i] = filePath;
1488
+ }
1489
+ await state.element.setInputFiles(files);
1490
+ return state.info;
1491
+ }
1492
+ catch (e) {
1493
+ await _commandError(state, e, this);
1494
+ }
1495
+ finally {
1496
+ await _commandFinally(state, this);
1362
1497
  }
1363
1498
  }
1364
1499
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1365
1500
  return await this._getText(selectors, 0, _params, options, info, world);
1366
1501
  }
1367
1502
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1503
+ const timeout = this._getFindElementTimeout(options);
1368
1504
  _validateSelectors(selectors);
1369
1505
  let screenshotId = null;
1370
1506
  let screenshotPath = null;
@@ -1374,7 +1510,7 @@ class StableBrowser {
1374
1510
  }
1375
1511
  info.operation = "getText";
1376
1512
  info.selectors = selectors;
1377
- let element = await this._locate(selectors, info, _params);
1513
+ let element = await this._locate(selectors, info, _params, timeout);
1378
1514
  if (climb > 0) {
1379
1515
  const climbArray = [];
1380
1516
  for (let i = 0; i < climb; i++) {
@@ -1441,6 +1577,7 @@ class StableBrowser {
1441
1577
  highlight: false,
1442
1578
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1443
1579
  text: `Verify element contains pattern: ${pattern}`,
1580
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1444
1581
  operation: "containsPattern",
1445
1582
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1446
1583
  };
@@ -1472,10 +1609,12 @@ class StableBrowser {
1472
1609
  await _commandError(state, e, this);
1473
1610
  }
1474
1611
  finally {
1475
- _commandFinally(state, this);
1612
+ await _commandFinally(state, this);
1476
1613
  }
1477
1614
  }
1478
1615
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1616
+ const timeout = this._getFindElementTimeout(options);
1617
+ const startTime = Date.now();
1479
1618
  const state = {
1480
1619
  selectors,
1481
1620
  _params,
@@ -1502,44 +1641,52 @@ class StableBrowser {
1502
1641
  }
1503
1642
  let foundObj = null;
1504
1643
  try {
1505
- await _preCommand(state, this);
1506
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1507
- if (foundObj && foundObj.element) {
1508
- await this.scrollIfNeeded(foundObj.element, state.info);
1509
- }
1510
- await _screenshot(state, this);
1511
- const dateAlternatives = findDateAlternatives(text);
1512
- const numberAlternatives = findNumberAlternatives(text);
1513
- if (dateAlternatives.date) {
1514
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1515
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1516
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1517
- return state.info;
1644
+ while (Date.now() - startTime < timeout) {
1645
+ try {
1646
+ await _preCommand(state, this);
1647
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1648
+ if (foundObj && foundObj.element) {
1649
+ await this.scrollIfNeeded(foundObj.element, state.info);
1518
1650
  }
1519
- }
1520
- throw new Error("element doesn't contain text " + text);
1521
- }
1522
- else if (numberAlternatives.number) {
1523
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1524
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1525
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1651
+ await _screenshot(state, this);
1652
+ const dateAlternatives = findDateAlternatives(text);
1653
+ const numberAlternatives = findNumberAlternatives(text);
1654
+ if (dateAlternatives.date) {
1655
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1656
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1657
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1658
+ return state.info;
1659
+ }
1660
+ }
1661
+ }
1662
+ else if (numberAlternatives.number) {
1663
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1664
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1665
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1666
+ return state.info;
1667
+ }
1668
+ }
1669
+ }
1670
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1526
1671
  return state.info;
1527
1672
  }
1528
1673
  }
1529
- throw new Error("element doesn't contain text " + text);
1530
- }
1531
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1532
- state.info.foundText = foundObj?.text;
1533
- state.info.value = foundObj?.value;
1534
- throw new Error("element doesn't contain text " + text);
1674
+ catch (e) {
1675
+ // Log error but continue retrying until timeout is reached
1676
+ this.logger.warn("Retrying containsText due to: " + e.message);
1677
+ }
1678
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1535
1679
  }
1536
- return state.info;
1680
+ state.info.foundText = foundObj?.text;
1681
+ state.info.value = foundObj?.value;
1682
+ throw new Error("element doesn't contain text " + text);
1537
1683
  }
1538
1684
  catch (e) {
1539
1685
  await _commandError(state, e, this);
1686
+ throw e;
1540
1687
  }
1541
1688
  finally {
1542
- _commandFinally(state, this);
1689
+ await _commandFinally(state, this);
1543
1690
  }
1544
1691
  }
1545
1692
  async waitForUserInput(message, world = null) {
@@ -1577,6 +1724,15 @@ class StableBrowser {
1577
1724
  // save the data to the file
1578
1725
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1579
1726
  }
1727
+ overwriteTestData(testData, world = null) {
1728
+ if (!testData) {
1729
+ return;
1730
+ }
1731
+ // if data file exists, load it
1732
+ const dataFile = _getDataFile(world, this.context, this);
1733
+ // save the data to the file
1734
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1735
+ }
1580
1736
  _getDataFilePath(fileName) {
1581
1737
  let dataFile = path.join(this.project_path, "data", fileName);
1582
1738
  if (fs.existsSync(dataFile)) {
@@ -1806,6 +1962,7 @@ class StableBrowser {
1806
1962
  else {
1807
1963
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1808
1964
  }
1965
+ return screenshotBuffer;
1809
1966
  }
1810
1967
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1811
1968
  const state = {
@@ -1828,7 +1985,7 @@ class StableBrowser {
1828
1985
  await _commandError(state, e, this);
1829
1986
  }
1830
1987
  finally {
1831
- _commandFinally(state, this);
1988
+ await _commandFinally(state, this);
1832
1989
  }
1833
1990
  }
1834
1991
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1841,6 +1998,7 @@ class StableBrowser {
1841
1998
  world,
1842
1999
  type: Types.EXTRACT,
1843
2000
  text: `Extract attribute from element`,
2001
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1844
2002
  operation: "extractAttribute",
1845
2003
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1846
2004
  allowDisabled: true,
@@ -1858,10 +2016,31 @@ class StableBrowser {
1858
2016
  case "value":
1859
2017
  state.value = await state.element.inputValue();
1860
2018
  break;
2019
+ case "text":
2020
+ state.value = await state.element.textContent();
2021
+ break;
1861
2022
  default:
1862
2023
  state.value = await state.element.getAttribute(attribute);
1863
2024
  break;
1864
2025
  }
2026
+ if (options !== null) {
2027
+ if (options.regex && options.regex !== "") {
2028
+ // Construct a regex pattern from the provided string
2029
+ const regex = options.regex.slice(1, -1);
2030
+ const regexPattern = new RegExp(regex, "g");
2031
+ const matches = state.value.match(regexPattern);
2032
+ if (matches) {
2033
+ let newValue = "";
2034
+ for (const match of matches) {
2035
+ newValue += match;
2036
+ }
2037
+ state.value = newValue;
2038
+ }
2039
+ }
2040
+ if (options.trimSpaces && options.trimSpaces === true) {
2041
+ state.value = state.value.trim();
2042
+ }
2043
+ }
1865
2044
  state.info.value = state.value;
1866
2045
  this.setTestData({ [variable]: state.value }, world);
1867
2046
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1872,7 +2051,7 @@ class StableBrowser {
1872
2051
  await _commandError(state, e, this);
1873
2052
  }
1874
2053
  finally {
1875
- _commandFinally(state, this);
2054
+ await _commandFinally(state, this);
1876
2055
  }
1877
2056
  }
1878
2057
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1887,6 +2066,7 @@ class StableBrowser {
1887
2066
  highlight: true,
1888
2067
  screenshot: true,
1889
2068
  text: `Verify element attribute`,
2069
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1890
2070
  operation: "verifyAttribute",
1891
2071
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1892
2072
  allowDisabled: true,
@@ -1896,12 +2076,15 @@ class StableBrowser {
1896
2076
  let expectedValue;
1897
2077
  try {
1898
2078
  await _preCommand(state, this);
1899
- expectedValue = state.value;
2079
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1900
2080
  state.info.expectedValue = expectedValue;
1901
2081
  switch (attribute) {
1902
2082
  case "innerText":
1903
2083
  val = String(await state.element.innerText());
1904
2084
  break;
2085
+ case "text":
2086
+ val = String(await state.element.textContent());
2087
+ break;
1905
2088
  case "value":
1906
2089
  val = String(await state.element.inputValue());
1907
2090
  break;
@@ -1941,7 +2124,7 @@ class StableBrowser {
1941
2124
  await _commandError(state, e, this);
1942
2125
  }
1943
2126
  finally {
1944
- _commandFinally(state, this);
2127
+ await _commandFinally(state, this);
1945
2128
  }
1946
2129
  }
1947
2130
  async extractEmailData(emailAddress, options, world) {
@@ -2193,6 +2376,7 @@ class StableBrowser {
2193
2376
  _reportToWorld(world, {
2194
2377
  type: Types.VERIFY_PAGE_PATH,
2195
2378
  text: "Verify page path",
2379
+ _text: "Verify the page path contains " + pathPart,
2196
2380
  screenshotId,
2197
2381
  result: error
2198
2382
  ? {
@@ -2210,27 +2394,89 @@ class StableBrowser {
2210
2394
  });
2211
2395
  }
2212
2396
  }
2213
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2397
+ async verifyPageTitle(title, options = {}, world = null) {
2398
+ const startTime = Date.now();
2399
+ let error = null;
2400
+ let screenshotId = null;
2401
+ let screenshotPath = null;
2402
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2403
+ const info = {};
2404
+ info.log = "***** verify page title " + title + " *****\n";
2405
+ info.operation = "verifyPageTitle";
2406
+ const newValue = await this._replaceWithLocalData(title, world);
2407
+ if (newValue !== title) {
2408
+ this.logger.info(title + "=" + newValue);
2409
+ title = newValue;
2410
+ }
2411
+ info.title = title;
2412
+ try {
2413
+ for (let i = 0; i < 30; i++) {
2414
+ const foundTitle = await this.page.title();
2415
+ if (!foundTitle.includes(title)) {
2416
+ if (i === 29) {
2417
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2418
+ }
2419
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2420
+ continue;
2421
+ }
2422
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2423
+ return info;
2424
+ }
2425
+ }
2426
+ catch (e) {
2427
+ //await this.closeUnexpectedPopups();
2428
+ this.logger.error("verify page title failed " + info.log);
2429
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2430
+ info.screenshotPath = screenshotPath;
2431
+ Object.assign(e, { info: info });
2432
+ error = e;
2433
+ // throw e;
2434
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2435
+ }
2436
+ finally {
2437
+ const endTime = Date.now();
2438
+ _reportToWorld(world, {
2439
+ type: Types.VERIFY_PAGE_PATH,
2440
+ text: "Verify page title",
2441
+ _text: "Verify the page title contains " + title,
2442
+ screenshotId,
2443
+ result: error
2444
+ ? {
2445
+ status: "FAILED",
2446
+ startTime,
2447
+ endTime,
2448
+ message: error?.message,
2449
+ }
2450
+ : {
2451
+ status: "PASSED",
2452
+ startTime,
2453
+ endTime,
2454
+ },
2455
+ info: info,
2456
+ });
2457
+ }
2458
+ }
2459
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2214
2460
  const frames = this.page.frames();
2215
2461
  let results = [];
2216
- let ignoreCase = false;
2462
+ // let ignoreCase = false;
2217
2463
  for (let i = 0; i < frames.length; i++) {
2218
2464
  if (dateAlternatives.date) {
2219
2465
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2220
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2466
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2221
2467
  result.frame = frames[i];
2222
2468
  results.push(result);
2223
2469
  }
2224
2470
  }
2225
2471
  else if (numberAlternatives.number) {
2226
2472
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2227
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2473
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2228
2474
  result.frame = frames[i];
2229
2475
  results.push(result);
2230
2476
  }
2231
2477
  }
2232
2478
  else {
2233
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2479
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2234
2480
  result.frame = frames[i];
2235
2481
  results.push(result);
2236
2482
  }
@@ -2249,11 +2495,15 @@ class StableBrowser {
2249
2495
  scroll: false,
2250
2496
  highlight: false,
2251
2497
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2252
- text: `Verify text exists in page`,
2498
+ text: `Verify the text '${maskValue(text)}' exists in page`,
2499
+ _text: `Verify the text '${text}' exists in page`,
2253
2500
  operation: "verifyTextExistInPage",
2254
2501
  log: "***** verify text " + text + " exists in page *****\n",
2255
2502
  };
2256
- const timeout = this._getLoadTimeout(options);
2503
+ if (testForRegex(text)) {
2504
+ text = text.replace(/\\"/g, '"');
2505
+ }
2506
+ const timeout = this._getFindElementTimeout(options);
2257
2507
  await new Promise((resolve) => setTimeout(resolve, 2000));
2258
2508
  const newValue = await this._replaceWithLocalData(text, world);
2259
2509
  if (newValue !== text) {
@@ -2266,7 +2516,15 @@ class StableBrowser {
2266
2516
  await _preCommand(state, this);
2267
2517
  state.info.text = text;
2268
2518
  while (true) {
2269
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2519
+ let resultWithElementsFound = {
2520
+ length: 0,
2521
+ };
2522
+ try {
2523
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2524
+ }
2525
+ catch (error) {
2526
+ // ignore
2527
+ }
2270
2528
  if (resultWithElementsFound.length === 0) {
2271
2529
  if (Date.now() - state.startTime > timeout) {
2272
2530
  throw new Error(`Text ${text} not found in page`);
@@ -2274,35 +2532,40 @@ class StableBrowser {
2274
2532
  await new Promise((resolve) => setTimeout(resolve, 1000));
2275
2533
  continue;
2276
2534
  }
2277
- if (resultWithElementsFound[0].randomToken) {
2278
- const frame = resultWithElementsFound[0].frame;
2279
- const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2280
- await this._highlightElements(frame, dataAttribute);
2281
- // if (world && world.screenshot && !world.screenshotPath) {
2282
- // console.log(`Highlighting for verify text is found while running from recorder`);
2283
- // this._highlightElements(frame, dataAttribute).then(async () => {
2284
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2285
- // this._unhighlightElements(frame, dataAttribute)
2286
- // .then(async () => {
2287
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2288
- // })
2289
- // .catch(
2290
- // (e) => {}
2291
- // console.error(e)
2292
- // );
2293
- // });
2294
- // }
2295
- const element = await frame.locator(dataAttribute).first();
2296
- // await new Promise((resolve) => setTimeout(resolve, 100));
2297
- // await this._unhighlightElements(frame, dataAttribute);
2298
- if (element) {
2299
- await this.scrollIfNeeded(element, state.info);
2300
- await element.dispatchEvent("bvt_verify_page_contains_text");
2301
- // await _screenshot(state, this, element);
2535
+ try {
2536
+ if (resultWithElementsFound[0].randomToken) {
2537
+ const frame = resultWithElementsFound[0].frame;
2538
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2539
+ await this._highlightElements(frame, dataAttribute);
2540
+ // if (world && world.screenshot && !world.screenshotPath) {
2541
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2542
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2543
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2544
+ // this._unhighlightElements(frame, dataAttribute)
2545
+ // .then(async () => {
2546
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2547
+ // })
2548
+ // .catch(
2549
+ // (e) => {}
2550
+ // console.error(e)
2551
+ // );
2552
+ // });
2553
+ // }
2554
+ const element = await frame.locator(dataAttribute).first();
2555
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2556
+ // await this._unhighlightElements(frame, dataAttribute);
2557
+ if (element) {
2558
+ await this.scrollIfNeeded(element, state.info);
2559
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2560
+ // await _screenshot(state, this, element);
2561
+ }
2302
2562
  }
2563
+ await _screenshot(state, this);
2564
+ return state.info;
2565
+ }
2566
+ catch (error) {
2567
+ console.error(error);
2303
2568
  }
2304
- await _screenshot(state, this);
2305
- return state.info;
2306
2569
  }
2307
2570
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2308
2571
  }
@@ -2310,7 +2573,7 @@ class StableBrowser {
2310
2573
  await _commandError(state, e, this);
2311
2574
  }
2312
2575
  finally {
2313
- _commandFinally(state, this);
2576
+ await _commandFinally(state, this);
2314
2577
  }
2315
2578
  }
2316
2579
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2323,11 +2586,15 @@ class StableBrowser {
2323
2586
  scroll: false,
2324
2587
  highlight: false,
2325
2588
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2326
- text: `Verify text does not exist in page`,
2589
+ text: `Verify the text '${maskValue(text)}' does not exist in page`,
2590
+ _text: `Verify the text '${text}' does not exist in page`,
2327
2591
  operation: "verifyTextNotExistInPage",
2328
2592
  log: "***** verify text " + text + " does not exist in page *****\n",
2329
2593
  };
2330
- const timeout = this._getLoadTimeout(options);
2594
+ if (testForRegex(text)) {
2595
+ text = text.replace(/\\"/g, '"');
2596
+ }
2597
+ const timeout = this._getFindElementTimeout(options);
2331
2598
  await new Promise((resolve) => setTimeout(resolve, 2000));
2332
2599
  const newValue = await this._replaceWithLocalData(text, world);
2333
2600
  if (newValue !== text) {
@@ -2339,8 +2606,16 @@ class StableBrowser {
2339
2606
  try {
2340
2607
  await _preCommand(state, this);
2341
2608
  state.info.text = text;
2609
+ let resultWithElementsFound = {
2610
+ length: null, // initial cannot be 0
2611
+ };
2342
2612
  while (true) {
2343
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2613
+ try {
2614
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2615
+ }
2616
+ catch (error) {
2617
+ // ignore
2618
+ }
2344
2619
  if (resultWithElementsFound.length === 0) {
2345
2620
  await _screenshot(state, this);
2346
2621
  return state.info;
@@ -2355,7 +2630,7 @@ class StableBrowser {
2355
2630
  await _commandError(state, e, this);
2356
2631
  }
2357
2632
  finally {
2358
- _commandFinally(state, this);
2633
+ await _commandFinally(state, this);
2359
2634
  }
2360
2635
  }
2361
2636
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2370,10 +2645,11 @@ class StableBrowser {
2370
2645
  highlight: false,
2371
2646
  type: Types.VERIFY_TEXT_WITH_RELATION,
2372
2647
  text: `Verify text with relation to another text`,
2648
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2373
2649
  operation: "verify_text_with_relation",
2374
2650
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2375
2651
  };
2376
- const timeout = this._getLoadTimeout(options);
2652
+ const timeout = this._getFindElementTimeout(options);
2377
2653
  await new Promise((resolve) => setTimeout(resolve, 2000));
2378
2654
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2379
2655
  if (newValue !== textAnchor) {
@@ -2391,8 +2667,16 @@ class StableBrowser {
2391
2667
  try {
2392
2668
  await _preCommand(state, this);
2393
2669
  state.info.text = textToVerify;
2670
+ let resultWithElementsFound = {
2671
+ length: 0,
2672
+ };
2394
2673
  while (true) {
2395
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2674
+ try {
2675
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2676
+ }
2677
+ catch (error) {
2678
+ // ignore
2679
+ }
2396
2680
  if (resultWithElementsFound.length === 0) {
2397
2681
  if (Date.now() - state.startTime > timeout) {
2398
2682
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2400,51 +2684,56 @@ class StableBrowser {
2400
2684
  await new Promise((resolve) => setTimeout(resolve, 1000));
2401
2685
  continue;
2402
2686
  }
2403
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2404
- foundAncore = true;
2405
- const result = resultWithElementsFound[i];
2406
- const token = result.randomToken;
2407
- const frame = result.frame;
2408
- let css = `[data-blinq-id-${token}]`;
2409
- const climbArray1 = [];
2410
- for (let i = 0; i < climb; i++) {
2411
- climbArray1.push("..");
2412
- }
2413
- let climbXpath = "xpath=" + climbArray1.join("/");
2414
- css = css + " >> " + climbXpath;
2415
- const count = await frame.locator(css).count();
2416
- for (let j = 0; j < count; j++) {
2417
- const continer = await frame.locator(css).nth(j);
2418
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2419
- if (result.elementCount > 0) {
2420
- const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2421
- await this._highlightElements(frame, dataAttribute);
2422
- //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2423
- // if (world && world.screenshot && !world.screenshotPath) {
2424
- // console.log(`Highlighting for vtrt while running from recorder`);
2425
- // this._highlightElements(frame, dataAttribute)
2426
- // .then(async () => {
2427
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2428
- // this._unhighlightElements(frame, dataAttribute).then(
2429
- // () => {}
2430
- // console.log(`Unhighlighting vrtr in recorder is successful`)
2431
- // );
2432
- // })
2433
- // .catch(e);
2434
- // }
2435
- //await this._highlightElements(frame, cssAnchor);
2436
- const element = await frame.locator(dataAttribute).first();
2437
- // await new Promise((resolve) => setTimeout(resolve, 100));
2438
- // await this._unhighlightElements(frame, dataAttribute);
2439
- if (element) {
2440
- await this.scrollIfNeeded(element, state.info);
2441
- await element.dispatchEvent("bvt_verify_page_contains_text");
2687
+ try {
2688
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2689
+ foundAncore = true;
2690
+ const result = resultWithElementsFound[i];
2691
+ const token = result.randomToken;
2692
+ const frame = result.frame;
2693
+ let css = `[data-blinq-id-${token}]`;
2694
+ const climbArray1 = [];
2695
+ for (let i = 0; i < climb; i++) {
2696
+ climbArray1.push("..");
2697
+ }
2698
+ let climbXpath = "xpath=" + climbArray1.join("/");
2699
+ css = css + " >> " + climbXpath;
2700
+ const count = await frame.locator(css).count();
2701
+ for (let j = 0; j < count; j++) {
2702
+ const continer = await frame.locator(css).nth(j);
2703
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2704
+ if (result.elementCount > 0) {
2705
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2706
+ await this._highlightElements(frame, dataAttribute);
2707
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2708
+ // if (world && world.screenshot && !world.screenshotPath) {
2709
+ // console.log(`Highlighting for vtrt while running from recorder`);
2710
+ // this._highlightElements(frame, dataAttribute)
2711
+ // .then(async () => {
2712
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2713
+ // this._unhighlightElements(frame, dataAttribute).then(
2714
+ // () => {}
2715
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2716
+ // );
2717
+ // })
2718
+ // .catch(e);
2719
+ // }
2720
+ //await this._highlightElements(frame, cssAnchor);
2721
+ const element = await frame.locator(dataAttribute).first();
2722
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2723
+ // await this._unhighlightElements(frame, dataAttribute);
2724
+ if (element) {
2725
+ await this.scrollIfNeeded(element, state.info);
2726
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2727
+ }
2728
+ await _screenshot(state, this);
2729
+ return state.info;
2442
2730
  }
2443
- await _screenshot(state, this);
2444
- return state.info;
2445
2731
  }
2446
2732
  }
2447
2733
  }
2734
+ catch (error) {
2735
+ console.error(error);
2736
+ }
2448
2737
  }
2449
2738
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2450
2739
  }
@@ -2452,8 +2741,32 @@ class StableBrowser {
2452
2741
  await _commandError(state, e, this);
2453
2742
  }
2454
2743
  finally {
2455
- _commandFinally(state, this);
2744
+ await _commandFinally(state, this);
2745
+ }
2746
+ }
2747
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2748
+ const frames = this.page.frames();
2749
+ let results = [];
2750
+ let ignoreCase = false;
2751
+ for (let i = 0; i < frames.length; i++) {
2752
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2753
+ result.frame = frames[i];
2754
+ const climbArray = [];
2755
+ for (let i = 0; i < climb; i++) {
2756
+ climbArray.push("..");
2757
+ }
2758
+ let climbXpath = "xpath=" + climbArray.join("/");
2759
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2760
+ const count = await frames[i].locator(newLocator).count();
2761
+ if (count > 0) {
2762
+ result.elementCount = count;
2763
+ result.locator = newLocator;
2764
+ results.push(result);
2765
+ }
2456
2766
  }
2767
+ // state.info.results = results;
2768
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2769
+ return resultWithElementsFound;
2457
2770
  }
2458
2771
  async visualVerification(text, options = {}, world = null) {
2459
2772
  const startTime = Date.now();
@@ -2473,10 +2786,13 @@ class StableBrowser {
2473
2786
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2474
2787
  info.screenshotPath = screenshotPath;
2475
2788
  const screenshot = await this.takeScreenshot();
2476
- const request = {
2477
- method: "POST",
2789
+ let request = {
2790
+ method: "post",
2791
+ maxBodyLength: Infinity,
2478
2792
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2479
2793
  headers: {
2794
+ "x-bvt-project-id": path.basename(this.project_path),
2795
+ "x-source": "aaa",
2480
2796
  "Content-Type": "application/json",
2481
2797
  Authorization: `Bearer ${process.env.TOKEN}`,
2482
2798
  },
@@ -2485,7 +2801,7 @@ class StableBrowser {
2485
2801
  screenshot: screenshot,
2486
2802
  }),
2487
2803
  };
2488
- let result = await this.context.api.request(request);
2804
+ const result = await axios.request(request);
2489
2805
  if (result.data.status !== true) {
2490
2806
  throw new Error("Visual validation failed");
2491
2807
  }
@@ -2513,6 +2829,7 @@ class StableBrowser {
2513
2829
  _reportToWorld(world, {
2514
2830
  type: Types.VERIFY_VISUAL,
2515
2831
  text: "Visual verification",
2832
+ _text: "Visual verification of " + text,
2516
2833
  screenshotId,
2517
2834
  result: error
2518
2835
  ? {
@@ -2779,6 +3096,15 @@ class StableBrowser {
2779
3096
  }
2780
3097
  return timeout;
2781
3098
  }
3099
+ _getFindElementTimeout(options) {
3100
+ if (options && options.timeout) {
3101
+ return options.timeout;
3102
+ }
3103
+ if (this.configuration.find_element_timeout) {
3104
+ return this.configuration.find_element_timeout;
3105
+ }
3106
+ return 30000;
3107
+ }
2782
3108
  async saveStoreState(path = null, world = null) {
2783
3109
  const storageState = await this.page.context().storageState();
2784
3110
  //const testDataFile = _getDataFile(world, this.context, this);
@@ -2790,6 +3116,12 @@ class StableBrowser {
2790
3116
  await this.setTestData({ storageState: storageState }, world);
2791
3117
  }
2792
3118
  }
3119
+ async restoreSaveState(path = null, world = null) {
3120
+ await refreshBrowser(this, path, world);
3121
+ this.registerEventListeners(this.context);
3122
+ registerNetworkEvents(this.world, this, this.context, this.page);
3123
+ registerDownloadEvent(this.page, this.world, this.context);
3124
+ }
2793
3125
  async waitForPageLoad(options = {}, world = null) {
2794
3126
  let timeout = this._getLoadTimeout(options);
2795
3127
  const promiseArray = [];
@@ -2857,6 +3189,7 @@ class StableBrowser {
2857
3189
  highlight: false,
2858
3190
  type: Types.CLOSE_PAGE,
2859
3191
  text: `Close page`,
3192
+ _text: `Close the page`,
2860
3193
  operation: "closePage",
2861
3194
  log: "***** close page *****\n",
2862
3195
  throwError: false,
@@ -2870,11 +3203,98 @@ class StableBrowser {
2870
3203
  await _commandError(state, e, this);
2871
3204
  }
2872
3205
  finally {
2873
- _commandFinally(state, this);
3206
+ await _commandFinally(state, this);
3207
+ }
3208
+ }
3209
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3210
+ let operation = null;
3211
+ if (!options || !options.operation) {
3212
+ throw new Error("operation is not defined");
3213
+ }
3214
+ operation = options.operation;
3215
+ // validate operation is one of the supported operations
3216
+ if (operation != "click" && operation != "hover+click") {
3217
+ throw new Error("operation is not supported");
3218
+ }
3219
+ const state = {
3220
+ options,
3221
+ world,
3222
+ locate: false,
3223
+ scroll: false,
3224
+ highlight: false,
3225
+ type: Types.TABLE_OPERATION,
3226
+ text: `Table operation`,
3227
+ _text: `Table ${operation} operation`,
3228
+ operation: operation,
3229
+ log: "***** Table operation *****\n",
3230
+ };
3231
+ const timeout = this._getFindElementTimeout(options);
3232
+ try {
3233
+ await _preCommand(state, this);
3234
+ const start = Date.now();
3235
+ let cellArea = null;
3236
+ while (true) {
3237
+ try {
3238
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3239
+ if (cellArea) {
3240
+ break;
3241
+ }
3242
+ }
3243
+ catch (e) {
3244
+ // ignore
3245
+ }
3246
+ if (Date.now() - start > timeout) {
3247
+ throw new Error(`Cell not found in table`);
3248
+ }
3249
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3250
+ }
3251
+ switch (operation) {
3252
+ case "click":
3253
+ if (!options.css) {
3254
+ // will click in the center of the cell
3255
+ let xOffset = 0;
3256
+ let yOffset = 0;
3257
+ if (options.xOffset) {
3258
+ xOffset = options.xOffset;
3259
+ }
3260
+ if (options.yOffset) {
3261
+ yOffset = options.yOffset;
3262
+ }
3263
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3264
+ }
3265
+ else {
3266
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3267
+ if (results.length === 0) {
3268
+ throw new Error(`Element not found in cell area`);
3269
+ }
3270
+ state.element = results[0];
3271
+ await performAction("click", state.element, options, this, state, _params);
3272
+ }
3273
+ break;
3274
+ case "hover+click":
3275
+ if (!options.css) {
3276
+ throw new Error("css is not defined");
3277
+ }
3278
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3279
+ if (results.length === 0) {
3280
+ throw new Error(`Element not found in cell area`);
3281
+ }
3282
+ state.element = results[0];
3283
+ await performAction("hover+click", state.element, options, this, state, _params);
3284
+ break;
3285
+ default:
3286
+ throw new Error("operation is not supported");
3287
+ }
3288
+ }
3289
+ catch (e) {
3290
+ await _commandError(state, e, this);
3291
+ }
3292
+ finally {
3293
+ await _commandFinally(state, this);
2874
3294
  }
2875
3295
  }
2876
3296
  saveTestDataAsGlobal(options, world) {
2877
- const dataFile = this._getDataFile(world);
3297
+ const dataFile = _getDataFile(world, this.context, this);
2878
3298
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2879
3299
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2880
3300
  }
@@ -2904,6 +3324,7 @@ class StableBrowser {
2904
3324
  _reportToWorld(world, {
2905
3325
  type: Types.SET_VIEWPORT,
2906
3326
  text: "set viewport size to " + width + "x" + hight,
3327
+ _text: "Set the viewport size to " + width + "x" + hight,
2907
3328
  screenshotId,
2908
3329
  result: error
2909
3330
  ? {
@@ -2974,7 +3395,39 @@ class StableBrowser {
2974
3395
  console.log("#-#");
2975
3396
  }
2976
3397
  }
3398
+ async beforeScenario(world, scenario) {
3399
+ this.beforeScenarioCalled = true;
3400
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3401
+ this.scenarioName = scenario.pickle.name;
3402
+ }
3403
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3404
+ this.featureName = scenario.gherkinDocument.feature.name;
3405
+ }
3406
+ if (this.context) {
3407
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3408
+ }
3409
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3410
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3411
+ // check if @global_test_data tag is present
3412
+ if (this.tags.includes("@global_test_data")) {
3413
+ this.saveTestDataAsGlobal({}, world);
3414
+ }
3415
+ }
3416
+ // update test data based on feature/scenario
3417
+ let envName = null;
3418
+ if (this.context && this.context.environment) {
3419
+ envName = this.context.environment.name;
3420
+ }
3421
+ if (!process.env.TEMP_RUN) {
3422
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3423
+ }
3424
+ await loadBrunoParams(this.context, this.context.environment.name);
3425
+ }
3426
+ async afterScenario(world, scenario) { }
2977
3427
  async beforeStep(world, step) {
3428
+ if (!this.beforeScenarioCalled) {
3429
+ this.beforeScenario(world, step);
3430
+ }
2978
3431
  if (this.stepIndex === undefined) {
2979
3432
  this.stepIndex = 0;
2980
3433
  }
@@ -2991,22 +3444,48 @@ class StableBrowser {
2991
3444
  else {
2992
3445
  this.stepName = "step " + this.stepIndex;
2993
3446
  }
2994
- if (this.context) {
2995
- this.context.examplesRow = extractStepExampleParameters(step);
2996
- }
2997
3447
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2998
3448
  if (this.context.browserObject.context) {
2999
3449
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3000
3450
  }
3001
3451
  }
3002
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3003
- this.tags = step.pickle.tags.map((tag) => tag.name);
3004
- // check if @global_test_data tag is present
3005
- if (this.tags.includes("@global_test_data")) {
3006
- this.saveTestDataAsGlobal({}, world);
3452
+ if (this.initSnapshotTaken === false) {
3453
+ this.initSnapshotTaken = true;
3454
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3455
+ const snapshot = await this.getAriaSnapshot();
3456
+ if (snapshot) {
3457
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3458
+ }
3007
3459
  }
3008
3460
  }
3009
3461
  }
3462
+ async getAriaSnapshot() {
3463
+ try {
3464
+ // find the page url
3465
+ const url = await this.page.url();
3466
+ // extract the path from the url
3467
+ const path = new URL(url).pathname;
3468
+ // get the page title
3469
+ const title = await this.page.title();
3470
+ // go over other frams
3471
+ const frames = this.page.frames();
3472
+ const snapshots = [];
3473
+ const content = [`- path: ${path}`, `- title: ${title}`];
3474
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3475
+ for (let i = 0; i < frames.length; i++) {
3476
+ content.push(`- frame: ${i}`);
3477
+ const frame = frames[i];
3478
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3479
+ content.push(snapshot);
3480
+ }
3481
+ return content.join("\n");
3482
+ }
3483
+ catch (e) {
3484
+ console.log("Error in getAriaSnapshot");
3485
+ console.debug(e);
3486
+ }
3487
+ return null;
3488
+ }
3010
3489
  async afterStep(world, step) {
3011
3490
  this.stepName = null;
3012
3491
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3014,11 +3493,25 @@ class StableBrowser {
3014
3493
  await this.context.browserObject.context.tracing.stopChunk({
3015
3494
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3016
3495
  });
3496
+ if (world && world.attach) {
3497
+ await world.attach(JSON.stringify({
3498
+ type: "trace",
3499
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3500
+ }), "application/json+trace");
3501
+ }
3502
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3017
3503
  }
3018
3504
  }
3019
3505
  if (this.context) {
3020
3506
  this.context.examplesRow = null;
3021
3507
  }
3508
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3509
+ const snapshot = await this.getAriaSnapshot();
3510
+ if (snapshot) {
3511
+ const obj = {};
3512
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3513
+ }
3514
+ }
3022
3515
  }
3023
3516
  }
3024
3517
  function createTimedPromise(promise, label) {