automation_model 1.0.587-dev → 1.0.587-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,21 @@ 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, 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
18
  import { locate_element } from "./locate_element.js";
19
19
  import { randomUUID } from "crypto";
20
20
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
21
21
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
22
22
  import { LocatorLog } from "./locator_log.js";
23
+ import axios from "axios";
24
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
23
25
  export const Types = {
24
26
  CLICK: "click_element",
27
+ WAIT_ELEMENT: "wait_element",
25
28
  NAVIGATE: "navigate",
26
29
  FILL: "fill_element",
27
30
  EXECUTE: "execute_page_method",
@@ -43,6 +46,7 @@ export const Types = {
43
46
  UNCHECK: "uncheck_element",
44
47
  EXTRACT: "extract_attribute",
45
48
  CLOSE_PAGE: "close_page",
49
+ TABLE_OPERATION: "table_operation",
46
50
  SET_DATE_TIME: "set_date_time",
47
51
  SET_VIEWPORT: "set_viewport",
48
52
  VERIFY_VISUAL: "verify_visual",
@@ -51,8 +55,12 @@ export const Types = {
51
55
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
52
56
  VERIFY_ATTRIBUTE: "verify_element_attribute",
53
57
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
58
+ BRUNO: "bruno",
54
59
  };
55
60
  export const apps = {};
61
+ const formatElementName = (elementName) => {
62
+ return elementName ? JSON.stringify(elementName) : "element";
63
+ };
56
64
  class StableBrowser {
57
65
  browser;
58
66
  page;
@@ -66,6 +74,7 @@ class StableBrowser {
66
74
  appName = "main";
67
75
  tags = null;
68
76
  isRecording = false;
77
+ initSnapshotTaken = false;
69
78
  constructor(browser, page, logger = null, context = null, world = null) {
70
79
  this.browser = browser;
71
80
  this.page = page;
@@ -172,6 +181,30 @@ class StableBrowser {
172
181
  await this.waitForPageLoad();
173
182
  }
174
183
  }
184
+ async switchTab(tabTitleOrIndex) {
185
+ // first check if the tabNameOrIndex is a number
186
+ let index = parseInt(tabTitleOrIndex);
187
+ if (!isNaN(index)) {
188
+ if (index >= 0 && index < this.context.pages.length) {
189
+ this.page = this.context.pages[index];
190
+ this.context.page = this.page;
191
+ await this.page.bringToFront();
192
+ return;
193
+ }
194
+ }
195
+ // if the tabNameOrIndex is a string, find the tab by name
196
+ for (let i = 0; i < this.context.pages.length; i++) {
197
+ let page = this.context.pages[i];
198
+ let title = await page.title();
199
+ if (title.includes(tabTitleOrIndex)) {
200
+ this.page = page;
201
+ this.context.page = this.page;
202
+ await this.page.bringToFront();
203
+ return;
204
+ }
205
+ }
206
+ throw new Error("Tab not found: " + tabTitleOrIndex);
207
+ }
175
208
  registerConsoleLogListener(page, context) {
176
209
  if (!this.context.webLogger) {
177
210
  this.context.webLogger = [];
@@ -236,6 +269,9 @@ class StableBrowser {
236
269
  // await closeUnexpectedPopups(this.page);
237
270
  // }
238
271
  async goto(url, world = null) {
272
+ if (!url) {
273
+ throw new Error("url is null, verify that the environment file is correct");
274
+ }
239
275
  if (!url.startsWith("http")) {
240
276
  url = "https://" + url;
241
277
  }
@@ -264,7 +300,7 @@ class StableBrowser {
264
300
  _commandError(state, error, this);
265
301
  }
266
302
  finally {
267
- _commandFinally(state, this);
303
+ await _commandFinally(state, this);
268
304
  }
269
305
  }
270
306
  async _getLocator(locator, scope, _params) {
@@ -345,7 +381,7 @@ class StableBrowser {
345
381
  return resultCss;
346
382
  }
347
383
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
348
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
384
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
349
385
  const locator = scope.locator(query);
350
386
  const count = await locator.count();
351
387
  if (!tag1) {
@@ -365,6 +401,12 @@ class StableBrowser {
365
401
  if (!el.setAttribute) {
366
402
  el = el.parentElement;
367
403
  }
404
+ // remove any attributes start with data-blinq-id
405
+ // for (let i = 0; i < el.attributes.length; i++) {
406
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
407
+ // el.removeAttribute(el.attributes[i].name);
408
+ // }
409
+ // }
368
410
  el.setAttribute("data-blinq-id-" + randomToken, "");
369
411
  return true;
370
412
  }, [tag1, randomToken]))) {
@@ -374,7 +416,7 @@ class StableBrowser {
374
416
  }
375
417
  return { elementCount: tagCount, randomToken };
376
418
  }
377
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false) {
419
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
378
420
  if (!info) {
379
421
  info = {};
380
422
  }
@@ -397,10 +439,11 @@ class StableBrowser {
397
439
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
398
440
  let locator = null;
399
441
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
400
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
442
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
443
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
401
444
  if (!locatorString) {
402
445
  info.failCause.textNotFound = true;
403
- info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
446
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
404
447
  return;
405
448
  }
406
449
  locator = await this._getLocator({ css: locatorString }, scope, _params);
@@ -410,7 +453,7 @@ class StableBrowser {
410
453
  let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
411
454
  if (result.elementCount === 0) {
412
455
  info.failCause.textNotFound = true;
413
- info.failCause.lastError = "failed to locate element by text: " + text;
456
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
414
457
  return;
415
458
  }
416
459
  locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
@@ -462,9 +505,11 @@ class StableBrowser {
462
505
  info.printMessages = {};
463
506
  }
464
507
  if (info.locatorLog && !visible) {
508
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
465
509
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
466
510
  }
467
511
  if (info.locatorLog && !enabled) {
512
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
468
513
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
469
514
  }
470
515
  if (!info.printMessages[j.toString()]) {
@@ -544,7 +589,28 @@ class StableBrowser {
544
589
  }
545
590
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
546
591
  if (!element.rerun) {
547
- return element;
592
+ const randomToken = Math.random().toString(36).substring(7);
593
+ element.evaluate((el, randomToken) => {
594
+ el.setAttribute("data-blinq-id-" + randomToken, "");
595
+ }, randomToken);
596
+ // if (element._frame) {
597
+ // return element;
598
+ // }
599
+ const scope = element._frame ?? element.page();
600
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
601
+ let prefixSelector = "";
602
+ const frameControlSelector = " >> internal:control=enter-frame";
603
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
604
+ if (frameSelectorIndex !== -1) {
605
+ // remove everything after the >> internal:control=enter-frame
606
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
607
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
608
+ }
609
+ // if (element?._frame?._selector) {
610
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
611
+ // }
612
+ const newSelector = prefixSelector + newElementSelector;
613
+ return scope.locator(newSelector);
548
614
  }
549
615
  }
550
616
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -617,7 +683,7 @@ class StableBrowser {
617
683
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
618
684
  if (Date.now() - startTime > timeout) {
619
685
  info.failCause.iframeNotFound = true;
620
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
686
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
621
687
  throw new Error("unable to locate iframe " + selectors.iframe_src);
622
688
  }
623
689
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -691,18 +757,13 @@ class StableBrowser {
691
757
  }
692
758
  // info.log += "scanning locators in priority 1" + "\n";
693
759
  let onlyPriority3 = selectorsLocators[0].priority === 3;
694
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled);
760
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
695
761
  if (result.foundElements.length === 0) {
696
762
  // info.log += "scanning locators in priority 2" + "\n";
697
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled);
698
- }
699
- if (result.foundElements.length === 0 && onlyPriority3) {
700
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
763
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
701
764
  }
702
- else {
703
- if (result.foundElements.length === 0 && !highPriorityOnly) {
704
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
705
- }
765
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
766
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
706
767
  }
707
768
  let foundElements = result.foundElements;
708
769
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -758,6 +819,11 @@ class StableBrowser {
758
819
  visibleOnly = false;
759
820
  }
760
821
  await new Promise((resolve) => setTimeout(resolve, 1000));
822
+ // sheck of more of half of the timeout has passed
823
+ if (Date.now() - startTime > timeout / 2) {
824
+ highPriorityOnly = false;
825
+ visibleOnly = false;
826
+ }
761
827
  }
762
828
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
763
829
  // if (info.locatorLog) {
@@ -768,10 +834,12 @@ class StableBrowser {
768
834
  // }
769
835
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
770
836
  info.failCause.locatorNotFound = true;
771
- info.failCause.lastError = "failed to locate unique element";
837
+ if (!info?.failCause?.lastError) {
838
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
839
+ }
772
840
  throw new Error("failed to locate first element no elements found, " + info.log);
773
841
  }
774
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false) {
842
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
775
843
  let foundElements = [];
776
844
  const result = {
777
845
  foundElements: foundElements,
@@ -779,7 +847,7 @@ class StableBrowser {
779
847
  for (let i = 0; i < locatorsGroup.length; i++) {
780
848
  let foundLocators = [];
781
849
  try {
782
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled);
850
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
783
851
  }
784
852
  catch (e) {
785
853
  // this call can fail it the browser is navigating
@@ -787,7 +855,7 @@ class StableBrowser {
787
855
  // this.logger.debug(e);
788
856
  foundLocators = [];
789
857
  try {
790
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled);
858
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
791
859
  }
792
860
  catch (e) {
793
861
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -802,9 +870,40 @@ class StableBrowser {
802
870
  result.locatorIndex = i;
803
871
  }
804
872
  if (foundLocators.length > 1) {
805
- info.failCause.foundMultiple = true;
806
- if (info.locatorLog) {
807
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
873
+ // remove elements that consume the same space with 10 pixels tolerance
874
+ const boxes = [];
875
+ for (let j = 0; j < foundLocators.length; j++) {
876
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
877
+ }
878
+ for (let j = 0; j < boxes.length; j++) {
879
+ for (let k = 0; k < boxes.length; k++) {
880
+ if (j === k) {
881
+ continue;
882
+ }
883
+ // check if x, y, width, height are the same with 10 pixels tolerance
884
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
885
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
886
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
887
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
888
+ // as the element is not unique, will remove it
889
+ boxes.splice(k, 1);
890
+ k--;
891
+ }
892
+ }
893
+ }
894
+ if (boxes.length === 1) {
895
+ result.foundElements.push({
896
+ locator: boxes[0].locator.first(),
897
+ box: boxes[0].box,
898
+ unique: true,
899
+ });
900
+ result.locatorIndex = i;
901
+ }
902
+ else {
903
+ info.failCause.foundMultiple = true;
904
+ if (info.locatorLog) {
905
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
906
+ }
808
907
  }
809
908
  }
810
909
  }
@@ -852,7 +951,7 @@ class StableBrowser {
852
951
  await _commandError(state, "timeout looking for " + elementDescription, this);
853
952
  }
854
953
  finally {
855
- _commandFinally(state, this);
954
+ await _commandFinally(state, this);
856
955
  }
857
956
  }
858
957
  }
@@ -901,7 +1000,7 @@ class StableBrowser {
901
1000
  await _commandError(state, "timeout looking for " + elementDescription, this);
902
1001
  }
903
1002
  finally {
904
- _commandFinally(state, this);
1003
+ await _commandFinally(state, this);
905
1004
  }
906
1005
  }
907
1006
  }
@@ -915,25 +1014,14 @@ class StableBrowser {
915
1014
  options,
916
1015
  world,
917
1016
  text: "Click element",
1017
+ _text: "Click on " + selectors.element_name,
918
1018
  type: Types.CLICK,
919
1019
  operation: "click",
920
1020
  log: "***** click on " + selectors.element_name + " *****\n",
921
1021
  };
922
1022
  try {
923
1023
  await _preCommand(state, this);
924
- // if (state.options && state.options.context) {
925
- // state.selectors.locators[0].text = state.options.context;
926
- // }
927
- try {
928
- await state.element.click();
929
- // await new Promise((resolve) => setTimeout(resolve, 1000));
930
- }
931
- catch (e) {
932
- // await this.closeUnexpectedPopups();
933
- state.element = await this._locate(selectors, state.info, _params);
934
- await state.element.dispatchEvent("click");
935
- // await new Promise((resolve) => setTimeout(resolve, 1000));
936
- }
1024
+ await performAction("click", state.element, options, this, state, _params);
937
1025
  await this.waitForPageLoad();
938
1026
  return state.info;
939
1027
  }
@@ -941,9 +1029,41 @@ class StableBrowser {
941
1029
  await _commandError(state, e, this);
942
1030
  }
943
1031
  finally {
944
- _commandFinally(state, this);
1032
+ await _commandFinally(state, this);
945
1033
  }
946
1034
  }
1035
+ async waitForElement(selectors, _params, options = {}, world = null) {
1036
+ const timeout = this._getFindElementTimeout(options);
1037
+ const state = {
1038
+ selectors,
1039
+ _params,
1040
+ options,
1041
+ world,
1042
+ text: "Wait for element",
1043
+ _text: "Wait for " + selectors.element_name,
1044
+ type: Types.WAIT_ELEMENT,
1045
+ operation: "waitForElement",
1046
+ log: "***** wait for " + selectors.element_name + " *****\n",
1047
+ };
1048
+ let found = false;
1049
+ try {
1050
+ await _preCommand(state, this);
1051
+ // if (state.options && state.options.context) {
1052
+ // state.selectors.locators[0].text = state.options.context;
1053
+ // }
1054
+ await state.element.waitFor({ timeout: timeout });
1055
+ found = true;
1056
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1057
+ }
1058
+ catch (e) {
1059
+ console.error("Error on waitForElement", e);
1060
+ // await _commandError(state, e, this);
1061
+ }
1062
+ finally {
1063
+ await _commandFinally(state, this);
1064
+ }
1065
+ return found;
1066
+ }
947
1067
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
948
1068
  const state = {
949
1069
  selectors,
@@ -952,6 +1072,7 @@ class StableBrowser {
952
1072
  world,
953
1073
  type: checked ? Types.CHECK : Types.UNCHECK,
954
1074
  text: checked ? `Check element` : `Uncheck element`,
1075
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
955
1076
  operation: "setCheck",
956
1077
  log: "***** check " + selectors.element_name + " *****\n",
957
1078
  };
@@ -961,9 +1082,15 @@ class StableBrowser {
961
1082
  // let element = await this._locate(selectors, info, _params);
962
1083
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
963
1084
  try {
964
- // await this._highlightElements(element);
1085
+ // if (world && world.screenshot && !world.screenshotPath) {
1086
+ // console.log(`Highlighting while running from recorder`);
1087
+ await this._highlightElements(state.element);
965
1088
  await state.element.setChecked(checked);
966
1089
  await new Promise((resolve) => setTimeout(resolve, 1000));
1090
+ // await this._unHighlightElements(element);
1091
+ // }
1092
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1093
+ // await this._unHighlightElements(element);
967
1094
  }
968
1095
  catch (e) {
969
1096
  if (e.message && e.message.includes("did not change its state")) {
@@ -984,7 +1111,7 @@ class StableBrowser {
984
1111
  await _commandError(state, e, this);
985
1112
  }
986
1113
  finally {
987
- _commandFinally(state, this);
1114
+ await _commandFinally(state, this);
988
1115
  }
989
1116
  }
990
1117
  async hover(selectors, _params, options = {}, world = null) {
@@ -995,22 +1122,13 @@ class StableBrowser {
995
1122
  world,
996
1123
  type: Types.HOVER,
997
1124
  text: `Hover element`,
1125
+ _text: `Hover on ${selectors.element_name}`,
998
1126
  operation: "hover",
999
1127
  log: "***** hover " + selectors.element_name + " *****\n",
1000
1128
  };
1001
1129
  try {
1002
1130
  await _preCommand(state, this);
1003
- try {
1004
- await state.element.hover();
1005
- await new Promise((resolve) => setTimeout(resolve, 1000));
1006
- }
1007
- catch (e) {
1008
- //await this.closeUnexpectedPopups();
1009
- state.info.log += "hover failed, will try again" + "\n";
1010
- state.element = await this._locate(selectors, state.info, _params);
1011
- await state.element.hover({ timeout: 10000 });
1012
- await new Promise((resolve) => setTimeout(resolve, 1000));
1013
- }
1131
+ await performAction("hover", state.element, options, this, state, _params);
1014
1132
  await _screenshot(state, this);
1015
1133
  await this.waitForPageLoad();
1016
1134
  return state.info;
@@ -1019,7 +1137,7 @@ class StableBrowser {
1019
1137
  await _commandError(state, e, this);
1020
1138
  }
1021
1139
  finally {
1022
- _commandFinally(state, this);
1140
+ await _commandFinally(state, this);
1023
1141
  }
1024
1142
  }
1025
1143
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1034,6 +1152,7 @@ class StableBrowser {
1034
1152
  value: values.toString(),
1035
1153
  type: Types.SELECT,
1036
1154
  text: `Select option: ${values}`,
1155
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1037
1156
  operation: "selectOption",
1038
1157
  log: "***** select option " + selectors.element_name + " *****\n",
1039
1158
  };
@@ -1054,7 +1173,7 @@ class StableBrowser {
1054
1173
  await _commandError(state, e, this);
1055
1174
  }
1056
1175
  finally {
1057
- _commandFinally(state, this);
1176
+ await _commandFinally(state, this);
1058
1177
  }
1059
1178
  }
1060
1179
  async type(_value, _params = null, options = {}, world = null) {
@@ -1068,6 +1187,7 @@ class StableBrowser {
1068
1187
  highlight: false,
1069
1188
  type: Types.TYPE_PRESS,
1070
1189
  text: `Type value: ${_value}`,
1190
+ _text: `Type value: ${_value}`,
1071
1191
  operation: "type",
1072
1192
  log: "",
1073
1193
  };
@@ -1099,7 +1219,7 @@ class StableBrowser {
1099
1219
  await _commandError(state, e, this);
1100
1220
  }
1101
1221
  finally {
1102
- _commandFinally(state, this);
1222
+ await _commandFinally(state, this);
1103
1223
  }
1104
1224
  }
1105
1225
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1135,7 +1255,7 @@ class StableBrowser {
1135
1255
  await _commandError(state, e, this);
1136
1256
  }
1137
1257
  finally {
1138
- _commandFinally(state, this);
1258
+ await _commandFinally(state, this);
1139
1259
  }
1140
1260
  }
1141
1261
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1147,6 +1267,7 @@ class StableBrowser {
1147
1267
  world,
1148
1268
  type: Types.SET_DATE_TIME,
1149
1269
  text: `Set date time value: ${value}`,
1270
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1150
1271
  operation: "setDateTime",
1151
1272
  log: "***** set date time value " + selectors.element_name + " *****\n",
1152
1273
  throwError: false,
@@ -1154,7 +1275,7 @@ class StableBrowser {
1154
1275
  try {
1155
1276
  await _preCommand(state, this);
1156
1277
  try {
1157
- await state.element.click();
1278
+ await performAction("click", state.element, options, this, state, _params);
1158
1279
  await new Promise((resolve) => setTimeout(resolve, 500));
1159
1280
  if (format) {
1160
1281
  state.value = dayjs(state.value).format(format);
@@ -1203,7 +1324,7 @@ class StableBrowser {
1203
1324
  await _commandError(state, e, this);
1204
1325
  }
1205
1326
  finally {
1206
- _commandFinally(state, this);
1327
+ await _commandFinally(state, this);
1207
1328
  }
1208
1329
  }
1209
1330
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1218,9 +1339,13 @@ class StableBrowser {
1218
1339
  world,
1219
1340
  type: Types.FILL,
1220
1341
  text: `Click type input with value: ${_value}`,
1342
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1221
1343
  operation: "clickType",
1222
1344
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1223
1345
  };
1346
+ if (!options) {
1347
+ options = {};
1348
+ }
1224
1349
  if (newValue !== _value) {
1225
1350
  //this.logger.info(_value + "=" + newValue);
1226
1351
  _value = newValue;
@@ -1228,7 +1353,7 @@ class StableBrowser {
1228
1353
  try {
1229
1354
  await _preCommand(state, this);
1230
1355
  state.info.value = _value;
1231
- if (options === null || options === undefined || !options.press) {
1356
+ if (!options.press) {
1232
1357
  try {
1233
1358
  let currentValue = await state.element.inputValue();
1234
1359
  if (currentValue) {
@@ -1239,13 +1364,9 @@ class StableBrowser {
1239
1364
  this.logger.info("unable to clear input value");
1240
1365
  }
1241
1366
  }
1242
- if (options === null || options === undefined || options.press) {
1243
- try {
1244
- await state.element.click({ timeout: 5000 });
1245
- }
1246
- catch (e) {
1247
- await state.element.dispatchEvent("click");
1248
- }
1367
+ if (options.press) {
1368
+ options.timeout = 5000;
1369
+ await performAction("click", state.element, options, this, state, _params);
1249
1370
  }
1250
1371
  else {
1251
1372
  try {
@@ -1283,7 +1404,12 @@ class StableBrowser {
1283
1404
  await this.waitForPageLoad();
1284
1405
  }
1285
1406
  else if (enter === false) {
1286
- await state.element.dispatchEvent("change");
1407
+ try {
1408
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1409
+ }
1410
+ catch (e) {
1411
+ // ignore
1412
+ }
1287
1413
  //await this.page.keyboard.press("Tab");
1288
1414
  }
1289
1415
  else {
@@ -1298,7 +1424,7 @@ class StableBrowser {
1298
1424
  await _commandError(state, e, this);
1299
1425
  }
1300
1426
  finally {
1301
- _commandFinally(state, this);
1427
+ await _commandFinally(state, this);
1302
1428
  }
1303
1429
  }
1304
1430
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1328,13 +1454,14 @@ class StableBrowser {
1328
1454
  await _commandError(state, e, this);
1329
1455
  }
1330
1456
  finally {
1331
- _commandFinally(state, this);
1457
+ await _commandFinally(state, this);
1332
1458
  }
1333
1459
  }
1334
1460
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1335
1461
  return await this._getText(selectors, 0, _params, options, info, world);
1336
1462
  }
1337
1463
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1464
+ const timeout = this._getFindElementTimeout(options);
1338
1465
  _validateSelectors(selectors);
1339
1466
  let screenshotId = null;
1340
1467
  let screenshotPath = null;
@@ -1344,7 +1471,7 @@ class StableBrowser {
1344
1471
  }
1345
1472
  info.operation = "getText";
1346
1473
  info.selectors = selectors;
1347
- let element = await this._locate(selectors, info, _params);
1474
+ let element = await this._locate(selectors, info, _params, timeout);
1348
1475
  if (climb > 0) {
1349
1476
  const climbArray = [];
1350
1477
  for (let i = 0; i < climb; i++) {
@@ -1363,6 +1490,18 @@ class StableBrowser {
1363
1490
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1364
1491
  try {
1365
1492
  await this._highlightElements(element);
1493
+ // if (world && world.screenshot && !world.screenshotPath) {
1494
+ // // console.log(`Highlighting for get text while running from recorder`);
1495
+ // this._highlightElements(element)
1496
+ // .then(async () => {
1497
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1498
+ // this._unhighlightElements(element).then(
1499
+ // () => {}
1500
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1501
+ // );
1502
+ // })
1503
+ // .catch(e);
1504
+ // }
1366
1505
  const elementText = await element.innerText();
1367
1506
  return {
1368
1507
  text: elementText,
@@ -1374,7 +1513,7 @@ class StableBrowser {
1374
1513
  }
1375
1514
  catch (e) {
1376
1515
  //await this.closeUnexpectedPopups();
1377
- this.logger.info("no innerText will use textContent");
1516
+ this.logger.info("no innerText, will use textContent");
1378
1517
  const elementText = await element.textContent();
1379
1518
  return { text: elementText, screenshotId, screenshotPath, value: value };
1380
1519
  }
@@ -1399,6 +1538,7 @@ class StableBrowser {
1399
1538
  highlight: false,
1400
1539
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1401
1540
  text: `Verify element contains pattern: ${pattern}`,
1541
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1402
1542
  operation: "containsPattern",
1403
1543
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1404
1544
  };
@@ -1430,10 +1570,12 @@ class StableBrowser {
1430
1570
  await _commandError(state, e, this);
1431
1571
  }
1432
1572
  finally {
1433
- _commandFinally(state, this);
1573
+ await _commandFinally(state, this);
1434
1574
  }
1435
1575
  }
1436
1576
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1577
+ const timeout = this._getFindElementTimeout(options);
1578
+ const startTime = Date.now();
1437
1579
  const state = {
1438
1580
  selectors,
1439
1581
  _params,
@@ -1460,62 +1602,54 @@ class StableBrowser {
1460
1602
  }
1461
1603
  let foundObj = null;
1462
1604
  try {
1463
- await _preCommand(state, this);
1464
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1465
- if (foundObj && foundObj.element) {
1466
- await this.scrollIfNeeded(foundObj.element, state.info);
1467
- }
1468
- await _screenshot(state, this);
1469
- const dateAlternatives = findDateAlternatives(text);
1470
- const numberAlternatives = findNumberAlternatives(text);
1471
- if (dateAlternatives.date) {
1472
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1473
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1474
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1475
- return state.info;
1605
+ while (Date.now() - startTime < timeout) {
1606
+ try {
1607
+ await _preCommand(state, this);
1608
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1609
+ if (foundObj && foundObj.element) {
1610
+ await this.scrollIfNeeded(foundObj.element, state.info);
1476
1611
  }
1477
- }
1478
- throw new Error("element doesn't contain text " + text);
1479
- }
1480
- else if (numberAlternatives.number) {
1481
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1482
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1483
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1612
+ await _screenshot(state, this);
1613
+ const dateAlternatives = findDateAlternatives(text);
1614
+ const numberAlternatives = findNumberAlternatives(text);
1615
+ if (dateAlternatives.date) {
1616
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1617
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1618
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1619
+ return state.info;
1620
+ }
1621
+ }
1622
+ }
1623
+ else if (numberAlternatives.number) {
1624
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1625
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1626
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1627
+ return state.info;
1628
+ }
1629
+ }
1630
+ }
1631
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1484
1632
  return state.info;
1485
1633
  }
1486
1634
  }
1487
- throw new Error("element doesn't contain text " + text);
1488
- }
1489
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1490
- state.info.foundText = foundObj?.text;
1491
- state.info.value = foundObj?.value;
1492
- throw new Error("element doesn't contain text " + text);
1635
+ catch (e) {
1636
+ // Log error but continue retrying until timeout is reached
1637
+ this.logger.warn("Retrying containsText due to: " + e.message);
1638
+ }
1639
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1493
1640
  }
1494
- return state.info;
1641
+ state.info.foundText = foundObj?.text;
1642
+ state.info.value = foundObj?.value;
1643
+ throw new Error("element doesn't contain text " + text);
1495
1644
  }
1496
1645
  catch (e) {
1497
1646
  await _commandError(state, e, this);
1647
+ throw e;
1498
1648
  }
1499
1649
  finally {
1500
- _commandFinally(state, this);
1650
+ await _commandFinally(state, this);
1501
1651
  }
1502
1652
  }
1503
- _getDataFile(world = null) {
1504
- let dataFile = null;
1505
- if (world && world.reportFolder) {
1506
- dataFile = path.join(world.reportFolder, "data.json");
1507
- }
1508
- else if (this.reportFolder) {
1509
- dataFile = path.join(this.reportFolder, "data.json");
1510
- }
1511
- else if (this.context && this.context.reportFolder) {
1512
- dataFile = path.join(this.context.reportFolder, "data.json");
1513
- }
1514
- else {
1515
- dataFile = "data.json";
1516
- }
1517
- return dataFile;
1518
- }
1519
1653
  async waitForUserInput(message, world = null) {
1520
1654
  if (!message) {
1521
1655
  message = "# Wait for user input. Press any key to continue";
@@ -1544,13 +1678,22 @@ class StableBrowser {
1544
1678
  return;
1545
1679
  }
1546
1680
  // if data file exists, load it
1547
- const dataFile = this._getDataFile(world);
1681
+ const dataFile = _getDataFile(world, this.context, this);
1548
1682
  let data = this.getTestData(world);
1549
1683
  // merge the testData with the existing data
1550
1684
  Object.assign(data, testData);
1551
1685
  // save the data to the file
1552
1686
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1553
1687
  }
1688
+ overwriteTestData(testData, world = null) {
1689
+ if (!testData) {
1690
+ return;
1691
+ }
1692
+ // if data file exists, load it
1693
+ const dataFile = _getDataFile(world, this.context, this);
1694
+ // save the data to the file
1695
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1696
+ }
1554
1697
  _getDataFilePath(fileName) {
1555
1698
  let dataFile = path.join(this.project_path, "data", fileName);
1556
1699
  if (fs.existsSync(dataFile)) {
@@ -1647,7 +1790,7 @@ class StableBrowser {
1647
1790
  }
1648
1791
  }
1649
1792
  getTestData(world = null) {
1650
- const dataFile = this._getDataFile(world);
1793
+ const dataFile = _getDataFile(world, this.context, this);
1651
1794
  let data = {};
1652
1795
  if (fs.existsSync(dataFile)) {
1653
1796
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1734,6 +1877,15 @@ class StableBrowser {
1734
1877
  document.documentElement.clientWidth,
1735
1878
  ])));
1736
1879
  let screenshotBuffer = null;
1880
+ // if (focusedElement) {
1881
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1882
+ // await this._unhighlightElements(focusedElement);
1883
+ // await new Promise((resolve) => setTimeout(resolve, 100));
1884
+ // console.log(`Unhighlighted previous element`);
1885
+ // }
1886
+ // if (focusedElement) {
1887
+ // await this._highlightElements(focusedElement);
1888
+ // }
1737
1889
  if (this.context.browserName === "chromium") {
1738
1890
  const client = await playContext.newCDPSession(this.page);
1739
1891
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1755,6 +1907,10 @@ class StableBrowser {
1755
1907
  else {
1756
1908
  screenshotBuffer = await this.page.screenshot();
1757
1909
  }
1910
+ // if (focusedElement) {
1911
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1912
+ // await this._unhighlightElements(focusedElement);
1913
+ // }
1758
1914
  let image = await Jimp.read(screenshotBuffer);
1759
1915
  // Get the image dimensions
1760
1916
  const { width, height } = image.bitmap;
@@ -1767,6 +1923,7 @@ class StableBrowser {
1767
1923
  else {
1768
1924
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1769
1925
  }
1926
+ return screenshotBuffer;
1770
1927
  }
1771
1928
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1772
1929
  const state = {
@@ -1789,7 +1946,7 @@ class StableBrowser {
1789
1946
  await _commandError(state, e, this);
1790
1947
  }
1791
1948
  finally {
1792
- _commandFinally(state, this);
1949
+ await _commandFinally(state, this);
1793
1950
  }
1794
1951
  }
1795
1952
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1802,8 +1959,10 @@ class StableBrowser {
1802
1959
  world,
1803
1960
  type: Types.EXTRACT,
1804
1961
  text: `Extract attribute from element`,
1962
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1805
1963
  operation: "extractAttribute",
1806
1964
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1965
+ allowDisabled: true,
1807
1966
  };
1808
1967
  await new Promise((resolve) => setTimeout(resolve, 2000));
1809
1968
  try {
@@ -1818,6 +1977,9 @@ class StableBrowser {
1818
1977
  case "value":
1819
1978
  state.value = await state.element.inputValue();
1820
1979
  break;
1980
+ case "text":
1981
+ state.value = await state.element.textContent();
1982
+ break;
1821
1983
  default:
1822
1984
  state.value = await state.element.getAttribute(attribute);
1823
1985
  break;
@@ -1825,13 +1987,14 @@ class StableBrowser {
1825
1987
  state.info.value = state.value;
1826
1988
  this.setTestData({ [variable]: state.value }, world);
1827
1989
  this.logger.info("set test data: " + variable + "=" + state.value);
1990
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1828
1991
  return state.info;
1829
1992
  }
1830
1993
  catch (e) {
1831
1994
  await _commandError(state, e, this);
1832
1995
  }
1833
1996
  finally {
1834
- _commandFinally(state, this);
1997
+ await _commandFinally(state, this);
1835
1998
  }
1836
1999
  }
1837
2000
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1846,18 +2009,25 @@ class StableBrowser {
1846
2009
  highlight: true,
1847
2010
  screenshot: true,
1848
2011
  text: `Verify element attribute`,
2012
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1849
2013
  operation: "verifyAttribute",
1850
2014
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1851
- allowDisabled: true
2015
+ allowDisabled: true,
1852
2016
  };
1853
2017
  await new Promise((resolve) => setTimeout(resolve, 2000));
1854
2018
  let val;
2019
+ let expectedValue;
1855
2020
  try {
1856
2021
  await _preCommand(state, this);
2022
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2023
+ state.info.expectedValue = expectedValue;
1857
2024
  switch (attribute) {
1858
2025
  case "innerText":
1859
2026
  val = String(await state.element.innerText());
1860
2027
  break;
2028
+ case "text":
2029
+ val = String(await state.element.textContent());
2030
+ break;
1861
2031
  case "value":
1862
2032
  val = String(await state.element.inputValue());
1863
2033
  break;
@@ -1875,26 +2045,29 @@ class StableBrowser {
1875
2045
  val = String(await state.element.getAttribute(attribute));
1876
2046
  break;
1877
2047
  }
2048
+ state.info.value = val;
1878
2049
  let regex;
1879
- if (value.startsWith("/") && value.endsWith("/")) {
1880
- const patternBody = value.slice(1, -1);
2050
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2051
+ const patternBody = expectedValue.slice(1, -1);
1881
2052
  regex = new RegExp(patternBody, "g");
1882
2053
  }
1883
2054
  else {
1884
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2055
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1885
2056
  regex = new RegExp(escapedPattern, "g");
1886
2057
  }
1887
2058
  if (!val.match(regex)) {
1888
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
2059
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2060
+ state.info.failCause.assertionFailed = true;
2061
+ state.info.failCause.lastError = errorMessage;
2062
+ throw new Error(errorMessage);
1889
2063
  }
1890
- state.info.expectedValue = val;
1891
2064
  return state.info;
1892
2065
  }
1893
2066
  catch (e) {
1894
2067
  await _commandError(state, e, this);
1895
2068
  }
1896
2069
  finally {
1897
- _commandFinally(state, this);
2070
+ await _commandFinally(state, this);
1898
2071
  }
1899
2072
  }
1900
2073
  async extractEmailData(emailAddress, options, world) {
@@ -1986,27 +2159,32 @@ class StableBrowser {
1986
2159
  async _highlightElements(scope, css) {
1987
2160
  try {
1988
2161
  if (!scope) {
2162
+ // console.log(`Scope is not defined`);
1989
2163
  return;
1990
2164
  }
1991
2165
  if (!css) {
1992
2166
  scope
1993
2167
  .evaluate((node) => {
1994
2168
  if (node && node.style) {
1995
- let originalBorder = node.style.outline;
2169
+ let originalOutline = node.style.outline;
2170
+ // console.log(`Original outline was: ${originalOutline}`);
2171
+ // node.__previousOutline = originalOutline;
1996
2172
  node.style.outline = "2px solid red";
2173
+ // console.log(`New outline is: ${node.style.outline}`);
1997
2174
  if (window) {
1998
2175
  window.addEventListener("beforeunload", function (e) {
1999
- node.style.outline = originalBorder;
2176
+ node.style.outline = originalOutline;
2000
2177
  });
2001
2178
  }
2002
2179
  setTimeout(function () {
2003
- node.style.outline = originalBorder;
2180
+ node.style.outline = originalOutline;
2004
2181
  }, 2000);
2005
2182
  }
2006
2183
  })
2007
2184
  .then(() => { })
2008
2185
  .catch((e) => {
2009
2186
  // ignore
2187
+ // console.error(`Could not highlight node : ${e}`);
2010
2188
  });
2011
2189
  }
2012
2190
  else {
@@ -2022,17 +2200,18 @@ class StableBrowser {
2022
2200
  if (!element.style) {
2023
2201
  return;
2024
2202
  }
2025
- var originalBorder = element.style.outline;
2203
+ let originalOutline = element.style.outline;
2204
+ element.__previousOutline = originalOutline;
2026
2205
  // Set the new border to be red and 2px solid
2027
2206
  element.style.outline = "2px solid red";
2028
2207
  if (window) {
2029
2208
  window.addEventListener("beforeunload", function (e) {
2030
- element.style.outline = originalBorder;
2209
+ element.style.outline = originalOutline;
2031
2210
  });
2032
2211
  }
2033
2212
  // Set a timeout to revert to the original border after 2 seconds
2034
2213
  setTimeout(function () {
2035
- element.style.outline = originalBorder;
2214
+ element.style.outline = originalOutline;
2036
2215
  }, 2000);
2037
2216
  }
2038
2217
  return;
@@ -2040,6 +2219,7 @@ class StableBrowser {
2040
2219
  .then(() => { })
2041
2220
  .catch((e) => {
2042
2221
  // ignore
2222
+ // console.error(`Could not highlight css: ${e}`);
2043
2223
  });
2044
2224
  }
2045
2225
  }
@@ -2047,6 +2227,54 @@ class StableBrowser {
2047
2227
  console.debug(error);
2048
2228
  }
2049
2229
  }
2230
+ // async _unhighlightElements(scope, css) {
2231
+ // try {
2232
+ // if (!scope) {
2233
+ // return;
2234
+ // }
2235
+ // if (!css) {
2236
+ // scope
2237
+ // .evaluate((node) => {
2238
+ // if (node && node.style) {
2239
+ // if (!node.__previousOutline) {
2240
+ // node.style.outline = "";
2241
+ // } else {
2242
+ // node.style.outline = node.__previousOutline;
2243
+ // }
2244
+ // }
2245
+ // })
2246
+ // .then(() => {})
2247
+ // .catch((e) => {
2248
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2249
+ // });
2250
+ // } else {
2251
+ // scope
2252
+ // .evaluate(([css]) => {
2253
+ // if (!css) {
2254
+ // return;
2255
+ // }
2256
+ // let elements = Array.from(document.querySelectorAll(css));
2257
+ // for (i = 0; i < elements.length; i++) {
2258
+ // let element = elements[i];
2259
+ // if (!element.style) {
2260
+ // return;
2261
+ // }
2262
+ // if (!element.__previousOutline) {
2263
+ // element.style.outline = "";
2264
+ // } else {
2265
+ // element.style.outline = element.__previousOutline;
2266
+ // }
2267
+ // }
2268
+ // })
2269
+ // .then(() => {})
2270
+ // .catch((e) => {
2271
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2272
+ // });
2273
+ // }
2274
+ // } catch (error) {
2275
+ // // console.debug(error);
2276
+ // }
2277
+ // }
2050
2278
  async verifyPagePath(pathPart, options = {}, world = null) {
2051
2279
  const startTime = Date.now();
2052
2280
  let error = null;
@@ -2091,6 +2319,69 @@ class StableBrowser {
2091
2319
  _reportToWorld(world, {
2092
2320
  type: Types.VERIFY_PAGE_PATH,
2093
2321
  text: "Verify page path",
2322
+ _text: "Verify the page path contains " + pathPart,
2323
+ screenshotId,
2324
+ result: error
2325
+ ? {
2326
+ status: "FAILED",
2327
+ startTime,
2328
+ endTime,
2329
+ message: error?.message,
2330
+ }
2331
+ : {
2332
+ status: "PASSED",
2333
+ startTime,
2334
+ endTime,
2335
+ },
2336
+ info: info,
2337
+ });
2338
+ }
2339
+ }
2340
+ async verifyPageTitle(title, options = {}, world = null) {
2341
+ const startTime = Date.now();
2342
+ let error = null;
2343
+ let screenshotId = null;
2344
+ let screenshotPath = null;
2345
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2346
+ const info = {};
2347
+ info.log = "***** verify page title " + title + " *****\n";
2348
+ info.operation = "verifyPageTitle";
2349
+ const newValue = await this._replaceWithLocalData(title, world);
2350
+ if (newValue !== title) {
2351
+ this.logger.info(title + "=" + newValue);
2352
+ title = newValue;
2353
+ }
2354
+ info.title = title;
2355
+ try {
2356
+ for (let i = 0; i < 30; i++) {
2357
+ const foundTitle = await this.page.title();
2358
+ if (!foundTitle.includes(title)) {
2359
+ if (i === 29) {
2360
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2361
+ }
2362
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2363
+ continue;
2364
+ }
2365
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2366
+ return info;
2367
+ }
2368
+ }
2369
+ catch (e) {
2370
+ //await this.closeUnexpectedPopups();
2371
+ this.logger.error("verify page title failed " + info.log);
2372
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2373
+ info.screenshotPath = screenshotPath;
2374
+ Object.assign(e, { info: info });
2375
+ error = e;
2376
+ // throw e;
2377
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2378
+ }
2379
+ finally {
2380
+ const endTime = Date.now();
2381
+ _reportToWorld(world, {
2382
+ type: Types.VERIFY_PAGE_PATH,
2383
+ text: "Verify page title",
2384
+ _text: "Verify the page title contains " + title,
2094
2385
  screenshotId,
2095
2386
  result: error
2096
2387
  ? {
@@ -2108,27 +2399,27 @@ class StableBrowser {
2108
2399
  });
2109
2400
  }
2110
2401
  }
2111
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2402
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2112
2403
  const frames = this.page.frames();
2113
2404
  let results = [];
2114
- let ignoreCase = false;
2405
+ // let ignoreCase = false;
2115
2406
  for (let i = 0; i < frames.length; i++) {
2116
2407
  if (dateAlternatives.date) {
2117
2408
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2118
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2409
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2119
2410
  result.frame = frames[i];
2120
2411
  results.push(result);
2121
2412
  }
2122
2413
  }
2123
2414
  else if (numberAlternatives.number) {
2124
2415
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2125
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2416
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2126
2417
  result.frame = frames[i];
2127
2418
  results.push(result);
2128
2419
  }
2129
2420
  }
2130
2421
  else {
2131
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2422
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2132
2423
  result.frame = frames[i];
2133
2424
  results.push(result);
2134
2425
  }
@@ -2147,11 +2438,15 @@ class StableBrowser {
2147
2438
  scroll: false,
2148
2439
  highlight: false,
2149
2440
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2150
- text: `Verify text exists in page`,
2441
+ text: `Verify the text '${text}' exists in page`,
2442
+ _text: `Verify the text '${text}' exists in page`,
2151
2443
  operation: "verifyTextExistInPage",
2152
2444
  log: "***** verify text " + text + " exists in page *****\n",
2153
2445
  };
2154
- const timeout = this._getLoadTimeout(options);
2446
+ if (testForRegex(text)) {
2447
+ text = text.replace(/\\"/g, '"');
2448
+ }
2449
+ const timeout = this._getFindElementTimeout(options);
2155
2450
  await new Promise((resolve) => setTimeout(resolve, 2000));
2156
2451
  const newValue = await this._replaceWithLocalData(text, world);
2157
2452
  if (newValue !== text) {
@@ -2164,7 +2459,15 @@ class StableBrowser {
2164
2459
  await _preCommand(state, this);
2165
2460
  state.info.text = text;
2166
2461
  while (true) {
2167
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2462
+ let resultWithElementsFound = {
2463
+ length: 0,
2464
+ };
2465
+ try {
2466
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2467
+ }
2468
+ catch (error) {
2469
+ // ignore
2470
+ }
2168
2471
  if (resultWithElementsFound.length === 0) {
2169
2472
  if (Date.now() - state.startTime > timeout) {
2170
2473
  throw new Error(`Text ${text} not found in page`);
@@ -2172,18 +2475,40 @@ class StableBrowser {
2172
2475
  await new Promise((resolve) => setTimeout(resolve, 1000));
2173
2476
  continue;
2174
2477
  }
2175
- if (resultWithElementsFound[0].randomToken) {
2176
- const frame = resultWithElementsFound[0].frame;
2177
- const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2178
- await this._highlightElements(frame, dataAttribute);
2179
- const element = await frame.locator(dataAttribute).first();
2180
- if (element) {
2181
- await this.scrollIfNeeded(element, state.info);
2182
- await element.dispatchEvent("bvt_verify_page_contains_text");
2478
+ try {
2479
+ if (resultWithElementsFound[0].randomToken) {
2480
+ const frame = resultWithElementsFound[0].frame;
2481
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2482
+ await this._highlightElements(frame, dataAttribute);
2483
+ // if (world && world.screenshot && !world.screenshotPath) {
2484
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2485
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2486
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2487
+ // this._unhighlightElements(frame, dataAttribute)
2488
+ // .then(async () => {
2489
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2490
+ // })
2491
+ // .catch(
2492
+ // (e) => {}
2493
+ // console.error(e)
2494
+ // );
2495
+ // });
2496
+ // }
2497
+ const element = await frame.locator(dataAttribute).first();
2498
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2499
+ // await this._unhighlightElements(frame, dataAttribute);
2500
+ if (element) {
2501
+ await this.scrollIfNeeded(element, state.info);
2502
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2503
+ // await _screenshot(state, this, element);
2504
+ }
2183
2505
  }
2506
+ await _screenshot(state, this);
2507
+ return state.info;
2508
+ }
2509
+ catch (error) {
2510
+ console.error(error);
2184
2511
  }
2185
- await _screenshot(state, this);
2186
- return state.info;
2187
2512
  }
2188
2513
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2189
2514
  }
@@ -2191,7 +2516,7 @@ class StableBrowser {
2191
2516
  await _commandError(state, e, this);
2192
2517
  }
2193
2518
  finally {
2194
- _commandFinally(state, this);
2519
+ await _commandFinally(state, this);
2195
2520
  }
2196
2521
  }
2197
2522
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2204,11 +2529,15 @@ class StableBrowser {
2204
2529
  scroll: false,
2205
2530
  highlight: false,
2206
2531
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2207
- text: `Verify text does not exist in page`,
2532
+ text: `Verify the text '${text}' does not exist in page`,
2533
+ _text: `Verify the text '${text}' does not exist in page`,
2208
2534
  operation: "verifyTextNotExistInPage",
2209
2535
  log: "***** verify text " + text + " does not exist in page *****\n",
2210
2536
  };
2211
- const timeout = this._getLoadTimeout(options);
2537
+ if (testForRegex(text)) {
2538
+ text = text.replace(/\\"/g, '"');
2539
+ }
2540
+ const timeout = this._getFindElementTimeout(options);
2212
2541
  await new Promise((resolve) => setTimeout(resolve, 2000));
2213
2542
  const newValue = await this._replaceWithLocalData(text, world);
2214
2543
  if (newValue !== text) {
@@ -2220,8 +2549,16 @@ class StableBrowser {
2220
2549
  try {
2221
2550
  await _preCommand(state, this);
2222
2551
  state.info.text = text;
2552
+ let resultWithElementsFound = {
2553
+ length: null, // initial cannot be 0
2554
+ };
2223
2555
  while (true) {
2224
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2556
+ try {
2557
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2558
+ }
2559
+ catch (error) {
2560
+ // ignore
2561
+ }
2225
2562
  if (resultWithElementsFound.length === 0) {
2226
2563
  await _screenshot(state, this);
2227
2564
  return state.info;
@@ -2236,7 +2573,7 @@ class StableBrowser {
2236
2573
  await _commandError(state, e, this);
2237
2574
  }
2238
2575
  finally {
2239
- _commandFinally(state, this);
2576
+ await _commandFinally(state, this);
2240
2577
  }
2241
2578
  }
2242
2579
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2251,10 +2588,11 @@ class StableBrowser {
2251
2588
  highlight: false,
2252
2589
  type: Types.VERIFY_TEXT_WITH_RELATION,
2253
2590
  text: `Verify text with relation to another text`,
2591
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2254
2592
  operation: "verify_text_with_relation",
2255
2593
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2256
2594
  };
2257
- const timeout = this._getLoadTimeout(options);
2595
+ const timeout = this._getFindElementTimeout(options);
2258
2596
  await new Promise((resolve) => setTimeout(resolve, 2000));
2259
2597
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2260
2598
  if (newValue !== textAnchor) {
@@ -2272,8 +2610,16 @@ class StableBrowser {
2272
2610
  try {
2273
2611
  await _preCommand(state, this);
2274
2612
  state.info.text = textToVerify;
2613
+ let resultWithElementsFound = {
2614
+ length: 0,
2615
+ };
2275
2616
  while (true) {
2276
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2617
+ try {
2618
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2619
+ }
2620
+ catch (error) {
2621
+ // ignore
2622
+ }
2277
2623
  if (resultWithElementsFound.length === 0) {
2278
2624
  if (Date.now() - state.startTime > timeout) {
2279
2625
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2281,37 +2627,56 @@ class StableBrowser {
2281
2627
  await new Promise((resolve) => setTimeout(resolve, 1000));
2282
2628
  continue;
2283
2629
  }
2284
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2285
- foundAncore = true;
2286
- const result = resultWithElementsFound[i];
2287
- const token = result.randomToken;
2288
- const frame = result.frame;
2289
- let css = `[data-blinq-id-${token}]`;
2290
- const climbArray1 = [];
2291
- for (let i = 0; i < climb; i++) {
2292
- climbArray1.push("..");
2293
- }
2294
- let climbXpath = "xpath=" + climbArray1.join("/");
2295
- css = css + " >> " + climbXpath;
2296
- const count = await frame.locator(css).count();
2297
- for (let j = 0; j < count; j++) {
2298
- const continer = await frame.locator(css).nth(j);
2299
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2300
- if (result.elementCount > 0) {
2301
- const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2302
- //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2303
- await this._highlightElements(frame, dataAttribute);
2304
- //await this._highlightElements(frame, cssAnchor);
2305
- const element = await frame.locator(dataAttribute).first();
2306
- if (element) {
2307
- await this.scrollIfNeeded(element, state.info);
2308
- await element.dispatchEvent("bvt_verify_page_contains_text");
2630
+ try {
2631
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2632
+ foundAncore = true;
2633
+ const result = resultWithElementsFound[i];
2634
+ const token = result.randomToken;
2635
+ const frame = result.frame;
2636
+ let css = `[data-blinq-id-${token}]`;
2637
+ const climbArray1 = [];
2638
+ for (let i = 0; i < climb; i++) {
2639
+ climbArray1.push("..");
2640
+ }
2641
+ let climbXpath = "xpath=" + climbArray1.join("/");
2642
+ css = css + " >> " + climbXpath;
2643
+ const count = await frame.locator(css).count();
2644
+ for (let j = 0; j < count; j++) {
2645
+ const continer = await frame.locator(css).nth(j);
2646
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2647
+ if (result.elementCount > 0) {
2648
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2649
+ await this._highlightElements(frame, dataAttribute);
2650
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2651
+ // if (world && world.screenshot && !world.screenshotPath) {
2652
+ // console.log(`Highlighting for vtrt while running from recorder`);
2653
+ // this._highlightElements(frame, dataAttribute)
2654
+ // .then(async () => {
2655
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2656
+ // this._unhighlightElements(frame, dataAttribute).then(
2657
+ // () => {}
2658
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2659
+ // );
2660
+ // })
2661
+ // .catch(e);
2662
+ // }
2663
+ //await this._highlightElements(frame, cssAnchor);
2664
+ const element = await frame.locator(dataAttribute).first();
2665
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2666
+ // await this._unhighlightElements(frame, dataAttribute);
2667
+ if (element) {
2668
+ await this.scrollIfNeeded(element, state.info);
2669
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2670
+ }
2671
+ await _screenshot(state, this);
2672
+ return state.info;
2309
2673
  }
2310
- await _screenshot(state, this);
2311
- return state.info;
2312
2674
  }
2313
2675
  }
2314
2676
  }
2677
+ catch (error) {
2678
+ console.error(error);
2679
+ }
2315
2680
  }
2316
2681
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2317
2682
  }
@@ -2319,9 +2684,33 @@ class StableBrowser {
2319
2684
  await _commandError(state, e, this);
2320
2685
  }
2321
2686
  finally {
2322
- _commandFinally(state, this);
2687
+ await _commandFinally(state, this);
2323
2688
  }
2324
2689
  }
2690
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2691
+ const frames = this.page.frames();
2692
+ let results = [];
2693
+ let ignoreCase = false;
2694
+ for (let i = 0; i < frames.length; i++) {
2695
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2696
+ result.frame = frames[i];
2697
+ const climbArray = [];
2698
+ for (let i = 0; i < climb; i++) {
2699
+ climbArray.push("..");
2700
+ }
2701
+ let climbXpath = "xpath=" + climbArray.join("/");
2702
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2703
+ const count = await frames[i].locator(newLocator).count();
2704
+ if (count > 0) {
2705
+ result.elementCount = count;
2706
+ result.locator = newLocator;
2707
+ results.push(result);
2708
+ }
2709
+ }
2710
+ // state.info.results = results;
2711
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2712
+ return resultWithElementsFound;
2713
+ }
2325
2714
  async visualVerification(text, options = {}, world = null) {
2326
2715
  const startTime = Date.now();
2327
2716
  let error = null;
@@ -2340,10 +2729,13 @@ class StableBrowser {
2340
2729
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2341
2730
  info.screenshotPath = screenshotPath;
2342
2731
  const screenshot = await this.takeScreenshot();
2343
- const request = {
2344
- method: "POST",
2732
+ let request = {
2733
+ method: "post",
2734
+ maxBodyLength: Infinity,
2345
2735
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2346
2736
  headers: {
2737
+ "x-bvt-project-id": path.basename(this.project_path),
2738
+ "x-source": "aaa",
2347
2739
  "Content-Type": "application/json",
2348
2740
  Authorization: `Bearer ${process.env.TOKEN}`,
2349
2741
  },
@@ -2352,7 +2744,7 @@ class StableBrowser {
2352
2744
  screenshot: screenshot,
2353
2745
  }),
2354
2746
  };
2355
- let result = await this.context.api.request(request);
2747
+ const result = await axios.request(request);
2356
2748
  if (result.data.status !== true) {
2357
2749
  throw new Error("Visual validation failed");
2358
2750
  }
@@ -2380,6 +2772,7 @@ class StableBrowser {
2380
2772
  _reportToWorld(world, {
2381
2773
  type: Types.VERIFY_VISUAL,
2382
2774
  text: "Visual verification",
2775
+ _text: "Visual verification of " + text,
2383
2776
  screenshotId,
2384
2777
  result: error
2385
2778
  ? {
@@ -2646,6 +3039,32 @@ class StableBrowser {
2646
3039
  }
2647
3040
  return timeout;
2648
3041
  }
3042
+ _getFindElementTimeout(options) {
3043
+ if (options && options.timeout) {
3044
+ return options.timeout;
3045
+ }
3046
+ if (this.configuration.find_element_timeout) {
3047
+ return this.configuration.find_element_timeout;
3048
+ }
3049
+ return 30000;
3050
+ }
3051
+ async saveStoreState(path = null, world = null) {
3052
+ const storageState = await this.page.context().storageState();
3053
+ //const testDataFile = _getDataFile(world, this.context, this);
3054
+ if (path) {
3055
+ // save { storageState: storageState } into the path
3056
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3057
+ }
3058
+ else {
3059
+ await this.setTestData({ storageState: storageState }, world);
3060
+ }
3061
+ }
3062
+ async restoreSaveState(path = null, world = null) {
3063
+ await refreshBrowser(this, path, world);
3064
+ this.registerEventListeners(this.context);
3065
+ registerNetworkEvents(this.world, this, this.context, this.page);
3066
+ registerDownloadEvent(this.page, this.world, this.context);
3067
+ }
2649
3068
  async waitForPageLoad(options = {}, world = null) {
2650
3069
  let timeout = this._getLoadTimeout(options);
2651
3070
  const promiseArray = [];
@@ -2713,6 +3132,7 @@ class StableBrowser {
2713
3132
  highlight: false,
2714
3133
  type: Types.CLOSE_PAGE,
2715
3134
  text: `Close page`,
3135
+ _text: `Close the page`,
2716
3136
  operation: "closePage",
2717
3137
  log: "***** close page *****\n",
2718
3138
  throwError: false,
@@ -2726,11 +3146,98 @@ class StableBrowser {
2726
3146
  await _commandError(state, e, this);
2727
3147
  }
2728
3148
  finally {
2729
- _commandFinally(state, this);
3149
+ await _commandFinally(state, this);
3150
+ }
3151
+ }
3152
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3153
+ let operation = null;
3154
+ if (!options || !options.operation) {
3155
+ throw new Error("operation is not defined");
3156
+ }
3157
+ operation = options.operation;
3158
+ // validate operation is one of the supported operations
3159
+ if (operation != "click" && operation != "hover+click") {
3160
+ throw new Error("operation is not supported");
3161
+ }
3162
+ const state = {
3163
+ options,
3164
+ world,
3165
+ locate: false,
3166
+ scroll: false,
3167
+ highlight: false,
3168
+ type: Types.TABLE_OPERATION,
3169
+ text: `Table operation`,
3170
+ _text: `Table ${operation} operation`,
3171
+ operation: operation,
3172
+ log: "***** Table operation *****\n",
3173
+ };
3174
+ const timeout = this._getFindElementTimeout(options);
3175
+ try {
3176
+ await _preCommand(state, this);
3177
+ const start = Date.now();
3178
+ let cellArea = null;
3179
+ while (true) {
3180
+ try {
3181
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3182
+ if (cellArea) {
3183
+ break;
3184
+ }
3185
+ }
3186
+ catch (e) {
3187
+ // ignore
3188
+ }
3189
+ if (Date.now() - start > timeout) {
3190
+ throw new Error(`Cell not found in table`);
3191
+ }
3192
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3193
+ }
3194
+ switch (operation) {
3195
+ case "click":
3196
+ if (!options.css) {
3197
+ // will click in the center of the cell
3198
+ let xOffset = 0;
3199
+ let yOffset = 0;
3200
+ if (options.xOffset) {
3201
+ xOffset = options.xOffset;
3202
+ }
3203
+ if (options.yOffset) {
3204
+ yOffset = options.yOffset;
3205
+ }
3206
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3207
+ }
3208
+ else {
3209
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3210
+ if (results.length === 0) {
3211
+ throw new Error(`Element not found in cell area`);
3212
+ }
3213
+ state.element = results[0];
3214
+ await performAction("click", state.element, options, this, state, _params);
3215
+ }
3216
+ break;
3217
+ case "hover+click":
3218
+ if (!options.css) {
3219
+ throw new Error("css is not defined");
3220
+ }
3221
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3222
+ if (results.length === 0) {
3223
+ throw new Error(`Element not found in cell area`);
3224
+ }
3225
+ state.element = results[0];
3226
+ await performAction("hover+click", state.element, options, this, state, _params);
3227
+ break;
3228
+ default:
3229
+ throw new Error("operation is not supported");
3230
+ }
3231
+ }
3232
+ catch (e) {
3233
+ await _commandError(state, e, this);
3234
+ }
3235
+ finally {
3236
+ await _commandFinally(state, this);
2730
3237
  }
2731
3238
  }
2732
3239
  saveTestDataAsGlobal(options, world) {
2733
- const dataFile = this._getDataFile(world);
3240
+ const dataFile = _getDataFile(world, this.context, this);
2734
3241
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2735
3242
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2736
3243
  }
@@ -2760,6 +3267,7 @@ class StableBrowser {
2760
3267
  _reportToWorld(world, {
2761
3268
  type: Types.SET_VIEWPORT,
2762
3269
  text: "set viewport size to " + width + "x" + hight,
3270
+ _text: "Set the viewport size to " + width + "x" + hight,
2763
3271
  screenshotId,
2764
3272
  result: error
2765
3273
  ? {
@@ -2847,6 +3355,9 @@ class StableBrowser {
2847
3355
  else {
2848
3356
  this.stepName = "step " + this.stepIndex;
2849
3357
  }
3358
+ if (this.context) {
3359
+ this.context.examplesRow = extractStepExampleParameters(step);
3360
+ }
2850
3361
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2851
3362
  if (this.context.browserObject.context) {
2852
3363
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2859,6 +3370,41 @@ class StableBrowser {
2859
3370
  this.saveTestDataAsGlobal({}, world);
2860
3371
  }
2861
3372
  }
3373
+ if (this.initSnapshotTaken === false) {
3374
+ this.initSnapshotTaken = true;
3375
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3376
+ const snapshot = await this.getAriaSnapshot();
3377
+ if (snapshot) {
3378
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3379
+ }
3380
+ }
3381
+ }
3382
+ }
3383
+ async getAriaSnapshot() {
3384
+ try {
3385
+ // find the page url
3386
+ const url = await this.page.url();
3387
+ // extract the path from the url
3388
+ const path = new URL(url).pathname;
3389
+ // get the page title
3390
+ const title = await this.page.title();
3391
+ // go over other frams
3392
+ const frames = this.page.frames();
3393
+ const snapshots = [];
3394
+ const content = [`- path: ${path}`, `- title: ${title}`];
3395
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3396
+ for (let i = 0; i < frames.length; i++) {
3397
+ content.push(`- frame: ${i}`);
3398
+ const frame = frames[i];
3399
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3400
+ content.push(snapshot);
3401
+ }
3402
+ return content.join("\n");
3403
+ }
3404
+ catch (e) {
3405
+ console.error(e);
3406
+ }
3407
+ return null;
2862
3408
  }
2863
3409
  async afterStep(world, step) {
2864
3410
  this.stepName = null;
@@ -2867,6 +3413,23 @@ class StableBrowser {
2867
3413
  await this.context.browserObject.context.tracing.stopChunk({
2868
3414
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
2869
3415
  });
3416
+ if (world && world.attach) {
3417
+ await world.attach(JSON.stringify({
3418
+ type: "trace",
3419
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3420
+ }), "application/json+trace");
3421
+ }
3422
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3423
+ }
3424
+ }
3425
+ if (this.context) {
3426
+ this.context.examplesRow = null;
3427
+ }
3428
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3429
+ const snapshot = await this.getAriaSnapshot();
3430
+ if (snapshot) {
3431
+ const obj = {};
3432
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
2870
3433
  }
2871
3434
  }
2872
3435
  }