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