automation_model 1.0.592-dev → 1.0.592-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, 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,9 +506,11 @@ class StableBrowser {
462
506
  info.printMessages = {};
463
507
  }
464
508
  if (info.locatorLog && !visible) {
509
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
465
510
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
466
511
  }
467
512
  if (info.locatorLog && !enabled) {
513
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
468
514
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
469
515
  }
470
516
  if (!info.printMessages[j.toString()]) {
@@ -544,7 +590,28 @@ class StableBrowser {
544
590
  }
545
591
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
546
592
  if (!element.rerun) {
547
- 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);
548
615
  }
549
616
  }
550
617
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -617,7 +684,7 @@ class StableBrowser {
617
684
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
618
685
  if (Date.now() - startTime > timeout) {
619
686
  info.failCause.iframeNotFound = true;
620
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
687
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
621
688
  throw new Error("unable to locate iframe " + selectors.iframe_src);
622
689
  }
623
690
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -691,18 +758,13 @@ class StableBrowser {
691
758
  }
692
759
  // info.log += "scanning locators in priority 1" + "\n";
693
760
  let onlyPriority3 = selectorsLocators[0].priority === 3;
694
- 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);
695
762
  if (result.foundElements.length === 0) {
696
763
  // info.log += "scanning locators in priority 2" + "\n";
697
- 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);
698
765
  }
699
- if (result.foundElements.length === 0 && onlyPriority3) {
700
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
701
- }
702
- else {
703
- if (result.foundElements.length === 0 && !highPriorityOnly) {
704
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
705
- }
766
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
767
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
706
768
  }
707
769
  let foundElements = result.foundElements;
708
770
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -758,6 +820,11 @@ class StableBrowser {
758
820
  visibleOnly = false;
759
821
  }
760
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
+ }
761
828
  }
762
829
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
763
830
  // if (info.locatorLog) {
@@ -768,10 +835,12 @@ class StableBrowser {
768
835
  // }
769
836
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
770
837
  info.failCause.locatorNotFound = true;
771
- info.failCause.lastError = "failed to locate unique element";
838
+ if (!info?.failCause?.lastError) {
839
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
840
+ }
772
841
  throw new Error("failed to locate first element no elements found, " + info.log);
773
842
  }
774
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false) {
843
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
775
844
  let foundElements = [];
776
845
  const result = {
777
846
  foundElements: foundElements,
@@ -779,7 +848,7 @@ class StableBrowser {
779
848
  for (let i = 0; i < locatorsGroup.length; i++) {
780
849
  let foundLocators = [];
781
850
  try {
782
- 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);
783
852
  }
784
853
  catch (e) {
785
854
  // this call can fail it the browser is navigating
@@ -787,7 +856,7 @@ class StableBrowser {
787
856
  // this.logger.debug(e);
788
857
  foundLocators = [];
789
858
  try {
790
- 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);
791
860
  }
792
861
  catch (e) {
793
862
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -802,9 +871,40 @@ class StableBrowser {
802
871
  result.locatorIndex = i;
803
872
  }
804
873
  if (foundLocators.length > 1) {
805
- info.failCause.foundMultiple = true;
806
- if (info.locatorLog) {
807
- 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
+ }
808
908
  }
809
909
  }
810
910
  }
@@ -852,7 +952,7 @@ class StableBrowser {
852
952
  await _commandError(state, "timeout looking for " + elementDescription, this);
853
953
  }
854
954
  finally {
855
- _commandFinally(state, this);
955
+ await _commandFinally(state, this);
856
956
  }
857
957
  }
858
958
  }
@@ -901,7 +1001,7 @@ class StableBrowser {
901
1001
  await _commandError(state, "timeout looking for " + elementDescription, this);
902
1002
  }
903
1003
  finally {
904
- _commandFinally(state, this);
1004
+ await _commandFinally(state, this);
905
1005
  }
906
1006
  }
907
1007
  }
@@ -915,25 +1015,14 @@ class StableBrowser {
915
1015
  options,
916
1016
  world,
917
1017
  text: "Click element",
1018
+ _text: "Click on " + selectors.element_name,
918
1019
  type: Types.CLICK,
919
1020
  operation: "click",
920
1021
  log: "***** click on " + selectors.element_name + " *****\n",
921
1022
  };
922
1023
  try {
923
1024
  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
- }
1025
+ await performAction("click", state.element, options, this, state, _params);
937
1026
  await this.waitForPageLoad();
938
1027
  return state.info;
939
1028
  }
@@ -941,8 +1030,40 @@ class StableBrowser {
941
1030
  await _commandError(state, e, this);
942
1031
  }
943
1032
  finally {
944
- _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);
945
1065
  }
1066
+ return found;
946
1067
  }
