automation_model 1.0.615-dev → 1.0.615-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,19 +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
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";
23
24
  import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
+ import { loadBrunoParams } from "./bruno.js";
24
27
  export const Types = {
25
28
  CLICK: "click_element",
29
+ WAIT_ELEMENT: "wait_element",
26
30
  NAVIGATE: "navigate",
27
31
  FILL: "fill_element",
28
32
  EXECUTE: "execute_page_method",
@@ -44,6 +48,7 @@ export const Types = {
44
48
  UNCHECK: "uncheck_element",
45
49
  EXTRACT: "extract_attribute",
46
50
  CLOSE_PAGE: "close_page",
51
+ TABLE_OPERATION: "table_operation",
47
52
  SET_DATE_TIME: "set_date_time",
48
53
  SET_VIEWPORT: "set_viewport",
49
54
  VERIFY_VISUAL: "verify_visual",
@@ -52,6 +57,9 @@ export const Types = {
52
57
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
53
58
  VERIFY_ATTRIBUTE: "verify_element_attribute",
54
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",
55
63
  };
56
64
  export const apps = {};
57
65
  const formatElementName = (elementName) => {
@@ -70,6 +78,7 @@ class StableBrowser {
70
78
  appName = "main";
71
79
  tags = null;
72
80
  isRecording = false;
81
+ initSnapshotTaken = false;
73
82
  constructor(browser, page, logger = null, context = null, world = null) {
74
83
  this.browser = browser;
75
84
  this.page = page;
@@ -176,6 +185,30 @@ class StableBrowser {
176
185
  await this.waitForPageLoad();
177
186
  }
178
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
+ }
179
212
  registerConsoleLogListener(page, context) {
180
213
  if (!this.context.webLogger) {
181
214
  this.context.webLogger = [];
@@ -271,7 +304,7 @@ class StableBrowser {
271
304
  _commandError(state, error, this);
272
305
  }
273
306
  finally {
274
- _commandFinally(state, this);
307
+ await _commandFinally(state, this);
275
308
  }
276
309
  }
277
310
  async _getLocator(locator, scope, _params) {
@@ -352,7 +385,7 @@ class StableBrowser {
352
385
  return resultCss;
353
386
  }
354
387
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
355
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
388
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
356
389
  const locator = scope.locator(query);
357
390
  const count = await locator.count();
358
391
  if (!tag1) {
@@ -372,6 +405,12 @@ class StableBrowser {
372
405
  if (!el.setAttribute) {
373
406
  el = el.parentElement;
374
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
+ // }
375
414
  el.setAttribute("data-blinq-id-" + randomToken, "");
376
415
  return true;
377
416
  }, [tag1, randomToken]))) {
@@ -558,9 +597,24 @@ class StableBrowser {
558
597
  element.evaluate((el, randomToken) => {
559
598
  el.setAttribute("data-blinq-id-" + randomToken, "");
560
599
  }, randomToken);
600
+ // if (element._frame) {
601
+ // return element;
602
+ // }
561
603
  const scope = element._frame ?? element.page();
562
- const newSelector = scope.locator("[data-blinq-id-" + randomToken + "]");
563
- 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);
564
618
  }
565
619
  }
566
620
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -712,14 +766,9 @@ class StableBrowser {
712
766
  // info.log += "scanning locators in priority 2" + "\n";
713
767
  result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
714
768
  }
715
- if (result.foundElements.length === 0 && onlyPriority3) {
769
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
716
770
  result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
717
771
  }
718
- else {
719
- if (result.foundElements.length === 0 && !highPriorityOnly) {
720
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
721
- }
722
- }
723
772
  let foundElements = result.foundElements;
724
773
  if (foundElements.length === 1 && foundElements[0].unique) {
725
774
  info.box = foundElements[0].box;
@@ -774,6 +823,11 @@ class StableBrowser {
774
823
  visibleOnly = false;
775
824
  }
776
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
+ }
777
831
  }
778
832
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
779
833
  // if (info.locatorLog) {
@@ -820,9 +874,40 @@ class StableBrowser {
820
874
  result.locatorIndex = i;
821
875
  }
822
876
  if (foundLocators.length > 1) {
823
- info.failCause.foundMultiple = true;
824
- if (info.locatorLog) {
825
- 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
+ }
826
911
  }
827
912
  }
828
913
  }
@@ -870,7 +955,7 @@ class StableBrowser {
870
955
  await _commandError(state, "timeout looking for " + elementDescription, this);
871
956
  }
872
957
  finally {
873
- _commandFinally(state, this);
958
+ await _commandFinally(state, this);
874
959
  }
875
960
  }
876
961
  }
