automation_model 1.0.596-dev → 1.0.596-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.
@@ -7,20 +7,25 @@ import path from "path";
7
7
  import reg_parser from "regex-parser";
8
8
  import { findDateAlternatives, findNumberAlternatives } from "./analyze_helper.js";
9
9
  import { getDateTimeValue } from "./date_time.js";
10
+ import drawRectangle from "./drawRect.js";
10
11
  //import { closeUnexpectedPopups } from "./popups.js";
11
12
  import { getTableCells, getTableData } from "./table_analyze.js";
12
- 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";
13
14
  import csv from "csv-parser";
14
15
  import { Readable } from "node:stream";
15
16
  import readline from "readline";
16
- import { getContext } from "./init_browser.js";
17
+ import { getContext, refreshBrowser } from "./init_browser.js";
18
+ import { getTestData } from "./auto_page.js";
17
19
  import { locate_element } from "./locate_element.js";
18
20
  import { randomUUID } from "crypto";
19
21
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
20
22
  import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
21
23
  import { LocatorLog } from "./locator_log.js";
24
+ import axios from "axios";
25
+ import { _findCellArea, findElementsInArea } from "./table_helper.js";
22
26
  export const Types = {
23
27
  CLICK: "click_element",
28
+ WAIT_ELEMENT: "wait_element",
24
29
  NAVIGATE: "navigate",
25
30
  FILL: "fill_element",
26
31
  EXECUTE: "execute_page_method",
@@ -42,6 +47,7 @@ export const Types = {
42
47
  UNCHECK: "uncheck_element",
43
48
  EXTRACT: "extract_attribute",
44
49
  CLOSE_PAGE: "close_page",
50
+ TABLE_OPERATION: "table_operation",
45
51
  SET_DATE_TIME: "set_date_time",
46
52
  SET_VIEWPORT: "set_viewport",
47
53
  VERIFY_VISUAL: "verify_visual",
@@ -50,8 +56,12 @@ export const Types = {
50
56
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
51
57
  VERIFY_ATTRIBUTE: "verify_element_attribute",
52
58
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
59
+ BRUNO: "bruno",
53
60
  };
54
61
  export const apps = {};
62
+ const formatElementName = (elementName) => {
63
+ return elementName ? JSON.stringify(elementName) : "element";
64
+ };
55
65
  class StableBrowser {
56
66
  browser;
57
67
  page;
@@ -65,6 +75,7 @@ class StableBrowser {
65
75
  appName = "main";
66
76
  tags = null;
67
77
  isRecording = false;
78
+ initSnapshotTaken = false;
68
79
  constructor(browser, page, logger = null, context = null, world = null) {
69
80
  this.browser = browser;
70
81
  this.page = page;
@@ -171,6 +182,30 @@ class StableBrowser {
171
182
  await this.waitForPageLoad();
172
183
  }
173
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
+ }
174
209
  registerConsoleLogListener(page, context) {
175
210
  if (!this.context.webLogger) {
176
211
  this.context.webLogger = [];
@@ -235,6 +270,9 @@ class StableBrowser {
235
270
  // await closeUnexpectedPopups(this.page);
236
271
  // }
237
272
  async goto(url, world = null) {
273
+ if (!url) {
274
+ throw new Error("url is null, verify that the environment file is correct");
275
+ }
238
276
  if (!url.startsWith("http")) {
239
277
  url = "https://" + url;
240
278
  }
@@ -252,7 +290,7 @@ class StableBrowser {
252
290
  highlight: false,
253
291
  };
254
292
  try {
255
- await _preCommand(state, this, world);
293
+ await _preCommand(state, this);
256
294
  await this.page.goto(url, {
257
295
  timeout: 60000,
258
296
  });
@@ -263,7 +301,7 @@ class StableBrowser {
263
301
  _commandError(state, error, this);
264
302
  }
265
303
  finally {
266
- _commandFinally(state, this);
304
+ await _commandFinally(state, this);
267
305
  }
268
306
  }
269
307
  async _getLocator(locator, scope, _params) {
@@ -344,7 +382,7 @@ class StableBrowser {
344
382
  return resultCss;
345
383
  }
346
384
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
347
- const query = _convertToRegexQuery(text1, regex1, !partial1, ignoreCase);
385
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
348
386
  const locator = scope.locator(query);
349
387
  const count = await locator.count();
350
388
  if (!tag1) {
@@ -364,6 +402,12 @@ class StableBrowser {
364
402
  if (!el.setAttribute) {
365
403
  el = el.parentElement;
366
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
+ // }
367
411
  el.setAttribute("data-blinq-id-" + randomToken, "");
368
412
  return true;
369
413
  }, [tag1, randomToken]))) {
@@ -373,7 +417,7 @@ class StableBrowser {
373
417
  }
374
418
  return { elementCount: tagCount, randomToken };
375
419
  }
376
- 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) {
377
421
  if (!info) {
378
422
  info = {};
379
423
  }
@@ -396,10 +440,11 @@ class StableBrowser {
396
440
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
397
441
  let locator = null;
398
442
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
399
- 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);
400
445
  if (!locatorString) {
401
446
  info.failCause.textNotFound = true;
402
- 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}`;
403
448
  return;
404
449
  }
405
450
  locator = await this._getLocator({ css: locatorString }, scope, _params);
@@ -409,7 +454,7 @@ class StableBrowser {
409
454
  let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
410
455
  if (result.elementCount === 0) {
411
456
  info.failCause.textNotFound = true;
412
- info.failCause.lastError = "failed to locate element by text: " + text;
457
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
413
458
  return;
414
459
  }
415
460
  locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
@@ -461,11 +506,11 @@ class StableBrowser {
461
506
  info.printMessages = {};
462
507
  }
463
508
  if (info.locatorLog && !visible) {
464
- info.failCause.lastError = "element " + originalLocatorSearch + " is not 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) {
468
- info.failCause.lastError = "element " + originalLocatorSearch + " is disabled, ";
513
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
469
514
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
470
515
  }
471
516
  if (!info.printMessages[j.toString()]) {
@@ -545,7 +590,28 @@ class StableBrowser {
545
590
  }
546
591
  let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
547
592
  if (!element.rerun) {
548
- 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);
549
615
  }
550
616
  }
551
617
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -618,7 +684,7 @@ class StableBrowser {
618
684
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
619
685
  if (Date.now() - startTime > timeout) {
620
686
  info.failCause.iframeNotFound = true;
621
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
687
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
622
688
  throw new Error("unable to locate iframe " + selectors.iframe_src);
623
689
  }
624
690
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -692,18 +758,13 @@ class StableBrowser {
692
758
  }
693
759
  // info.log += "scanning locators in priority 1" + "\n";
694
760
  let onlyPriority3 = selectorsLocators[0].priority === 3;
695
- 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);
696
762
  if (result.foundElements.length === 0) {
697
763
  // info.log += "scanning locators in priority 2" + "\n";
698
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled);
699
- }
700
- if (result.foundElements.length === 0 && onlyPriority3) {
701
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
764
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
702
765
  }
703
- else {
704
- if (result.foundElements.length === 0 && !highPriorityOnly) {
705
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled);
706
- }
766
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
767
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
707
768
  }
708
769
  let foundElements = result.foundElements;
709
770
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -759,6 +820,11 @@ class StableBrowser {
759
820
  visibleOnly = false;
760
821
  }
761
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
+ }
762
828
  }
763
829
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
764
830
  // if (info.locatorLog) {
@@ -770,11 +836,11 @@ class StableBrowser {
770
836
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
771
837
  info.failCause.locatorNotFound = true;
772
838
  if (!info?.failCause?.lastError) {
773
- 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"}`;
774
840
  }
775
841
  throw new Error("failed to locate first element no elements found, " + info.log);
776
842
  }
777
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false) {
843
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
778
844
  let foundElements = [];
779
845
  const result = {
780
846
  foundElements: foundElements,
@@ -782,7 +848,7 @@ class StableBrowser {
782
848
  for (let i = 0; i < locatorsGroup.length; i++) {
783
849
  let foundLocators = [];
784
850
  try {
785
- 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);
786
852
  }
787
853
  catch (e) {
788
854
  // this call can fail it the browser is navigating
@@ -790,7 +856,7 @@ class StableBrowser {
790
856
  // this.logger.debug(e);
791
857
  foundLocators = [];
792
858
  try {
793
- 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);
794
860
  }
795
861
  catch (e) {
796
862
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -805,9 +871,40 @@ class StableBrowser {
805
871
  result.locatorIndex = i;
806
872
  }
807
873
  if (foundLocators.length > 1) {
808
- info.failCause.foundMultiple = true;
809
- if (info.locatorLog) {
810
- 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
+ }
811
908
  }
812
909
  }
813
910
  }
@@ -826,7 +923,7 @@ class StableBrowser {
826
923
  operation: "simpleClick",
827
924
  log: "***** click on " + elementDescription + " *****\n",
828
925
  };
829
- _preCommand(state, this, world);
926
+ _preCommand(state, this);
830
927
  const startTime = Date.now();
831
928
  let timeout = 30000;
832
929
  if (options && options.timeout) {
@@ -855,7 +952,7 @@ class StableBrowser {
855
952
  await _commandError(state, "timeout looking for " + elementDescription, this);
856
953
  }
857
954
  finally {
858
- _commandFinally(state, this);
955
+ await _commandFinally(state, this);
859
956
  }
860
957
  }
861
958
  }
@@ -875,7 +972,7 @@ class StableBrowser {
875
972
  operation: "simpleClickType",
876
973
  log: "***** click type on " + elementDescription + " *****\n",
877
974
  };
878
- _preCommand(state, this, world);
975
+ _preCommand(state, this);
879
976
  const startTime = Date.now();
880
977
  let timeout = 30000;
881
978
  if (options && options.timeout) {
@@ -904,7 +1001,7 @@ class StableBrowser {
904
1001
  await _commandError(state, "timeout looking for " + elementDescription, this);
905
1002
  }
906
1003
  finally {
907
- _commandFinally(state, this);
1004
+ await _commandFinally(state, this);
908
1005
  }
909
1006
  }
910
1007
  }
@@ -918,25 +1015,14 @@ class StableBrowser {
918
1015
  options,
919
1016
  world,
920
1017
  text: "Click element",
1018
+ _text: "Click on " + selectors.element_name,
921
1019
  type: Types.CLICK,
922
1020
  operation: "click",
923
1021
  log: "***** click on " + selectors.element_name + " *****\n",
924
1022
  };
925
1023
  try {
926
- await _preCommand(state, this, world);
927
- // if (state.options && state.options.context) {
928
- // state.selectors.locators[0].text = state.options.context;
929
- // }
930
- try {
931
- await state.element.click();
932
- // await new Promise((resolve) => setTimeout(resolve, 1000));
933
- }
934
- catch (e) {
935
- // await this.closeUnexpectedPopups();
936
- state.element = await this._locate(selectors, state.info, _params);
937
- await state.element.dispatchEvent("click");
938
- // await new Promise((resolve) => setTimeout(resolve, 1000));
939
- }
1024
+ await _preCommand(state, this);
1025
+ await performAction("click", state.element, options, this, state, _params);
940
1026
  await this.waitForPageLoad();
941
1027
  return state.info;
942
1028
  }
@@ -944,8 +1030,40 @@ class StableBrowser {
944
1030
  await _commandError(state, e, this);
945
1031
  }
946
1032
  finally {
947
- _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);
948
1065
  }
1066
+ return found;
949
1067
  }
950
1068
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
951
1069
  const state = {
@@ -955,22 +1073,23 @@ class StableBrowser {
955
1073
  world,
956
1074
  type: checked ? Types.CHECK : Types.UNCHECK,
957
1075
  text: checked ? `Check element` : `Uncheck element`,
1076
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
958
1077
  operation: "setCheck",
959
1078
  log: "***** check " + selectors.element_name + " *****\n",
960
1079
  };
961
1080
  try {
962
- await _preCommand(state, this, world);
1081
+ await _preCommand(state, this);
963
1082
  state.info.checked = checked;
964
1083
  // let element = await this._locate(selectors, info, _params);
965
1084
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
966
1085
  try {
967
- if (world && world.screenshot && !world.screenshotPath) {
968
- // console.log(`Highlighting while running from recorder`);
969
- await this._highlightElements(element);
970
- await new Promise((resolve) => setTimeout(resolve, 100));
971
- await this._unHighlightElements(element);
972
- }
1086
+ // if (world && world.screenshot && !world.screenshotPath) {
1087
+ // console.log(`Highlighting while running from recorder`);
1088
+ await this._highlightElements(state.element);
973
1089
  await state.element.setChecked(checked);
1090
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1091
+ // await this._unHighlightElements(element);
1092
+ // }
974
1093
  // await new Promise((resolve) => setTimeout(resolve, 1000));
975
1094
  // await this._unHighlightElements(element);
976
1095
  }
@@ -993,7 +1112,7 @@ class StableBrowser {
993
1112
  await _commandError(state, e, this);
994
1113
  }
995
1114
  finally {
996
- _commandFinally(state, this);
1115
+ await _commandFinally(state, this);
997
1116
  }
998
1117
  }
999
1118
  async hover(selectors, _params, options = {}, world = null) {
@@ -1004,25 +1123,14 @@ class StableBrowser {
1004
1123
  world,
1005
1124
  type: Types.HOVER,
1006
1125
  text: `Hover element`,
1126
+ _text: `Hover on ${selectors.element_name}`,
1007
1127
  operation: "hover",
1008
1128
  log: "***** hover " + selectors.element_name + " *****\n",
1009
1129
  };
1010
1130
  try {
1011
- await _preCommand(state, this, world);
1012
- try {
1013
- await state.element.hover();
1014
- await _screenshot(state, this);
1015
- await new Promise((resolve) => setTimeout(resolve, 1000));
1016
- }
1017
- catch (e) {
1018
- //await this.closeUnexpectedPopups();
1019
- state.info.log += "hover failed, will try again" + "\n";
1020
- state.element = await this._locate(selectors, state.info, _params);
1021
- await state.element.hover({ timeout: 10000 });
1022
- await _screenshot(state, this);
1023
- await new Promise((resolve) => setTimeout(resolve, 1000));
1024
- }
1025
- // await _screenshot(state, this);
1131
+ await _preCommand(state, this);
1132
+ await performAction("hover", state.element, options, this, state, _params);
1133
+ await _screenshot(state, this);
1026
1134
  await this.waitForPageLoad();
1027
1135
  return state.info;
1028
1136
  }
@@ -1030,7 +1138,7 @@ class StableBrowser {
1030
1138
  await _commandError(state, e, this);
1031
1139
  }
1032
1140
  finally {
1033
- _commandFinally(state, this);
1141
+ await _commandFinally(state, this);
1034
1142
  }
1035
1143
  }
1036
1144
  async selectOption(selectors, values, _params = null, options = {}, world = null) {
@@ -1045,11 +1153,12 @@ class StableBrowser {
1045
1153
  value: values.toString(),
1046
1154
  type: Types.SELECT,
1047
1155
  text: `Select option: ${values}`,
1156
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1048
1157
  operation: "selectOption",
1049
1158
  log: "***** select option " + selectors.element_name + " *****\n",
1050
1159
  };
1051
1160
  try {
1052
- await _preCommand(state, this, world);
1161
+ await _preCommand(state, this);
1053
1162
  try {
1054
1163
  await state.element.selectOption(values);
1055
1164
  }
@@ -1065,7 +1174,7 @@ class StableBrowser {
1065
1174
  await _commandError(state, e, this);
1066
1175
  }
1067
1176
  finally {
1068
- _commandFinally(state, this);
1177
+ await _commandFinally(state, this);
1069
1178
  }
1070
1179
  }
1071
1180
  async type(_value, _params = null, options = {}, world = null) {
@@ -1079,11 +1188,12 @@ class StableBrowser {
1079
1188
  highlight: false,
1080
1189
  type: Types.TYPE_PRESS,
1081
1190
  text: `Type value: ${_value}`,
1191
+ _text: `Type value: ${_value}`,
1082
1192
  operation: "type",
1083
1193
  log: "",
1084
1194
  };
1085
1195
  try {
1086
- await _preCommand(state, this, world);
1196
+ await _preCommand(state, this);
1087
1197
  const valueSegment = state.value.split("&&");
1088
1198
  for (let i = 0; i < valueSegment.length; i++) {
1089
1199
  if (i > 0) {
@@ -1110,7 +1220,7 @@ class StableBrowser {
1110
1220
  await _commandError(state, e, this);
1111
1221
  }
1112
1222
  finally {
1113
- _commandFinally(state, this);
1223
+ await _commandFinally(state, this);
1114
1224
  }
1115
1225
  }
1116
1226
  async setInputValue(selectors, value, _params = null, options = {}, world = null) {
@@ -1126,7 +1236,7 @@ class StableBrowser {
1126
1236
  log: "***** set input value " + selectors.element_name + " *****\n",
1127
1237
  };
1128
1238
  try {
1129
- await _preCommand(state, this, world);
1239
+ await _preCommand(state, this);
1130
1240
  let value = await this._replaceWithLocalData(state.value, this);
1131
1241
  try {
1132
1242
  await state.element.evaluateHandle((el, value) => {
@@ -1146,7 +1256,7 @@ class StableBrowser {
1146
1256
  await _commandError(state, e, this);
1147
1257
  }
1148
1258
  finally {
1149
- _commandFinally(state, this);
1259
+ await _commandFinally(state, this);
1150
1260
  }
1151
1261
  }
1152
1262
  async setDateTime(selectors, value, format = null, enter = false, _params = null, options = {}, world = null) {
@@ -1158,14 +1268,15 @@ class StableBrowser {
1158
1268
  world,
1159
1269
  type: Types.SET_DATE_TIME,
1160
1270
  text: `Set date time value: ${value}`,
1271
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1161
1272
  operation: "setDateTime",
1162
1273
  log: "***** set date time value " + selectors.element_name + " *****\n",
1163
1274
  throwError: false,
1164
1275
  };
1165
1276
  try {
1166
- await _preCommand(state, this, world);
1277
+ await _preCommand(state, this);
1167
1278
  try {
1168
- await state.element.click();
1279
+ await performAction("click", state.element, options, this, state, _params);
1169
1280
  await new Promise((resolve) => setTimeout(resolve, 500));
1170
1281
  if (format) {
1171
1282
  state.value = dayjs(state.value).format(format);
@@ -1214,7 +1325,7 @@ class StableBrowser {
1214
1325
  await _commandError(state, e, this);
1215
1326
  }
1216
1327
  finally {
1217
- _commandFinally(state, this);
1328
+ await _commandFinally(state, this);
1218
1329
  }
1219
1330
  }
1220
1331
  async clickType(selectors, _value, enter = false, _params = null, options = {}, world = null) {
@@ -1229,17 +1340,21 @@ class StableBrowser {
1229
1340
  world,
1230
1341
  type: Types.FILL,
1231
1342
  text: `Click type input with value: ${_value}`,
1343
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1232
1344
  operation: "clickType",
1233
1345
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1234
1346
  };
1347
+ if (!options) {
1348
+ options = {};
1349
+ }
1235
1350
  if (newValue !== _value) {
1236
1351
  //this.logger.info(_value + "=" + newValue);
1237
1352
  _value = newValue;
1238
1353
  }
1239
1354
  try {
1240
- await _preCommand(state, this, world);
1355
+ await _preCommand(state, this);
1241
1356
  state.info.value = _value;
1242
- if (options === null || options === undefined || !options.press) {
1357
+ if (!options.press) {
1243
1358
  try {
1244
1359
  let currentValue = await state.element.inputValue();
1245
1360
  if (currentValue) {
@@ -1250,13 +1365,9 @@ class StableBrowser {
1250
1365
  this.logger.info("unable to clear input value");
1251
1366
  }
1252
1367
  }
1253
- if (options === null || options === undefined || options.press) {
1254
- try {
1255
- await state.element.click({ timeout: 5000 });
1256
- }
1257
- catch (e) {
1258
- await state.element.dispatchEvent("click");
1259
- }
1368
+ if (options.press) {
1369
+ options.timeout = 5000;
1370
+ await performAction("click", state.element, options, this, state, _params);
1260
1371
  }
1261
1372
  else {
1262
1373
  try {
@@ -1314,7 +1425,7 @@ class StableBrowser {
1314
1425
  await _commandError(state, e, this);
1315
1426
  }
1316
1427
  finally {
1317
- _commandFinally(state, this);
1428
+ await _commandFinally(state, this);
1318
1429
  }
1319
1430
  }
1320
1431
  async fill(selectors, value, enter = false, _params = null, options = {}, world = null) {
@@ -1330,7 +1441,7 @@ class StableBrowser {
1330
1441
  log: "***** fill on " + selectors.element_name + " with value " + value + "*****\n",
1331
1442
  };
1332
1443
  try {
1333
- await _preCommand(state, this, world);
1444
+ await _preCommand(state, this);
1334
1445
  await state.element.fill(value);
1335
1446
  await state.element.dispatchEvent("change");
1336
1447
  if (enter) {
@@ -1344,13 +1455,14 @@ class StableBrowser {
1344
1455
  await _commandError(state, e, this);
1345
1456
  }
1346
1457
  finally {
1347
- _commandFinally(state, this);
1458
+ await _commandFinally(state, this);
1348
1459
  }
1349
1460
  }
1350
1461
  async getText(selectors, _params = null, options = {}, info = {}, world = null) {
1351
1462
  return await this._getText(selectors, 0, _params, options, info, world);
1352
1463
  }
1353
1464
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1465
+ const timeout = this._getFindElementTimeout(options);
1354
1466
  _validateSelectors(selectors);
1355
1467
  let screenshotId = null;
1356
1468
  let screenshotPath = null;
@@ -1360,7 +1472,7 @@ class StableBrowser {
1360
1472
  }
1361
1473
  info.operation = "getText";
1362
1474
  info.selectors = selectors;
1363
- let element = await this._locate(selectors, info, _params);
1475
+ let element = await this._locate(selectors, info, _params, timeout);
1364
1476
  if (climb > 0) {
1365
1477
  const climbArray = [];
1366
1478
  for (let i = 0; i < climb; i++) {
@@ -1376,19 +1488,21 @@ class StableBrowser {
1376
1488
  catch (e) {
1377
1489
  //ignore
1378
1490
  }
1379
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info, element));
1491
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1380
1492
  try {
1381
- if (world && world.screenshot && !world.screenshotPath) {
1382
- // console.log(`Highlighting for get text while running from recorder`);
1383
- this._highlightElements(element)
1384
- .then(async () => {
1385
- await new Promise((resolve) => setTimeout(resolve, 1000));
1386
- this._unhighlightElements(element).then(() => { }
1387
- // console.log(`Unhighlighting vrtr in recorder is successful`)
1388
- );
1389
- })
1390
- .catch(e);
1391
- }
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
+ // }
1392
1506
  const elementText = await element.innerText();
1393
1507
  return {
1394
1508
  text: elementText,
@@ -1425,6 +1539,7 @@ class StableBrowser {
1425
1539
  highlight: false,
1426
1540
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1427
1541
  text: `Verify element contains pattern: ${pattern}`,
1542
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1428
1543
  operation: "containsPattern",
1429
1544
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1430
1545
  };
@@ -1435,13 +1550,13 @@ class StableBrowser {
1435
1550
  }
1436
1551
  let foundObj = null;
1437
1552
  try {
1438
- await _preCommand(state, this, world);
1553
+ await _preCommand(state, this);
1439
1554
  state.info.pattern = pattern;
1440
1555
  foundObj = await this._getText(selectors, 0, _params, options, state.info, world);
1441
1556
  if (foundObj && foundObj.element) {
1442
1557
  await this.scrollIfNeeded(foundObj.element, state.info);
1443
1558
  }
1444
- await _screenshot(state, this, foundObj.element);
1559
+ await _screenshot(state, this);
1445
1560
  let escapedText = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
1446
1561
  pattern = pattern.replace("{text}", escapedText);
1447
1562
  let regex = new RegExp(pattern, "im");
@@ -1456,10 +1571,12 @@ class StableBrowser {
1456
1571
  await _commandError(state, e, this);
1457
1572
  }
1458
1573
  finally {
1459
- _commandFinally(state, this);
1574
+ await _commandFinally(state, this);
1460
1575
  }
1461
1576
  }
1462
1577
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1578
+ const timeout = this._getFindElementTimeout(options);
1579
+ const startTime = Date.now();
1463
1580
  const state = {
1464
1581
  selectors,
1465
1582
  _params,
@@ -1486,61 +1603,53 @@ class StableBrowser {
1486
1603
  }
1487
1604
  let foundObj = null;
1488
1605
  try {
1489
- await _preCommand(state, this, world);
1490
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1491
- if (foundObj && foundObj.element) {
1492
- await this.scrollIfNeeded(foundObj.element, state.info);
1493
- }
1494
- await _screenshot(state, this, foundObj.element);
1495
- const dateAlternatives = findDateAlternatives(text);
1496
- const numberAlternatives = findNumberAlternatives(text);
1497
- if (dateAlternatives.date) {
1498
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1499
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1500
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1501
- 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);
1502
1612
  }
1503
- }
1504
- throw new Error("element doesn't contain text " + text);
1505
- }
1506
- else if (numberAlternatives.number) {
1507
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1508
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1509
- 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)) {
1510
1633
  return state.info;
1511
1634
  }
1512
1635
  }
1513
- throw new Error("element doesn't contain text " + text);
1514
- }
1515
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1516
- state.info.foundText = foundObj?.text;
1517
- state.info.value = foundObj?.value;
1518
- 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
1519
1641
  }
1520
- 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);
1521
1645
  }
1522
1646
  catch (e) {
1523
1647
  await _commandError(state, e, this);
1648
+ throw e;
1524
1649
  }
1525
1650
  finally {
1526
- _commandFinally(state, this);
1527
- }
1528
- }
1529
- _getDataFile(world = null) {
1530
- let dataFile = null;
1531
- if (world && world.reportFolder) {
1532
- dataFile = path.join(world.reportFolder, "data.json");
1533
- }
1534
- else if (this.reportFolder) {
1535
- dataFile = path.join(this.reportFolder, "data.json");
1536
- }
1537
- else if (this.context && this.context.reportFolder) {
1538
- dataFile = path.join(this.context.reportFolder, "data.json");
1651
+ await _commandFinally(state, this);
1539
1652
  }
1540
- else {
1541
- dataFile = "data.json";
1542
- }
1543
- return dataFile;
1544
1653
  }
1545
1654
  async waitForUserInput(message, world = null) {
1546
1655
  if (!message) {
@@ -1570,13 +1679,22 @@ class StableBrowser {
1570
1679
  return;
1571
1680
  }
1572
1681
  // if data file exists, load it
1573
- const dataFile = this._getDataFile(world);
1682
+ const dataFile = _getDataFile(world, this.context, this);
1574
1683
  let data = this.getTestData(world);
1575
1684
  // merge the testData with the existing data
1576
1685
  Object.assign(data, testData);
1577
1686
  // save the data to the file
1578
1687
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1579
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
+ }
1580
1698
  _getDataFilePath(fileName) {
1581
1699
  let dataFile = path.join(this.project_path, "data", fileName);
1582
1700
  if (fs.existsSync(dataFile)) {
@@ -1673,14 +1791,14 @@ class StableBrowser {
1673
1791
  }
1674
1792
  }
1675
1793
  getTestData(world = null) {
1676
- const dataFile = this._getDataFile(world);
1794
+ const dataFile = _getDataFile(world, this.context, this);
1677
1795
  let data = {};
1678
1796
  if (fs.existsSync(dataFile)) {
1679
1797
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1680
1798
  }
1681
1799
  return data;
1682
1800
  }
1683
- async _screenShot(options = {}, world = null, info = null, focusedElement = null) {
1801
+ async _screenShot(options = {}, world = null, info = null) {
1684
1802
  // collect url/path/title
1685
1803
  if (info) {
1686
1804
  if (!info.title) {
@@ -1709,7 +1827,7 @@ class StableBrowser {
1709
1827
  const uuidStr = "id_" + randomUUID();
1710
1828
  const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
1711
1829
  try {
1712
- await this.takeScreenshot(screenshotPath, focusedElement);
1830
+ await this.takeScreenshot(screenshotPath);
1713
1831
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1714
1832
  // // save the buffer to the screenshot path asynchrously
1715
1833
  // fs.writeFile(screenshotPath, buffer, (err) => {
@@ -1720,7 +1838,7 @@ class StableBrowser {
1720
1838
  result.screenshotId = uuidStr;
1721
1839
  result.screenshotPath = screenshotPath;
1722
1840
  if (info && info.box) {
1723
- // await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1841
+ await drawRectangle(screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1724
1842
  }
1725
1843
  }
1726
1844
  catch (e) {
@@ -1730,7 +1848,7 @@ class StableBrowser {
1730
1848
  else if (options && options.screenshot) {
1731
1849
  result.screenshotPath = options.screenshotPath;
1732
1850
  try {
1733
- await this.takeScreenshot(options.screenshotPath, focusedElement);
1851
+ await this.takeScreenshot(options.screenshotPath);
1734
1852
  // let buffer = await this.page.screenshot({ timeout: 4000 });
1735
1853
  // // save the buffer to the screenshot path asynchrously
1736
1854
  // fs.writeFile(options.screenshotPath, buffer, (err) => {
@@ -1743,12 +1861,12 @@ class StableBrowser {
1743
1861
  this.logger.info("unable to take screenshot, ignored");
1744
1862
  }
1745
1863
  if (info && info.box) {
1746
- // await drawRectangle(options.screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1864
+ await drawRectangle(options.screenshotPath, info.box.x, info.box.y, info.box.width, info.box.height);
1747
1865
  }
1748
1866
  }
1749
1867
  return result;
1750
1868
  }
1751
- async takeScreenshot(screenshotPath, focusedElement = null) {
1869
+ async takeScreenshot(screenshotPath) {
1752
1870
  const playContext = this.context.playContext;
1753
1871
  // Using CDP to capture the screenshot
1754
1872
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
@@ -1766,9 +1884,9 @@ class StableBrowser {
1766
1884
  // await new Promise((resolve) => setTimeout(resolve, 100));
1767
1885
  // console.log(`Unhighlighted previous element`);
1768
1886
  // }
1769
- if (focusedElement) {
1770
- await this._highlightElements(focusedElement);
1771
- }
1887
+ // if (focusedElement) {
1888
+ // await this._highlightElements(focusedElement);
1889
+ // }
1772
1890
  if (this.context.browserName === "chromium") {
1773
1891
  const client = await playContext.newCDPSession(this.page);
1774
1892
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1790,10 +1908,10 @@ class StableBrowser {
1790
1908
  else {
1791
1909
  screenshotBuffer = await this.page.screenshot();
1792
1910
  }
1793
- if (focusedElement) {
1794
- // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1795
- await this._unhighlightElements(focusedElement);
1796
- }
1911
+ // if (focusedElement) {
1912
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1913
+ // await this._unhighlightElements(focusedElement);
1914
+ // }
1797
1915
  let image = await Jimp.read(screenshotBuffer);
1798
1916
  // Get the image dimensions
1799
1917
  const { width, height } = image.bitmap;
@@ -1806,6 +1924,7 @@ class StableBrowser {
1806
1924
  else {
1807
1925
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1808
1926
  }
1927
+ return screenshotBuffer;
1809
1928
  }
1810
1929
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1811
1930
  const state = {
@@ -1820,7 +1939,7 @@ class StableBrowser {
1820
1939
  };
1821
1940
  await new Promise((resolve) => setTimeout(resolve, 2000));
1822
1941
  try {
1823
- await _preCommand(state, this, world);
1942
+ await _preCommand(state, this);
1824
1943
  await expect(state.element).toHaveCount(1, { timeout: 10000 });
1825
1944
  return state.info;
1826
1945
  }
@@ -1828,7 +1947,7 @@ class StableBrowser {
1828
1947
  await _commandError(state, e, this);
1829
1948
  }
1830
1949
  finally {
1831
- _commandFinally(state, this);
1950
+ await _commandFinally(state, this);
1832
1951
  }
1833
1952
  }
1834
1953
  async extractAttribute(selectors, attribute, variable, _params = null, options = {}, world = null) {
@@ -1841,12 +1960,14 @@ class StableBrowser {
1841
1960
  world,
1842
1961
  type: Types.EXTRACT,
1843
1962
  text: `Extract attribute from element`,
1963
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1844
1964
  operation: "extractAttribute",
1845
1965
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1966
+ allowDisabled: true,
1846
1967
  };
1847
1968
  await new Promise((resolve) => setTimeout(resolve, 2000));
1848
1969
  try {
1849
- await _preCommand(state, this, world);
1970
+ await _preCommand(state, this);
1850
1971
  switch (attribute) {
1851
1972
  case "inner_text":
1852
1973
  state.value = await state.element.innerText();
@@ -1857,6 +1978,9 @@ class StableBrowser {
1857
1978
  case "value":
1858
1979
  state.value = await state.element.inputValue();
1859
1980
  break;
1981
+ case "text":
1982
+ state.value = await state.element.textContent();
1983
+ break;
1860
1984
  default:
1861
1985
  state.value = await state.element.getAttribute(attribute);
1862
1986
  break;
@@ -1864,14 +1988,14 @@ class StableBrowser {
1864
1988
  state.info.value = state.value;
1865
1989
  this.setTestData({ [variable]: state.value }, world);
1866
1990
  this.logger.info("set test data: " + variable + "=" + state.value);
1867
- await new Promise((resolve) => setTimeout(resolve, 500));
1991
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1868
1992
  return state.info;
1869
1993
  }
1870
1994
  catch (e) {
1871
1995
  await _commandError(state, e, this);
1872
1996
  }
1873
1997
  finally {
1874
- _commandFinally(state, this);
1998
+ await _commandFinally(state, this);
1875
1999
  }
1876
2000
  }
1877
2001
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
@@ -1886,18 +2010,25 @@ class StableBrowser {
1886
2010
  highlight: true,
1887
2011
  screenshot: true,
1888
2012
  text: `Verify element attribute`,
2013
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1889
2014
  operation: "verifyAttribute",
1890
2015
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1891
2016
  allowDisabled: true,
1892
2017
  };
1893
2018
  await new Promise((resolve) => setTimeout(resolve, 2000));
1894
2019
  let val;
2020
+ let expectedValue;
1895
2021
  try {
1896
- await _preCommand(state, this, world);
2022
+ await _preCommand(state, this);
2023
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2024
+ state.info.expectedValue = expectedValue;
1897
2025
  switch (attribute) {
1898
2026
  case "innerText":
1899
2027
  val = String(await state.element.innerText());
1900
2028
  break;
2029
+ case "text":
2030
+ val = String(await state.element.textContent());
2031
+ break;
1901
2032
  case "value":
1902
2033
  val = String(await state.element.inputValue());
1903
2034
  break;
@@ -1915,26 +2046,29 @@ class StableBrowser {
1915
2046
  val = String(await state.element.getAttribute(attribute));
1916
2047
  break;
1917
2048
  }
2049
+ state.info.value = val;
1918
2050
  let regex;
1919
- if (value.startsWith("/") && value.endsWith("/")) {
1920
- const patternBody = value.slice(1, -1);
2051
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2052
+ const patternBody = expectedValue.slice(1, -1);
1921
2053
  regex = new RegExp(patternBody, "g");
1922
2054
  }
1923
2055
  else {
1924
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2056
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1925
2057
  regex = new RegExp(escapedPattern, "g");
1926
2058
  }
1927
2059
  if (!val.match(regex)) {
1928
- 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);
1929
2064
  }
1930
- state.info.expectedValue = val;
1931
2065
  return state.info;
1932
2066
  }
1933
2067
  catch (e) {
1934
2068
  await _commandError(state, e, this);
1935
2069
  }
1936
2070
  finally {
1937
- _commandFinally(state, this);
2071
+ await _commandFinally(state, this);
1938
2072
  }
1939
2073
  }
1940
2074
  async extractEmailData(emailAddress, options, world) {
@@ -2035,21 +2169,20 @@ class StableBrowser {
2035
2169
  if (node && node.style) {
2036
2170
  let originalOutline = node.style.outline;
2037
2171
  // console.log(`Original outline was: ${originalOutline}`);
2038
- node.__previousOutline = originalOutline;
2172
+ // node.__previousOutline = originalOutline;
2039
2173
  node.style.outline = "2px solid red";
2040
2174
  // console.log(`New outline is: ${node.style.outline}`);
2041
- // if (window) {
2042
- // window.addEventListener("beforeunload", function (e) {
2043
- // node.style.outline = originalBorder;
2044
- // });
2045
- // }
2046
- // setTimeout(function () {
2047
- // node.style.outline = originalBorder;
2048
- //}, 2000);
2175
+ if (window) {
2176
+ window.addEventListener("beforeunload", function (e) {
2177
+ node.style.outline = originalOutline;
2178
+ });
2179
+ }
2180
+ setTimeout(function () {
2181
+ node.style.outline = originalOutline;
2182
+ }, 2000);
2049
2183
  }
2050
2184
  })
2051
- .then(() => {
2052
- })
2185
+ .then(() => { })
2053
2186
  .catch((e) => {
2054
2187
  // ignore
2055
2188
  // console.error(`Could not highlight node : ${e}`);
@@ -2072,20 +2205,19 @@ class StableBrowser {
2072
2205
  element.__previousOutline = originalOutline;
2073
2206
  // Set the new border to be red and 2px solid
2074
2207
  element.style.outline = "2px solid red";
2075
- // if (window) {
2076
- // window.addEventListener("beforeunload", function (e) {
2077
- // element.style.outline = originalBorder;
2078
- // });
2079
- // }
2208
+ if (window) {
2209
+ window.addEventListener("beforeunload", function (e) {
2210
+ element.style.outline = originalOutline;
2211
+ });
2212
+ }
2080
2213
  // Set a timeout to revert to the original border after 2 seconds
2081
- // setTimeout(function () {
2082
- // element.style.outline = originalBorder;
2083
- // }, 2000);
2214
+ setTimeout(function () {
2215
+ element.style.outline = originalOutline;
2216
+ }, 2000);
2084
2217
  }
2085
2218
  return;
2086
2219
  }, [css])
2087
- .then(() => {
2088
- })
2220
+ .then(() => { })
2089
2221
  .catch((e) => {
2090
2222
  // ignore
2091
2223
  // console.error(`Could not highlight css: ${e}`);
@@ -2096,60 +2228,54 @@ class StableBrowser {
2096
2228
  console.debug(error);
2097
2229
  }
2098
2230
  }
2099
- async _unhighlightElements(scope, css) {
2100
- try {
2101
- if (!scope) {
2102
- return;
2103
- }
2104
- if (!css) {
2105
- scope
2106
- .evaluate((node) => {
2107
- if (node && node.style) {
2108
- if (!node.__previousOutline) {
2109
- node.style.outline = "";
2110
- }
2111
- else {
2112
- node.style.outline = node.__previousOutline;
2113
- }
2114
- }
2115
- })
2116
- .then(() => {
2117
- })
2118
- .catch((e) => {
2119
- // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2120
- });
2121
- }
2122
- else {
2123
- scope
2124
- .evaluate(([css]) => {
2125
- if (!css) {
2126
- return;
2127
- }
2128
- let elements = Array.from(document.querySelectorAll(css));
2129
- for (i = 0; i < elements.length; i++) {
2130
- let element = elements[i];
2131
- if (!element.style) {
2132
- return;
2133
- }
2134
- if (!element.__previousOutline) {
2135
- element.style.outline = "";
2136
- }
2137
- else {
2138
- element.style.outline = element.__previousOutline;
2139
- }
2140
- }
2141
- })
2142
- .then(() => {
2143
- })
2144
- .catch((e) => {
2145
- // console.error(`Error while unhighlighting element in css: ${e}`);
2146
- });
2147
- }
2148
- }
2149
- catch (error) {
2150
- // console.debug(error);
2151
- }
2152
- }
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
+ // }
2153
2279
  async verifyPagePath(pathPart, options = {}, world = null) {
2154
2280
  const startTime = Date.now();
2155
2281
  let error = null;
@@ -2194,6 +2320,7 @@ class StableBrowser {
2194
2320
  _reportToWorld(world, {
2195
2321
  type: Types.VERIFY_PAGE_PATH,
2196
2322
  text: "Verify page path",
2323
+ _text: "Verify the page path contains " + pathPart,
2197
2324
  screenshotId,
2198
2325
  result: error
2199
2326
  ? {
@@ -2211,27 +2338,89 @@ class StableBrowser {
2211
2338
  });
2212
2339
  }
2213
2340
  }
2214
- 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) {
2215
2404
  const frames = this.page.frames();
2216
2405
  let results = [];
2217
- let ignoreCase = false;
2406
+ // let ignoreCase = false;
2218
2407
  for (let i = 0; i < frames.length; i++) {
2219
2408
  if (dateAlternatives.date) {
2220
2409
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2221
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2410
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2222
2411
  result.frame = frames[i];
2223
2412
  results.push(result);
2224
2413
  }
2225
2414
  }
2226
2415
  else if (numberAlternatives.number) {
2227
2416
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2228
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2417
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2229
2418
  result.frame = frames[i];
2230
2419
  results.push(result);
2231
2420
  }
2232
2421
  }
2233
2422
  else {
2234
- 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, {});
2235
2424
  result.frame = frames[i];
2236
2425
  results.push(result);
2237
2426
  }
@@ -2250,11 +2439,15 @@ class StableBrowser {
2250
2439
  scroll: false,
2251
2440
  highlight: false,
2252
2441
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2253
- text: `Verify text exists in page`,
2442
+ text: `Verify the text '${text}' exists in page`,
2443
+ _text: `Verify the text '${text}' exists in page`,
2254
2444
  operation: "verifyTextExistInPage",
2255
2445
  log: "***** verify text " + text + " exists in page *****\n",
2256
2446
  };
2257
- const timeout = this._getLoadTimeout(options);
2447
+ if (testForRegex(text)) {
2448
+ text = text.replace(/\\"/g, '"');
2449
+ }
2450
+ const timeout = this._getFindElementTimeout(options);
2258
2451
  await new Promise((resolve) => setTimeout(resolve, 2000));
2259
2452
  const newValue = await this._replaceWithLocalData(text, world);
2260
2453
  if (newValue !== text) {
@@ -2264,10 +2457,18 @@ class StableBrowser {
2264
2457
  let dateAlternatives = findDateAlternatives(text);
2265
2458
  let numberAlternatives = findNumberAlternatives(text);
2266
2459
  try {
2267
- await _preCommand(state, this, world);
2460
+ await _preCommand(state, this);
2268
2461
  state.info.text = text;
2269
2462
  while (true) {
2270
- 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
+ }
2271
2472
  if (resultWithElementsFound.length === 0) {
2272
2473
  if (Date.now() - state.startTime > timeout) {
2273
2474
  throw new Error(`Text ${text} not found in page`);
@@ -2275,35 +2476,40 @@ class StableBrowser {
2275
2476
  await new Promise((resolve) => setTimeout(resolve, 1000));
2276
2477
  continue;
2277
2478
  }
2278
- if (resultWithElementsFound[0].randomToken) {
2279
- const frame = resultWithElementsFound[0].frame;
2280
- const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2281
- if (world && world.screenshot && !world.screenshotPath) {
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) {
2282
2485
  // console.log(`Highlighting for verify text is found while running from recorder`);
2283
- this._highlightElements(frame, dataAttribute).then(async () => {
2284
- await new Promise((resolve) => setTimeout(resolve, 1000));
2285
- this._unhighlightElements(frame, dataAttribute)
2286
- .then(async () => {
2287
- // console.log(`Unhighlighted frame dataAttribute successfully`);
2288
- })
2289
- .catch((e) => { }
2290
- // console.error(e)
2291
- );
2292
- });
2293
- }
2294
- const element = await frame.locator(dataAttribute).first();
2295
- // await new Promise((resolve) => setTimeout(resolve, 100));
2296
- // await this._unhighlightElements(frame, dataAttribute);
2297
- if (element) {
2298
- await this.scrollIfNeeded(element, state.info);
2299
- await element.dispatchEvent("bvt_verify_page_contains_text");
2300
- await _screenshot(state, this, element);
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
+ }
2301
2506
  }
2302
- }
2303
- else {
2304
2507
  await _screenshot(state, this);
2508
+ return state.info;
2509
+ }
2510
+ catch (error) {
2511
+ console.error(error);
2305
2512
  }
2306
- return state.info;
2307
2513
  }
2308
2514
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2309
2515
  }
@@ -2311,7 +2517,7 @@ class StableBrowser {
2311
2517
  await _commandError(state, e, this);
2312
2518
  }
2313
2519
  finally {
2314
- _commandFinally(state, this);
2520
+ await _commandFinally(state, this);
2315
2521
  }
2316
2522
  }
2317
2523
  async waitForTextToDisappear(text, options = {}, world = null) {
@@ -2324,11 +2530,15 @@ class StableBrowser {
2324
2530
  scroll: false,
2325
2531
  highlight: false,
2326
2532
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2327
- 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`,
2328
2535
  operation: "verifyTextNotExistInPage",
2329
2536
  log: "***** verify text " + text + " does not exist in page *****\n",
2330
2537
  };
2331
- const timeout = this._getLoadTimeout(options);
2538
+ if (testForRegex(text)) {
2539
+ text = text.replace(/\\"/g, '"');
2540
+ }
2541
+ const timeout = this._getFindElementTimeout(options);
2332
2542
  await new Promise((resolve) => setTimeout(resolve, 2000));
2333
2543
  const newValue = await this._replaceWithLocalData(text, world);
2334
2544
  if (newValue !== text) {
@@ -2338,10 +2548,18 @@ class StableBrowser {
2338
2548
  let dateAlternatives = findDateAlternatives(text);
2339
2549
  let numberAlternatives = findNumberAlternatives(text);
2340
2550
  try {
2341
- await _preCommand(state, this, world);
2551
+ await _preCommand(state, this);
2342
2552
  state.info.text = text;
2553
+ let resultWithElementsFound = {
2554
+ length: null, // initial cannot be 0
2555
+ };
2343
2556
  while (true) {
2344
- 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
+ }
2345
2563
  if (resultWithElementsFound.length === 0) {
2346
2564
  await _screenshot(state, this);
2347
2565
  return state.info;
@@ -2356,7 +2574,7 @@ class StableBrowser {
2356
2574
  await _commandError(state, e, this);
2357
2575
  }
2358
2576
  finally {
2359
- _commandFinally(state, this);
2577
+ await _commandFinally(state, this);
2360
2578
  }
2361
2579
  }
2362
2580
  async verifyTextRelatedToText(textAnchor, climb, textToVerify, options = {}, world = null) {
@@ -2371,10 +2589,11 @@ class StableBrowser {
2371
2589
  highlight: false,
2372
2590
  type: Types.VERIFY_TEXT_WITH_RELATION,
2373
2591
  text: `Verify text with relation to another text`,
2592
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2374
2593
  operation: "verify_text_with_relation",
2375
2594
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2376
2595
  };
2377
- const timeout = this._getLoadTimeout(options);
2596
+ const timeout = this._getFindElementTimeout(options);
2378
2597
  await new Promise((resolve) => setTimeout(resolve, 2000));
2379
2598
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2380
2599
  if (newValue !== textAnchor) {
@@ -2390,10 +2609,18 @@ class StableBrowser {
2390
2609
  let numberAlternatives = findNumberAlternatives(textToVerify);
2391
2610
  let foundAncore = false;
2392
2611
  try {
2393
- await _preCommand(state, this, world);
2612
+ await _preCommand(state, this);
2394
2613
  state.info.text = textToVerify;
2614
+ let resultWithElementsFound = {
2615
+ length: 0,
2616
+ };
2395
2617
  while (true) {
2396
- 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
+ }
2397
2624
  if (resultWithElementsFound.length === 0) {
2398
2625
  if (Date.now() - state.startTime > timeout) {
2399
2626
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2401,52 +2628,56 @@ class StableBrowser {
2401
2628
  await new Promise((resolve) => setTimeout(resolve, 1000));
2402
2629
  continue;
2403
2630
  }
2404
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2405
- foundAncore = true;
2406
- const result = resultWithElementsFound[i];
2407
- const token = result.randomToken;
2408
- const frame = result.frame;
2409
- let css = `[data-blinq-id-${token}]`;
2410
- const climbArray1 = [];
2411
- for (let i = 0; i < climb; i++) {
2412
- climbArray1.push("..");
2413
- }
2414
- let climbXpath = "xpath=" + climbArray1.join("/");
2415
- css = css + " >> " + climbXpath;
2416
- const count = await frame.locator(css).count();
2417
- for (let j = 0; j < count; j++) {
2418
- const continer = await frame.locator(css).nth(j);
2419
- const result = await this._locateElementByText(continer, textToVerify, "*", false, true, true, {});
2420
- if (result.elementCount > 0) {
2421
- const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2422
- //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2423
- if (world && world.screenshot && !world.screenshotPath) {
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) {
2424
2653
  // console.log(`Highlighting for vtrt while running from recorder`);
2425
- this._highlightElements(frame, dataAttribute)
2426
- .then(async () => {
2427
- await new Promise((resolve) => setTimeout(resolve, 1000));
2428
- this._unhighlightElements(frame, dataAttribute).then(() => { }
2429
- // console.log(`Unhighlighting vrtr in recorder is successful`)
2430
- );
2431
- })
2432
- .catch(e);
2433
- }
2434
- //await this._highlightElements(frame, cssAnchor);
2435
- const element = await frame.locator(dataAttribute).first();
2436
- // await new Promise((resolve) => setTimeout(resolve, 100));
2437
- // await this._unhighlightElements(frame, dataAttribute);
2438
- if (element) {
2439
- await this.scrollIfNeeded(element, state.info);
2440
- await element.dispatchEvent("bvt_verify_page_contains_text");
2441
- await _screenshot(state, this, element);
2442
- }
2443
- else {
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
+ }
2444
2672
  await _screenshot(state, this);
2673
+ return state.info;
2445
2674
  }
2446
- return state.info;
2447
2675
  }
2448
2676
  }
2449
2677
  }
2678
+ catch (error) {
2679
+ console.error(error);
2680
+ }
2450
2681
  }
2451
2682
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2452
2683
  }
@@ -2454,9 +2685,33 @@ class StableBrowser {
2454
2685
  await _commandError(state, e, this);
2455
2686
  }
2456
2687
  finally {
2457
- _commandFinally(state, this);
2688
+ await _commandFinally(state, this);
2458
2689
  }
2459
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
+ }
2460
2715
  async visualVerification(text, options = {}, world = null) {
2461
2716
  const startTime = Date.now();
2462
2717
  let error = null;
@@ -2475,10 +2730,13 @@ class StableBrowser {
2475
2730
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2476
2731
  info.screenshotPath = screenshotPath;
2477
2732
  const screenshot = await this.takeScreenshot();
2478
- const request = {
2479
- method: "POST",
2733
+ let request = {
2734
+ method: "post",
2735
+ maxBodyLength: Infinity,
2480
2736
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2481
2737
  headers: {
2738
+ "x-bvt-project-id": path.basename(this.project_path),
2739
+ "x-source": "aaa",
2482
2740
  "Content-Type": "application/json",
2483
2741
  Authorization: `Bearer ${process.env.TOKEN}`,
2484
2742
  },
@@ -2487,7 +2745,7 @@ class StableBrowser {
2487
2745
  screenshot: screenshot,
2488
2746
  }),
2489
2747
  };
2490
- let result = await this.context.api.request(request);
2748
+ const result = await axios.request(request);
2491
2749
  if (result.data.status !== true) {
2492
2750
  throw new Error("Visual validation failed");
2493
2751
  }
@@ -2515,6 +2773,7 @@ class StableBrowser {
2515
2773
  _reportToWorld(world, {
2516
2774
  type: Types.VERIFY_VISUAL,
2517
2775
  text: "Visual verification",
2776
+ _text: "Visual verification of " + text,
2518
2777
  screenshotId,
2519
2778
  result: error
2520
2779
  ? {
@@ -2565,14 +2824,14 @@ class StableBrowser {
2565
2824
  info.selectors = selectors;
2566
2825
  try {
2567
2826
  let table = await this._locate(selectors, info, _params);
2568
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info, table));
2827
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2569
2828
  const tableData = await getTableData(this.page, table);
2570
2829
  return tableData;
2571
2830
  }
2572
2831
  catch (e) {
2573
2832
  this.logger.error("getTableData failed " + info.log);
2574
2833
  this.logger.error(e);
2575
- ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info, table));
2834
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2576
2835
  info.screenshotPath = screenshotPath;
2577
2836
  Object.assign(e, { info: info });
2578
2837
  error = e;
@@ -2781,6 +3040,32 @@ class StableBrowser {
2781
3040
  }
2782
3041
  return timeout;
2783
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
+ }
2784
3069
  async waitForPageLoad(options = {}, world = null) {
2785
3070
  let timeout = this._getLoadTimeout(options);
2786
3071
  const promiseArray = [];
@@ -2848,12 +3133,13 @@ class StableBrowser {
2848
3133
  highlight: false,
2849
3134
  type: Types.CLOSE_PAGE,
2850
3135
  text: `Close page`,
3136
+ _text: `Close the page`,
2851
3137
  operation: "closePage",
2852
3138
  log: "***** close page *****\n",
2853
3139
  throwError: false,
2854
3140
  };
2855
3141
  try {
2856
- await _preCommand(state, this, world);
3142
+ await _preCommand(state, this);
2857
3143
  await this.page.close();
2858
3144
  }
2859
3145
  catch (e) {
@@ -2861,11 +3147,98 @@ class StableBrowser {
2861
3147
  await _commandError(state, e, this);
2862
3148
  }
2863
3149
  finally {
2864
- _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);
2865
3238
  }
2866
3239
  }
2867
3240
  saveTestDataAsGlobal(options, world) {
2868
- const dataFile = this._getDataFile(world);
3241
+ const dataFile = _getDataFile(world, this.context, this);
2869
3242
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2870
3243
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2871
3244
  }
@@ -2895,6 +3268,7 @@ class StableBrowser {
2895
3268
  _reportToWorld(world, {
2896
3269
  type: Types.SET_VIEWPORT,
2897
3270
  text: "set viewport size to " + width + "x" + hight,
3271
+ _text: "Set the viewport size to " + width + "x" + hight,
2898
3272
  screenshotId,
2899
3273
  result: error
2900
3274
  ? {
@@ -2965,7 +3339,38 @@ class StableBrowser {
2965
3339
  console.log("#-#");
2966
3340
  }
2967
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
+ if (!process.env.TEMP_RUN) {
3366
+ await getTestData(envName, world, undefined, this.featureName, this.scenarioName);
3367
+ }
3368
+ }
3369
+ async afterScenario(world, scenario) { }
2968
3370
  async beforeStep(world, step) {
3371
+ if (!this.beforeScenarioCalled) {
3372
+ this.beforeScenario(world, step);
3373
+ }
2969
3374
  if (this.stepIndex === undefined) {
2970
3375
  this.stepIndex = 0;
2971
3376
  }
@@ -2982,22 +3387,47 @@ class StableBrowser {
2982
3387
  else {
2983
3388
  this.stepName = "step " + this.stepIndex;
2984
3389
  }
2985
- if (this.context) {
2986
- this.context.examplesRow = extractStepExampleParameters(step);
2987
- }
2988
3390
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2989
3391
  if (this.context.browserObject.context) {
2990
3392
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
2991
3393
  }
2992
3394
  }
2993
- if (this.tags === null && step && step.pickle && step.pickle.tags) {
2994
- this.tags = step.pickle.tags.map((tag) => tag.name);
2995
- // check if @global_test_data tag is present
2996
- if (this.tags.includes("@global_test_data")) {
2997
- this.saveTestDataAsGlobal({}, world);
3395
+ if (this.initSnapshotTaken === false) {
3396
+ this.initSnapshotTaken = true;
3397
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3398
+ const snapshot = await this.getAriaSnapshot();
3399
+ if (snapshot) {
3400
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3401
+ }
2998
3402
  }
2999
3403
  }
3000
3404
  }
3405
+ async getAriaSnapshot() {
3406
+ try {
3407
+ // find the page url
3408
+ const url = await this.page.url();
3409
+ // extract the path from the url
3410
+ const path = new URL(url).pathname;
3411
+ // get the page title
3412
+ const title = await this.page.title();
3413
+ // go over other frams
3414
+ const frames = this.page.frames();
3415
+ const snapshots = [];
3416
+ const content = [`- path: ${path}`, `- title: ${title}`];
3417
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3418
+ for (let i = 0; i < frames.length; i++) {
3419
+ content.push(`- frame: ${i}`);
3420
+ const frame = frames[i];
3421
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3422
+ content.push(snapshot);
3423
+ }
3424
+ return content.join("\n");
3425
+ }
3426
+ catch (e) {
3427
+ console.error(e);
3428
+ }
3429
+ return null;
3430
+ }
3001
3431
  async afterStep(world, step) {
3002
3432
  this.stepName = null;
3003
3433
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
@@ -3005,11 +3435,25 @@ class StableBrowser {
3005
3435
  await this.context.browserObject.context.tracing.stopChunk({
3006
3436
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
3007
3437
  });
3438
+ if (world && world.attach) {
3439
+ await world.attach(JSON.stringify({
3440
+ type: "trace",
3441
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3442
+ }), "application/json+trace");
3443
+ }
3444
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3008
3445
  }
3009
3446
  }
3010
3447
  if (this.context) {
3011
3448
  this.context.examplesRow = null;
3012
3449
  }
3450
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3451
+ const snapshot = await this.getAriaSnapshot();
3452
+ if (snapshot) {
3453
+ const obj = {};
3454
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3455
+ }
3456
+ }
3013
3457
  }
3014
3458
  }
3015
3459
  function createTimedPromise(promise, label) {