automation_model 1.0.580-dev → 1.0.580-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 { _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;
@@ -236,6 +245,9 @@ class StableBrowser {
236
245
  // await closeUnexpectedPopups(this.page);
237
246
  // }
238
247
  async goto(url, world = null) {
248
+ if (!url) {
249
+ throw new Error("url is null, verify that the environment file is correct");
250
+ }
239
251
  if (!url.startsWith("http")) {
240
252
  url = "https://" + url;
241
253
  }
@@ -331,7 +343,7 @@ class StableBrowser {
331
343
  if (result.elementCount === 0) {
332
344
  return;
333
345
  }
334
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
346
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
335
347
  // css climb to parent element
336
348
  const climbArray = [];
337
349
  for (let i = 0; i < climb; i++) {
@@ -345,53 +357,42 @@ class StableBrowser {
345
357
  return resultCss;
346
358
  }
347
359
  async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params) {
348
- //const stringifyText = JSON.stringify(text);
349
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial, ignoreCase]) => {
350
- const options = {
351
- innerText: true,
352
- ignoreCase: ignoreCase,
353
- };
354
- if (regex) {
355
- options.singleRegex = true;
356
- }
357
- if (tag) {
358
- options.tag = tag;
359
- }
360
- if (!(partial === true)) {
361
- options.exactMatch = true;
362
- }
363
- if (text.startsWith("/") && text.endsWith("/")) {
364
- if (text.length < 3) {
365
- throw new Error("Invalid regex pattern: empty pattern");
366
- }
367
- try {
368
- const pattern = text.slice(1, -1);
369
- new RegExp(pattern); // Validate pattern
370
- text = pattern;
371
- options.singleRegex = true;
372
- }
373
- catch (e) {
374
- throw new Error(`Invalid regex pattern: ${e.message}`);
375
- }
376
- }
377
- const elements = window.findMatchingElements(text, options);
378
- let randomToken = null;
379
- const foundElements = [];
380
- let elementCount = 0;
381
- if (elements.length > 0) {
382
- for (let i = 0; i < elements.length; i++) {
383
- if (randomToken === null) {
384
- randomToken = Math.random().toString(36).substring(7);
360
+ const query = `${_convertToRegexQuery(text1, regex1, !partial1, ignoreCase)}`;
361
+ const locator = scope.locator(query);
362
+ const count = await locator.count();
363
+ if (!tag1) {
364
+ tag1 = "*";
365
+ }
366
+ const randomToken = Math.random().toString(36).substring(7);
367
+ let tagCount = 0;
368
+ for (let i = 0; i < count; i++) {
369
+ const element = locator.nth(i);
370
+ // check if the tag matches
371
+ if (!(await element.evaluate((el, [tag, randomToken]) => {
372
+ if (!tag.startsWith("*")) {
373
+ if (el.tagName.toLowerCase() !== tag) {
374
+ return false;
385
375
  }
386
- let element = elements[i];
387
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
388
- elementCount++;
389
376
  }
377
+ if (!el.setAttribute) {
378
+ el = el.parentElement;
379
+ }
380
+ // remove any attributes start with data-blinq-id
381
+ // for (let i = 0; i < el.attributes.length; i++) {
382
+ // if (el.attributes[i].name.startsWith("data-blinq-id")) {
383
+ // el.removeAttribute(el.attributes[i].name);
384
+ // }
385
+ // }
386
+ el.setAttribute("data-blinq-id-" + randomToken, "");
387
+ return true;
388
+ }, [tag1, randomToken]))) {
389
+ continue;
390
390
  }
391
- return { elementCount: elementCount, randomToken: randomToken };
392
- }, [text1, tag1, regex1, partial1, ignoreCase]);
391
+ tagCount++;
392
+ }
393
+ return { elementCount: tagCount, randomToken };
393
394
  }
394
- async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true) {
395
+ async _collectLocatorInformation(selectorHierarchy, index = 0, scope, foundLocators, _params, info, visibleOnly = true, allowDisabled = false, element_name = null) {
395
396
  if (!info) {
396
397
  info = {};
397
398
  }
@@ -414,10 +415,11 @@ class StableBrowser {
414
415
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
415
416
  let locator = null;
416
417
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
417
- let locatorString = await this._locateElmentByTextClimbCss(scope, locatorSearch.text, locatorSearch.climb, locatorSearch.css, _params);
418
+ const replacedText = await this._replaceWithLocalData(locatorSearch.text, this.world);
419
+ let locatorString = await this._locateElmentByTextClimbCss(scope, replacedText, locatorSearch.climb, locatorSearch.css, _params);
418
420
  if (!locatorString) {
419
421
  info.failCause.textNotFound = true;
420
- info.failCause.lastError = "failed to locate element by text: " + locatorSearch.text;
422
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${locatorSearch.text}`;
421
423
  return;
422
424
  }
423
425
  locator = await this._getLocator({ css: locatorString }, scope, _params);
@@ -427,10 +429,10 @@ class StableBrowser {
427
429
  let result = await this._locateElementByText(scope, text, locatorSearch.tag, false, locatorSearch.partial === true, true, _params);
428
430
  if (result.elementCount === 0) {
429
431
  info.failCause.textNotFound = true;
430
- info.failCause.lastError = "failed to locate element by text: " + text;
432
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
431
433
  return;
432
434
  }
433
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
435
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
434
436
  if (locatorSearch.childCss) {
435
437
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
436
438
  }
@@ -466,7 +468,7 @@ class StableBrowser {
466
468
  if (!visibleOnly) {
467
469
  visible = true;
468
470
  }
469
- if (visible && enabled) {
471
+ if (visible && (allowDisabled || enabled)) {
470
472
  foundLocators.push(locator.nth(j));
471
473
  if (info.locatorLog) {
472
474
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
@@ -479,9 +481,11 @@ class StableBrowser {
479
481
  info.printMessages = {};
480
482
  }
481
483
  if (info.locatorLog && !visible) {
484
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
482
485
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
483
486
  }
484
487
  if (info.locatorLog && !enabled) {
488
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
485
489
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
486
490
  }
487
491
  if (!info.printMessages[j.toString()]) {
@@ -549,7 +553,7 @@ class StableBrowser {
549
553
  }
550
554
  return { rerun: false };
551
555
  }
552
- async _locate(selectors, info, _params, timeout) {
556
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
553
557
  if (!timeout) {
554
558
  timeout = 30000;
555
559
  }
@@ -559,9 +563,30 @@ class StableBrowser {
559
563
  let selector = selectors.locators[j];
560
564
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
561
565
  }
562
- let element = await this._locate_internal(selectors, info, _params, timeout);
566
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
563
567
  if (!element.rerun) {
564
- return element;
568
+ const randomToken = Math.random().toString(36).substring(7);
569
+ element.evaluate((el, randomToken) => {
570
+ el.setAttribute("data-blinq-id-" + randomToken, "");
571
+ }, randomToken);
572
+ // if (element._frame) {
573
+ // return element;
574
+ // }
575
+ const scope = element._frame ?? element.page();
576
+ let newElementSelector = "[data-blinq-id-" + randomToken + "]";
577
+ let prefixSelector = "";
578
+ const frameControlSelector = " >> internal:control=enter-frame";
579
+ const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
580
+ if (frameSelectorIndex !== -1) {
581
+ // remove everything after the >> internal:control=enter-frame
582
+ const frameSelector = element._selector.substring(0, frameSelectorIndex);
583
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
584
+ }
585
+ // if (element?._frame?._selector) {
586
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
587
+ // }
588
+ const newSelector = prefixSelector + newElementSelector;
589
+ return scope.locator(newSelector);
565
590
  }
566
591
  }
567
592
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -634,7 +659,7 @@ class StableBrowser {
634
659
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
635
660
  if (Date.now() - startTime > timeout) {
636
661
  info.failCause.iframeNotFound = true;
637
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
662
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
638
663
  throw new Error("unable to locate iframe " + selectors.iframe_src);
639
664
  }
640
665
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -659,7 +684,7 @@ class StableBrowser {
659
684
  return bodyContent;
660
685
  });
661
686
  }
662
- async _locate_internal(selectors, info, _params, timeout = 30000) {
687
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
663
688
  if (!info) {
664
689
  info = {};
665
690
  info.failCause = {};
@@ -708,18 +733,13 @@ class StableBrowser {
708
733
  }
709
734
  // info.log += "scanning locators in priority 1" + "\n";
710
735
  let onlyPriority3 = selectorsLocators[0].priority === 3;
711
- result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly);
736
+ result = await this._scanLocatorsGroup(locatorsByPriority["1"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
712
737
  if (result.foundElements.length === 0) {
713
738
  // info.log += "scanning locators in priority 2" + "\n";
714
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
715
- }
716
- if (result.foundElements.length === 0 && onlyPriority3) {
717
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
739
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
718
740
  }
719
- else {
720
- if (result.foundElements.length === 0 && !highPriorityOnly) {
721
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
722
- }
741
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
742
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
723
743
  }
724
744
  let foundElements = result.foundElements;
725
745
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -763,7 +783,7 @@ class StableBrowser {
763
783
  break;
764
784
  }
765
785
  if (Date.now() - startTime > highPriorityTimeout) {
766
- info.log += "high priority timeout, will try all elements" + "\n";
786
+ //info.log += "high priority timeout, will try all elements" + "\n";
767
787
  highPriorityOnly = false;
768
788
  if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
769
789
  lazy_scroll = true;
@@ -771,10 +791,15 @@ class StableBrowser {
771
791
  }
772
792
  }
773
793
  if (Date.now() - startTime > visibleOnlyTimeout) {
774
- info.log += "visible only timeout, will try all elements" + "\n";
794
+ //info.log += "visible only timeout, will try all elements" + "\n";
775
795
  visibleOnly = false;
776
796
  }
777
797
  await new Promise((resolve) => setTimeout(resolve, 1000));
798
+ // sheck of more of half of the timeout has passed
799
+ if (Date.now() - startTime > timeout / 2) {
800
+ highPriorityOnly = false;
801
+ visibleOnly = false;
802
+ }
778
803
  }
779
804
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
780
805
  // if (info.locatorLog) {
@@ -785,10 +810,12 @@ class StableBrowser {
785
810
  // }
786
811
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
787
812
  info.failCause.locatorNotFound = true;
788
- info.failCause.lastError = "failed to locate unique element";
813
+ if (!info?.failCause?.lastError) {
814
+ info.failCause.lastError = `failed to locate ${formatElementName(selectors.element_name)}, ${locatorsCount > 0 ? `${locatorsCount} matching elements found` : "no matching elements found"}`;
815
+ }
789
816
  throw new Error("failed to locate first element no elements found, " + info.log);
790
817
  }
791
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
818
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
792
819
  let foundElements = [];
793
820
  const result = {
794
821
  foundElements: foundElements,
@@ -796,7 +823,7 @@ class StableBrowser {
796
823
  for (let i = 0; i < locatorsGroup.length; i++) {
797
824
  let foundLocators = [];
798
825
  try {
799
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
826
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
800
827
  }
801
828
  catch (e) {
802
829
  // this call can fail it the browser is navigating
@@ -804,7 +831,7 @@ class StableBrowser {
804
831
  // this.logger.debug(e);
805
832
  foundLocators = [];
806
833
  try {
807
- await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly);
834
+ await this._collectLocatorInformation(locatorsGroup, i, this.page, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
808
835
  }
809
836
  catch (e) {
810
837
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -819,9 +846,40 @@ class StableBrowser {
819
846
  result.locatorIndex = i;
820
847
  }
821
848
  if (foundLocators.length > 1) {
822
- info.failCause.foundMultiple = true;
823
- if (info.locatorLog) {
824
- info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
849
+ // remove elements that consume the same space with 10 pixels tolerance
850
+ const boxes = [];
851
+ for (let j = 0; j < foundLocators.length; j++) {
852
+ boxes.push({ box: await foundLocators[j].boundingBox(), locator: foundLocators[j] });
853
+ }
854
+ for (let j = 0; j < boxes.length; j++) {
855
+ for (let k = 0; k < boxes.length; k++) {
856
+ if (j === k) {
857
+ continue;
858
+ }
859
+ // check if x, y, width, height are the same with 10 pixels tolerance
860
+ if (Math.abs(boxes[j].box.x - boxes[k].box.x) < 10 &&
861
+ Math.abs(boxes[j].box.y - boxes[k].box.y) < 10 &&
862
+ Math.abs(boxes[j].box.width - boxes[k].box.width) < 10 &&
863
+ Math.abs(boxes[j].box.height - boxes[k].box.height) < 10) {
864
+ // as the element is not unique, will remove it
865
+ boxes.splice(k, 1);
866
+ k--;
867
+ }
868
+ }
869
+ }
870
+ if (boxes.length === 1) {
871
+ result.foundElements.push({
872
+ locator: boxes[0].locator.first(),
873
+ box: boxes[0].box,
874
+ unique: true,
875
+ });
876
+ result.locatorIndex = i;
877
+ }
878
+ else {
879
+ info.failCause.foundMultiple = true;
880
+ if (info.locatorLog) {
881
+ info.locatorLog.setLocatorSearchStatus(JSON.stringify(locatorsGroup[i]), "FOUND_NOT_UNIQUE");
882
+ }
825
883
  }
826
884
  }
827
885
  }
@@ -932,25 +990,14 @@ class StableBrowser {
932
990
  options,
933
991
  world,
934
992
  text: "Click element",
993
+ _text: "Click on " + selectors.element_name,
935
994
  type: Types.CLICK,
936
995
  operation: "click",
937
996
  log: "***** click on " + selectors.element_name + " *****\n",
938
997
  };
939
998
  try {
940
999
  await _preCommand(state, this);
941
- // if (state.options && state.options.context) {
942
- // state.selectors.locators[0].text = state.options.context;
943
- // }
944
- try {
945
- await state.element.click();
946
- // await new Promise((resolve) => setTimeout(resolve, 1000));
947
- }
948
- catch (e) {
949
- // await this.closeUnexpectedPopups();
950
- state.element = await this._locate(selectors, state.info, _params);
951
- await state.element.dispatchEvent("click");
952
- // await new Promise((resolve) => setTimeout(resolve, 1000));
953
- }
1000
+ await performAction("click", state.element, options, this, state, _params);
954
1001
  await this.waitForPageLoad();
955
1002
  return state.info;
956
1003
  }
@@ -961,6 +1008,38 @@ class StableBrowser {
961
1008
  _commandFinally(state, this);
962
1009
  }
963
1010
  }
1011
+ async waitForElement(selectors, _params, options = {}, world = null) {
1012
+ const timeout = this._getFindElementTimeout(options);
1013
+ const state = {
1014
+ selectors,
1015
+ _params,
1016
+ options,
1017
+ world,
1018
+ text: "Wait for element",
1019
+ _text: "Wait for " + selectors.element_name,
1020
+ type: Types.WAIT_ELEMENT,
1021
+ operation: "waitForElement",
1022
+ log: "***** wait for " + selectors.element_name + " *****\n",
1023
+ };
1024
+ let found = false;
1025
+ try {
1026
+ await _preCommand(state, this);
1027
+ // if (state.options && state.options.context) {
1028
+ // state.selectors.locators[0].text = state.options.context;
1029
+ // }
1030
+ await state.element.waitFor({ timeout: timeout });
1031
+ found = true;
1032
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1033
+ }
1034
+ catch (e) {
1035
+ console.error("Error on waitForElement", e);
1036
+ // await _commandError(state, e, this);
1037
+ }
1038
+ finally {
1039
+ _commandFinally(state, this);
1040
+ }
1041
+ return found;
1042
+ }
964
1043
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
965
1044
  const state = {
966
1045
  selectors,
@@ -969,6 +1048,7 @@ class StableBrowser {
969
1048
  world,
970
1049
  type: checked ? Types.CHECK : Types.UNCHECK,
971
1050
  text: checked ? `Check element` : `Uncheck element`,
1051
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
972
1052
  operation: "setCheck",
973
1053
  log: "***** check " + selectors.element_name + " *****\n",
974
1054
  };
@@ -978,9 +1058,15 @@ class StableBrowser {
978
1058
  // let element = await this._locate(selectors, info, _params);
979
1059
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
980
1060
  try {
981
- // await this._highlightElements(element);
1061
+ // if (world && world.screenshot && !world.screenshotPath) {
1062
+ // console.log(`Highlighting while running from recorder`);
1063
+ await this._highlightElements(state.element);
982
1064
  await state.element.setChecked(checked);
983
1065
  await new Promise((resolve) => setTimeout(resolve, 1000));
1066
+ // await this._unHighlightElements(element);
1067
+ // }
1068
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1069
+ // await this._unHighlightElements(element);
984
1070
  }
985
1071
  catch (e) {
986
1072
  if (e.message && e.message.includes("did not change its state")) {
@@ -1012,22 +1098,13 @@ class StableBrowser {
1012
1098
  world,
1013
1099
  type: Types.HOVER,
1014
1100
  text: `Hover element`,
1101
+ _text: `Hover on ${selectors.element_name}`,
1015
1102
  operation: "hover",
1016
1103
  log: "***** hover " + selectors.element_name + " *****\n",
1017
1104
  };
1018
1105
  try {
1019
1106
  await _preCommand(state, this);
1020
- try {
1021
- await state.element.hover();
1022
- await new Promise((resolve) => setTimeout(resolve, 1000));
1023
- }
1024
- catch (e) {
1025
- //await this.closeUnexpectedPopups();
1026
- state.info.log += "hover failed, will try again" + "\n";
1027
- state.element = await this._locate(selectors, state.info, _params);
1028
- await state.element.hover({ timeout: 10000 });
1029
- await new Promise((resolve) => setTimeout(resolve, 1000));
1030
- }
1107
+ await performAction("hover", state.element, options, this, state, _params);
1031
1108
  await _screenshot(state, this);
1032
1109
  await this.waitForPageLoad();
1033
1110
  return state.info;
@@ -1051,6 +1128,7 @@ class StableBrowser {
1051
1128
  value: values.toString(),
1052
1129
  type: Types.SELECT,
1053
1130
  text: `Select option: ${values}`,
1131
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1054
1132
  operation: "selectOption",
1055
1133
  log: "***** select option " + selectors.element_name + " *****\n",
1056
1134
  };
@@ -1085,6 +1163,7 @@ class StableBrowser {
1085
1163
  highlight: false,
1086
1164
  type: Types.TYPE_PRESS,
1087
1165
  text: `Type value: ${_value}`,
1166
+ _text: `Type value: ${_value}`,
1088
1167
  operation: "type",
1089
1168
  log: "",
1090
1169
  };
@@ -1164,6 +1243,7 @@ class StableBrowser {
1164
1243
  world,
1165
1244
  type: Types.SET_DATE_TIME,
1166
1245
  text: `Set date time value: ${value}`,
1246
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1167
1247
  operation: "setDateTime",
1168
1248
  log: "***** set date time value " + selectors.element_name + " *****\n",
1169
1249
  throwError: false,
@@ -1171,7 +1251,7 @@ class StableBrowser {
1171
1251
  try {
1172
1252
  await _preCommand(state, this);
1173
1253
  try {
1174
- await state.element.click();
1254
+ await performAction("click", state.element, options, this, state, _params);
1175
1255
  await new Promise((resolve) => setTimeout(resolve, 500));
1176
1256
  if (format) {
1177
1257
  state.value = dayjs(state.value).format(format);
@@ -1235,9 +1315,13 @@ class StableBrowser {
1235
1315
  world,
1236
1316
  type: Types.FILL,
1237
1317
  text: `Click type input with value: ${_value}`,
1318
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1238
1319
  operation: "clickType",
1239
1320
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1240
1321
  };
1322
+ if (!options) {
1323
+ options = {};
1324
+ }
1241
1325
  if (newValue !== _value) {
1242
1326
  //this.logger.info(_value + "=" + newValue);
1243
1327
  _value = newValue;
@@ -1245,7 +1329,7 @@ class StableBrowser {
1245
1329
  try {
1246
1330
  await _preCommand(state, this);
1247
1331
  state.info.value = _value;
1248
- if (options === null || options === undefined || !options.press) {
1332
+ if (!options.press) {
1249
1333
  try {
1250
1334
  let currentValue = await state.element.inputValue();
1251
1335
  if (currentValue) {
@@ -1256,13 +1340,9 @@ class StableBrowser {
1256
1340
  this.logger.info("unable to clear input value");
1257
1341
  }
1258
1342
  }
1259
- if (options === null || options === undefined || options.press) {
1260
- try {
1261
- await state.element.click({ timeout: 5000 });
1262
- }
1263
- catch (e) {
1264
- await state.element.dispatchEvent("click");
1265
- }
1343
+ if (options.press) {
1344
+ options.timeout = 5000;
1345
+ await performAction("click", state.element, options, this, state, _params);
1266
1346
  }
1267
1347
  else {
1268
1348
  try {
@@ -1300,7 +1380,12 @@ class StableBrowser {
1300
1380
  await this.waitForPageLoad();
1301
1381
  }
1302
1382
  else if (enter === false) {
1303
- await state.element.dispatchEvent("change");
1383
+ try {
1384
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1385
+ }
1386
+ catch (e) {
1387
+ // ignore
1388
+ }
1304
1389
  //await this.page.keyboard.press("Tab");
1305
1390
  }
1306
1391
  else {
@@ -1352,6 +1437,7 @@ class StableBrowser {
1352
1437
  return await this._getText(selectors, 0, _params, options, info, world);
1353
1438
  }
1354
1439
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1440
+ const timeout = this._getFindElementTimeout(options);
1355
1441
  _validateSelectors(selectors);
1356
1442
  let screenshotId = null;
1357
1443
  let screenshotPath = null;
@@ -1361,7 +1447,7 @@ class StableBrowser {
1361
1447
  }
1362
1448
  info.operation = "getText";
1363
1449
  info.selectors = selectors;
1364
- let element = await this._locate(selectors, info, _params);
1450
+ let element = await this._locate(selectors, info, _params, timeout);
1365
1451
  if (climb > 0) {
1366
1452
  const climbArray = [];
1367
1453
  for (let i = 0; i < climb; i++) {
@@ -1380,6 +1466,18 @@ class StableBrowser {
1380
1466
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1381
1467
  try {
1382
1468
  await this._highlightElements(element);
1469
+ // if (world && world.screenshot && !world.screenshotPath) {
1470
+ // // console.log(`Highlighting for get text while running from recorder`);
1471
+ // this._highlightElements(element)
1472
+ // .then(async () => {
1473
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
1474
+ // this._unhighlightElements(element).then(
1475
+ // () => {}
1476
+ // // console.log(`Unhighlighting vrtr in recorder is successful`)
1477
+ // );
1478
+ // })
1479
+ // .catch(e);
1480
+ // }
1383
1481
  const elementText = await element.innerText();
1384
1482
  return {
1385
1483
  text: elementText,
@@ -1391,7 +1489,7 @@ class StableBrowser {
1391
1489
  }
1392
1490
  catch (e) {
1393
1491
  //await this.closeUnexpectedPopups();
1394
- this.logger.info("no innerText will use textContent");
1492
+ this.logger.info("no innerText, will use textContent");
1395
1493
  const elementText = await element.textContent();
1396
1494
  return { text: elementText, screenshotId, screenshotPath, value: value };
1397
1495
  }
@@ -1416,6 +1514,7 @@ class StableBrowser {
1416
1514
  highlight: false,
1417
1515
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1418
1516
  text: `Verify element contains pattern: ${pattern}`,
1517
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1419
1518
  operation: "containsPattern",
1420
1519
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1421
1520
  };
@@ -1451,6 +1550,8 @@ class StableBrowser {
1451
1550
  }
1452
1551
  }
1453
1552
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1553
+ const timeout = this._getFindElementTimeout(options);
1554
+ const startTime = Date.now();
1454
1555
  const state = {
1455
1556
  selectors,
1456
1557
  _params,
@@ -1477,62 +1578,54 @@ class StableBrowser {
1477
1578
  }
1478
1579
  let foundObj = null;
1479
1580
  try {
1480
- await _preCommand(state, this);
1481
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1482
- if (foundObj && foundObj.element) {
1483
- await this.scrollIfNeeded(foundObj.element, state.info);
1484
- }
1485
- await _screenshot(state, this);
1486
- const dateAlternatives = findDateAlternatives(text);
1487
- const numberAlternatives = findNumberAlternatives(text);
1488
- if (dateAlternatives.date) {
1489
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1490
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1491
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1492
- return state.info;
1581
+ while (Date.now() - startTime < timeout) {
1582
+ try {
1583
+ await _preCommand(state, this);
1584
+ foundObj = await this._getText(selectors, climb, _params, { timeout: 3000 }, state.info, world);
1585
+ if (foundObj && foundObj.element) {
1586
+ await this.scrollIfNeeded(foundObj.element, state.info);
1493
1587
  }
1494
- }
1495
- throw new Error("element doesn't contain text " + text);
1496
- }
1497
- else if (numberAlternatives.number) {
1498
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1499
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1500
- foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1588
+ await _screenshot(state, this);
1589
+ const dateAlternatives = findDateAlternatives(text);
1590
+ const numberAlternatives = findNumberAlternatives(text);
1591
+ if (dateAlternatives.date) {
1592
+ for (let i = 0; i < dateAlternatives.dates.length; i++) {
1593
+ if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1594
+ foundObj?.value?.includes(dateAlternatives.dates[i])) {
1595
+ return state.info;
1596
+ }
1597
+ }
1598
+ }
1599
+ else if (numberAlternatives.number) {
1600
+ for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1601
+ if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1602
+ foundObj?.value?.includes(numberAlternatives.numbers[i])) {
1603
+ return state.info;
1604
+ }
1605
+ }
1606
+ }
1607
+ else if (foundObj?.text.includes(text) || foundObj?.value?.includes(text)) {
1501
1608
  return state.info;
1502
1609
  }
1503
1610
  }
1504
- throw new Error("element doesn't contain text " + text);
1505
- }
1506
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1507
- state.info.foundText = foundObj?.text;
1508
- state.info.value = foundObj?.value;
1509
- throw new Error("element doesn't contain text " + text);
1611
+ catch (e) {
1612
+ // Log error but continue retrying until timeout is reached
1613
+ this.logger.warn("Retrying containsText due to: " + e.message);
1614
+ }
1615
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
1510
1616
  }
1511
- return state.info;
1617
+ state.info.foundText = foundObj?.text;
1618
+ state.info.value = foundObj?.value;
1619
+ throw new Error("element doesn't contain text " + text);
1512
1620
  }
1513
1621
  catch (e) {
1514
1622
  await _commandError(state, e, this);
1623
+ throw e;
1515
1624
  }
1516
1625
  finally {
1517
1626
  _commandFinally(state, this);
1518
1627
  }
1519
1628
  }
1520
- _getDataFile(world = null) {
1521
- let dataFile = null;
1522
- if (world && world.reportFolder) {
1523
- dataFile = path.join(world.reportFolder, "data.json");
1524
- }
1525
- else if (this.reportFolder) {
1526
- dataFile = path.join(this.reportFolder, "data.json");
1527
- }
1528
- else if (this.context && this.context.reportFolder) {
1529
- dataFile = path.join(this.context.reportFolder, "data.json");
1530
- }
1531
- else {
1532
- dataFile = "data.json";
1533
- }
1534
- return dataFile;
1535
- }
1536
1629
  async waitForUserInput(message, world = null) {
1537
1630
  if (!message) {
1538
1631
  message = "# Wait for user input. Press any key to continue";
@@ -1561,13 +1654,22 @@ class StableBrowser {
1561
1654
  return;
1562
1655
  }
1563
1656
  // if data file exists, load it
1564
- const dataFile = this._getDataFile(world);
1657
+ const dataFile = _getDataFile(world, this.context, this);
1565
1658
  let data = this.getTestData(world);
1566
1659
  // merge the testData with the existing data
1567
1660
  Object.assign(data, testData);
1568
1661
  // save the data to the file
1569
1662
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1570
1663
  }
1664
+ overwriteTestData(testData, world = null) {
1665
+ if (!testData) {
1666
+ return;
1667
+ }
1668
+ // if data file exists, load it
1669
+ const dataFile = _getDataFile(world, this.context, this);
1670
+ // save the data to the file
1671
+ fs.writeFileSync(dataFile, JSON.stringify(testData, null, 2));
1672
+ }
1571
1673
  _getDataFilePath(fileName) {
1572
1674
  let dataFile = path.join(this.project_path, "data", fileName);
1573
1675
  if (fs.existsSync(dataFile)) {
@@ -1664,7 +1766,7 @@ class StableBrowser {
1664
1766
  }
1665
1767
  }
1666
1768
  getTestData(world = null) {
1667
- const dataFile = this._getDataFile(world);
1769
+ const dataFile = _getDataFile(world, this.context, this);
1668
1770
  let data = {};
1669
1771
  if (fs.existsSync(dataFile)) {
1670
1772
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1751,6 +1853,15 @@ class StableBrowser {
1751
1853
  document.documentElement.clientWidth,
1752
1854
  ])));
1753
1855
  let screenshotBuffer = null;
1856
+ // if (focusedElement) {
1857
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1858
+ // await this._unhighlightElements(focusedElement);
1859
+ // await new Promise((resolve) => setTimeout(resolve, 100));
1860
+ // console.log(`Unhighlighted previous element`);
1861
+ // }
1862
+ // if (focusedElement) {
1863
+ // await this._highlightElements(focusedElement);
1864
+ // }
1754
1865
  if (this.context.browserName === "chromium") {
1755
1866
  const client = await playContext.newCDPSession(this.page);
1756
1867
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1772,6 +1883,10 @@ class StableBrowser {
1772
1883
  else {
1773
1884
  screenshotBuffer = await this.page.screenshot();
1774
1885
  }
1886
+ // if (focusedElement) {
1887
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1888
+ // await this._unhighlightElements(focusedElement);
1889
+ // }
1775
1890
  let image = await Jimp.read(screenshotBuffer);
1776
1891
  // Get the image dimensions
1777
1892
  const { width, height } = image.bitmap;
@@ -1784,6 +1899,7 @@ class StableBrowser {
1784
1899
  else {
1785
1900
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1786
1901
  }
1902
+ return screenshotBuffer;
1787
1903
  }
1788
1904
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1789
1905
  const state = {
@@ -1819,8 +1935,10 @@ class StableBrowser {
1819
1935
  world,
1820
1936
  type: Types.EXTRACT,
1821
1937
  text: `Extract attribute from element`,
1938
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1822
1939
  operation: "extractAttribute",
1823
1940
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1941
+ allowDisabled: true,
1824
1942
  };
1825
1943
  await new Promise((resolve) => setTimeout(resolve, 2000));
1826
1944
  try {
@@ -1835,6 +1953,9 @@ class StableBrowser {
1835
1953
  case "value":
1836
1954
  state.value = await state.element.inputValue();
1837
1955
  break;
1956
+ case "text":
1957
+ state.value = await state.element.textContent();
1958
+ break;
1838
1959
  default:
1839
1960
  state.value = await state.element.getAttribute(attribute);
1840
1961
  break;
@@ -1842,6 +1963,7 @@ class StableBrowser {
1842
1963
  state.info.value = state.value;
1843
1964
  this.setTestData({ [variable]: state.value }, world);
1844
1965
  this.logger.info("set test data: " + variable + "=" + state.value);
1966
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1845
1967
  return state.info;
1846
1968
  }
1847
1969
  catch (e) {
@@ -1860,18 +1982,28 @@ class StableBrowser {
1860
1982
  options,
1861
1983
  world,
1862
1984
  type: Types.VERIFY_ATTRIBUTE,
1985
+ highlight: true,
1986
+ screenshot: true,
1863
1987
  text: `Verify element attribute`,
1988
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1864
1989
  operation: "verifyAttribute",
1865
1990
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1991
+ allowDisabled: true,
1866
1992
  };
1867
1993
  await new Promise((resolve) => setTimeout(resolve, 2000));
1868
1994
  let val;
1995
+ let expectedValue;
1869
1996
  try {
1870
1997
  await _preCommand(state, this);
1998
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1999
+ state.info.expectedValue = expectedValue;
1871
2000
  switch (attribute) {
1872
2001
  case "innerText":
1873
2002
  val = String(await state.element.innerText());
1874
2003
  break;
2004
+ case "text":
2005
+ val = String(await state.element.textContent());
2006
+ break;
1875
2007
  case "value":
1876
2008
  val = String(await state.element.inputValue());
1877
2009
  break;
@@ -1889,19 +2021,22 @@ class StableBrowser {
1889
2021
  val = String(await state.element.getAttribute(attribute));
1890
2022
  break;
1891
2023
  }
2024
+ state.info.value = val;
1892
2025
  let regex;
1893
- if (value.startsWith("/") && value.endsWith("/")) {
1894
- const patternBody = value.slice(1, -1);
2026
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2027
+ const patternBody = expectedValue.slice(1, -1);
1895
2028
  regex = new RegExp(patternBody, "g");
1896
2029
  }
1897
2030
  else {
1898
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2031
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1899
2032
  regex = new RegExp(escapedPattern, "g");
1900
2033
  }
1901
2034
  if (!val.match(regex)) {
1902
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
2035
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2036
+ state.info.failCause.assertionFailed = true;
2037
+ state.info.failCause.lastError = errorMessage;
2038
+ throw new Error(errorMessage);
1903
2039
  }
1904
- state.info.expectedValue = val;
1905
2040
  return state.info;
1906
2041
  }
1907
2042
  catch (e) {
@@ -2000,27 +2135,32 @@ class StableBrowser {
2000
2135
  async _highlightElements(scope, css) {
2001
2136
  try {
2002
2137
  if (!scope) {
2138
+ // console.log(`Scope is not defined`);
2003
2139
  return;
2004
2140
  }
2005
2141
  if (!css) {
2006
2142
  scope
2007
2143
  .evaluate((node) => {
2008
2144
  if (node && node.style) {
2009
- let originalBorder = node.style.border;
2010
- node.style.border = "2px solid red";
2145
+ let originalOutline = node.style.outline;
2146
+ // console.log(`Original outline was: ${originalOutline}`);
2147
+ // node.__previousOutline = originalOutline;
2148
+ node.style.outline = "2px solid red";
2149
+ // console.log(`New outline is: ${node.style.outline}`);
2011
2150
  if (window) {
2012
2151
  window.addEventListener("beforeunload", function (e) {
2013
- node.style.border = originalBorder;
2152
+ node.style.outline = originalOutline;
2014
2153
  });
2015
2154
  }
2016
2155
  setTimeout(function () {
2017
- node.style.border = originalBorder;
2156
+ node.style.outline = originalOutline;
2018
2157
  }, 2000);
2019
2158
  }
2020
2159
  })
2021
2160
  .then(() => { })
2022
2161
  .catch((e) => {
2023
2162
  // ignore
2163
+ // console.error(`Could not highlight node : ${e}`);
2024
2164
  });
2025
2165
  }
2026
2166
  else {
@@ -2036,17 +2176,18 @@ class StableBrowser {
2036
2176
  if (!element.style) {
2037
2177
  return;
2038
2178
  }
2039
- var originalBorder = element.style.border;
2179
+ let originalOutline = element.style.outline;
2180
+ element.__previousOutline = originalOutline;
2040
2181
  // Set the new border to be red and 2px solid
2041
- element.style.border = "2px solid red";
2182
+ element.style.outline = "2px solid red";
2042
2183
  if (window) {
2043
2184
  window.addEventListener("beforeunload", function (e) {
2044
- element.style.border = originalBorder;
2185
+ element.style.outline = originalOutline;
2045
2186
  });
2046
2187
  }
2047
2188
  // Set a timeout to revert to the original border after 2 seconds
2048
2189
  setTimeout(function () {
2049
- element.style.border = originalBorder;
2190
+ element.style.outline = originalOutline;
2050
2191
  }, 2000);
2051
2192
  }
2052
2193
  return;
@@ -2054,6 +2195,7 @@ class StableBrowser {
2054
2195
  .then(() => { })
2055
2196
  .catch((e) => {
2056
2197
  // ignore
2198
+ // console.error(`Could not highlight css: ${e}`);
2057
2199
  });
2058
2200
  }
2059
2201
  }
@@ -2061,6 +2203,54 @@ class StableBrowser {
2061
2203
  console.debug(error);
2062
2204
  }
2063
2205
  }
2206
+ // async _unhighlightElements(scope, css) {
2207
+ // try {
2208
+ // if (!scope) {
2209
+ // return;
2210
+ // }
2211
+ // if (!css) {
2212
+ // scope
2213
+ // .evaluate((node) => {
2214
+ // if (node && node.style) {
2215
+ // if (!node.__previousOutline) {
2216
+ // node.style.outline = "";
2217
+ // } else {
2218
+ // node.style.outline = node.__previousOutline;
2219
+ // }
2220
+ // }
2221
+ // })
2222
+ // .then(() => {})
2223
+ // .catch((e) => {
2224
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2225
+ // });
2226
+ // } else {
2227
+ // scope
2228
+ // .evaluate(([css]) => {
2229
+ // if (!css) {
2230
+ // return;
2231
+ // }
2232
+ // let elements = Array.from(document.querySelectorAll(css));
2233
+ // for (i = 0; i < elements.length; i++) {
2234
+ // let element = elements[i];
2235
+ // if (!element.style) {
2236
+ // return;
2237
+ // }
2238
+ // if (!element.__previousOutline) {
2239
+ // element.style.outline = "";
2240
+ // } else {
2241
+ // element.style.outline = element.__previousOutline;
2242
+ // }
2243
+ // }
2244
+ // })
2245
+ // .then(() => {})
2246
+ // .catch((e) => {
2247
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2248
+ // });
2249
+ // }
2250
+ // } catch (error) {
2251
+ // // console.debug(error);
2252
+ // }
2253
+ // }
2064
2254
  async verifyPagePath(pathPart, options = {}, world = null) {
2065
2255
  const startTime = Date.now();
2066
2256
  let error = null;
@@ -2105,6 +2295,69 @@ class StableBrowser {
2105
2295
  _reportToWorld(world, {
2106
2296
  type: Types.VERIFY_PAGE_PATH,
2107
2297
  text: "Verify page path",
2298
+ _text: "Verify the page path contains " + pathPart,
2299
+ screenshotId,
2300
+ result: error
2301
+ ? {
2302
+ status: "FAILED",
2303
+ startTime,
2304
+ endTime,
2305
+ message: error?.message,
2306
+ }
2307
+ : {
2308
+ status: "PASSED",
2309
+ startTime,
2310
+ endTime,
2311
+ },
2312
+ info: info,
2313
+ });
2314
+ }
2315
+ }
2316
+ async verifyPageTitle(title, options = {}, world = null) {
2317
+ const startTime = Date.now();
2318
+ let error = null;
2319
+ let screenshotId = null;
2320
+ let screenshotPath = null;
2321
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2322
+ const info = {};
2323
+ info.log = "***** verify page title " + title + " *****\n";
2324
+ info.operation = "verifyPageTitle";
2325
+ const newValue = await this._replaceWithLocalData(title, world);
2326
+ if (newValue !== title) {
2327
+ this.logger.info(title + "=" + newValue);
2328
+ title = newValue;
2329
+ }
2330
+ info.title = title;
2331
+ try {
2332
+ for (let i = 0; i < 30; i++) {
2333
+ const foundTitle = await this.page.title();
2334
+ if (!foundTitle.includes(title)) {
2335
+ if (i === 29) {
2336
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2337
+ }
2338
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2339
+ continue;
2340
+ }
2341
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2342
+ return info;
2343
+ }
2344
+ }
2345
+ catch (e) {
2346
+ //await this.closeUnexpectedPopups();
2347
+ this.logger.error("verify page title failed " + info.log);
2348
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2349
+ info.screenshotPath = screenshotPath;
2350
+ Object.assign(e, { info: info });
2351
+ error = e;
2352
+ // throw e;
2353
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2354
+ }
2355
+ finally {
2356
+ const endTime = Date.now();
2357
+ _reportToWorld(world, {
2358
+ type: Types.VERIFY_PAGE_PATH,
2359
+ text: "Verify page title",
2360
+ _text: "Verify the page title contains " + title,
2108
2361
  screenshotId,
2109
2362
  result: error
2110
2363
  ? {
@@ -2122,27 +2375,27 @@ class StableBrowser {
2122
2375
  });
2123
2376
  }
2124
2377
  }
2125
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2378
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2126
2379
  const frames = this.page.frames();
2127
2380
  let results = [];
2128
- let ignoreCase = !(text.startsWith("/") && text.endsWith("/"));
2381
+ // let ignoreCase = false;
2129
2382
  for (let i = 0; i < frames.length; i++) {
2130
2383
  if (dateAlternatives.date) {
2131
2384
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2132
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2385
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2133
2386
  result.frame = frames[i];
2134
2387
  results.push(result);
2135
2388
  }
2136
2389
  }
2137
2390
  else if (numberAlternatives.number) {
2138
2391
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2139
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, ignoreCase, {});
2392
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2140
2393
  result.frame = frames[i];
2141
2394
  results.push(result);
2142
2395
  }
2143
2396
  }
2144
2397
  else {
2145
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, ignoreCase, {});
2398
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2146
2399
  result.frame = frames[i];
2147
2400
  results.push(result);
2148
2401
  }
@@ -2161,11 +2414,15 @@ class StableBrowser {
2161
2414
  scroll: false,
2162
2415
  highlight: false,
2163
2416
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2164
- text: `Verify text exists in page`,
2417
+ text: `Verify the text '${text}' exists in page`,
2418
+ _text: `Verify the text '${text}' exists in page`,
2165
2419
  operation: "verifyTextExistInPage",
2166
2420
  log: "***** verify text " + text + " exists in page *****\n",
2167
2421
  };
2168
- const timeout = this._getLoadTimeout(options);
2422
+ if (testForRegex(text)) {
2423
+ text = text.replace(/\\"/g, '"');
2424
+ }
2425
+ const timeout = this._getFindElementTimeout(options);
2169
2426
  await new Promise((resolve) => setTimeout(resolve, 2000));
2170
2427
  const newValue = await this._replaceWithLocalData(text, world);
2171
2428
  if (newValue !== text) {
@@ -2178,7 +2435,15 @@ class StableBrowser {
2178
2435
  await _preCommand(state, this);
2179
2436
  state.info.text = text;
2180
2437
  while (true) {
2181
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2438
+ let resultWithElementsFound = {
2439
+ length: 0,
2440
+ };
2441
+ try {
2442
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2443
+ }
2444
+ catch (error) {
2445
+ // ignore
2446
+ }
2182
2447
  if (resultWithElementsFound.length === 0) {
2183
2448
  if (Date.now() - state.startTime > timeout) {
2184
2449
  throw new Error(`Text ${text} not found in page`);
@@ -2186,18 +2451,40 @@ class StableBrowser {
2186
2451
  await new Promise((resolve) => setTimeout(resolve, 1000));
2187
2452
  continue;
2188
2453
  }
2189
- if (resultWithElementsFound[0].randomToken) {
2190
- const frame = resultWithElementsFound[0].frame;
2191
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2192
- await this._highlightElements(frame, dataAttribute);
2193
- const element = await frame.locator(dataAttribute).first();
2194
- if (element) {
2195
- await this.scrollIfNeeded(element, state.info);
2196
- await element.dispatchEvent("bvt_verify_page_contains_text");
2454
+ try {
2455
+ if (resultWithElementsFound[0].randomToken) {
2456
+ const frame = resultWithElementsFound[0].frame;
2457
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2458
+ await this._highlightElements(frame, dataAttribute);
2459
+ // if (world && world.screenshot && !world.screenshotPath) {
2460
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2461
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2462
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2463
+ // this._unhighlightElements(frame, dataAttribute)
2464
+ // .then(async () => {
2465
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2466
+ // })
2467
+ // .catch(
2468
+ // (e) => {}
2469
+ // console.error(e)
2470
+ // );
2471
+ // });
2472
+ // }
2473
+ const element = await frame.locator(dataAttribute).first();
2474
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2475
+ // await this._unhighlightElements(frame, dataAttribute);
2476
+ if (element) {
2477
+ await this.scrollIfNeeded(element, state.info);
2478
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2479
+ // await _screenshot(state, this, element);
2480
+ }
2197
2481
  }
2482
+ await _screenshot(state, this);
2483
+ return state.info;
2484
+ }
2485
+ catch (error) {
2486
+ console.error(error);
2198
2487
  }
2199
- await _screenshot(state, this);
2200
- return state.info;
2201
2488
  }
2202
2489
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2203
2490
  }
@@ -2218,11 +2505,15 @@ class StableBrowser {
2218
2505
  scroll: false,
2219
2506
  highlight: false,
2220
2507
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2221
- text: `Verify text does not exist in page`,
2508
+ text: `Verify the text '${text}' does not exist in page`,
2509
+ _text: `Verify the text '${text}' does not exist in page`,
2222
2510
  operation: "verifyTextNotExistInPage",
2223
2511
  log: "***** verify text " + text + " does not exist in page *****\n",
2224
2512
  };
2225
- const timeout = this._getLoadTimeout(options);
2513
+ if (testForRegex(text)) {
2514
+ text = text.replace(/\\"/g, '"');
2515
+ }
2516
+ const timeout = this._getFindElementTimeout(options);
2226
2517
  await new Promise((resolve) => setTimeout(resolve, 2000));
2227
2518
  const newValue = await this._replaceWithLocalData(text, world);
2228
2519
  if (newValue !== text) {
@@ -2234,8 +2525,16 @@ class StableBrowser {
2234
2525
  try {
2235
2526
  await _preCommand(state, this);
2236
2527
  state.info.text = text;
2528
+ let resultWithElementsFound = {
2529
+ length: null, // initial cannot be 0
2530
+ };
2237
2531
  while (true) {
2238
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2532
+ try {
2533
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2534
+ }
2535
+ catch (error) {
2536
+ // ignore
2537
+ }
2239
2538
  if (resultWithElementsFound.length === 0) {
2240
2539
  await _screenshot(state, this);
2241
2540
  return state.info;
@@ -2265,10 +2564,11 @@ class StableBrowser {
2265
2564
  highlight: false,
2266
2565
  type: Types.VERIFY_TEXT_WITH_RELATION,
2267
2566
  text: `Verify text with relation to another text`,
2567
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2268
2568
  operation: "verify_text_with_relation",
2269
2569
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2270
2570
  };
2271
- const timeout = this._getLoadTimeout(options);
2571
+ const timeout = this._getFindElementTimeout(options);
2272
2572
  await new Promise((resolve) => setTimeout(resolve, 2000));
2273
2573
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2274
2574
  if (newValue !== textAnchor) {
@@ -2286,8 +2586,16 @@ class StableBrowser {
2286
2586
  try {
2287
2587
  await _preCommand(state, this);
2288
2588
  state.info.text = textToVerify;
2589
+ let resultWithElementsFound = {
2590
+ length: 0,
2591
+ };
2289
2592
  while (true) {
2290
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2593
+ try {
2594
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2595
+ }
2596
+ catch (error) {
2597
+ // ignore
2598
+ }
2291
2599
  if (resultWithElementsFound.length === 0) {
2292
2600
  if (Date.now() - state.startTime > timeout) {
2293
2601
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2295,51 +2603,56 @@ class StableBrowser {
2295
2603
  await new Promise((resolve) => setTimeout(resolve, 1000));
2296
2604
  continue;
2297
2605
  }
2298
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2299
- foundAncore = true;
2300
- const result = resultWithElementsFound[i];
2301
- const token = result.randomToken;
2302
- const frame = result.frame;
2303
- const css = `[data-blinq-id="blinq-id-${token}"]`;
2304
- const findResult = await frame.evaluate(([css, climb, textToVerify, token]) => {
2305
- const elements = Array.from(document.querySelectorAll(css));
2306
- for (let i = 0; i < elements.length; i++) {
2307
- const element = elements[i];
2308
- let climbParent = element;
2309
- for (let j = 0; j < climb; j++) {
2310
- climbParent = climbParent.parentElement;
2311
- if (!climbParent) {
2312
- break;
2606
+ try {
2607
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2608
+ foundAncore = true;
2609
+ const result = resultWithElementsFound[i];
2610
+ const token = result.randomToken;
2611
+ const frame = result.frame;
2612
+ let css = `[data-blinq-id-${token}]`;
2613
+ const climbArray1 = [];
2614
+ for (let i = 0; i < climb; i++) {
2615
+ climbArray1.push("..");
2616
+ }
2617
+ let climbXpath = "xpath=" + climbArray1.join("/");
2618
+ css = css + " >> " + climbXpath;
2619
+ const count = await frame.locator(css).count();
2620
+ for (let j = 0; j < count; j++) {
2621
+ const continer = await frame.locator(css).nth(j);
2622
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, true, true, {});
2623
+ if (result.elementCount > 0) {
2624
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2625
+ await this._highlightElements(frame, dataAttribute);
2626
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2627
+ // if (world && world.screenshot && !world.screenshotPath) {
2628
+ // console.log(`Highlighting for vtrt while running from recorder`);
2629
+ // this._highlightElements(frame, dataAttribute)
2630
+ // .then(async () => {
2631
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2632
+ // this._unhighlightElements(frame, dataAttribute).then(
2633
+ // () => {}
2634
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2635
+ // );
2636
+ // })
2637
+ // .catch(e);
2638
+ // }
2639
+ //await this._highlightElements(frame, cssAnchor);
2640
+ const element = await frame.locator(dataAttribute).first();
2641
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2642
+ // await this._unhighlightElements(frame, dataAttribute);
2643
+ if (element) {
2644
+ await this.scrollIfNeeded(element, state.info);
2645
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2313
2646
  }
2314
- }
2315
- if (!climbParent) {
2316
- continue;
2317
- }
2318
- const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
2319
- if (foundElements.length > 0) {
2320
- // set the container element attribute
2321
- element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
2322
- climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
2323
- foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
2324
- return { found: true };
2647
+ await _screenshot(state, this);
2648
+ return state.info;
2325
2649
  }
2326
2650
  }
2327
- return { found: false };
2328
- }, [css, climb, textToVerify, result.randomToken]);
2329
- if (findResult.found === true) {
2330
- const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
2331
- const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2332
- await this._highlightElements(frame, dataAttribute);
2333
- await this._highlightElements(frame, cssAnchor);
2334
- const element = await frame.locator(dataAttribute).first();
2335
- if (element) {
2336
- await this.scrollIfNeeded(element, state.info);
2337
- await element.dispatchEvent("bvt_verify_page_contains_text");
2338
- }
2339
- await _screenshot(state, this);
2340
- return state.info;
2341
2651
  }
2342
2652
  }
2653
+ catch (error) {
2654
+ console.error(error);
2655
+ }
2343
2656
  }
2344
2657
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2345
2658
  }
@@ -2350,6 +2663,30 @@ class StableBrowser {
2350
2663
  _commandFinally(state, this);
2351
2664
  }
2352
2665
  }
2666
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2667
+ const frames = this.page.frames();
2668
+ let results = [];
2669
+ let ignoreCase = false;
2670
+ for (let i = 0; i < frames.length; i++) {
2671
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2672
+ result.frame = frames[i];
2673
+ const climbArray = [];
2674
+ for (let i = 0; i < climb; i++) {
2675
+ climbArray.push("..");
2676
+ }
2677
+ let climbXpath = "xpath=" + climbArray.join("/");
2678
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2679
+ const count = await frames[i].locator(newLocator).count();
2680
+ if (count > 0) {
2681
+ result.elementCount = count;
2682
+ result.locator = newLocator;
2683
+ results.push(result);
2684
+ }
2685
+ }
2686
+ // state.info.results = results;
2687
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2688
+ return resultWithElementsFound;
2689
+ }
2353
2690
  async visualVerification(text, options = {}, world = null) {
2354
2691
  const startTime = Date.now();
2355
2692
  let error = null;
@@ -2368,10 +2705,13 @@ class StableBrowser {
2368
2705
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2369
2706
  info.screenshotPath = screenshotPath;
2370
2707
  const screenshot = await this.takeScreenshot();
2371
- const request = {
2372
- method: "POST",
2708
+ let request = {
2709
+ method: "post",
2710
+ maxBodyLength: Infinity,
2373
2711
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2374
2712
  headers: {
2713
+ "x-bvt-project-id": path.basename(this.project_path),
2714
+ "x-source": "aaa",
2375
2715
  "Content-Type": "application/json",
2376
2716
  Authorization: `Bearer ${process.env.TOKEN}`,
2377
2717
  },
@@ -2380,7 +2720,7 @@ class StableBrowser {
2380
2720
  screenshot: screenshot,
2381
2721
  }),
2382
2722
  };
2383
- let result = await this.context.api.request(request);
2723
+ const result = await axios.request(request);
2384
2724
  if (result.data.status !== true) {
2385
2725
  throw new Error("Visual validation failed");
2386
2726
  }
@@ -2408,6 +2748,7 @@ class StableBrowser {
2408
2748
  _reportToWorld(world, {
2409
2749
  type: Types.VERIFY_VISUAL,
2410
2750
  text: "Visual verification",
2751
+ _text: "Visual verification of " + text,
2411
2752
  screenshotId,
2412
2753
  result: error
2413
2754
  ? {
@@ -2674,6 +3015,32 @@ class StableBrowser {
2674
3015
  }
2675
3016
  return timeout;
2676
3017
  }
3018
+ _getFindElementTimeout(options) {
3019
+ if (options && options.timeout) {
3020
+ return options.timeout;
3021
+ }
3022
+ if (this.configuration.find_element_timeout) {
3023
+ return this.configuration.find_element_timeout;
3024
+ }
3025
+ return 30000;
3026
+ }
3027
+ async saveStoreState(path = null, world = null) {
3028
+ const storageState = await this.page.context().storageState();
3029
+ //const testDataFile = _getDataFile(world, this.context, this);
3030
+ if (path) {
3031
+ // save { storageState: storageState } into the path
3032
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3033
+ }
3034
+ else {
3035
+ await this.setTestData({ storageState: storageState }, world);
3036
+ }
3037
+ }
3038
+ async restoreSaveState(path = null, world = null) {
3039
+ await refreshBrowser(this, path, world);
3040
+ this.registerEventListeners(this.context);
3041
+ registerNetworkEvents(this.world, this, this.context, this.page);
3042
+ registerDownloadEvent(this.page, this.world, this.context);
3043
+ }
2677
3044
  async waitForPageLoad(options = {}, world = null) {
2678
3045
  let timeout = this._getLoadTimeout(options);
2679
3046
  const promiseArray = [];
@@ -2741,6 +3108,7 @@ class StableBrowser {
2741
3108
  highlight: false,
2742
3109
  type: Types.CLOSE_PAGE,
2743
3110
  text: `Close page`,
3111
+ _text: `Close the page`,
2744
3112
  operation: "closePage",
2745
3113
  log: "***** close page *****\n",
2746
3114
  throwError: false,
@@ -2757,8 +3125,95 @@ class StableBrowser {
2757
3125
  _commandFinally(state, this);
2758
3126
  }
2759
3127
  }
3128
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3129
+ let operation = null;
3130
+ if (!options || !options.operation) {
3131
+ throw new Error("operation is not defined");
3132
+ }
3133
+ operation = options.operation;
3134
+ // validate operation is one of the supported operations
3135
+ if (operation != "click" && operation != "hover+click") {
3136
+ throw new Error("operation is not supported");
3137
+ }
3138
+ const state = {
3139
+ options,
3140
+ world,
3141
+ locate: false,
3142
+ scroll: false,
3143
+ highlight: false,
3144
+ type: Types.TABLE_OPERATION,
3145
+ text: `Table operation`,
3146
+ _text: `Table ${operation} operation`,
3147
+ operation: operation,
3148
+ log: "***** Table operation *****\n",
3149
+ };
3150
+ const timeout = this._getFindElementTimeout(options);
3151
+ try {
3152
+ await _preCommand(state, this);
3153
+ const start = Date.now();
3154
+ let cellArea = null;
3155
+ while (true) {
3156
+ try {
3157
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3158
+ if (cellArea) {
3159
+ break;
3160
+ }
3161
+ }
3162
+ catch (e) {
3163
+ // ignore
3164
+ }
3165
+ if (Date.now() - start > timeout) {
3166
+ throw new Error(`Cell not found in table`);
3167
+ }
3168
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3169
+ }
3170
+ switch (operation) {
3171
+ case "click":
3172
+ if (!options.css) {
3173
+ // will click in the center of the cell
3174
+ let xOffset = 0;
3175
+ let yOffset = 0;
3176
+ if (options.xOffset) {
3177
+ xOffset = options.xOffset;
3178
+ }
3179
+ if (options.yOffset) {
3180
+ yOffset = options.yOffset;
3181
+ }
3182
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3183
+ }
3184
+ else {
3185
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3186
+ if (results.length === 0) {
3187
+ throw new Error(`Element not found in cell area`);
3188
+ }
3189
+ state.element = results[0];
3190
+ await performAction("click", state.element, options, this, state, _params);
3191
+ }
3192
+ break;
3193
+ case "hover+click":
3194
+ if (!options.css) {
3195
+ throw new Error("css is not defined");
3196
+ }
3197
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3198
+ if (results.length === 0) {
3199
+ throw new Error(`Element not found in cell area`);
3200
+ }
3201
+ state.element = results[0];
3202
+ await performAction("hover+click", state.element, options, this, state, _params);
3203
+ break;
3204
+ default:
3205
+ throw new Error("operation is not supported");
3206
+ }
3207
+ }
3208
+ catch (e) {
3209
+ await _commandError(state, e, this);
3210
+ }
3211
+ finally {
3212
+ _commandFinally(state, this);
3213
+ }
3214
+ }
2760
3215
  saveTestDataAsGlobal(options, world) {
2761
- const dataFile = this._getDataFile(world);
3216
+ const dataFile = _getDataFile(world, this.context, this);
2762
3217
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2763
3218
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2764
3219
  }
@@ -2788,6 +3243,7 @@ class StableBrowser {
2788
3243
  _reportToWorld(world, {
2789
3244
  type: Types.SET_VIEWPORT,
2790
3245
  text: "set viewport size to " + width + "x" + hight,
3246
+ _text: "Set the viewport size to " + width + "x" + hight,
2791
3247
  screenshotId,
2792
3248
  result: error
2793
3249
  ? {
@@ -2875,6 +3331,9 @@ class StableBrowser {
2875
3331
  else {
2876
3332
  this.stepName = "step " + this.stepIndex;
2877
3333
  }
3334
+ if (this.context) {
3335
+ this.context.examplesRow = extractStepExampleParameters(step);
3336
+ }
2878
3337
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2879
3338
  if (this.context.browserObject.context) {
2880
3339
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2887,6 +3346,41 @@ class StableBrowser {
2887
3346
  this.saveTestDataAsGlobal({}, world);
2888
3347
  }
2889
3348
  }
3349
+ if (this.initSnapshotTaken === false) {
3350
+ this.initSnapshotTaken = true;
3351
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3352
+ const snapshot = await this.getAriaSnapshot();
3353
+ if (snapshot) {
3354
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3355
+ }
3356
+ }
3357
+ }
3358
+ }
3359
+ async getAriaSnapshot() {
3360
+ try {
3361
+ // find the page url
3362
+ const url = await this.page.url();
3363
+ // extract the path from the url
3364
+ const path = new URL(url).pathname;
3365
+ // get the page title
3366
+ const title = await this.page.title();
3367
+ // go over other frams
3368
+ const frames = this.page.frames();
3369
+ const snapshots = [];
3370
+ const content = [`- path: ${path}`, `- title: ${title}`];
3371
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3372
+ for (let i = 0; i < frames.length; i++) {
3373
+ content.push(`- frame: ${i}`);
3374
+ const frame = frames[i];
3375
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3376
+ content.push(snapshot);
3377
+ }
3378
+ return content.join("\n");
3379
+ }
3380
+ catch (e) {
3381
+ console.error(e);
3382
+ }
3383
+ return null;
2890
3384
  }
2891
3385
  async afterStep(world, step) {
2892
3386
  this.stepName = null;
@@ -2895,6 +3389,23 @@ class StableBrowser {
2895
3389
  await this.context.browserObject.context.tracing.stopChunk({
2896
3390
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
2897
3391
  });
3392
+ if (world && world.attach) {
3393
+ await world.attach(JSON.stringify({
3394
+ type: "trace",
3395
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3396
+ }), "application/json+trace");
3397
+ }
3398
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3399
+ }
3400
+ }
3401
+ if (this.context) {
3402
+ this.context.examplesRow = null;
3403
+ }
3404
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3405
+ const snapshot = await this.getAriaSnapshot();
3406
+ if (snapshot) {
3407
+ const obj = {};
3408
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
2898
3409
  }
2899
3410
  }
2900
3411
  }