@@ -919,7 +1004,7 @@ class StableBrowser {
919
1004
  await _commandError(state, "timeout looking for " + elementDescription, this);
920
1005
  }
921
1006
  finally {
922
- _commandFinally(state, this);
1007
+ await _commandFinally(state, this);
923
1008
  }
924
1009
  }
925
1010
  }
@@ -933,25 +1018,14 @@ class StableBrowser {
933
1018
  options,
934
1019
  world,
935
1020
  text: "Click element",
1021
+ _text: "Click on " + selectors.element_name,
936
1022
  type: Types.CLICK,
937
1023
  operation: "click",
938
1024
  log: "***** click on " + selectors.element_name + " *****\n",
939
1025
  };
940
1026
  try {
941
1027
  await _preCommand(state, this);
942
- // if (state.options && state.options.context) {
943
- // state.selectors.locators[0].text = state.options.context;
944
- // }
945
- try {
946
- await state.element.click();
947
- // await new Promise((resolve) => setTimeout(resolve, 1000));
948
- }
949
- catch (e) {
950
- // await this.closeUnexpectedPopups();
951
- state.element = await this._locate(selectors, state.info, _params);
952
- await state.element.dispatchEvent("click");
953
- // await new Promise((resolve) => setTimeout(resolve, 1000));
954
- }
1028
+ await performAction("click", state.element, options, this, state, _params);
955
1029
  await this.waitForPageLoad();
956
1030
  return state.info;
957
1031
  }
@@ -959,9 +1033,41 @@ class StableBrowser {
959
1033
  await _commandError(state, e, this);
960
1034
  }
961
1035
  finally {
962
- _commandFinally(state, this);
1036
+ await _commandFinally(state, this);
963
1037
  }
