automation_model 1.0.598-dev → 1.0.598-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, } 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,7 @@ 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",
54
61
  };
55
62
  export const apps = {};
56
63
  const formatElementName = (elementName) => {
@@ -69,6 +76,7 @@ class StableBrowser {
69
76
  appName = "main";
70
77
  tags = null;
71
78
  isRecording = false;
79
+ initSnapshotTaken = false;
72
80
  constructor(browser, page, logger = null, context = null, world = null) {
73
81
  this.browser = browser;
74
82
  this.page = page;
@@ -175,6 +183,30 @@ class StableBrowser {
175
183
  await this.waitForPageLoad();
176
184
  }
177
185
  }
186
+ async switchTab(tabTitleOrIndex) {
187
+ // first check if the tabNameOrIndex is a number
188
+ let index = parseInt(tabTitleOrIndex);
189
+ if (!isNaN(index)) {
190
+ if (index >= 0 && index < this.context.pages.length) {
191
+ this.page = this.context.pages[index];
192
+ this.context.page = this.page;
193
+ await this.page.bringToFront();
194
+ return;
195
+ }
196
+ }
197
+ // if the tabNameOrIndex is a string, find the tab by name
198
+ for (let i = 0; i < this.context.pages.length; i++) {
199
+ let page = this.context.pages[i];
200
+ let title = await page.title();
201
+ if (title.includes(tabTitleOrIndex)) {
202
+ this.page = page;
203
+ this.context.page = this.page;
204
+ await this.page.bringToFront();
205
+ return;
206
+ }
207
+ }
208
+ throw new Error("Tab not found: " + tabTitleOrIndex);
209
+ }
178
210
  registerConsoleLogListener(page, context) {
179
211
  if (!this.context.webLogger) {
180
212
  this.context.webLogger = [];
@@ -239,6 +271,9 @@ class StableBrowser {
239
271
  // await closeUnexpectedPopups(this.page);
240
272
  // }
241
273
  async goto(url, world = null) {
274
+ if (!url) {
275
+ throw new Error("url is null, verify that the environment file is correct");
276
+ }
242
277
  if (!url.startsWith("http")) {
243
278
  url = "https://" + url;
244
279
  }
@@ -267,7 +302,7 @@ class StableBrowser {
267
302
  _commandError(state, error, this);
268
303
  }
269
304
  finally {
270
- _commandFinally(state, this);
305
+ await _commandFinally(state, this);
271
306
  }
272
307
  }
273
308
  async _getLocator(locator, scope, _params) {
@@ -348,7 +383,7 @@ class StableBrowser {
348
383
  return resultCss;
349
384
  }
350
385
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
351
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
386
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
352
387
  const locator = scope.locator(query);
353
388
  const count = await locator.count();
354
389
  if (!tag1) {
@@ -368,6 +403,12 @@ class StableBrowser {
368
403
  if (!el.setAttribute) {
369
404
  el = el.parentElement;
370
405
  }
406
+ // remove any attributes start with data-blinq-id
407
+ // for (let i = 0; i < el.attributes.length; i++) {
408
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
409
+ // el.removeAttribute(el.attributes[i].name);
410
+ // }
411
+ // }
371
412
  el.setAttribute("data-blinq-id-" + randomToken, "");
372
413
  return true;
373
414
  }, [tag1, randomToken]))) {
@@ -550,7 +591,28 @@ class StableBrowser {
550
591
  }
551
592
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
552
593
  if (!element.rerun) {
553
- return element;
594
+ const randomToken = Math.random().toString(36).substring(7);
595
+ element.evaluate((el, randomToken) => {
596
+ el.setAttribute("data-blinq-id-" + randomToken, "");
597
+ }, randomToken);
598
+ // if (element._frame) {
599
+ // return element;
600
+ // }
601
+ const scope = element._frame ?? element.page();
602
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
603
+ let prefixSelector = "";
604
+ const frameControlSelector = " >> internal:control=enter-frame";
605
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
606
+ if (frameSelectorIndex !== -1) {
607
+ // remove everything after the >> internal:control=enter-frame
608
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
609
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
610
+ }
611
+ // if (element?._frame?._selector) {
612
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
613
+ // }
614
+ const newSelector = prefixSelector + newElementSelector;
615
+ return scope.locator(newSelector);
554
616
  }
555
617
  }
556
618
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -702,14 +764,9 @@ class StableBrowser {
702
764
  // info.log += "scanning locators in priority 2" + "\n";
703
765
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
704
766
  }
705
- if (result.foundElements.length === 0 && onlyPriority3) {
767
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
706
768
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
707
769
  }
708
- else {
709
- if (result.foundElements.length === 0 && !highPriorityOnly) {
710
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
711
- }
712
- }
713
770
  let foundElements = result.foundElements;
714
771
  if (foundElements.length === 1 && foundElements[0].unique) {
715
772
  info.box = foundElements[0].box;
@@ -764,6 +821,11 @@ class StableBrowser {
764
821
  visibleOnly = false;
765
822
  }
766
823
  await new Promise((resolve) => setTimeout(resolve, 1000));
824
+ // sheck of more of half of the timeout has passed
825
+ if (Date.now() - startTime > timeout / 2) {
826
+ highPriorityOnly = false;
827
+ visibleOnly = false;
828
+ }
767
829
  }
768
830
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
769
831
  // if (info.locatorLog) {
@@ -810,9 +872,40 @@ class StableBrowser {
810
872
  result.locatorIndex = i;
811
873
  }
812
874
  if (foundLocators.length > 1) {
813
- info.failCause.foundMultiple = true;
814
- if (info.locatorLog) {
815
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
875
+ // remove elements that consume the same space with 10 pixels tolerance
876
+ const boxes = [];
877
+ for (let j = 0; j < foundLocators.length; j++) {
878
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
879
+ }
880
+ for (let j = 0; j < boxes.length; j++) {
881
+ for (let k = 0; k < boxes.length; k++) {
882
+ if (j === k) {
883
+ continue;
884
+ }
885
+ // check if x, y, width, height are the same with 10 pixels tolerance
886
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
887
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
888
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
889
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
890
+ // as the element is not unique, will remove it
891
+ boxes.splice(k, 1);
892
+ k--;
893
+ }
894
+ }
895
+ }
896
+ if (boxes.length === 1) {
897
+ result.foundElements.push({
898
+ locator: boxes[0].locator.first(),
899
+ box: boxes[0].box,
900
+ unique: true,
901
+ });
902
+ result.locatorIndex = i;
903
+ }
904
+ else {
905
+ info.failCause.foundMultiple = true;
906
+ if (info.locatorLog) {
907
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
908
+ }
816
909
  }
817
910
  }
818
911
  }
@@ -860,7 +953,7 @@ class StableBrowser {
860
953
  await _commandError(state, "timeout looking for " + elementDescription, this);
861
954
  }
862
955
  finally {
863
- _commandFinally(state, this);
956
+ await _commandFinally(state, this);
864
957
  }
865
958
  }
866
959
  }
@@ -909,7 +1002,7 @@ class StableBrowser {
909
1002
  await _commandError(state, "timeout looking for " + elementDescription, this);
910
1003
  }
911
1004
  finally {
912
- _commandFinally(state, this);
1005
+ await _commandFinally(state, this);
913
1006
  }
914
1007
  }