947
1068
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
948
1069
  const state = {
@@ -952,6 +1073,7 @@ class StableBrowser {
952
1073
  world,
953
1074
  type: checked ? Types.CHECK : Types.UNCHECK,
954
1075
  text: checked ? `Check element` : `Uncheck element`,
1076
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
955
1077
  operation: "setCheck",
956
1078
  log: "***** check " + selectors.element_name + " *****\n",
957
1079
  };
@@ -961,9 +1083,15 @@ class StableBrowser {
961
1083
  // let element = await this._locate(selectors, info, _params);
962
1084
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
963
1085
  try {
964
- // 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);
965
1089
  await state.element.setChecked(checked);
966
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);
967
1095
  }
968
1096
  catch (e) {
969
1097
  if (e.message && e.message.includes("did not change its state")) {
@@ -984,7 +1112,7 @@ class StableBrowser {
984
1112
  await _commandError(state, e, this);
985
1113
  }
986
1114
  finally {
987
- _commandFinally(state, this);
1115
+ await _commandFinally(state, this);
988
1116
  }
989
1117
  }
990
1118
  async hover(selectors, _params, options = {}, world = null) {
@@ -995,22 +1123,13 @@ class StableBrowser {
995
1123
  world,
996
1124
  type: Types.HOVER,
997
1125
  text: `Hover element`,
1126
+ _text: `Hover on ${selectors.element_name}`,
998
1127
  operation: "hover",
999
1128
  log: "***** hover " + selectors.element_name + " *****\n",
1000
1129
  };
1001
1130
  try {
1002
1131
  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
- }
1132
+ await performAction("hover", state.element, options, this, state, _params);
1014
1133
  await _screenshot(state, this);
1015
1134
  await this.waitForPageLoad();
1016
1135
  return state.info;
@@ -1019,7 +1138,7 @@ class StableBrowser {
1019
1138
  await _commandError(state, e, this);
1020
1139
  }
1021
1140
  finally {
1022
- _commandFinally(state, this);
1141
+ await _commandFinally(state, this);
1023
1142
  }
1024
1143
  }
1025
1144
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1034,6 +1153,7 @@ class StableBrowser {
1034
1153
  value: values.toString(),
1035
1154
  type: Types.SELECT,
1036
1155
  text: `Select option: ${values}`,
1156
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1037
1157
  operation: "selectOption",
1038
1158
  log: "***** select option " + selectors.element_name + " *****\n",
1039
1159
  };
@@ -1054,7 +1174,7 @@ class StableBrowser {
1054
1174
  await _commandError(state, e, this);
1055
1175
  }
1056
1176
  finally {
1057
- _commandFinally(state, this);
1177
+ await _commandFinally(state, this);
1058
1178
  }
1059
1179
  }
1060
1180
  async type(_value, _params = null, options = {}, world = null) {
@@ -1068,6 +1188,7 @@ class StableBrowser {
1068
1188
  highlight: false,
1069
1189
  type: Types.TYPE_PRESS,
1070
1190
  text: `Type value: ${_value}`,
1191
+ _text: `Type value: ${_value}`,
1071
1192
  operation: "type",
1072
1193
  log: "",
1073
1194
  };
@@ -1099,7 +1220,7 @@ class StableBrowser {
1099
1220
  await _commandError(state, e, this);
1100
1221
  }
1101
1222
  finally {
1102
- _commandFinally(state, this);
1223
+ await _commandFinally(state, this);
1103
1224
  }
1104
1225
  }
1105
1226
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1135,7 +1256,7 @@ class StableBrowser {
1135
1256
  await _commandError(state, e, this);
1136
1257
  }
1137
1258
  finally {
1138
- _commandFinally(state, this);
1259
+ await _commandFinally(state, this);
1139
1260
  }
1140
1261
  }