964
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);
1068
+ }
1069
+ return found;
1070
+ }
965
1071
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
966
1072
  const state = {
967
1073
  selectors,
@@ -970,6 +1076,7 @@ class StableBrowser {
970
1076
  world,
971
1077
  type: checked ? Types.CHECK : Types.UNCHECK,
972
1078
  text: checked ? `Check element` : `Uncheck element`,
1079
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
973
1080
  operation: "setCheck",
974
1081
  log: "***** check " + selectors.element_name + " *****\n",
975
1082
  };
@@ -981,7 +1088,7 @@ class StableBrowser {
981
1088
  try {
982
1089
  // if (world && world.screenshot && !world.screenshotPath) {
983
1090
  // console.log(`Highlighting while running from recorder`);
984
- await this._highlightElements(element);
1091
+ await this._highlightElements(state.element);
985
1092
  await state.element.setChecked(checked);
986
1093
  await new Promise((resolve) => setTimeout(resolve, 1000));
987
1094
  // await this._unHighlightElements(element);
@@ -1008,7 +1115,7 @@ class StableBrowser {
1008
1115
  await _commandError(state, e, this);
1009
1116
  }
1010
1117
  finally {
1011
- _commandFinally(state, this);
1118
+ await _commandFinally(state, this);
1012
1119
  }
1013
1120
  }
1014
1121
  async hover(selectors, _params, options = {}, world = null) {
@@ -1019,24 +1126,13 @@ class StableBrowser {
1019
1126
  world,
1020
1127
  type: Types.HOVER,
1021
1128
  text: `Hover element`,
1129
+ _text: `Hover on ${selectors.element_name}`,
1022
1130
  operation: "hover",
1023
1131
  log: "***** hover " + selectors.element_name + " *****\n",
1024
1132
  };
1025
1133
  try {
1026
1134
  await _preCommand(state, this);
1027
- try {
1028
- await state.element.hover();
1029
- // await _screenshot(state, this);
1030
- await new Promise((resolve) => setTimeout(resolve, 1000));
1031
- }
1032
- catch (e) {
1033
- //await this.closeUnexpectedPopups();
1034
- state.info.log += "hover failed, will try again" + "\n";
1035
- state.element = await this._locate(selectors, state.info, _params);
1036
- await state.element.hover({ timeout: 10000 });
1037
- // await _screenshot(state, this);
1038
- await new Promise((resolve) => setTimeout(resolve, 1000));
1039
- }
1135
+ await performAction("hover", state.element, options, this, state, _params);
1040
1136
  await _screenshot(state, this);
1041
1137
  await this.waitForPageLoad();
1042
1138
  return state.info;
@@ -1045,7 +1141,7 @@ class StableBrowser {
1045
1141
  await _commandError(state, e, this);
1046
1142
  }
1047
1143
  finally {
1048
- _commandFinally(state, this);
1144
+ await _commandFinally(state, this);
1049
1145
  }
1050
1146
  }
1051
1147
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1060,6 +1156,7 @@ class StableBrowser {
1060
1156
  value: values.toString(),
1061
1157
  type: Types.SELECT,
1062
1158
  text: `Select option: ${values}`,
1159
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1063
1160
  operation: "selectOption",
1064
1161
  log: "***** select option " + selectors.element_name + " *****\n",
1065
1162
  };
@@ -1080,7 +1177,7 @@ class StableBrowser {
1080
1177
  await _commandError(state, e, this);
1081
1178
  }
1082
1179
  finally {
1083
- _commandFinally(state, this);
1180
+ await _commandFinally(state, this);
1084
1181
  }
1085
1182
  }
1086
1183
  async type(_value, _params = null, options = {}, world = null) {
@@ -1094,6 +1191,7 @@ class StableBrowser {
1094
1191
  highlight: false,
1095
1192
  type: Types.TYPE_PRESS,
1096
1193
  text: `Type value: ${_value}`,
1194
+ _text: `Type value: ${_value}`,
1097
1195
  operation: "type",
1098
1196
  log: "",
1099
1197
  };
@@ -1125,7 +1223,7 @@ class StableBrowser {
1125
1223
  await _commandError(state, e, this);
1126
1224
  }
1127
1225
  finally {
1128
- _commandFinally(state, this);
1226
+ await _commandFinally(state, this);
1129
1227
  }
1130
1228
  }
1131
1229
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1161,7 +1259,7 @@ class StableBrowser {
1161
1259
  await _commandError(state, e, this);
1162
1260
  }
1163
1261
  finally {
1164
- _commandFinally(state, this);
1262
+ await _commandFinally(state, this);
1165
1263
  }
1166
1264
  }
1167
1265
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1173,6 +1271,7 @@ class StableBrowser {
1173
1271
  world,
1174
1272
  type: Types.SET_DATE_TIME,
1175
1273
  text: `Set date time value: ${value}`,
1274
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1176
1275
  operation: "setDateTime",
1177
1276
  log: "***** set date time value " + selectors.element_name + " *****\n",
1178
1277
  throwError: false,
@@ -1180,7 +1279,7 @@ class StableBrowser {
1180
1279
  try {
1181
1280
  await _preCommand(state, this);
1182
1281
  try {
1183
- await state.element.click();
1282
+ await performAction("click", state.element, options, this, state, _params);
1184
1283
  await new Promise((resolve) => setTimeout(resolve, 500));
1185
1284
  if (format) {
1186
1285
  state.value = dayjs(state.value).format(format);
@@ -1229,7 +1328,7 @@ class StableBrowser {
1229
1328
  await _commandError(state, e, this);
1230
1329
  }
1231
1330
  finally {
1232
- _commandFinally(state, this);
1331
+ await _commandFinally(state, this);
1233
1332
  }
1234
1333
  }
1235
1334
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1244,9 +1343,13 @@ class StableBrowser {
1244
1343
  world,
1245
1344
  type: Types.FILL,
1246
1345
  text: `Click type input with value: ${_value}`,
1346
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1247
1347
  operation: "clickType",
1248
1348
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1249
1349
  };
1350
+ if (!options) {
1351
+ options = {};
1352
+ }
1250
1353
  if (newValue !== _value) {
1251
1354
  //this.logger.info(_value + "=" + newValue);
1252
1355
  _value = newValue;
@@ -1254,7 +1357,7 @@ class StableBrowser {
1254
1357
  try {
1255
1358
  await _preCommand(state, this);
1256
1359
  state.info.value = _value;
1257
- if (options === null || options === undefined || !options.press) {
1360
+ if (!options.press) {
1258
1361
  try {
1259
1362
  let currentValue = await state.element.inputValue();
1260
1363
  if (currentValue) {
@@ -1265,13 +1368,9 @@ class StableBrowser {
1265
1368
  this.logger.info("unable to clear input value");
1266
1369
  }
1267
1370
  }
1268
- if (options === null || options === undefined || options.press) {
1269
- try {
1270
- await state.element.click({ timeout: 5000 });
1271
- }
1272
- catch (e) {
1273
- await state.element.dispatchEvent("click");
1274
- }
1371
+ if (options.press) {
1372
+ options.timeout = 5000;
1373
+ await performAction("click", state.element, options, this, state, _params);
1275
1374
  }
1276
1375
  else {
1277
1376
  try {
@@ -1329,7 +1428,7 @@ class StableBrowser {
1329
1428
  await _commandError(state, e, this);
1330
1429
  }
1331
1430
  finally {
1332
- _commandFinally(state, this);
1431
+ await _commandFinally(state, this);
1333
1432
  }
1334
1433
  }
1335
1434
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1359,13 +1458,49 @@ class StableBrowser {
1359
1458
  await _commandError(state, e, this);
1360
1459
  }
1361
1460
  finally {
1362
- _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);
1363
1497
  }
1364
1498
  }
1365
1499
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1366
1500
  return await this._getText(selectors, 0, _params, options, info, world);
1367
1501
  }
1368
1502
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1503
+ const timeout = this._getFindElementTimeout(options);
1369
1504
  _validateSelectors(selectors);
1370
1505
  let screenshotId = null;
1371
1506
  let screenshotPath = null;
@@ -1375,7 +1510,7 @@ class StableBrowser {
1375
1510
  }
1376
1511
  info.operation = "getText";
1377
1512
  info.selectors = selectors;
1378
- let element = await this._locate(selectors, info, _params);
1513
+ let element = await this._locate(selectors, info, _params, timeout);
1379
1514
  if (climb > 0) {
1380
1515
  const climbArray = [];
1381
1516
  for (let i = 0; i < climb; i++) {
@@ -1442,6 +1577,7 @@ class StableBrowser {
1442
1577
  highlight: false,
1443
1578
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1444
1579
  text: `Verify element contains pattern: ${pattern}`,
1580
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1445
1581
  operation: "containsPattern",
1446
1582
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1447
1583
  };
@@ -1473,10 +1609,12 @@ class StableBrowser {
1473
1609
  await _commandError(state, e, this);
1474
1610
  }
1475
1611
  finally {
1476
- _commandFinally(state, this);
1612
+ await _commandFinally(state, this);
1477
1613
  }
1478
1614
  }
1479
1615
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1616
+ const timeout = this._getFindElementTimeout(options);
1617
+ const startTime = Date.now();
1480
1618
  const state = {
1481
1619
  selectors,
1482
1620
  _params,
@@ -1503,44 +1641,52 @@ class StableBrowser {
1503
1641
  }
1504
1642
  let foundObj = null;
1505
1643
  try {
1506
- await _preCommand(state, this);
1507
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1508
- if (foundObj && foundObj.element) {
1509
- await this.scrollIfNeeded(foundObj.element, state.info);
1510
- }
1511
- await _screenshot(state, this);
1512
- const dateAlternatives = findDateAlternatives(text);
1513
- const numberAlternatives = findNumberAlternatives(text);
1514
- if (dateAlternatives.date) {
1515
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1516
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1517
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1518
- 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);
1519
1650
  }
1520
- }
1521
- throw new Error("element doesn't contain text " + text);
1522
- }
1523
- else if (numberAlternatives.number) {
1524
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1525
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1526
- 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)) {
1527
1671
  return state.info;
1528
1672
  }
1529
1673
  }
1530
- throw new Error("element doesn't contain text " + text);
1531
- }
1532
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1533
- state.info.foundText = foundObj?.text;
1534
- state.info.value = foundObj?.value;
1535
- 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
1536
1679
  }
1537
- 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);
1538
1683
  }
1539
1684
  catch (e) {
1540
1685
  await _commandError(state, e, this);
1686
+ throw e;
1541
1687
  }
1542
1688
  finally {
1543
- _commandFinally(state, this);
1689
+ await _commandFinally(state, this);
1544
1690
  }
1545
1691
  }
1546
1692
  async waitForUserInput(message, world = null) {
@@ -1578,6 +1724,15 @@ class StableBrowser {
1578
1724
  // save the data to the file
1579
1725
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1580
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
+ }
1581
1736
  _getDataFilePath(fileName) {
1582
1737
  let dataFile = path.join(this.project_path, "data", fileName);
1583
1738
  if (fs.existsSync(dataFile)) {
@@ -1830,7 +1985,7 @@ class StableBrowser {
1830
1985
  await _commandError(state, e, this);
1831
1986
  }
1832
1987
  finally {
1833
- _commandFinally(state, this);
1988
+ await _commandFinally(state, this);
1834
1989
  }
1835
1990
  }
1836
1991
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1843,6 +1998,7 @@ class StableBrowser {
1843
1998
  world,
1844
1999
  type: Types.EXTRACT,
1845
2000
  text: `Extract attribute from element`,
2001
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1846
2002
  operation: "extractAttribute",
1847
2003
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1848
2004
  allowDisabled: true,
@@ -1860,10 +2016,31 @@ class StableBrowser {
1860
2016
  case "value":
1861
2017
  state.value = await state.element.inputValue();
1862
2018
  break;
2019
+ case "text":
2020
+ state.value = await state.element.textContent();
2021
+ break;
1863
2022
  default:
1864
2023
  state.value = await state.element.getAttribute(attribute);
1865
2024
  break;
1866
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
+ }
1867
2044
  state.info.value = state.value;
1868
2045
  this.setTestData({ [variable]: state.value }, world);
1869
2046
  this.logger.info("set test data: " + variable + "=" + state.value);
@@ -1874,7 +2051,7 @@ class StableBrowser {
1874
2051
  await _commandError(state, e, this);
1875
2052
  }
1876
2053
  finally {
1877
- _commandFinally(state, this);
2054
+ await _commandFinally(state, this);
1878
2055
  }
1879
2056
  }
1880
2057
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1889,6 +2066,7 @@ class StableBrowser {
1889
2066
  highlight: true,
1890
2067
  screenshot: true,
1891
2068
  text: `Verify element attribute`,
2069
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1892
2070
  operation: "verifyAttribute",
1893
2071
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1894
2072
  allowDisabled: true,
@@ -1898,12 +2076,15 @@ class StableBrowser {
1898
2076
  let expectedValue;
1899
2077
  try {
1900
2078
  await _preCommand(state, this);
1901
- expectedValue = state.value;
2079
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1902
2080
  state.info.expectedValue = expectedValue;
1903
2081
  switch (attribute) {
1904
2082
  case "innerText":
1905
2083
  val = String(await state.element.innerText());
1906
2084
  break;
2085
+ case "text":
2086
+ val = String(await state.element.textContent());
2087
+ break;
1907
2088
  case "value":
1908
2089
  val = String(await state.element.inputValue());
1909
2090
  break;
@@ -1943,7 +2124,7 @@ class StableBrowser {
1943
2124
  await _commandError(state, e, this);
1944
2125
  }
1945
2126
  finally {
1946
- _commandFinally(state, this);
2127
+ await _commandFinally(state, this);
1947
2128
  }
1948
2129
  }
1949
2130
  async extractEmailData(emailAddress, options, world) {
@@ -2195,6 +2376,7 @@ class StableBrowser {
2195
2376
  _reportToWorld(world, {
2196
2377
  type: Types.VERIFY_PAGE_PATH,
2197
2378
  text: "Verify page path",
2379
+ _text: "Verify the page path contains " + pathPart,
2198
2380
  screenshotId,
2199
2381
  result: error
2200
2382
  ? {
@@ -2212,27 +2394,89 @@ class StableBrowser {
2212
2394
  });
2213
2395
  }
2214
2396
  }
2215
- 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) {
2216
2460
  const frames = this.page.frames();
2217
2461
  let results = [];
2218
- let ignoreCase = false;
2462
+ // let ignoreCase = false;
2219
2463
  for (let i = 0; i < frames.length; i++) {
2220
2464
  if (dateAlternatives.date) {
2221
2465
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2222
- 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, {});
2223
2467
  result.frame = frames[i];
2224
2468
  results.push(result);
2225
2469
  }
2226
2470
  }
2227
2471
  else if (numberAlternatives.number) {
2228
2472
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2229
- 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, {});
2230
2474
  result.frame = frames[i];
2231
2475
  results.push(result);
2232
2476
  }
2233
2477
  }
2234
2478
  else {
2235
- 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, {});
2236
2480
  result.frame = frames[i];
2237
2481
  results.push(result);
2238
2482
  }
@@ -2251,11 +2495,15 @@ class StableBrowser {
2251
2495
  scroll: false,
2252
2496
  highlight: false,
2253
2497
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2254
- 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`,
2255
2500
  operation: "verifyTextExistInPage",
2256
2501
  log: "***** verify text " + text + " exists in page *****\n",
2257
2502
  };
2258
- const timeout = this._getLoadTimeout(options);
2503
+ if (testForRegex(text)) {
2504
+ text = text.replace(/\\"/g, '"');
2505
+ }
2506
+ const timeout = this._getFindElementTimeout(options);
2259
2507
  await new Promise((resolve) => setTimeout(resolve, 2000));
2260
2508
  const newValue = await this._replaceWithLocalData(text, world);
2261
2509
  if (newValue !== text) {
@@ -2325,7 +2573,7 @@ class StableBrowser {
2325
2573
  await _commandError(state, e, this);
2326
2574
  }
2327
2575
  finally {
2328
- _commandFinally(state, this);
2576
+ await _commandFinally(state, this);
2329
2577
  }
2330
2578
  }
2331
2579
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2338,11 +2586,15 @@ class StableBrowser {
2338
2586
  scroll: false,
2339
2587
  highlight: false,
2340
2588
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2341
- 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`,
2342
2591
  operation: "verifyTextNotExistInPage",
2343
2592
  log: "***** verify text " + text + " does not exist in page *****\n",
2344
2593
  };
2345
- const timeout = this._getLoadTimeout(options);
2594
+ if (testForRegex(text)) {
2595
+ text = text.replace(/\\"/g, '"');
2596
+ }
2597
+ const timeout = this._getFindElementTimeout(options);
2346
2598
  await new Promise((resolve) => setTimeout(resolve, 2000));
2347
2599
  const newValue = await this._replaceWithLocalData(text, world);
2348
2600
  if (newValue !== text) {
@@ -2378,7 +2630,7 @@ class StableBrowser {
2378
2630
  await _commandError(state, e, this);
2379
2631
  }
2380
2632
  finally {
2381
- _commandFinally(state, this);
2633
+ await _commandFinally(state, this);
2382
2634
  }
2383
2635
  }
2384
2636
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2393,10 +2645,11 @@ class StableBrowser {
2393
2645
  highlight: false,
2394
2646
  type: Types.VERIFY_TEXT_WITH_RELATION,
2395
2647
  text: `Verify text with relation to another text`,
2648
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2396
2649
  operation: "verify_text_with_relation",
2397
2650
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2398
2651
  };
2399
- const timeout = this._getLoadTimeout(options);
2652
+ const timeout = this._getFindElementTimeout(options);
2400
2653
  await new Promise((resolve) => setTimeout(resolve, 2000));
2401
2654
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2402
2655
  if (newValue !== textAnchor) {
@@ -2419,7 +2672,7 @@ class StableBrowser {
2419
2672
  };
2420
2673
  while (true) {
2421
2674
  try {
2422
- resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2675
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2423
2676
  }
2424
2677
  catch (error) {
2425
2678
  // ignore
@@ -2447,7 +2700,7 @@ class StableBrowser {
2447
2700
  const count = await frame.locator(css).count();
2448
2701
  for (let j = 0; j < count; j++) {
2449
2702
  const continer = await frame.locator(css).nth(j);
2450
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2703
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2451
2704
  if (result.elementCount > 0) {
2452
2705
  const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2453
2706
  await this._highlightElements(frame, dataAttribute);
@@ -2488,9 +2741,33 @@ class StableBrowser {
2488
2741
  await _commandError(state, e, this);
2489
2742
  }
2490
2743
  finally {
2491
- _commandFinally(state, this);
2744
+ await _commandFinally(state, this);
2492
2745
  }
2493
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
+ }
2766
+ }
2767
+ // state.info.results = results;
2768
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2769
+ return resultWithElementsFound;
2770
+ }
2494
2771
  async visualVerification(text, options = {}, world = null) {
2495
2772
  const startTime = Date.now();
2496
2773
  let error = null;
@@ -2552,6 +2829,7 @@ class StableBrowser {
2552
2829
  _reportToWorld(world, {
2553
2830
  type: Types.VERIFY_VISUAL,
2554
2831
  text: "Visual verification",
2832
+ _text: "Visual verification of " + text,
2555
2833
  screenshotId,
2556
2834
  result: error
2557
2835
  ? {
@@ -2806,7 +3084,13 @@ class StableBrowser {
2806
3084
  }
2807
3085
  }
2808
3086
  async _replaceWithLocalData(value, world, _decrypt = true, totpWait = true) {
2809
- return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3087
+ try {
3088
+ return await replaceWithLocalTestData(value, world, _decrypt, totpWait, this.context, this);
3089
+ }
3090
+ catch (error) {
3091
+ this.logger.debug(error);
3092
+ throw error;
3093
+ }
2810
3094
  }
2811
3095
  _getLoadTimeout(options) {
2812
3096
  let timeout = 15000;
@@ -2818,6 +3102,15 @@ class StableBrowser {
2818
3102
  }
2819
3103
  return timeout;
2820
3104
  }
3105
+ _getFindElementTimeout(options) {
3106
+ if (options && options.timeout) {
3107
+ return options.timeout;
3108
+ }
3109
+ if (this.configuration.find_element_timeout) {
3110
+ return this.configuration.find_element_timeout;
3111
+ }
3112
+ return 30000;
3113
+ }
2821
3114
  async saveStoreState(path = null, world = null) {
2822
3115
  const storageState = await this.page.context().storageState();
2823
3116
  //const testDataFile = _getDataFile(world, this.context, this);
@@ -2834,6 +3127,9 @@ class StableBrowser {
2834
3127
  this.registerEventListeners(this.context);
2835
3128
  registerNetworkEvents(this.world, this, this.context, this.page);
2836
3129
  registerDownloadEvent(this.page, this.world, this.context);
3130
+ if (this.onRestoreSaveState) {
3131
+ this.onRestoreSaveState(path);
3132
+ }
2837
3133
  }
2838
3134
  async waitForPageLoad(options = {}, world = null) {
2839
3135
  let timeout = this._getLoadTimeout(options);
@@ -2902,6 +3198,7 @@ class StableBrowser {
2902
3198
  highlight: false,
2903
3199
  type: Types.CLOSE_PAGE,
2904
3200
  text: `Close page`,
3201
+ _text: `Close the page`,
2905
3202
  operation: "closePage",
2906
3203
  log: "***** close page *****\n",
2907
3204
  throwError: false,
@@ -2915,11 +3212,98 @@ class StableBrowser {
2915
3212
  await _commandError(state, e, this);
2916
3213
  }
2917
3214
  finally {
2918
- _commandFinally(state, this);
3215
+ await _commandFinally(state, this);
3216
+ }
3217
+ }
3218
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3219
+ let operation = null;
3220
+ if (!options || !options.operation) {
3221
+ throw new Error("operation is not defined");
3222
+ }
3223
+ operation = options.operation;
3224
+ // validate operation is one of the supported operations
3225
+ if (operation != "click" && operation != "hover+click") {
3226
+ throw new Error("operation is not supported");
3227
+ }
3228
+ const state = {
3229
+ options,
3230
+ world,
3231
+ locate: false,
3232
+ scroll: false,
3233
+ highlight: false,
3234
+ type: Types.TABLE_OPERATION,
3235
+ text: `Table operation`,
3236
+ _text: `Table ${operation} operation`,
3237
+ operation: operation,
3238
+ log: "***** Table operation *****\n",
3239
+ };
3240
+ const timeout = this._getFindElementTimeout(options);
3241
+ try {
3242
+ await _preCommand(state, this);
3243
+ const start = Date.now();
3244
+ let cellArea = null;
3245
+ while (true) {
3246
+ try {
3247
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3248
+ if (cellArea) {
3249
+ break;
3250
+ }
3251
+ }
3252
+ catch (e) {
3253
+ // ignore
3254
+ }
3255
+ if (Date.now() - start > timeout) {
3256
+ throw new Error(`Cell not found in table`);
3257
+ }
3258
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3259
+ }
3260
+ switch (operation) {
3261
+ case "click":
3262
+ if (!options.css) {
3263
+ // will click in the center of the cell
3264
+ let xOffset = 0;
3265
+ let yOffset = 0;
3266
+ if (options.xOffset) {
3267
+ xOffset = options.xOffset;
3268
+ }
3269
+ if (options.yOffset) {
3270
+ yOffset = options.yOffset;
3271
+ }
3272
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3273
+ }
3274
+ else {
3275
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3276
+ if (results.length === 0) {
3277
+ throw new Error(`Element not found in cell area`);
3278
+ }
3279
+ state.element = results[0];
3280
+ await performAction("click", state.element, options, this, state, _params);
3281
+ }
3282
+ break;
3283
+ case "hover+click":
3284
+ if (!options.css) {
3285
+ throw new Error("css is not defined");
3286
+ }
3287
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3288
+ if (results.length === 0) {
3289
+ throw new Error(`Element not found in cell area`);
3290
+ }
3291
+ state.element = results[0];
3292
+ await performAction("hover+click", state.element, options, this, state, _params);
3293
+ break;
3294
+ default:
3295
+ throw new Error("operation is not supported");
3296
+ }
3297
+ }
3298
+ catch (e) {
3299
+ await _commandError(state, e, this);
3300
+ }
3301
+ finally {
3302
+ await _commandFinally(state, this);
2919
3303
  }
2920
3304
  }
2921
3305
  saveTestDataAsGlobal(options, world) {
2922
- const dataFile = this._getDataFile(world);
3306
+ const dataFile = _getDataFile(world, this.context, this);
2923
3307
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2924
3308
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2925
3309
  }
@@ -2949,6 +3333,7 @@ class StableBrowser {
2949
3333
  _reportToWorld(world, {
2950
3334
  type: Types.SET_VIEWPORT,
2951
3335
  text: "set viewport size to " + width + "x" + hight,
3336
+ _text: "Set the viewport size to " + width + "x" + hight,
2952
3337
  screenshotId,
2953
3338
  result: error
2954
3339
  ? {
@@ -3019,7 +3404,39 @@ class StableBrowser {
3019
3404
  console.log("#-#");
3020
3405
  }
3021
3406
  }
3407
+ async beforeScenario(world, scenario) {
3408
+ this.beforeScenarioCalled = true;
3409
+ if (scenario && scenario.pickle && scenario.pickle.name) {
3410
+ this.scenarioName = scenario.pickle.name;
3411
+ }
3412
+ if (scenario && scenario.gherkinDocument && scenario.gherkinDocument.feature) {
3413
+ this.featureName = scenario.gherkinDocument.feature.name;
3414
+ }
3415
+ if (this.context) {
3416
+ this.context.examplesRow = extractStepExampleParameters(scenario);
3417
+ }
3418
+ if (this.tags === null && scenario && scenario.pickle && scenario.pickle.tags) {
3419
+ this.tags = scenario.pickle.tags.map((tag) => tag.name);
3420
+ // check if @global_test_data tag is present
3421
+ if (this.tags.includes("@global_test_data")) {
3422
+ this.saveTestDataAsGlobal({}, world);
3423
+ }
3424
+ }
3425
+ // update test data based on feature/scenario
3426
+ let envName = null;
3427
+ if (this.context && this.context.environment) {
3428
+ envName = this.context.environment.name;
3429
+ }
3430
+ if (!process.env.TEMP_RUN) {
3431
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3432
+ }
3433
+ await loadBrunoParams(this.context, this.context.environment.name);
3434
+ }
3435
+ async afterScenario(world, scenario) { }
3022
3436
  async beforeStep(world, step) {
3437
+ if (!this.beforeScenarioCalled) {
3438
+ this.beforeScenario(world, step);
3439
+ }
3023
3440
  if (this.stepIndex === undefined) {
3024
3441
  this.stepIndex = 0;
3025
3442
  }
@@ -3036,21 +3453,53 @@ class StableBrowser {
3036
3453
  else {
3037
3454
  this.stepName = "step " + this.stepIndex;
3038
3455
  }
3039
- if (this.context) {
3040
- this.context.examplesRow = extractStepExampleParameters(step);
3041
- }
3042
3456
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
3043
3457
  if (this.context.browserObject.context) {
3044
3458
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
3045
3459
  }
3046
3460
  }
3047
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
3048
- this.tags = step.pickle.tags.map((tag) => tag.name);
3049
- // check if @global_test_data tag is present
3050
- if (this.tags.includes("@global_test_data")) {
3051
- this.saveTestDataAsGlobal({}, world);
3461
+ if (this.initSnapshotTaken === false) {
3462
+ this.initSnapshotTaken = true;
3463
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3464
+ const snapshot = await this.getAriaSnapshot();
3465
+ if (snapshot) {
3466
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3467
+ }
3468
+ }
3469
+ }
3470
+ }
3471
+ async getAriaSnapshot() {
3472
+ try {
3473
+ // find the page url
3474
+ const url = await this.page.url();
3475
+ // extract the path from the url
3476
+ const path = new URL(url).pathname;
3477
+ // get the page title
3478
+ const title = await this.page.title();
3479
+ // go over other frams
3480
+ const frames = this.page.frames();
3481
+ const snapshots = [];
3482
+ const content = [`- path: ${path}`, `- title: ${title}`];
3483
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3484
+ for (let i = 0; i < frames.length; i++) {
3485
+ const frame = frames[i];
3486
+ try {
3487
+ // Ensure frame is attached and has body
3488
+ const body = frame.locator("body");
3489
+ await body.waitFor({ timeout }); // wait explicitly
3490
+ const snapshot = await body.ariaSnapshot({ timeout });
3491
+ content.push(`- frame: ${i}`);
3492
+ content.push(snapshot);
3493
+ }
3494
+ catch (innerErr) { }
3052
3495
  }
3496
+ return content.join("\n");
3497
+ }
3498
+ catch (e) {
3499
+ console.log("Error in getAriaSnapshot");
3500
+ //console.debug(e);
3053
3501
  }
3502
+ return null;
3054
3503
  }
3055
3504
  async afterStep(world, step) {
3056
3505
  this.stepName = null;
@@ -3059,11 +3508,25 @@ class StableBrowser {
3059
3508
  await this.context.browserObject.context.tracing.stopChunk({
3060
3509
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3061
3510
  });
3511
+ if (world && world.attach) {
3512
+ await world.attach(JSON.stringify({
3513
+ type: "trace",
3514
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3515
+ }), "application/json+trace");
3516
+ }
3517
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3062
3518
  }
3063
3519
  }
3064
3520
  if (this.context) {
3065
3521
  this.context.examplesRow = null;
3066
3522
  }
3523
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3524
+ const snapshot = await this.getAriaSnapshot();
3525
+ if (snapshot) {
3526
+ const obj = {};
3527
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3528
+ }
3529
+ }
3067
3530
  }
3068
3531
  }
3069
3532
  function createTimedPromise(promise, label) {