915
1008
  }
@@ -923,25 +1016,14 @@ class StableBrowser {
923
1016
  options,
924
1017
  world,
925
1018
  text: "Click element",
1019
+ _text: "Click on " + selectors.element_name,
926
1020
  type: Types.CLICK,
927
1021
  operation: "click",
928
1022
  log: "***** click on " + selectors.element_name + " *****\n",
929
1023
  };
930
1024
  try {
931
1025
  await _preCommand(state, this);
932
- // if (state.options && state.options.context) {
933
- // state.selectors.locators[0].text = state.options.context;
934
- // }
935
- try {
936
- await state.element.click();
937
- // await new Promise((resolve) => setTimeout(resolve, 1000));
938
- }
939
- catch (e) {
940
- // await this.closeUnexpectedPopups();
941
- state.element = await this._locate(selectors, state.info, _params);
942
- await state.element.dispatchEvent("click");
943
- // await new Promise((resolve) => setTimeout(resolve, 1000));
944
- }
1026
+ await performAction("click", state.element, options, this, state, _params);
945
1027
  await this.waitForPageLoad();
946
1028
  return state.info;
947
1029
  }
@@ -949,9 +1031,41 @@ class StableBrowser {
949
1031
  await _commandError(state, e, this);
950
1032
  }
951
1033
  finally {
952
- _commandFinally(state, this);
1034
+ await _commandFinally(state, this);
953
1035
  }
954
1036
  }
