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