automation_model 1.0.583-dev → 1.0.583-stage

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