1037
+ async waitForElement(selectors, _params, options = {}, world = null) {
1038
+ const timeout = this._getFindElementTimeout(options);
1039
+ const state = {
1040
+ selectors,
1041
+ _params,
1042
+ options,
1043
+ world,
1044
+ text: "Wait for element",
1045
+ _text: "Wait for " + selectors.element_name,
1046
+ type: Types.WAIT_ELEMENT,
1047
+ operation: "waitForElement",
1048
+ log: "***** wait for " + selectors.element_name + " *****\n",
1049
+ };
1050
+ let found = false;
1051
+ try {
1052
+ await _preCommand(state, this);
1053
+ // if (state.options && state.options.context) {
1054
+ // state.selectors.locators[0].text = state.options.context;
1055
+ // }
1056
+ await state.element.waitFor({ timeout: timeout });
1057
+ found = true;
1058
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1059
+ }
1060
+ catch (e) {
1061
+ console.error("Error on waitForElement", e);
1062
+ // await _commandError(state, e, this);
1063
+ }
1064
+ finally {
1065
+ await _commandFinally(state, this);
1066
+ }
1067
+ return found;
1068
+ }
955
1069
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
956
1070
  const state = {
957
1071
  selectors,
@@ -960,6 +1074,7 @@ class StableBrowser {
960
1074
  world,
961
1075
  type: checked ? Types.CHECK : Types.UNCHECK,
962
1076
  text: checked ? `Check element` : `Uncheck element`,
1077
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
963
1078
  operation: "setCheck",
964
1079
  log: "***** check " + selectors.element_name + " *****\n",
965
1080
  };
@@ -971,7 +1086,7 @@ class StableBrowser {
971
1086
  try {
972
1087
  // if (world && world.screenshot && !world.screenshotPath) {
973
1088
  // console.log(`Highlighting while running from recorder`);
974
- await this._highlightElements(element);
1089
+ await this._highlightElements(state.element);
975
1090
  await state.element.setChecked(checked);
976
1091
  await new Promise((resolve) => setTimeout(resolve, 1000));
977
1092
  // await this._unHighlightElements(element);
@@ -998,7 +1113,7 @@ class StableBrowser {
998
1113
  await _commandError(state, e, this);
999
1114
  }
1000
1115
  finally {
1001
- _commandFinally(state, this);
1116
+ await _commandFinally(state, this);
1002
1117
  }
1003
1118
  }
1004
1119
  async hover(selectors, _params, options = {}, world = null) {
@@ -1009,24 +1124,13 @@ class StableBrowser {
1009
1124
  world,
1010
1125
  type: Types.HOVER,
1011
1126
  text: `Hover element`,
1127
+ _text: `Hover on ${selectors.element_name}`,
1012
1128
  operation: "hover",
1013
1129
  log: "***** hover " + selectors.element_name + " *****\n",
1014
1130
  };
1015
1131
  try {
1016
1132
  await _preCommand(state, this);
1017
- try {
1018
- await state.element.hover();
1019
- // await _screenshot(state, this);
1020
- await new Promise((resolve) => setTimeout(resolve, 1000));
1021
- }
1022
- catch (e) {
1023
- //await this.closeUnexpectedPopups();
1024
- state.info.log += "hover failed, will try again" + "\n";
1025
- state.element = await this._locate(selectors, state.info, _params);
1026
- await state.element.hover({ timeout: 10000 });
1027
- // await _screenshot(state, this);
1028
- await new Promise((resolve) => setTimeout(resolve, 1000));
1029
- }
1133
+ await performAction("hover", state.element, options, this, state, _params);
1030
1134
  await _screenshot(state, this);
1031
1135
  await this.waitForPageLoad();
1032
1136
  return state.info;
@@ -1035,7 +1139,7 @@ class StableBrowser {
1035
1139
  await _commandError(state, e, this);
1036
1140
  }
1037
1141
  finally {
1038
- _commandFinally(state, this);
1142
+ await _commandFinally(state, this);
1039
1143
  }
1040
1144
  }
1041
1145
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1050,6 +1154,7 @@ class StableBrowser {
1050
1154
  value: values.toString(),
1051
1155
  type: Types.SELECT,
1052
1156
  text: `Select option: ${values}`,
1157
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1053
1158
  operation: "selectOption",
1054
1159
  log: "***** select option " + selectors.element_name + " *****\n",
1055
1160
  };
@@ -1070,7 +1175,7 @@ class StableBrowser {
1070
1175
  await _commandError(state, e, this);
1071
1176
  }
1072
1177
  finally {
1073
- _commandFinally(state, this);
1178
+ await _commandFinally(state, this);
1074
1179
  }
1075
1180
  }
1076
1181
  async type(_value, _params = null, options = {}, world = null) {
@@ -1084,6 +1189,7 @@ class StableBrowser {
1084
1189
  highlight: false,
1085
1190
  type: Types.TYPE_PRESS,
1086
1191
  text: `Type value: ${_value}`,
1192
+ _text: `Type value: ${_value}`,
1087
1193
  operation: "type",
1088
1194
  log: "",
1089
1195
  };
@@ -1115,7 +1221,7 @@ class StableBrowser {
1115
1221
  await _commandError(state, e, this);
1116
1222
  }
1117
1223
  finally {
1118
- _commandFinally(state, this);
1224
+ await _commandFinally(state, this);
1119
1225
  }
1120
1226
  }
1121
1227
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1151,7 +1257,7 @@ class StableBrowser {
1151
1257
  await _commandError(state, e, this);
1152
1258
  }
1153
1259
  finally {
1154
- _commandFinally(state, this);
1260
+ await _commandFinally(state, this);
1155
1261
  }
1156
1262
  }
1157
1263
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1163,6 +1269,7 @@ class StableBrowser {
1163
1269
  world,
1164
1270
  type: Types.SET_DATE_TIME,
1165
1271
  text: `Set date time value: ${value}`,
1272
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1166
1273
  operation: "setDateTime",
1167
1274
  log: "***** set date time value " + selectors.element_name + " *****\n",
1168
1275
  throwError: false,
@@ -1170,7 +1277,7 @@ class StableBrowser {
1170
1277
  try {
1171
1278
  await _preCommand(state, this);
1172
1279
  try {
1173
- await state.element.click();
1280
+ await performAction("click", state.element, options, this, state, _params);
1174
1281
  await new Promise((resolve) => setTimeout(resolve, 500));
1175
1282
  if (format) {
1176
1283
  state.value = dayjs(state.value).format(format);
@@ -1219,7 +1326,7 @@ class StableBrowser {
1219
1326
  await _commandError(state, e, this);
1220
1327
  }
1221
1328
  finally {
1222
- _commandFinally(state, this);
1329
+ await _commandFinally(state, this);
1223
1330
  }
1224
1331
  }
1225
1332
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1234,9 +1341,13 @@ class StableBrowser {
1234
1341
  world,
1235
1342
  type: Types.FILL,
1236
1343
  text: `Click type input with value: ${_value}`,
1344
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1237
1345
  operation: "clickType",
1238
1346
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1239
1347
  };
1348
+ if (!options) {
1349
+ options = {};
1350
+ }
1240
1351
  if (newValue !== _value) {
1241
1352
  //this.logger.info(_value + "=" + newValue);
1242
1353
  _value = newValue;
@@ -1244,7 +1355,7 @@ class StableBrowser {
1244
1355
  try {
1245
1356
  await _preCommand(state, this);
1246
1357
  state.info.value = _value;
1247
- if (options === null || options === undefined || !options.press) {
1358
+ if (!options.press) {
1248
1359
  try {
1249
1360
  let currentValue = await state.element.inputValue();
1250
1361
  if (currentValue) {
@@ -1255,13 +1366,9 @@ class StableBrowser {
1255
1366
  this.logger.info("unable to clear input value");
1256
1367
  }
1257
1368
  }
1258
- if (options === null || options === undefined || options.press) {
1259
- try {
1260
- await state.element.click({ timeout: 5000 });
1261
- }
1262
- catch (e) {
1263
- await state.element.dispatchEvent("click");
1264
- }
1369
+ if (options.press) {
1370
+ options.timeout = 5000;
1371
+ await performAction("click", state.element, options, this, state, _params);
1265
1372
  }
1266
1373
  else {
1267
1374
  try {
@@ -1319,7 +1426,7 @@ class StableBrowser {
1319
1426
  await _commandError(state, e, this);
1320
1427
  }
1321
1428
  finally {
1322
- _commandFinally(state, this);
1429
+ await _commandFinally(state, this);
1323
1430
  }
1324
1431
  }
1325
1432
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1349,13 +1456,14 @@ class StableBrowser {
1349
1456
  await _commandError(state, e, this);
1350
1457
  }
1351
1458
  finally {
1352
- _commandFinally(state, this);
1459
+ await _commandFinally(state, this);
1353
1460
  }
1354
1461
  }
1355
1462
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1356
1463
  return await this._getText(selectors, 0, _params, options, info, world);
1357
1464
  }
1358
1465
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1466
+ const timeout = this._getFindElementTimeout(options);
1359
1467
  _validateSelectors(selectors);
1360
1468
  let screenshotId = null;
1361
1469
  let screenshotPath = null;
@@ -1365,7 +1473,7 @@ class StableBrowser {
1365
1473
  }
1366
1474
  info.operation = "getText";
1367
1475
  info.selectors = selectors;
1368
- let element = await this._locate(selectors, info, _params);
1476
+ let element = await this._locate(selectors, info, _params, timeout);
1369
1477
  if (climb > 0) {
1370
1478
  const climbArray = [];
1371
1479
  for (let i = 0; i < climb; i++) {
@@ -1432,6 +1540,7 @@ class StableBrowser {
1432
1540
  highlight: false,
1433
1541
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1434
1542
  text: `Verify element contains pattern: ${pattern}`,
1543
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1435
1544
  operation: "containsPattern",
1436
1545
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1437
1546
  };
@@ -1463,10 +1572,12 @@ class StableBrowser {
1463
1572
  await _commandError(state, e, this);
1464
1573
  }
1465
1574
  finally {
1466
- _commandFinally(state, this);
1575
+ await _commandFinally(state, this);
1467
1576
  }
1468
1577
  }
1469
1578
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1579
+ const timeout = this._getFindElementTimeout(options);
1580
+ const startTime = Date.now();
1470
1581
  const state = {
1471
1582
  selectors,
1472
1583
  _params,
@@ -1493,62 +1604,54 @@ class StableBrowser {
1493
1604
  }
1494
1605
  let foundObj = null;
1495
1606
  try {
1496
- await _preCommand(state, this);
1497
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1498
- if (foundObj && foundObj.element) {
1499
- await this.scrollIfNeeded(foundObj.element, state.info);
1500
- }
1501
- await _screenshot(state, this);
1502
- const dateAlternatives = findDateAlternatives(text);
1503
- const numberAlternatives = findNumberAlternatives(text);
1504
- if (dateAlternatives.date) {
1505
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1506
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1507
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1508
- return state.info;
1607
+ while (Date.now() - startTime < timeout) {
1608
+ try {
1609
+ await _preCommand(state, this);
1610
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1611
+ if (foundObj && foundObj.element) {
1612
+ await this.scrollIfNeeded(foundObj.element, state.info);
1509
1613
  }
1510
- }
1511
- throw new Error("element doesn't contain text " + text);
1512
- }
1513
- else if (numberAlternatives.number) {
1514
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1515
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1516
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1614
+ await _screenshot(state, this);
1615
+ const dateAlternatives = findDateAlternatives(text);
1616
+ const numberAlternatives = findNumberAlternatives(text);
1617
+ if (dateAlternatives.date) {
1618
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1619
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1620
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1621
+ return state.info;
1622
+ }
1623
+ }
1624
+ }
1625
+ else if (numberAlternatives.number) {
1626
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1627
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1628
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1629
+ return state.info;
1630
+ }
1631
+ }
1632
+ }
1633
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1517
1634
  return state.info;
1518
1635
  }
1519
1636
  }
1520
- throw new Error("element doesn't contain text " + text);
1521
- }
1522
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1523
- state.info.foundText = foundObj?.text;
1524
- state.info.value = foundObj?.value;
1525
- throw new Error("element doesn't contain text " + text);
1637
+ catch (e) {
1638
+ // Log error but continue retrying until timeout is reached
1639
+ this.logger.warn("Retrying containsText due to: " + e.message);
1640
+ }
1641
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1526
1642
  }
1527
- return state.info;
1643
+ state.info.foundText = foundObj?.text;
1644
+ state.info.value = foundObj?.value;
1645
+ throw new Error("element doesn't contain text " + text);
1528
1646
  }
1529
1647
  catch (e) {
1530
1648
  await _commandError(state, e, this);
1649
+ throw e;
1531
1650
  }
1532
1651
  finally {
1533
- _commandFinally(state, this);
1652
+ await _commandFinally(state, this);
1534
1653
  }
1535
1654
  }
1536
- _getDataFile(world = null) {
1537
- let dataFile = null;
1538
- if (world && world.reportFolder) {
1539
- dataFile = path.join(world.reportFolder, "data.json");
1540
- }
1541
- else if (this.reportFolder) {
1542
- dataFile = path.join(this.reportFolder, "data.json");
1543
- }
1544
- else if (this.context && this.context.reportFolder) {
1545
- dataFile = path.join(this.context.reportFolder, "data.json");
1546
- }
1547
- else {
1548
- dataFile = "data.json";
1549
- }
1550
- return dataFile;
1551
- }
1552
1655
  async waitForUserInput(message, world = null) {
1553
1656
  if (!message) {
1554
1657
  message = "# Wait for user input. Press any key to continue";
@@ -1577,13 +1680,22 @@ class StableBrowser {
1577
1680
  return;
1578
1681
  }
1579
1682
  // if data file exists, load it
1580
- const dataFile = this._getDataFile(world);
1683
+ const dataFile = _getDataFile(world, this.context, this);
1581
1684
  let data = this.getTestData(world);
1582
1685
  // merge the testData with the existing data
1583
1686
  Object.assign(data, testData);
1584
1687
  // save the data to the file
1585
1688
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1586
1689
  }
1690
+ overwriteTestData(testData, world = null) {
1691
+ if (!testData) {
1692
+ return;
1693
+ }
1694
+ // if data file exists, load it
1695
+ const dataFile = _getDataFile(world, this.context, this);
1696
+ // save the data to the file
1697
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1698
+ }
1587
1699
  _getDataFilePath(fileName) {
1588
1700
  let dataFile = path.join(this.project_path, "data", fileName);
1589
1701
  if (fs.existsSync(dataFile)) {
@@ -1680,7 +1792,7 @@ class StableBrowser {
1680
1792
  }
1681
1793
  }
1682
1794
  getTestData(world = null) {
1683
- const dataFile = this._getDataFile(world);
1795
+ const dataFile = _getDataFile(world, this.context, this);
1684
1796
  let data = {};
1685
1797
  if (fs.existsSync(dataFile)) {
1686
1798
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1813,6 +1925,7 @@ class StableBrowser {
1813
1925
  else {
1814
1926
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1815
1927
  }
1928
+ return screenshotBuffer;
1816
1929
  }
1817
1930
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1818
1931
  const state = {
@@ -1835,7 +1948,7 @@ class StableBrowser {
1835
1948
  await _commandError(state, e, this);
1836
1949
  }
1837
1950
  finally {
1838
- _commandFinally(state, this);
1951
+ await _commandFinally(state, this);
1839
1952
  }
1840
1953
  }
1841
1954
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1848,6 +1961,7 @@ class StableBrowser {
1848
1961
  world,
1849
1962
  type: Types.EXTRACT,
1850
1963
  text: `Extract attribute from element`,
1964
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1851
1965
  operation: "extractAttribute",
1852
1966
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1853
1967
  allowDisabled: true,
@@ -1865,6 +1979,9 @@ class StableBrowser {
1865
1979
  case "value":
1866
1980
  state.value = await state.element.inputValue();
1867
1981
  break;
1982
+ case "text":
1983
+ state.value = await state.element.textContent();
1984
+ break;
1868
1985
  default:
1869
1986
  state.value = await state.element.getAttribute(attribute);
1870
1987
  break;
@@ -1879,7 +1996,7 @@ class StableBrowser {
1879
1996
  await _commandError(state, e, this);
1880
1997
  }
1881
1998
  finally {
1882
- _commandFinally(state, this);
1999
+ await _commandFinally(state, this);
1883
2000
  }
1884
2001
  }
1885
2002
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1894,18 +2011,25 @@ class StableBrowser {
1894
2011
  highlight: true,
1895
2012
  screenshot: true,
1896
2013
  text: `Verify element attribute`,
2014
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1897
2015
  operation: "verifyAttribute",
1898
2016
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1899
2017
  allowDisabled: true,
1900
2018
  };
1901
2019
  await new Promise((resolve) => setTimeout(resolve, 2000));
1902
2020
  let val;
2021
+ let expectedValue;
1903
2022
  try {
1904
2023
  await _preCommand(state, this);
2024
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2025
+ state.info.expectedValue = expectedValue;
1905
2026
  switch (attribute) {
1906
2027
  case "innerText":
1907
2028
  val = String(await state.element.innerText());
1908
2029
  break;
2030
+ case "text":
2031
+ val = String(await state.element.textContent());
2032
+ break;
1909
2033
  case "value":
1910
2034
  val = String(await state.element.inputValue());
1911
2035
  break;
@@ -1923,26 +2047,29 @@ class StableBrowser {
1923
2047
  val = String(await state.element.getAttribute(attribute));
1924
2048
  break;
1925
2049
  }
2050
+ state.info.value = val;
1926
2051
  let regex;
1927
- if (value.startsWith("/") && value.endsWith("/")) {
1928
- const patternBody = value.slice(1, -1);
2052
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2053
+ const patternBody = expectedValue.slice(1, -1);
1929
2054
  regex = new RegExp(patternBody, "g");
1930
2055
  }
1931
2056
  else {
1932
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2057
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1933
2058
  regex = new RegExp(escapedPattern, "g");
1934
2059
  }
1935
2060
  if (!val.match(regex)) {
1936
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
2061
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2062
+ state.info.failCause.assertionFailed = true;
2063
+ state.info.failCause.lastError = errorMessage;
2064
+ throw new Error(errorMessage);
1937
2065
  }
1938
- state.info.expectedValue = val;
1939
2066
  return state.info;
1940
2067
  }
1941
2068
  catch (e) {
1942
2069
  await _commandError(state, e, this);
1943
2070
  }
1944
2071
  finally {
1945
- _commandFinally(state, this);
2072
+ await _commandFinally(state, this);
1946
2073
  }
1947
2074
  }
1948
2075
  async extractEmailData(emailAddress, options, world) {
@@ -2048,7 +2175,7 @@ class StableBrowser {
2048
2175
  // console.log(`New outline is: ${node.style.outline}`);
2049
2176
  if (window) {
2050
2177
  window.addEventListener("beforeunload", function (e) {
2051
- node.style.outline = originalBorder;
2178
+ node.style.outline = originalOutline;
2052
2179
  });
2053
2180
  }
2054
2181
  setTimeout(function () {
@@ -2081,12 +2208,12 @@ class StableBrowser {
2081
2208
  element.style.outline = "2px solid red";
2082
2209
  if (window) {
2083
2210
  window.addEventListener("beforeunload", function (e) {
2084
- element.style.outline = originalBorder;
2211
+ element.style.outline = originalOutline;
2085
2212
  });
2086
2213
  }
2087
2214
  // Set a timeout to revert to the original border after 2 seconds
2088
2215
  setTimeout(function () {
2089
- element.style.outline = originalBorder;
2216
+ element.style.outline = originalOutline;
2090
2217
  }, 2000);
2091
2218
  }
2092
2219
  return;
@@ -2194,6 +2321,69 @@ class StableBrowser {
2194
2321
  _reportToWorld(world, {
2195
2322
  type: Types.VERIFY_PAGE_PATH,
2196
2323
  text: "Verify page path",
2324
+ _text: "Verify the page path contains " + pathPart,
2325
+ screenshotId,
2326
+ result: error
2327
+ ? {
2328
+ status: "FAILED",
2329
+ startTime,
2330
+ endTime,
2331
+ message: error?.message,
2332
+ }
2333
+ : {
2334
+ status: "PASSED",
2335
+ startTime,
2336
+ endTime,
2337
+ },
2338
+ info: info,
2339
+ });
2340
+ }
2341
+ }
2342
+ async verifyPageTitle(title, options = {}, world = null) {
2343
+ const startTime = Date.now();
2344
+ let error = null;
2345
+ let screenshotId = null;
2346
+ let screenshotPath = null;
2347
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2348
+ const info = {};
2349
+ info.log = "***** verify page title " + title + " *****\n";
2350
+ info.operation = "verifyPageTitle";
2351
+ const newValue = await this._replaceWithLocalData(title, world);
2352
+ if (newValue !== title) {
2353
+ this.logger.info(title + "=" + newValue);
2354
+ title = newValue;
2355
+ }
2356
+ info.title = title;
2357
+ try {
2358
+ for (let i = 0; i < 30; i++) {
2359
+ const foundTitle = await this.page.title();
2360
+ if (!foundTitle.includes(title)) {
2361
+ if (i === 29) {
2362
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2363
+ }
2364
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2365
+ continue;
2366
+ }
2367
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2368
+ return info;
2369
+ }
2370
+ }
2371
+ catch (e) {
2372
+ //await this.closeUnexpectedPopups();
2373
+ this.logger.error("verify page title failed " + info.log);
2374
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2375
+ info.screenshotPath = screenshotPath;
2376
+ Object.assign(e, { info: info });
2377
+ error = e;
2378
+ // throw e;
2379
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2380
+ }
2381
+ finally {
2382
+ const endTime = Date.now();
2383
+ _reportToWorld(world, {
2384
+ type: Types.VERIFY_PAGE_PATH,
2385
+ text: "Verify page title",
2386
+ _text: "Verify the page title contains " + title,
2197
2387
  screenshotId,
2198
2388
  result: error
2199
2389
  ? {
@@ -2211,27 +2401,27 @@ class StableBrowser {
2211
2401
  });
2212
2402
  }
2213
2403
  }
2214
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2404
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2215
2405
  const frames = this.page.frames();
2216
2406
  let results = [];
2217
- let ignoreCase = false;
2407
+ // let ignoreCase = false;
2218
2408
  for (let i = 0; i < frames.length; i++) {
2219
2409
  if (dateAlternatives.date) {
2220
2410
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2221
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2411
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2222
2412
  result.frame = frames[i];
2223
2413
  results.push(result);
2224
2414
  }
2225
2415
  }
2226
2416
  else if (numberAlternatives.number) {
2227
2417
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2228
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2418
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2229
2419
  result.frame = frames[i];
2230
2420
  results.push(result);
2231
2421
  }
2232
2422
  }
2233
2423
  else {
2234
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2424
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2235
2425
  result.frame = frames[i];
2236
2426
  results.push(result);
2237
2427
  }
@@ -2250,11 +2440,15 @@ class StableBrowser {
2250
2440
  scroll: false,
2251
2441
  highlight: false,
2252
2442
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2253
- text: `Verify text exists in page`,
2443
+ text: `Verify the text '${text}' exists in page`,
2444
+ _text: `Verify the text '${text}' exists in page`,
2254
2445
  operation: "verifyTextExistInPage",
2255
2446
  log: "***** verify text " + text + " exists in page *****\n",
2256
2447
  };
2257
- const timeout = this._getLoadTimeout(options);
2448
+ if (testForRegex(text)) {
2449
+ text = text.replace(/\\"/g, '"');
2450
+ }
2451
+ const timeout = this._getFindElementTimeout(options);
2258
2452
  await new Promise((resolve) => setTimeout(resolve, 2000));
2259
2453
  const newValue = await this._replaceWithLocalData(text, world);
2260
2454
  if (newValue !== text) {
@@ -2267,7 +2461,15 @@ class StableBrowser {
2267
2461
  await _preCommand(state, this);
2268
2462
  state.info.text = text;
2269
2463
  while (true) {
2270
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2464
+ let resultWithElementsFound = {
2465
+ length: 0,
2466
+ };
2467
+ try {
2468
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2469
+ }
2470
+ catch (error) {
2471
+ // ignore
2472
+ }
2271
2473
  if (resultWithElementsFound.length === 0) {
2272
2474
  if (Date.now() - state.startTime > timeout) {
2273
2475
  throw new Error(`Text ${text} not found in page`);
@@ -2275,35 +2477,40 @@ class StableBrowser {
2275
2477
  await new Promise((resolve) => setTimeout(resolve, 1000));
2276
2478
  continue;
2277
2479
  }
2278
- if (resultWithElementsFound[0].randomToken) {
2279
- const frame = resultWithElementsFound[0].frame;
2280
- const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2281
- await this._highlightElements(frame, dataAttribute);
2282
- // if (world && world.screenshot && !world.screenshotPath) {
2283
- // console.log(`Highlighting for verify text is found while running from recorder`);
2284
- // this._highlightElements(frame, dataAttribute).then(async () => {
2285
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2286
- // this._unhighlightElements(frame, dataAttribute)
2287
- // .then(async () => {
2288
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2289
- // })
2290
- // .catch(
2291
- // (e) => {}
2292
- // console.error(e)
2293
- // );
2294
- // });
2295
- // }
2296
- const element = await frame.locator(dataAttribute).first();
2297
- // await new Promise((resolve) => setTimeout(resolve, 100));
2298
- // await this._unhighlightElements(frame, dataAttribute);
2299
- if (element) {
2300
- await this.scrollIfNeeded(element, state.info);
2301
- await element.dispatchEvent("bvt_verify_page_contains_text");
2302
- // await _screenshot(state, this, element);
2480
+ try {
2481
+ if (resultWithElementsFound[0].randomToken) {
2482
+ const frame = resultWithElementsFound[0].frame;
2483
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2484
+ await this._highlightElements(frame, dataAttribute);
2485
+ // if (world && world.screenshot && !world.screenshotPath) {
2486
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2487
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2488
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2489
+ // this._unhighlightElements(frame, dataAttribute)
2490
+ // .then(async () => {
2491
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2492
+ // })
2493
+ // .catch(
2494
+ // (e) => {}
2495
+ // console.error(e)
2496
+ // );
2497
+ // });
2498
+ // }
2499
+ const element = await frame.locator(dataAttribute).first();
2500
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2501
+ // await this._unhighlightElements(frame, dataAttribute);
2502
+ if (element) {
2503
+ await this.scrollIfNeeded(element, state.info);
2504
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2505
+ // await _screenshot(state, this, element);
2506
+ }
2303
2507
  }
2508
+ await _screenshot(state, this);
2509
+ return state.info;
2510
+ }
2511
+ catch (error) {
2512
+ console.error(error);
2304
2513
  }
2305
- await _screenshot(state, this);
2306
- return state.info;
2307
2514
  }
2308
2515
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2309
2516
  }
@@ -2311,7 +2518,7 @@ class StableBrowser {
2311
2518
  await _commandError(state, e, this);
2312
2519
  }
2313
2520
  finally {
2314
- _commandFinally(state, this);
2521
+ await _commandFinally(state, this);
2315
2522
  }
2316
2523
  }
2317
2524
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2324,11 +2531,15 @@ class StableBrowser {
2324
2531
  scroll: false,
2325
2532
  highlight: false,
2326
2533
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2327
- text: `Verify text does not exist in page`,
2534
+ text: `Verify the text '${text}' does not exist in page`,
2535
+ _text: `Verify the text '${text}' does not exist in page`,
2328
2536
  operation: "verifyTextNotExistInPage",
2329
2537
  log: "***** verify text " + text + " does not exist in page *****\n",
2330
2538
  };
2331
- const timeout = this._getLoadTimeout(options);
2539
+ if (testForRegex(text)) {
2540
+ text = text.replace(/\\"/g, '"');
2541
+ }
2542
+ const timeout = this._getFindElementTimeout(options);
2332
2543
  await new Promise((resolve) => setTimeout(resolve, 2000));
2333
2544
  const newValue = await this._replaceWithLocalData(text, world);
2334
2545
  if (newValue !== text) {
@@ -2340,8 +2551,16 @@ class StableBrowser {
2340
2551
  try {
2341
2552
  await _preCommand(state, this);
2342
2553
  state.info.text = text;
2554
+ let resultWithElementsFound = {
2555
+ length: null, // initial cannot be 0
2556
+ };
2343
2557
  while (true) {
2344
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2558
+ try {
2559
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2560
+ }
2561
+ catch (error) {
2562
+ // ignore
2563
+ }
2345
2564
  if (resultWithElementsFound.length === 0) {
2346
2565
  await _screenshot(state, this);
2347
2566
  return state.info;
@@ -2356,7 +2575,7 @@ class StableBrowser {
2356
2575
  await _commandError(state, e, this);
2357
2576
  }
2358
2577
  finally {
2359
- _commandFinally(state, this);
2578
+ await _commandFinally(state, this);
2360
2579
  }
2361
2580
  }
2362
2581
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2371,10 +2590,11 @@ class StableBrowser {
2371
2590
  highlight: false,
2372
2591
  type: Types.VERIFY_TEXT_WITH_RELATION,
2373
2592
  text: `Verify text with relation to another text`,
2593
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2374
2594
  operation: "verify_text_with_relation",
2375
2595
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2376
2596
  };
2377
- const timeout = this._getLoadTimeout(options);
2597
+ const timeout = this._getFindElementTimeout(options);
2378
2598
  await new Promise((resolve) => setTimeout(resolve, 2000));
2379
2599
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2380
2600
  if (newValue !== textAnchor) {
@@ -2392,8 +2612,16 @@ class StableBrowser {
2392
2612
  try {
2393
2613
  await _preCommand(state, this);
2394
2614
  state.info.text = textToVerify;
2615
+ let resultWithElementsFound = {
2616
+ length: 0,
2617
+ };
2395
2618
  while (true) {
2396
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2619
+ try {
2620
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2621
+ }
2622
+ catch (error) {
2623
+ // ignore
2624
+ }
2397
2625
  if (resultWithElementsFound.length === 0) {
2398
2626
  if (Date.now() - state.startTime > timeout) {
2399
2627
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2401,51 +2629,56 @@ class StableBrowser {
2401
2629
  await new Promise((resolve) => setTimeout(resolve, 1000));
2402
2630
  continue;
2403
2631
  }
2404
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2405
- foundAncore = true;
2406
- const result = resultWithElementsFound[i];
2407
- const token = result.randomToken;
2408
- const frame = result.frame;
2409
- let css = `[data-blinq-id-${token}]`;
2410
- const climbArray1 = [];
2411
- for (let i = 0; i < climb; i++) {
2412
- climbArray1.push("..");
2413
- }
2414
- let climbXpath = "xpath=" + climbArray1.join("/");
2415
- css = css + " >> " + climbXpath;
2416
- const count = await frame.locator(css).count();
2417
- for (let j = 0; j < count; j++) {
2418
- const continer = await frame.locator(css).nth(j);
2419
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2420
- if (result.elementCount > 0) {
2421
- const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2422
- await this._highlightElements(frame, dataAttribute);
2423
- //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2424
- // if (world && world.screenshot && !world.screenshotPath) {
2425
- // console.log(`Highlighting for vtrt while running from recorder`);
2426
- // this._highlightElements(frame, dataAttribute)
2427
- // .then(async () => {
2428
- // await new Promise((resolve) => setTimeout(resolve, 1000));
2429
- // this._unhighlightElements(frame, dataAttribute).then(
2430
- // () => {}
2431
- // console.log(`Unhighlighting vrtr in recorder is successful`)
2432
- // );
2433
- // })
2434
- // .catch(e);
2435
- // }
2436
- //await this._highlightElements(frame, cssAnchor);
2437
- const element = await frame.locator(dataAttribute).first();
2438
- // await new Promise((resolve) => setTimeout(resolve, 100));
2439
- // await this._unhighlightElements(frame, dataAttribute);
2440
- if (element) {
2441
- await this.scrollIfNeeded(element, state.info);
2442
- await element.dispatchEvent("bvt_verify_page_contains_text");
2632
+ try {
2633
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2634
+ foundAncore = true;
2635
+ const result = resultWithElementsFound[i];
2636
+ const token = result.randomToken;
2637
+ const frame = result.frame;
2638
+ let css = `[data-blinq-id-${token}]`;
2639
+ const climbArray1 = [];
2640
+ for (let i = 0; i < climb; i++) {
2641
+ climbArray1.push("..");
2642
+ }
2643
+ let climbXpath = "xpath=" + climbArray1.join("/");
2644
+ css = css + " >> " + climbXpath;
2645
+ const count = await frame.locator(css).count();
2646
+ for (let j = 0; j < count; j++) {
2647
+ const continer = await frame.locator(css).nth(j);
2648
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2649
+ if (result.elementCount > 0) {
2650
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2651
+ await this._highlightElements(frame, dataAttribute);
2652
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2653
+ // if (world && world.screenshot && !world.screenshotPath) {
2654
+ // console.log(`Highlighting for vtrt while running from recorder`);
2655
+ // this._highlightElements(frame, dataAttribute)
2656
+ // .then(async () => {
2657
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2658
+ // this._unhighlightElements(frame, dataAttribute).then(
2659
+ // () => {}
2660
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2661
+ // );
2662
+ // })
2663
+ // .catch(e);
2664
+ // }
2665
+ //await this._highlightElements(frame, cssAnchor);
2666
+ const element = await frame.locator(dataAttribute).first();
2667
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2668
+ // await this._unhighlightElements(frame, dataAttribute);
2669
+ if (element) {
2670
+ await this.scrollIfNeeded(element, state.info);
2671
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2672
+ }
2673
+ await _screenshot(state, this);
2674
+ return state.info;
2443
2675
  }
2444
- await _screenshot(state, this);
2445
- return state.info;
2446
2676
  }
2447
2677
  }
2448
2678
  }
2679
+ catch (error) {
2680
+ console.error(error);
2681
+ }
2449
2682
  }
2450
2683
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2451
2684
  }
@@ -2453,8 +2686,32 @@ class StableBrowser {
2453
2686
  await _commandError(state, e, this);
2454
2687
  }
2455
2688
  finally {
2456
- _commandFinally(state, this);
2689
+ await _commandFinally(state, this);
2690
+ }
2691
+ }
2692
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2693
+ const frames = this.page.frames();
2694
+ let results = [];
2695
+ let ignoreCase = false;
2696
+ for (let i = 0; i < frames.length; i++) {
2697
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2698
+ result.frame = frames[i];
2699
+ const climbArray = [];
2700
+ for (let i = 0; i < climb; i++) {
2701
+ climbArray.push("..");
2702
+ }
2703
+ let climbXpath = "xpath=" + climbArray.join("/");
2704
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2705
+ const count = await frames[i].locator(newLocator).count();
2706
+ if (count > 0) {
2707
+ result.elementCount = count;
2708
+ result.locator = newLocator;
2709
+ results.push(result);
2710
+ }
2457
2711
  }
2712
+ // state.info.results = results;
2713
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2714
+ return resultWithElementsFound;
2458
2715
  }
2459
2716
  async visualVerification(text, options = {}, world = null) {
2460
2717
  const startTime = Date.now();
@@ -2474,10 +2731,13 @@ class StableBrowser {
2474
2731
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2475
2732
  info.screenshotPath = screenshotPath;
2476
2733
  const screenshot = await this.takeScreenshot();
2477
- const request = {
2478
- method: "POST",
2734
+ let request = {
2735
+ method: "post",
2736
+ maxBodyLength: Infinity,
2479
2737
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2480
2738
  headers: {
2739
+ "x-bvt-project-id": path.basename(this.project_path),
2740
+ "x-source": "aaa",
2481
2741
  "Content-Type": "application/json",
2482
2742
  Authorization: `Bearer ${process.env.TOKEN}`,
2483
2743
  },
@@ -2486,7 +2746,7 @@ class StableBrowser {
2486
2746
  screenshot: screenshot,
2487
2747
  }),
2488
2748
  };
2489
- let result = await this.context.api.request(request);
2749
+ const result = await axios.request(request);
2490
2750
  if (result.data.status !== true) {
2491
2751
  throw new Error("Visual validation failed");
2492
2752
  }
@@ -2514,6 +2774,7 @@ class StableBrowser {
2514
2774
  _reportToWorld(world, {
2515
2775
  type: Types.VERIFY_VISUAL,
2516
2776
  text: "Visual verification",
2777
+ _text: "Visual verification of " + text,
2517
2778
  screenshotId,
2518
2779
  result: error
2519
2780
  ? {
@@ -2780,6 +3041,32 @@ class StableBrowser {
2780
3041
  }
2781
3042
  return timeout;
2782
3043
  }
3044
+ _getFindElementTimeout(options) {
3045
+ if (options && options.timeout) {
3046
+ return options.timeout;
3047
+ }
3048
+ if (this.configuration.find_element_timeout) {
3049
+ return this.configuration.find_element_timeout;
3050
+ }
3051
+ return 30000;
3052
+ }
3053
+ async saveStoreState(path = null, world = null) {
3054
+ const storageState = await this.page.context().storageState();
3055
+ //const testDataFile = _getDataFile(world, this.context, this);
3056
+ if (path) {
3057
+ // save { storageState: storageState } into the path
3058
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3059
+ }
3060
+ else {
3061
+ await this.setTestData({ storageState: storageState }, world);
3062
+ }
3063
+ }
3064
+ async restoreSaveState(path = null, world = null) {
3065
+ await refreshBrowser(this, path, world);
3066
+ this.registerEventListeners(this.context);
3067
+ registerNetworkEvents(this.world, this, this.context, this.page);
3068
+ registerDownloadEvent(this.page, this.world, this.context);
3069
+ }
2783
3070
  async waitForPageLoad(options = {}, world = null) {
2784
3071
  let timeout = this._getLoadTimeout(options);
2785
3072
  const promiseArray = [];
@@ -2847,6 +3134,7 @@ class StableBrowser {
2847
3134
  highlight: false,
2848
3135
  type: Types.CLOSE_PAGE,
2849
3136
  text: `Close page`,
3137
+ _text: `Close the page`,
2850
3138
  operation: "closePage",
2851
3139
  log: "***** close page *****\n",
2852
3140
  throwError: false,
@@ -2860,11 +3148,98 @@ class StableBrowser {
2860
3148
  await _commandError(state, e, this);
2861
3149
  }
2862
3150
  finally {
2863
- _commandFinally(state, this);
3151
+ await _commandFinally(state, this);
3152
+ }
3153
+ }
3154
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3155
+ let operation = null;
3156
+ if (!options || !options.operation) {
3157
+ throw new Error("operation is not defined");
3158
+ }
3159
+ operation = options.operation;
3160
+ // validate operation is one of the supported operations
3161
+ if (operation != "click" && operation != "hover+click") {
3162
+ throw new Error("operation is not supported");
3163
+ }
3164
+ const state = {
3165
+ options,
3166
+ world,
3167
+ locate: false,
3168
+ scroll: false,
3169
+ highlight: false,
3170
+ type: Types.TABLE_OPERATION,
3171
+ text: `Table operation`,
3172
+ _text: `Table ${operation} operation`,
3173
+ operation: operation,
3174
+ log: "***** Table operation *****\n",
3175
+ };
3176
+ const timeout = this._getFindElementTimeout(options);
3177
+ try {
3178
+ await _preCommand(state, this);
3179
+ const start = Date.now();
3180
+ let cellArea = null;
3181
+ while (true) {
3182
+ try {
3183
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3184
+ if (cellArea) {
3185
+ break;
3186
+ }
3187
+ }
3188
+ catch (e) {
3189
+ // ignore
3190
+ }
3191
+ if (Date.now() - start > timeout) {
3192
+ throw new Error(`Cell not found in table`);
3193
+ }
3194
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3195
+ }
3196
+ switch (operation) {
3197
+ case "click":
3198
+ if (!options.css) {
3199
+ // will click in the center of the cell
3200
+ let xOffset = 0;
3201
+ let yOffset = 0;
3202
+ if (options.xOffset) {
3203
+ xOffset = options.xOffset;
3204
+ }
3205
+ if (options.yOffset) {
3206
+ yOffset = options.yOffset;
3207
+ }
3208
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3209
+ }
3210
+ else {
3211
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3212
+ if (results.length === 0) {
3213
+ throw new Error(`Element not found in cell area`);
3214
+ }
3215
+ state.element = results[0];
3216
+ await performAction("click", state.element, options, this, state, _params);
3217
+ }
3218
+ break;
3219
+ case "hover+click":
3220
+ if (!options.css) {
3221
+ throw new Error("css is not defined");
3222
+ }
3223
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3224
+ if (results.length === 0) {
3225
+ throw new Error(`Element not found in cell area`);
3226
+ }
3227
+ state.element = results[0];
3228
+ await performAction("hover+click", state.element, options, this, state, _params);
3229
+ break;
3230
+ default:
3231
+ throw new Error("operation is not supported");
3232
+ }
3233
+ }
3234
+ catch (e) {
3235
+ await _commandError(state, e, this);
3236
+ }
3237
+ finally {
3238
+ await _commandFinally(state, this);
2864
3239
  }
2865
3240
  }
2866
3241
  saveTestDataAsGlobal(options, world) {
2867
- const dataFile = this._getDataFile(world);
3242
+ const dataFile = _getDataFile(world, this.context, this);
2868
3243
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2869
3244
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2870
3245
  }
@@ -2894,6 +3269,7 @@ class StableBrowser {
2894
3269
  _reportToWorld(world, {
2895
3270
  type: Types.SET_VIEWPORT,
2896
3271
  text: "set viewport size to " + width + "x" + hight,
3272
+ _text: "Set the viewport size to " + width + "x" + hight,
2897
3273
  screenshotId,
2898
3274
  result: error
2899
3275
  ? {
@@ -2964,7 +3340,39 @@ class StableBrowser {
2964
3340
  console.log("#-#");
2965
3341
  }
2966
3342
  }
3343
+ async beforeScenario(world, scenario) {
3344
+ this.beforeScenarioCalled = true;
3345
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3346
+ this.scenarioName = scenario.pickle.name;
3347
+ }
3348
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3349
+ this.featureName = scenario.gherkinDocument.feature.name;
3350
+ }
3351
+ if (this.context) {
3352
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3353
+ }
3354
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3355
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3356
+ // check if @global_test_data tag is present
3357
+ if (this.tags.includes("@global_test_data")) {
3358
+ this.saveTestDataAsGlobal({}, world);
3359
+ }
3360
+ }
3361
+ // update test data based on feature/scenario
3362
+ let envName = null;
3363
+ if (this.context && this.context.environment) {
3364
+ envName = this.context.environment.name;
3365
+ }
3366
+ if (!process.env.TEMP_RUN) {
3367
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3368
+ }
3369
+ await loadBrunoParams(this.context, this.context.environment.name);
3370
+ }
3371
+ async afterScenario(world, scenario) { }
2967
3372
  async beforeStep(world, step) {
3373
+ if (!this.beforeScenarioCalled) {
3374
+ this.beforeScenario(world, step);
3375
+ }
2968
3376
  if (this.stepIndex === undefined) {
2969
3377
  this.stepIndex = 0;
2970
3378
  }
@@ -2981,22 +3389,47 @@ class StableBrowser {
2981
3389
  else {
2982
3390
  this.stepName = "step " + this.stepIndex;
2983
3391
  }
2984
- if (this.context) {
2985
- this.context.examplesRow = extractStepExampleParameters(step);
2986
- }
2987
3392
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2988
3393
  if (this.context.browserObject.context) {
2989
3394
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
2990
3395
  }
2991
3396
  }
2992
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
2993
- this.tags = step.pickle.tags.map((tag) => tag.name);
2994
- // check if @global_test_data tag is present
2995
- if (this.tags.includes("@global_test_data")) {
2996
- this.saveTestDataAsGlobal({}, world);
3397
+ if (this.initSnapshotTaken === false) {
3398
+ this.initSnapshotTaken = true;
3399
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3400
+ const snapshot = await this.getAriaSnapshot();
3401
+ if (snapshot) {
3402
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3403
+ }
2997
3404
  }
2998
3405
  }
2999
3406
  }
3407
+ async getAriaSnapshot() {
3408
+ try {
3409
+ // find the page url
3410
+ const url = await this.page.url();
3411
+ // extract the path from the url
3412
+ const path = new URL(url).pathname;
3413
+ // get the page title
3414
+ const title = await this.page.title();
3415
+ // go over other frams
3416
+ const frames = this.page.frames();
3417
+ const snapshots = [];
3418
+ const content = [`- path: ${path}`, `- title: ${title}`];
3419
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3420
+ for (let i = 0; i < frames.length; i++) {
3421
+ content.push(`- frame: ${i}`);
3422
+ const frame = frames[i];
3423
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3424
+ content.push(snapshot);
3425
+ }
3426
+ return content.join("\n");
3427
+ }
3428
+ catch (e) {
3429
+ console.error(e);
3430
+ }
3431
+ return null;
3432
+ }
3000
3433
  async afterStep(world, step) {
3001
3434
  this.stepName = null;
3002
3435
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3004,11 +3437,25 @@ class StableBrowser {
3004
3437
  await this.context.browserObject.context.tracing.stopChunk({
3005
3438
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3006
3439
  });
3440
+ if (world && world.attach) {
3441
+ await world.attach(JSON.stringify({
3442
+ type: "trace",
3443
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3444
+ }), "application/json+trace");
3445
+ }
3446
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3007
3447
  }
3008
3448
  }
3009
3449
  if (this.context) {
3010
3450
  this.context.examplesRow = null;
3011
3451
  }
3452
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3453
+ const snapshot = await this.getAriaSnapshot();
3454
+ if (snapshot) {
3455
+ const obj = {};
3456
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3457
+ }
3458
+ }
3012
3459
  }
3013
3460
  }
3014
3461
  function createTimedPromise(promise, label) {