1141
1262
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1147,6 +1268,7 @@ class StableBrowser {
1147
1268
  world,
1148
1269
  type: Types.SET_DATE_TIME,
1149
1270
  text: `Set date time value: ${value}`,
1271
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1150
1272
  operation: "setDateTime",
1151
1273
  log: "***** set date time value " + selectors.element_name + " *****\n",
1152
1274
  throwError: false,
@@ -1154,7 +1276,7 @@ class StableBrowser {
1154
1276
  try {
1155
1277
  await _preCommand(state, this);
1156
1278
  try {
1157
- await state.element.click();
1279
+ await performAction("click", state.element, options, this, state, _params);
1158
1280
  await new Promise((resolve) => setTimeout(resolve, 500));
1159
1281
  if (format) {
1160
1282
  state.value = dayjs(state.value).format(format);
@@ -1203,7 +1325,7 @@ class StableBrowser {
1203
1325
  await _commandError(state, e, this);
1204
1326
  }
1205
1327
  finally {
1206
- _commandFinally(state, this);
1328
+ await _commandFinally(state, this);
1207
1329
  }
1208
1330
  }
1209
1331
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1218,9 +1340,13 @@ class StableBrowser {
1218
1340
  world,
1219
1341
  type: Types.FILL,
1220
1342
  text: `Click type input with value: ${_value}`,
1343
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1221
1344
  operation: "clickType",
1222
1345
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1223
1346
  };
1347
+ if (!options) {
1348
+ options = {};
1349
+ }
1224
1350
  if (newValue !== _value) {
1225
1351
  //this.logger.info(_value + "=" + newValue);
1226
1352
  _value = newValue;
@@ -1228,7 +1354,7 @@ class StableBrowser {
1228
1354
  try {
1229
1355
  await _preCommand(state, this);
1230
1356
  state.info.value = _value;
1231
- if (options === null || options === undefined || !options.press) {
1357
+ if (!options.press) {
1232
1358
  try {
1233
1359
  let currentValue = await state.element.inputValue();
1234
1360
  if (currentValue) {
@@ -1239,13 +1365,9 @@ class StableBrowser {
1239
1365
  this.logger.info("unable to clear input value");
1240
1366
  }
1241
1367
  }
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
- }
1368
+ if (options.press) {
1369
+ options.timeout = 5000;
1370
+ await performAction("click", state.element, options, this, state, _params);
1249
1371
  }
1250
1372
  else {
1251
1373
  try {
@@ -1303,7 +1425,7 @@ class StableBrowser {
1303
1425
  await _commandError(state, e, this);
1304
1426
  }
1305
1427
  finally {
1306
- _commandFinally(state, this);
1428
+ await _commandFinally(state, this);
1307
1429
  }
1308
1430
  }
1309
1431
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1333,13 +1455,14 @@ class StableBrowser {
1333
1455
  await _commandError(state, e, this);
1334
1456
  }
1335
1457
  finally {
1336
- _commandFinally(state, this);
1458
+ await _commandFinally(state, this);
1337
1459
  }
1338
1460
  }
1339
1461
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1340
1462
  return await this._getText(selectors, 0, _params, options, info, world);
1341
1463
  }
1342
1464
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1465
+ const timeout = this._getFindElementTimeout(options);
1343
1466
  _validateSelectors(selectors);
1344
1467
  let screenshotId = null;
1345
1468
  let screenshotPath = null;
@@ -1349,7 +1472,7 @@ class StableBrowser {
1349
1472
  }
1350
1473
  info.operation = "getText";
1351
1474
  info.selectors = selectors;
1352
- let element = await this._locate(selectors, info, _params);
1475
+ let element = await this._locate(selectors, info, _params, timeout);
1353
1476
  if (climb > 0) {
1354
1477
  const climbArray = [];
1355
1478
  for (let i = 0; i < climb; i++) {
@@ -1368,6 +1491,18 @@ class StableBrowser {
1368
1491
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1369
1492
  try {
1370
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
+ // }
1371
1506
  const elementText = await element.innerText();
1372
1507
  return {
1373
1508
  text: elementText,
@@ -1379,7 +1514,7 @@ class StableBrowser {
1379
1514
  }
1380
1515
  catch (e) {
1381
1516
  //await this.closeUnexpectedPopups();
1382
- this.logger.info("no innerText will use textContent");
1517
+ this.logger.info("no innerText, will use textContent");
1383
1518
  const elementText = await element.textContent();
1384
1519
  return { text: elementText, screenshotId, screenshotPath, value: value };
1385
1520
  }
@@ -1404,6 +1539,7 @@ class StableBrowser {
1404
1539
  highlight: false,
1405
1540
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1406
1541
  text: `Verify element contains pattern: ${pattern}`,
1542
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1407
1543
  operation: "containsPattern",
1408
1544
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1409
1545
  };
@@ -1435,10 +1571,12 @@ class StableBrowser {
1435
1571
  await _commandError(state, e, this);
1436
1572
  }
1437
1573
  finally {
1438
- _commandFinally(state, this);
1574
+ await _commandFinally(state, this);
1439
1575
  }
1440
1576
  }
1441
1577
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1578
+ const timeout = this._getFindElementTimeout(options);
1579
+ const startTime = Date.now();
1442
1580
  const state = {
1443
1581
  selectors,
1444
1582
  _params,
@@ -1465,62 +1603,54 @@ class StableBrowser {
1465
1603
  }
1466
1604
  let foundObj = null;
1467
1605
  try {
1468
- await _preCommand(state, this);
1469
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1470
- if (foundObj && foundObj.element) {
1471
- await this.scrollIfNeeded(foundObj.element, state.info);
1472
- }
1473
- await _screenshot(state, this);
1474
- const dateAlternatives = findDateAlternatives(text);
1475
- const numberAlternatives = findNumberAlternatives(text);
1476
- if (dateAlternatives.date) {
1477
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1478
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1479
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1480
- 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);
1481
1612
  }
1482
- }
1483
- throw new Error("element doesn't contain text " + text);
1484
- }
1485
- else if (numberAlternatives.number) {
1486
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1487
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1488
- 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)) {
1489
1633
  return state.info;
1490
1634
  }
1491
1635
  }
1492
- throw new Error("element doesn't contain text " + text);
1493
- }
1494
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1495
- state.info.foundText = foundObj?.text;
1496
- state.info.value = foundObj?.value;
1497
- 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
1498
1641
  }
1499
- 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);
1500
1645
  }
1501
1646
  catch (e) {
1502
1647
  await _commandError(state, e, this);
1648
+ throw e;
1503
1649
  }
1504
1650
  finally {
1505
- _commandFinally(state, this);
1651
+ await _commandFinally(state, this);
1506
1652
  }
1507
1653
  }
1508
- _getDataFile(world = null) {
1509
- let dataFile = null;
1510
- if (world && world.reportFolder) {
1511
- dataFile = path.join(world.reportFolder, "data.json");
1512
- }
1513
- else if (this.reportFolder) {
1514
- dataFile = path.join(this.reportFolder, "data.json");
1515
- }
1516
- else if (this.context && this.context.reportFolder) {
1517
- dataFile = path.join(this.context.reportFolder, "data.json");
1518
- }
1519
- else {
1520
- dataFile = "data.json";
1521
- }
1522
- return dataFile;
1523
- }
1524
1654
  async waitForUserInput(message, world = null) {
1525
1655
  if (!message) {
1526
1656
  message = "# Wait for user input. Press any key to continue";
@@ -1549,13 +1679,22 @@ class StableBrowser {
1549
1679
  return;
1550
1680
  }
1551
1681
  // if data file exists, load it
1552
- const dataFile = this._getDataFile(world);
1682
+ const dataFile = _getDataFile(world, this.context, this);
1553
1683
  let data = this.getTestData(world);
1554
1684
  // merge the testData with the existing data
1555
1685
  Object.assign(data, testData);
1556
1686
  // save the data to the file
1557
1687
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1558
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
+ }
1559
1698
  _getDataFilePath(fileName) {
1560
1699
  let dataFile = path.join(this.project_path, "data", fileName);
1561
1700
  if (fs.existsSync(dataFile)) {
@@ -1652,7 +1791,7 @@ class StableBrowser {
1652
1791
  }
1653
1792
  }
1654
1793
  getTestData(world = null) {
1655
- const dataFile = this._getDataFile(world);
1794
+ const dataFile = _getDataFile(world, this.context, this);
1656
1795
  let data = {};
1657
1796
  if (fs.existsSync(dataFile)) {
1658
1797
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1739,6 +1878,15 @@ class StableBrowser {
1739
1878
  document.documentElement.clientWidth,
1740
1879
  ])));
1741
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
+ // }
1742
1890
  if (this.context.browserName === "chromium") {
1743
1891
  const client = await playContext.newCDPSession(this.page);
1744
1892
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1760,6 +1908,10 @@ class StableBrowser {
1760
1908
  else {
1761
1909
  screenshotBuffer = await this.page.screenshot();
1762
1910
  }
1911
+ // if (focusedElement) {
1912
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1913
+ // await this._unhighlightElements(focusedElement);
1914
+ // }
1763
1915
  let image = await Jimp.read(screenshotBuffer);
1764
1916
  // Get the image dimensions
1765
1917
  const { width, height } = image.bitmap;
@@ -1772,6 +1924,7 @@ class StableBrowser {
1772
1924
  else {
1773
1925
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1774
1926
  }
1927
+ return screenshotBuffer;
1775
1928
  }
1776
1929
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1777
1930
  const state = {
@@ -1794,7 +1947,7 @@ class StableBrowser {
1794
1947
  await _commandError(state, e, this);
1795
1948
  }
1796
1949
  finally {
1797
- _commandFinally(state, this);
1950
+ await _commandFinally(state, this);
1798
1951
  }
1799
1952
  }
1800
1953
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1807,8 +1960,10 @@ class StableBrowser {
1807
1960
  world,
1808
1961
  type: Types.EXTRACT,
1809
1962
  text: `Extract attribute from element`,
1963
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1810
1964
  operation: "extractAttribute",
1811
1965
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1966
+ allowDisabled: true,
1812
1967
  };
1813
1968
  await new Promise((resolve) => setTimeout(resolve, 2000));
1814
1969
  try {
@@ -1823,6 +1978,9 @@ class StableBrowser {
1823
1978
  case "value":
1824
1979
  state.value = await state.element.inputValue();
1825
1980
  break;
1981
+ case "text":
1982
+ state.value = await state.element.textContent();
1983
+ break;
1826
1984
  default:
1827
1985
  state.value = await state.element.getAttribute(attribute);
1828
1986
  break;
@@ -1830,13 +1988,14 @@ class StableBrowser {
1830
1988
  state.info.value = state.value;
1831
1989
  this.setTestData({ [variable]: state.value }, world);
1832
1990
  this.logger.info("set test data: " + variable + "=" + state.value);
1991
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1833
1992
  return state.info;
1834
1993
  }
1835
1994
  catch (e) {
1836
1995
  await _commandError(state, e, this);
1837
1996
  }
1838
1997
  finally {
1839
- _commandFinally(state, this);
1998
+ await _commandFinally(state, this);
1840
1999
  }
1841
2000
  }
1842
2001
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1851,18 +2010,25 @@ class StableBrowser {
1851
2010
  highlight: true,
1852
2011
  screenshot: true,
1853
2012
  text: `Verify element attribute`,
2013
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1854
2014
  operation: "verifyAttribute",
1855
2015
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1856
2016
  allowDisabled: true,
1857
2017
  };
1858
2018
  await new Promise((resolve) => setTimeout(resolve, 2000));
1859
2019
  let val;
2020
+ let expectedValue;
1860
2021
  try {
1861
2022
  await _preCommand(state, this);
2023
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2024
+ state.info.expectedValue = expectedValue;
1862
2025
  switch (attribute) {
1863
2026
  case "innerText":
1864
2027
  val = String(await state.element.innerText());
1865
2028
  break;
2029
+ case "text":
2030
+ val = String(await state.element.textContent());
2031
+ break;
1866
2032
  case "value":
1867
2033
  val = String(await state.element.inputValue());
1868
2034
  break;
@@ -1880,26 +2046,29 @@ class StableBrowser {
1880
2046
  val = String(await state.element.getAttribute(attribute));
1881
2047
  break;
1882
2048
  }
2049
+ state.info.value = val;
1883
2050
  let regex;
1884
- if (value.startsWith("/") && value.endsWith("/")) {
1885
- const patternBody = value.slice(1, -1);
2051
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2052
+ const patternBody = expectedValue.slice(1, -1);
1886
2053
  regex = new RegExp(patternBody, "g");
1887
2054
  }
1888
2055
  else {
1889
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2056
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1890
2057
  regex = new RegExp(escapedPattern, "g");
1891
2058
  }
1892
2059
  if (!val.match(regex)) {
1893
- 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);
1894
2064
  }
1895
- state.info.expectedValue = val;
1896
2065
  return state.info;
1897
2066
  }
1898
2067
  catch (e) {
1899
2068
  await _commandError(state, e, this);
1900
2069
  }
1901
2070
  finally {
1902
- _commandFinally(state, this);
2071
+ await _commandFinally(state, this);
1903
2072
  }
1904
2073
  }
1905
2074
  async extractEmailData(emailAddress, options, world) {
@@ -1991,27 +2160,32 @@ class StableBrowser {
1991
2160
  async _highlightElements(scope, css) {
1992
2161
  try {
1993
2162
  if (!scope) {
2163
+ // console.log(`Scope is not defined`);
1994
2164
  return;
1995
2165
  }
1996
2166
  if (!css) {
1997
2167
  scope
1998
2168
  .evaluate((node) => {
1999
2169
  if (node && node.style) {
2000
- let originalBorder = node.style.outline;
2170
+ let originalOutline = node.style.outline;
2171
+ // console.log(`Original outline was: ${originalOutline}`);
2172
+ // node.__previousOutline = originalOutline;
2001
2173
  node.style.outline = "2px solid red";
2174
+ // console.log(`New outline is: ${node.style.outline}`);
2002
2175
  if (window) {
2003
2176
  window.addEventListener("beforeunload", function (e) {
2004
- node.style.outline = originalBorder;
2177
+ node.style.outline = originalOutline;
2005
2178
  });
2006
2179
  }
2007
2180
  setTimeout(function () {
2008
- node.style.outline = originalBorder;
2181
+ node.style.outline = originalOutline;
2009
2182
  }, 2000);
2010
2183
  }
2011
2184
  })
2012
2185
  .then(() => { })
2013
2186
  .catch((e) => {
2014
2187
  // ignore
2188
+ // console.error(`Could not highlight node : ${e}`);
2015
2189
  });
2016
2190
  }
2017
2191
  else {
@@ -2027,17 +2201,18 @@ class StableBrowser {
2027
2201
  if (!element.style) {
2028
2202
  return;
2029
2203
  }
2030
- var originalBorder = element.style.outline;
2204
+ let originalOutline = element.style.outline;
2205
+ element.__previousOutline = originalOutline;
2031
2206
  // Set the new border to be red and 2px solid
2032
2207
  element.style.outline = "2px solid red";
2033
2208
  if (window) {
2034
2209
  window.addEventListener("beforeunload", function (e) {
2035
- element.style.outline = originalBorder;
2210
+ element.style.outline = originalOutline;
2036
2211
  });
2037
2212
  }
2038
2213
  // Set a timeout to revert to the original border after 2 seconds
2039
2214
  setTimeout(function () {
2040
- element.style.outline = originalBorder;
2215
+ element.style.outline = originalOutline;
2041
2216
  }, 2000);
2042
2217
  }
2043
2218
  return;
@@ -2045,6 +2220,7 @@ class StableBrowser {
2045
2220
  .then(() => { })
2046
2221
  .catch((e) => {
2047
2222
  // ignore
2223
+ // console.error(`Could not highlight css: ${e}`);
2048
2224
  });
2049
2225
  }
2050
2226
  }
@@ -2052,6 +2228,54 @@ class StableBrowser {
2052
2228
  console.debug(error);
2053
2229
  }
2054
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
+ // }
2055
2279
  async verifyPagePath(pathPart, options = {}, world = null) {
2056
2280
  const startTime = Date.now();
2057
2281
  let error = null;
@@ -2096,6 +2320,7 @@ class StableBrowser {
2096
2320
  _reportToWorld(world, {
2097
2321
  type: Types.VERIFY_PAGE_PATH,
2098
2322
  text: "Verify page path",
2323
+ _text: "Verify the page path contains " + pathPart,
2099
2324
  screenshotId,
2100
2325
  result: error
2101
2326
  ? {
@@ -2113,27 +2338,89 @@ class StableBrowser {
2113
2338
  });
2114
2339
  }
2115
2340
  }
2116
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
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,
2386
+ screenshotId,
2387
+ result: error
2388
+ ? {
2389
+ status: "FAILED",
2390
+ startTime,
2391
+ endTime,
2392
+ message: error?.message,
2393
+ }
2394
+ : {
2395
+ status: "PASSED",
2396
+ startTime,
2397
+ endTime,
2398
+ },
2399
+ info: info,
2400
+ });
2401
+ }
2402
+ }
2403
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2117
2404
  const frames = this.page.frames();
2118
2405
  let results = [];
2119
- let ignoreCase = false;
2406
+ // let ignoreCase = false;
2120
2407
  for (let i = 0; i < frames.length; i++) {
2121
2408
  if (dateAlternatives.date) {
2122
2409
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2123
- 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, {});
2124
2411
  result.frame = frames[i];
2125
2412
  results.push(result);
2126
2413
  }
2127
2414
  }
2128
2415
  else if (numberAlternatives.number) {
2129
2416
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2130
- 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, {});
2131
2418
  result.frame = frames[i];
2132
2419
  results.push(result);
2133
2420
  }
2134
2421
  }
2135
2422
  else {
2136
- 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, {});
2137
2424
  result.frame = frames[i];
2138
2425
  results.push(result);
2139
2426
  }
@@ -2152,11 +2439,15 @@ class StableBrowser {
2152
2439
  scroll: false,
2153
2440
  highlight: false,
2154
2441
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2155
- text: `Verify text exists in page`,
2442
+ text: `Verify the text '${text}' exists in page`,
2443
+ _text: `Verify the text '${text}' exists in page`,
2156
2444
  operation: "verifyTextExistInPage",
2157
2445
  log: "***** verify text " + text + " exists in page *****\n",
2158
2446
  };
2159
- const timeout = this._getLoadTimeout(options);
2447
+ if (testForRegex(text)) {
2448
+ text = text.replace(/\\"/g, '"');
2449
+ }
2450
+ const timeout = this._getFindElementTimeout(options);
2160
2451
  await new Promise((resolve) => setTimeout(resolve, 2000));
2161
2452
  const newValue = await this._replaceWithLocalData(text, world);
2162
2453
  if (newValue !== text) {
@@ -2169,7 +2460,15 @@ class StableBrowser {
2169
2460
  await _preCommand(state, this);
2170
2461
  state.info.text = text;
2171
2462
  while (true) {
2172
- 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
+ }
2173
2472
  if (resultWithElementsFound.length === 0) {
2174
2473
  if (Date.now() - state.startTime > timeout) {
2175
2474
  throw new Error(`Text ${text} not found in page`);
@@ -2177,18 +2476,40 @@ class StableBrowser {
2177
2476
  await new Promise((resolve) => setTimeout(resolve, 1000));
2178
2477
  continue;
2179
2478
  }
2180
- if (resultWithElementsFound[0].randomToken) {
2181
- const frame = resultWithElementsFound[0].frame;
2182
- const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2183
- await this._highlightElements(frame, dataAttribute);
2184
- const element = await frame.locator(dataAttribute).first();
2185
- if (element) {
2186
- await this.scrollIfNeeded(element, state.info);
2187
- 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
+ }
2188
2506
  }
2507
+ await _screenshot(state, this);
2508
+ return state.info;
2509
+ }
2510
+ catch (error) {
2511
+ console.error(error);
2189
2512
  }
2190
- await _screenshot(state, this);
2191
- return state.info;
2192
2513
  }
2193
2514
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2194
2515
  }
@@ -2196,7 +2517,7 @@ class StableBrowser {
2196
2517
  await _commandError(state, e, this);
2197
2518
  }
2198
2519
  finally {
2199
- _commandFinally(state, this);
2520
+ await _commandFinally(state, this);
2200
2521
  }
2201
2522
  }
2202
2523
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2209,11 +2530,15 @@ class StableBrowser {
2209
2530
  scroll: false,
2210
2531
  highlight: false,
2211
2532
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2212
- 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`,
2213
2535
  operation: "verifyTextNotExistInPage",
2214
2536
  log: "***** verify text " + text + " does not exist in page *****\n",
2215
2537
  };
2216
- const timeout = this._getLoadTimeout(options);
2538
+ if (testForRegex(text)) {
2539
+ text = text.replace(/\\"/g, '"');
2540
+ }
2541
+ const timeout = this._getFindElementTimeout(options);
2217
2542
  await new Promise((resolve) => setTimeout(resolve, 2000));
2218
2543
  const newValue = await this._replaceWithLocalData(text, world);
2219
2544
  if (newValue !== text) {
@@ -2225,8 +2550,16 @@ class StableBrowser {
2225
2550
  try {
2226
2551
  await _preCommand(state, this);
2227
2552
  state.info.text = text;
2553
+ let resultWithElementsFound = {
2554
+ length: null, // initial cannot be 0
2555
+ };
2228
2556
  while (true) {
2229
- 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
+ }
2230
2563
  if (resultWithElementsFound.length === 0) {
2231
2564
  await _screenshot(state, this);
2232
2565
  return state.info;
@@ -2241,7 +2574,7 @@ class StableBrowser {
2241
2574
  await _commandError(state, e, this);
2242
2575
  }
2243
2576
  finally {
2244
- _commandFinally(state, this);
2577
+ await _commandFinally(state, this);
2245
2578
  }
2246
2579
  }
2247
2580
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2256,10 +2589,11 @@ class StableBrowser {
2256
2589
  highlight: false,
2257
2590
  type: Types.VERIFY_TEXT_WITH_RELATION,
2258
2591
  text: `Verify text with relation to another text`,
2592
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2259
2593
  operation: "verify_text_with_relation",
2260
2594
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2261
2595
  };
2262
- const timeout = this._getLoadTimeout(options);
2596
+ const timeout = this._getFindElementTimeout(options);
2263
2597
  await new Promise((resolve) => setTimeout(resolve, 2000));
2264
2598
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2265
2599
  if (newValue !== textAnchor) {
@@ -2277,8 +2611,16 @@ class StableBrowser {
2277
2611
  try {
2278
2612
  await _preCommand(state, this);
2279
2613
  state.info.text = textToVerify;
2614
+ let resultWithElementsFound = {
2615
+ length: 0,
2616
+ };
2280
2617
  while (true) {
2281
- 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
+ }
2282
2624
  if (resultWithElementsFound.length === 0) {
2283
2625
  if (Date.now() - state.startTime > timeout) {
2284
2626
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2286,37 +2628,56 @@ class StableBrowser {
2286
2628
  await new Promise((resolve) => setTimeout(resolve, 1000));
2287
2629
  continue;
2288
2630
  }
2289
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2290
- foundAncore = true;
2291
- const result = resultWithElementsFound[i];
2292
- const token = result.randomToken;
2293
- const frame = result.frame;
2294
- let css = `[data-blinq-id-${token}]`;
2295
- const climbArray1 = [];
2296
- for (let i = 0; i < climb; i++) {
2297
- climbArray1.push("..");
2298
- }
2299
- let climbXpath = "xpath=" + climbArray1.join("/");
2300
- css = css + " >> " + climbXpath;
2301
- const count = await frame.locator(css).count();
2302
- for (let j = 0; j < count; j++) {
2303
- const continer = await frame.locator(css).nth(j);
2304
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2305
- if (result.elementCount > 0) {
2306
- const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2307
- //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2308
- await this._highlightElements(frame, dataAttribute);
2309
- //await this._highlightElements(frame, cssAnchor);
2310
- const element = await frame.locator(dataAttribute).first();
2311
- if (element) {
2312
- await this.scrollIfNeeded(element, state.info);
2313
- 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;
2314
2674
  }
2315
- await _screenshot(state, this);
2316
- return state.info;
2317
2675
  }
2318
2676
  }
2319
2677
  }
2678
+ catch (error) {
2679
+ console.error(error);
2680
+ }
2320
2681
  }
2321
2682
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2322
2683
  }
@@ -2324,8 +2685,32 @@ class StableBrowser {
2324
2685
  await _commandError(state, e, this);
2325
2686
  }
2326
2687
  finally {
2327
- _commandFinally(state, this);
2688
+ await _commandFinally(state, this);
2689
+ }
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
+ }
2328
2710
  }
2711
+ // state.info.results = results;
2712
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2713
+ return resultWithElementsFound;
2329
2714
  }
2330
2715
  async visualVerification(text, options = {}, world = null) {
2331
2716
  const startTime = Date.now();
@@ -2345,10 +2730,13 @@ class StableBrowser {
2345
2730
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2346
2731
  info.screenshotPath = screenshotPath;
2347
2732
  const screenshot = await this.takeScreenshot();
2348
- const request = {
2349
- method: "POST",
2733
+ let request = {
2734
+ method: "post",
2735
+ maxBodyLength: Infinity,
2350
2736
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2351
2737
  headers: {
2738
+ "x-bvt-project-id": path.basename(this.project_path),
2739
+ "x-source": "aaa",
2352
2740
  "Content-Type": "application/json",
2353
2741
  Authorization: `Bearer ${process.env.TOKEN}`,
2354
2742
  },
@@ -2357,7 +2745,7 @@ class StableBrowser {
2357
2745
  screenshot: screenshot,
2358
2746
  }),
2359
2747
  };
2360
- let result = await this.context.api.request(request);
2748
+ const result = await axios.request(request);
2361
2749
  if (result.data.status !== true) {
2362
2750
  throw new Error("Visual validation failed");
2363
2751
  }
@@ -2385,6 +2773,7 @@ class StableBrowser {
2385
2773
  _reportToWorld(world, {
2386
2774
  type: Types.VERIFY_VISUAL,
2387
2775
  text: "Visual verification",
2776
+ _text: "Visual verification of " + text,
2388
2777
  screenshotId,
2389
2778
  result: error
2390
2779
  ? {
@@ -2651,6 +3040,32 @@ class StableBrowser {
2651
3040
  }
2652
3041
  return timeout;
2653
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
+ }
2654
3069
  async waitForPageLoad(options = {}, world = null) {
2655
3070
  let timeout = this._getLoadTimeout(options);
2656
3071
  const promiseArray = [];
@@ -2718,6 +3133,7 @@ class StableBrowser {
2718
3133
  highlight: false,
2719
3134
  type: Types.CLOSE_PAGE,
2720
3135
  text: `Close page`,
3136
+ _text: `Close the page`,
2721
3137
  operation: "closePage",
2722
3138
  log: "***** close page *****\n",
2723
3139
  throwError: false,
@@ -2731,11 +3147,98 @@ class StableBrowser {
2731
3147
  await _commandError(state, e, this);
2732
3148
  }
2733
3149
  finally {
2734
- _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);
2735
3238
  }
2736
3239
  }
2737
3240
  saveTestDataAsGlobal(options, world) {
2738
- const dataFile = this._getDataFile(world);
3241
+ const dataFile = _getDataFile(world, this.context, this);
2739
3242
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2740
3243
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2741
3244
  }
@@ -2765,6 +3268,7 @@ class StableBrowser {
2765
3268
  _reportToWorld(world, {
2766
3269
  type: Types.SET_VIEWPORT,
2767
3270
  text: "set viewport size to " + width + "x" + hight,
3271
+ _text: "Set the viewport size to " + width + "x" + hight,
2768
3272
  screenshotId,
2769
3273
  result: error
2770
3274
  ? {
@@ -2835,7 +3339,36 @@ class StableBrowser {
2835
3339
  console.log("#-#");
2836
3340
  }
2837
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) { }
2838
3368
  async beforeStep(world, step) {
3369
+ if (!this.beforeScenarioCalled) {
3370
+ this.beforeScenario(world, step);
3371
+ }
2839
3372
  if (this.stepIndex === undefined) {
2840
3373
  this.stepIndex = 0;
2841
3374
  }
@@ -2857,14 +3390,42 @@ class StableBrowser {
2857
3390
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
2858
3391
  }
2859
3392
  }
2860
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
2861
- this.tags = step.pickle.tags.map((tag) => tag.name);
2862
- // check if @global_test_data tag is present
2863
- if (this.tags.includes("@global_test_data")) {
2864
- 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
+ }
2865
3400
  }
2866
3401
  }
2867
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
+ }
2868
3429
  async afterStep(world, step) {
2869
3430
  this.stepName = null;
2870
3431
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -2872,6 +3433,23 @@ class StableBrowser {
2872
3433
  await this.context.browserObject.context.tracing.stopChunk({
2873
3434
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
2874
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`);
3443
+ }
3444
+ }
3445
+ if (this.context) {
3446
+ this.context.examplesRow = null;
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");
2875
3453
  }
2876
3454
  }
2877
3455
  }