automation_model 1.0.577-dev → 1.0.577-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,66 +338,60 @@ 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++) {
338
349
  climbArray.push("..");
339
350
  }
340
351
  let climbXpath = "xpath=" + climbArray.join("/");
341
- return textElementCss + " >> " + climbXpath + " >> " + css;
342
- }
343
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
344
- //const stringifyText = JSON.stringify(text);
345
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
346
- const options = {
347
- innerText: true,
348
- };
349
- if (regex) {
350
- options.singleRegex = true;
351
- }
352
- if (tag) {
353
- options.tag = tag;
354
- }
355
- if (!(partial === true)) {
356
- options.exactMatch = true;
357
- }
358
- if (text.startsWith("/") && text.endsWith("/")) {
359
- if (text.length < 3) {
360
- throw new Error("Invalid regex pattern: empty pattern");
361
- }
362
- try {
363
- const pattern = text.slice(1, -1);
364
- new RegExp(pattern); // Validate pattern
365
- text = pattern;
366
- options.singleRegex = true;
367
- }
368
- catch (e) {
369
- throw new Error(`Invalid regex pattern: ${e.message}`);
370
- }
371
- }
372
- const elements = window.findMatchingElements(text, options);
373
- let randomToken = null;
374
- const foundElements = [];
375
- let elementCount = 0;
376
- if (elements.length > 0) {
377
- for (let i = 0; i < elements.length; i++) {
378
- if (randomToken === null) {
379
- randomToken = Math.random().toString(36).substring(7);
352
+ let resultCss = textElementCss + " >> " + climbXpath;
353
+ if (css) {
354
+ resultCss = resultCss + " >> " + css;
355
+ }
356
+ return resultCss;
357
+ }
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;
380
374
  }
381
- let element = elements[i];
382
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
383
- elementCount++;
384
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;
385
389
  }
386
- return { elementCount: elementCount, randomToken: randomToken };
387
- }, [text1, tag1, regex1, partial1]);
390
+ tagCount++;
391
+ }
392
+ return { elementCount: tagCount, randomToken };
388
393
  }
389
- 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) {
390
395
  if (!info) {
391
396
  info = {};
392
397
  }
@@ -409,23 +414,24 @@ class StableBrowser {
409
414
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
410
415
  let locator = null;
411
416
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
412
- 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);
413
419
  if (!locatorString) {
414
420
  info.failCause.textNotFound = true;
415
- 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}`;
416
422
  return;
417
423
  }
418
424
  locator = await this._getLocator({ css: locatorString }, scope, _params);
419
425
  }
420
426
  else if (locatorSearch.text) {
421
427
  let text = _fixUsingParams(locatorSearch.text, _params);
422
- 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);
423
429
  if (result.elementCount === 0) {
424
430
  info.failCause.textNotFound = true;
425
- info.failCause.lastError = "failed to locate element by text: " + text;
431
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
426
432
  return;
427
433
  }
428
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
434
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
429
435
  if (locatorSearch.childCss) {
430
436
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
431
437
  }
@@ -461,7 +467,7 @@ class StableBrowser {
461
467
  if (!visibleOnly) {
462
468
  visible = true;
463
469
  }
464
- if (visible && enabled) {
470
+ if (visible && (allowDisabled || enabled)) {
465
471
  foundLocators.push(locator.nth(j));
466
472
  if (info.locatorLog) {
467
473
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
@@ -474,9 +480,11 @@ class StableBrowser {
474
480
  info.printMessages = {};
475
481
  }
476
482
  if (info.locatorLog && !visible) {
483
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
477
484
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
478
485
  }
479
486
  if (info.locatorLog && !enabled) {
487
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
480
488
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
481
489
  }
482
490
  if (!info.printMessages[j.toString()]) {
@@ -544,7 +552,7 @@ class StableBrowser {
544
552
  }
545
553
  return { rerun: false };
546
554
  }
547
- async _locate(selectors, info, _params, timeout) {
555
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
548
556
  if (!timeout) {
549
557
  timeout = 30000;
550
558
  }
@@ -554,9 +562,30 @@ class StableBrowser {
554
562
  let selector = selectors.locators[j];
555
563
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
556
564
  }
557
- let element = await this._locate_internal(selectors, info, _params, timeout);
565
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
558
566
  if (!element.rerun) {
559
- 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);
560
589
  }
561
590
  }
562
591
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -629,7 +658,7 @@ class StableBrowser {
629
658
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
630
659
  if (Date.now() - startTime > timeout) {
631
660
  info.failCause.iframeNotFound = true;
632
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
661
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
633
662
  throw new Error("unable to locate iframe " + selectors.iframe_src);
634
663
  }
635
664
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -654,7 +683,7 @@ class StableBrowser {
654
683
  return bodyContent;
655
684
  });
656
685
  }
657
- async _locate_internal(selectors, info, _params, timeout = 30000) {
686
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
658
687
  if (!info) {
659
688
  info = {};
660
689
  info.failCause = {};
@@ -703,18 +732,13 @@ class StableBrowser {
703
732
  }
704
733
  // info.log += "scanning locators in priority 1" + "\n";
705
734
  let onlyPriority3 = selectorsLocators[0].priority === 3;
706
- 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);
707
736
  if (result.foundElements.length === 0) {
708
737
  // info.log += "scanning locators in priority 2" + "\n";
709
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
738
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
710
739
  }
711
- if (result.foundElements.length === 0 && onlyPriority3) {
712
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
713
- }
714
- else {
715
- if (result.foundElements.length === 0 && !highPriorityOnly) {
716
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
717
- }
740
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
741
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
718
742
  }
719
743
  let foundElements = result.foundElements;
720
744
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -758,7 +782,7 @@ class StableBrowser {
758
782
  break;
759
783
  }
760
784
  if (Date.now() - startTime > highPriorityTimeout) {
761
- info.log += "high priority timeout, will try all elements" + "\n";
785
+ //info.log += "high priority timeout, will try all elements" + "\n";
762
786
  highPriorityOnly = false;
763
787
  if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
764
788
  lazy_scroll = true;
@@ -766,10 +790,15 @@ class StableBrowser {
766
790
  }
767
791
  }
768
792
  if (Date.now() - startTime > visibleOnlyTimeout) {
769
- info.log += "visible only timeout, will try all elements" + "\n";
793
+ //info.log += "visible only timeout, will try all elements" + "\n";
770
794
  visibleOnly = false;
771
795
  }
772
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
+ }
773
802
  }
774
803
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
775
804
  // if (info.locatorLog) {
@@ -780,10 +809,12 @@ class StableBrowser {
780
809
  // }
781
810
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
782
811
  info.failCause.locatorNotFound = true;
783
- 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
+ }
784
815
  throw new Error("failed to locate first element no elements found, " + info.log);
785
816
  }
786
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
817
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
787
818
  let foundElements = [];
788
819
  const result = {
789
820
  foundElements: foundElements,
@@ -791,7 +822,7 @@ class StableBrowser {
791
822
  for (let i = 0; i < locatorsGroup.length; i++) {
792
823
  let foundLocators = [];
793
824
  try {
794
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
825
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
795
826
  }
796
827
  catch (e) {
797
828
  // this call can fail it the browser is navigating
@@ -799,7 +830,7 @@ class StableBrowser {
799
830
  // this.logger.debug(e);
800
831
  foundLocators = [];
801
832
  try {
802
- 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);
803
834
  }
804
835
  catch (e) {
805
836
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -814,9 +845,40 @@ class StableBrowser {
814
845
  result.locatorIndex = i;
815
846
  }
816
847
  if (foundLocators.length > 1) {
817
- info.failCause.foundMultiple = true;
818
- if (info.locatorLog) {
819
- 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
+ }
820
882
  }
821
883
  }
822
884
  }
@@ -927,25 +989,14 @@ class StableBrowser {
927
989
  options,
928
990
  world,
929
991
  text: "Click element",
992
+ _text: "Click on " + selectors.element_name,
930
993
  type: Types.CLICK,
931
994
  operation: "click",
932
995
  log: "***** click on " + selectors.element_name + " *****\n",
933
996
  };
934
997
  try {
935
998
  await _preCommand(state, this);
936
- // if (state.options && state.options.context) {
937
- // state.selectors.locators[0].text = state.options.context;
938
- // }
939
- try {
940
- await state.element.click();
941
- // await new Promise((resolve) => setTimeout(resolve, 1000));
942
- }
943
- catch (e) {
944
- // await this.closeUnexpectedPopups();
945
- state.element = await this._locate(selectors, state.info, _params);
946
- await state.element.dispatchEvent("click");
947
- // await new Promise((resolve) => setTimeout(resolve, 1000));
948
- }
999
+ await performAction("click", state.element, options, this, state, _params);
949
1000
  await this.waitForPageLoad();
950
1001
  return state.info;
951
1002
  }
@@ -956,6 +1007,38 @@ class StableBrowser {
956
1007
  _commandFinally(state, this);
957
1008
  }
958
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
+ }
959
1042
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
960
1043
  const state = {
961
1044
  selectors,
@@ -964,6 +1047,7 @@ class StableBrowser {
964
1047
  world,
965
1048
  type: checked ? Types.CHECK : Types.UNCHECK,
966
1049
  text: checked ? `Check element` : `Uncheck element`,
1050
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
967
1051
  operation: "setCheck",
968
1052
  log: "***** check " + selectors.element_name + " *****\n",
969
1053
  };
@@ -973,9 +1057,15 @@ class StableBrowser {
973
1057
  // let element = await this._locate(selectors, info, _params);
974
1058
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
975
1059
  try {
976
- // 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);
977
1063
  await state.element.setChecked(checked);
978
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);
979
1069
  }
980
1070
  catch (e) {
981
1071
  if (e.message && e.message.includes("did not change its state")) {
@@ -1007,22 +1097,13 @@ class StableBrowser {
1007
1097
  world,
1008
1098
  type: Types.HOVER,
1009
1099
  text: `Hover element`,
1100
+ _text: `Hover on ${selectors.element_name}`,
1010
1101
  operation: "hover",
1011
1102
  log: "***** hover " + selectors.element_name + " *****\n",
1012
1103
  };
1013
1104
  try {
1014
1105
  await _preCommand(state, this);
1015
- try {
1016
- await state.element.hover();
1017
- await new Promise((resolve) => setTimeout(resolve, 1000));
1018
- }
1019
- catch (e) {
1020
- //await this.closeUnexpectedPopups();
1021
- state.info.log += "hover failed, will try again" + "\n";
1022
- state.element = await this._locate(selectors, state.info, _params);
1023
- await state.element.hover({ timeout: 10000 });
1024
- await new Promise((resolve) => setTimeout(resolve, 1000));
1025
- }
1106
+ await performAction("hover", state.element, options, this, state, _params);
1026
1107
  await _screenshot(state, this);
1027
1108
  await this.waitForPageLoad();
1028
1109
  return state.info;
@@ -1046,6 +1127,7 @@ class StableBrowser {
1046
1127
  value: values.toString(),
1047
1128
  type: Types.SELECT,
1048
1129
  text: `Select option: ${values}`,
1130
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1049
1131
  operation: "selectOption",
1050
1132
  log: "***** select option " + selectors.element_name + " *****\n",
1051
1133
  };
@@ -1080,6 +1162,7 @@ class StableBrowser {
1080
1162
  highlight: false,
1081
1163
  type: Types.TYPE_PRESS,
1082
1164
  text: `Type value: ${_value}`,
1165
+ _text: `Type value: ${_value}`,
1083
1166
  operation: "type",
1084
1167
  log: "",
1085
1168
  };
@@ -1159,6 +1242,7 @@ class StableBrowser {
1159
1242
  world,
1160
1243
  type: Types.SET_DATE_TIME,
1161
1244
  text: `Set date time value: ${value}`,
1245
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1162
1246
  operation: "setDateTime",
1163
1247
  log: "***** set date time value " + selectors.element_name + " *****\n",
1164
1248
  throwError: false,
@@ -1166,7 +1250,7 @@ class StableBrowser {
1166
1250
  try {
1167
1251
  await _preCommand(state, this);
1168
1252
  try {
1169
- await state.element.click();
1253
+ await performAction("click", state.element, options, this, state, _params);
1170
1254
  await new Promise((resolve) => setTimeout(resolve, 500));
1171
1255
  if (format) {
1172
1256
  state.value = dayjs(state.value).format(format);
@@ -1230,9 +1314,13 @@ class StableBrowser {
1230
1314
  world,
1231
1315
  type: Types.FILL,
1232
1316
  text: `Click type input with value: ${_value}`,
1317
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1233
1318
  operation: "clickType",
1234
1319
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1235
1320
  };
1321
+ if (!options) {
1322
+ options = {};
1323
+ }
1236
1324
  if (newValue !== _value) {
1237
1325
  //this.logger.info(_value + "=" + newValue);
1238
1326
  _value = newValue;
@@ -1240,7 +1328,7 @@ class StableBrowser {
1240
1328
  try {
1241
1329
  await _preCommand(state, this);
1242
1330
  state.info.value = _value;
1243
- if (options === null || options === undefined || !options.press) {
1331
+ if (!options.press) {
1244
1332
  try {
1245
1333
  let currentValue = await state.element.inputValue();
1246
1334
  if (currentValue) {
@@ -1251,13 +1339,9 @@ class StableBrowser {
1251
1339
  this.logger.info("unable to clear input value");
1252
1340
  }
1253
1341
  }
1254
- if (options === null || options === undefined || options.press) {
1255
- try {
1256
- await state.element.click({ timeout: 5000 });
1257
- }
1258
- catch (e) {
1259
- await state.element.dispatchEvent("click");
1260
- }
1342
+ if (options.press) {
1343
+ options.timeout = 5000;
1344
+ await performAction("click", state.element, options, this, state, _params);
1261
1345
  }
1262
1346
  else {
1263
1347
  try {
@@ -1295,7 +1379,12 @@ class StableBrowser {
1295
1379
  await this.waitForPageLoad();
1296
1380
  }
1297
1381
  else if (enter === false) {
1298
- await state.element.dispatchEvent("change");
1382
+ try {
1383
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1384
+ }
1385
+ catch (e) {
1386
+ // ignore
1387
+ }
1299
1388
  //await this.page.keyboard.press("Tab");
1300
1389
  }
1301
1390
  else {
@@ -1347,6 +1436,7 @@ class StableBrowser {
1347
1436
  return await this._getText(selectors, 0, _params, options, info, world);
1348
1437
  }
1349
1438
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1439
+ const timeout = this._getFindElementTimeout(options);
1350
1440
  _validateSelectors(selectors);
1351
1441
  let screenshotId = null;
1352
1442
  let screenshotPath = null;
@@ -1356,7 +1446,7 @@ class StableBrowser {
1356
1446
  }
1357
1447
  info.operation = "getText";
1358
1448
  info.selectors = selectors;
1359
- let element = await this._locate(selectors, info, _params);
1449
+ let element = await this._locate(selectors, info, _params, timeout);
1360
1450
  if (climb > 0) {
1361
1451
  const climbArray = [];
1362
1452
  for (let i = 0; i < climb; i++) {
@@ -1375,6 +1465,18 @@ class StableBrowser {
1375
1465
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1376
1466
  try {
1377
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
+ // }
1378
1480
  const elementText = await element.innerText();
1379
1481
  return {
1380
1482
  text: elementText,
@@ -1386,7 +1488,7 @@ class StableBrowser {
1386
1488
  }
1387
1489
  catch (e) {
1388
1490
  //await this.closeUnexpectedPopups();
1389
- this.logger.info("no innerText will use textContent");
1491
+ this.logger.info("no innerText, will use textContent");
1390
1492
  const elementText = await element.textContent();
1391
1493
  return { text: elementText, screenshotId, screenshotPath, value: value };
1392
1494
  }
@@ -1411,6 +1513,7 @@ class StableBrowser {
1411
1513
  highlight: false,
1412
1514
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1413
1515
  text: `Verify element contains pattern: ${pattern}`,
1516
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1414
1517
  operation: "containsPattern",
1415
1518
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1416
1519
  };
@@ -1446,6 +1549,8 @@ class StableBrowser {
1446
1549
  }
1447
1550
  }
1448
1551
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1552
+ const timeout = this._getFindElementTimeout(options);
1553
+ const startTime = Date.now();
1449
1554
  const state = {
1450
1555
  selectors,
1451
1556
  _params,
@@ -1472,62 +1577,54 @@ class StableBrowser {
1472
1577
  }
1473
1578
  let foundObj = null;
1474
1579
  try {
1475
- await _preCommand(state, this);
1476
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1477
- if (foundObj && foundObj.element) {
1478
- await this.scrollIfNeeded(foundObj.element, state.info);
1479
- }
1480
- await _screenshot(state, this);
1481
- const dateAlternatives = findDateAlternatives(text);
1482
- const numberAlternatives = findNumberAlternatives(text);
1483
- if (dateAlternatives.date) {
1484
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1485
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1486
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1487
- 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);
1488
1586
  }
1489
- }
1490
- throw new Error("element doesn't contain text " + text);
1491
- }
1492
- else if (numberAlternatives.number) {
1493
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1494
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1495
- 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)) {
1496
1607
  return state.info;
1497
1608
  }
1498
1609
  }
1499
- throw new Error("element doesn't contain text " + text);
1500
- }
1501
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1502
- state.info.foundText = foundObj?.text;
1503
- state.info.value = foundObj?.value;
1504
- 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
1505
1615
  }
1506
- 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);
1507
1619
  }
1508
1620
  catch (e) {
1509
1621
  await _commandError(state, e, this);
1622
+ throw e;
1510
1623
  }
1511
1624
  finally {
1512
1625
  _commandFinally(state, this);
1513
1626
  }
1514
1627
  }
1515
- _getDataFile(world = null) {
1516
- let dataFile = null;
1517
- if (world && world.reportFolder) {
1518
- dataFile = path.join(world.reportFolder, "data.json");
1519
- }
1520
- else if (this.reportFolder) {
1521
- dataFile = path.join(this.reportFolder, "data.json");
1522
- }
1523
- else if (this.context && this.context.reportFolder) {
1524
- dataFile = path.join(this.context.reportFolder, "data.json");
1525
- }
1526
- else {
1527
- dataFile = "data.json";
1528
- }
1529
- return dataFile;
1530
- }
1531
1628
  async waitForUserInput(message, world = null) {
1532
1629
  if (!message) {
1533
1630
  message = "# Wait for user input. Press any key to continue";
@@ -1556,13 +1653,22 @@ class StableBrowser {
1556
1653
  return;
1557
1654
  }
1558
1655
  // if data file exists, load it
1559
- const dataFile = this._getDataFile(world);
1656
+ const dataFile = _getDataFile(world, this.context, this);
1560
1657
  let data = this.getTestData(world);
1561
1658
  // merge the testData with the existing data
1562
1659
  Object.assign(data, testData);
1563
1660
  // save the data to the file
1564
1661
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1565
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
+ }
1566
1672
  _getDataFilePath(fileName) {
1567
1673
  let dataFile = path.join(this.project_path, "data", fileName);
1568
1674
  if (fs.existsSync(dataFile)) {
@@ -1659,7 +1765,7 @@ class StableBrowser {
1659
1765
  }
1660
1766
  }
1661
1767
  getTestData(world = null) {
1662
- const dataFile = this._getDataFile(world);
1768
+ const dataFile = _getDataFile(world, this.context, this);
1663
1769
  let data = {};
1664
1770
  if (fs.existsSync(dataFile)) {
1665
1771
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1746,6 +1852,15 @@ class StableBrowser {
1746
1852
  document.documentElement.clientWidth,
1747
1853
  ])));
1748
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
+ // }
1749
1864
  if (this.context.browserName === "chromium") {
1750
1865
  const client = await playContext.newCDPSession(this.page);
1751
1866
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1767,6 +1882,10 @@ class StableBrowser {
1767
1882
  else {
1768
1883
  screenshotBuffer = await this.page.screenshot();
1769
1884
  }
1885
+ // if (focusedElement) {
1886
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1887
+ // await this._unhighlightElements(focusedElement);
1888
+ // }
1770
1889
  let image = await Jimp.read(screenshotBuffer);
1771
1890
  // Get the image dimensions
1772
1891
  const { width, height } = image.bitmap;
@@ -1779,6 +1898,7 @@ class StableBrowser {
1779
1898
  else {
1780
1899
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1781
1900
  }
1901
+ return screenshotBuffer;
1782
1902
  }
1783
1903
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1784
1904
  const state = {
@@ -1814,8 +1934,10 @@ class StableBrowser {
1814
1934
  world,
1815
1935
  type: Types.EXTRACT,
1816
1936
  text: `Extract attribute from element`,
1937
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1817
1938
  operation: "extractAttribute",
1818
1939
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1940
+ allowDisabled: true,
1819
1941
  };
1820
1942
  await new Promise((resolve) => setTimeout(resolve, 2000));
1821
1943
  try {
@@ -1830,6 +1952,9 @@ class StableBrowser {
1830
1952
  case "value":
1831
1953
  state.value = await state.element.inputValue();
1832
1954
  break;
1955
+ case "text":
1956
+ state.value = await state.element.textContent();
1957
+ break;
1833
1958
  default:
1834
1959
  state.value = await state.element.getAttribute(attribute);
1835
1960
  break;
@@ -1837,6 +1962,7 @@ class StableBrowser {
1837
1962
  state.info.value = state.value;
1838
1963
  this.setTestData({ [variable]: state.value }, world);
1839
1964
  this.logger.info("set test data: " + variable + "=" + state.value);
1965
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1840
1966
  return state.info;
1841
1967
  }
1842
1968
  catch (e) {
@@ -1855,18 +1981,28 @@ class StableBrowser {
1855
1981
  options,
1856
1982
  world,
1857
1983
  type: Types.VERIFY_ATTRIBUTE,
1984
+ highlight: true,
1985
+ screenshot: true,
1858
1986
  text: `Verify element attribute`,
1987
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1859
1988
  operation: "verifyAttribute",
1860
1989
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1990
+ allowDisabled: true,
1861
1991
  };
1862
1992
  await new Promise((resolve) => setTimeout(resolve, 2000));
1863
1993
  let val;
1994
+ let expectedValue;
1864
1995
  try {
1865
1996
  await _preCommand(state, this);
1997
+ expectedValue = await replaceWithLocalTestData(state.value, world);
1998
+ state.info.expectedValue = expectedValue;
1866
1999
  switch (attribute) {
1867
2000
  case "innerText":
1868
2001
  val = String(await state.element.innerText());
1869
2002
  break;
2003
+ case "text":
2004
+ val = String(await state.element.textContent());
2005
+ break;
1870
2006
  case "value":
1871
2007
  val = String(await state.element.inputValue());
1872
2008
  break;
@@ -1884,19 +2020,22 @@ class StableBrowser {
1884
2020
  val = String(await state.element.getAttribute(attribute));
1885
2021
  break;
1886
2022
  }
2023
+ state.info.value = val;
1887
2024
  let regex;
1888
- if (value.startsWith("/") && value.endsWith("/")) {
1889
- const patternBody = value.slice(1, -1);
2025
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2026
+ const patternBody = expectedValue.slice(1, -1);
1890
2027
  regex = new RegExp(patternBody, "g");
1891
2028
  }
1892
2029
  else {
1893
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2030
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1894
2031
  regex = new RegExp(escapedPattern, "g");
1895
2032
  }
1896
2033
  if (!val.match(regex)) {
1897
- 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);
1898
2038
  }
1899
- state.info.expectedValue = val;
1900
2039
  return state.info;
1901
2040
  }
1902
2041
  catch (e) {
@@ -1995,27 +2134,32 @@ class StableBrowser {
1995
2134
  async _highlightElements(scope, css) {
1996
2135
  try {
1997
2136
  if (!scope) {
2137
+ // console.log(`Scope is not defined`);
1998
2138
  return;
1999
2139
  }
2000
2140
  if (!css) {
2001
2141
  scope
2002
2142
  .evaluate((node) => {
2003
2143
  if (node && node.style) {
2004
- let originalBorder = node.style.border;
2005
- 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}`);
2006
2149
  if (window) {
2007
2150
  window.addEventListener("beforeunload", function (e) {
2008
- node.style.border = originalBorder;
2151
+ node.style.outline = originalOutline;
2009
2152
  });
2010
2153
  }
2011
2154
  setTimeout(function () {
2012
- node.style.border = originalBorder;
2155
+ node.style.outline = originalOutline;
2013
2156
  }, 2000);
2014
2157
  }
2015
2158
  })
2016
2159
  .then(() => { })
2017
2160
  .catch((e) => {
2018
2161
  // ignore
2162
+ // console.error(`Could not highlight node : ${e}`);
2019
2163
  });
2020
2164
  }
2021
2165
  else {
@@ -2031,17 +2175,18 @@ class StableBrowser {
2031
2175
  if (!element.style) {
2032
2176
  return;
2033
2177
  }
2034
- var originalBorder = element.style.border;
2178
+ let originalOutline = element.style.outline;
2179
+ element.__previousOutline = originalOutline;
2035
2180
  // Set the new border to be red and 2px solid
2036
- element.style.border = "2px solid red";
2181
+ element.style.outline = "2px solid red";
2037
2182
  if (window) {
2038
2183
  window.addEventListener("beforeunload", function (e) {
2039
- element.style.border = originalBorder;
2184
+ element.style.outline = originalOutline;
2040
2185
  });
2041
2186
  }
2042
2187
  // Set a timeout to revert to the original border after 2 seconds
2043
2188
  setTimeout(function () {
2044
- element.style.border = originalBorder;
2189
+ element.style.outline = originalOutline;
2045
2190
  }, 2000);
2046
2191
  }
2047
2192
  return;
@@ -2049,6 +2194,7 @@ class StableBrowser {
2049
2194
  .then(() => { })
2050
2195
  .catch((e) => {
2051
2196
  // ignore
2197
+ // console.error(`Could not highlight css: ${e}`);
2052
2198
  });
2053
2199
  }
2054
2200
  }
@@ -2056,6 +2202,54 @@ class StableBrowser {
2056
2202
  console.debug(error);
2057
2203
  }
2058
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
+ // }
2059
2253
  async verifyPagePath(pathPart, options = {}, world = null) {
2060
2254
  const startTime = Date.now();
2061
2255
  let error = null;
@@ -2100,6 +2294,7 @@ class StableBrowser {
2100
2294
  _reportToWorld(world, {
2101
2295
  type: Types.VERIFY_PAGE_PATH,
2102
2296
  text: "Verify page path",
2297
+ _text: "Verify the page path contains " + pathPart,
2103
2298
  screenshotId,
2104
2299
  result: error
2105
2300
  ? {
@@ -2117,26 +2312,89 @@ class StableBrowser {
2117
2312
  });
2118
2313
  }
2119
2314
  }
2120
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
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,
2360
+ screenshotId,
2361
+ result: error
2362
+ ? {
2363
+ status: "FAILED",
2364
+ startTime,
2365
+ endTime,
2366
+ message: error?.message,
2367
+ }
2368
+ : {
2369
+ status: "PASSED",
2370
+ startTime,
2371
+ endTime,
2372
+ },
2373
+ info: info,
2374
+ });
2375
+ }
2376
+ }
2377
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2121
2378
  const frames = this.page.frames();
2122
2379
  let results = [];
2380
+ // let ignoreCase = false;
2123
2381
  for (let i = 0; i < frames.length; i++) {
2124
2382
  if (dateAlternatives.date) {
2125
2383
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2126
- 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, {});
2127
2385
  result.frame = frames[i];
2128
2386
  results.push(result);
2129
2387
  }
2130
2388
  }
2131
2389
  else if (numberAlternatives.number) {
2132
2390
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2133
- 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, {});
2134
2392
  result.frame = frames[i];
2135
2393
  results.push(result);
2136
2394
  }
2137
2395
  }
2138
2396
  else {
2139
- 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, {});
2140
2398
  result.frame = frames[i];
2141
2399
  results.push(result);
2142
2400
  }
@@ -2155,11 +2413,15 @@ class StableBrowser {
2155
2413
  scroll: false,
2156
2414
  highlight: false,
2157
2415
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2158
- text: `Verify text exists in page`,
2416
+ text: `Verify the text '${text}' exists in page`,
2417
+ _text: `Verify the text '${text}' exists in page`,
2159
2418
  operation: "verifyTextExistInPage",
2160
2419
  log: "***** verify text " + text + " exists in page *****\n",
2161
2420
  };
2162
- const timeout = this._getLoadTimeout(options);
2421
+ if (testForRegex(text)) {
2422
+ text = text.replace(/\\"/g, '"');
2423
+ }
2424
+ const timeout = this._getFindElementTimeout(options);
2163
2425
  await new Promise((resolve) => setTimeout(resolve, 2000));
2164
2426
  const newValue = await this._replaceWithLocalData(text, world);
2165
2427
  if (newValue !== text) {
@@ -2172,7 +2434,15 @@ class StableBrowser {
2172
2434
  await _preCommand(state, this);
2173
2435
  state.info.text = text;
2174
2436
  while (true) {
2175
- 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
+ }
2176
2446
  if (resultWithElementsFound.length === 0) {
2177
2447
  if (Date.now() - state.startTime > timeout) {
2178
2448
  throw new Error(`Text ${text} not found in page`);
@@ -2180,18 +2450,40 @@ class StableBrowser {
2180
2450
  await new Promise((resolve) => setTimeout(resolve, 1000));
2181
2451
  continue;
2182
2452
  }
2183
- if (resultWithElementsFound[0].randomToken) {
2184
- const frame = resultWithElementsFound[0].frame;
2185
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2186
- await this._highlightElements(frame, dataAttribute);
2187
- const element = await frame.locator(dataAttribute).first();
2188
- if (element) {
2189
- await this.scrollIfNeeded(element, state.info);
2190
- 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
+ }
2191
2480
  }
2481
+ await _screenshot(state, this);
2482
+ return state.info;
2483
+ }
2484
+ catch (error) {
2485
+ console.error(error);
2192
2486
  }
2193
- await _screenshot(state, this);
2194
- return state.info;
2195
2487
  }
2196
2488
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2197
2489
  }
@@ -2212,11 +2504,15 @@ class StableBrowser {
2212
2504
  scroll: false,
2213
2505
  highlight: false,
2214
2506
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2215
- 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`,
2216
2509
  operation: "verifyTextNotExistInPage",
2217
2510
  log: "***** verify text " + text + " does not exist in page *****\n",
2218
2511
  };
2219
- const timeout = this._getLoadTimeout(options);
2512
+ if (testForRegex(text)) {
2513
+ text = text.replace(/\\"/g, '"');
2514
+ }
2515
+ const timeout = this._getFindElementTimeout(options);
2220
2516
  await new Promise((resolve) => setTimeout(resolve, 2000));
2221
2517
  const newValue = await this._replaceWithLocalData(text, world);
2222
2518
  if (newValue !== text) {
@@ -2228,8 +2524,16 @@ class StableBrowser {
2228
2524
  try {
2229
2525
  await _preCommand(state, this);
2230
2526
  state.info.text = text;
2527
+ let resultWithElementsFound = {
2528
+ length: null, // initial cannot be 0
2529
+ };
2231
2530
  while (true) {
2232
- 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
+ }
2233
2537
  if (resultWithElementsFound.length === 0) {
2234
2538
  await _screenshot(state, this);
2235
2539
  return state.info;
@@ -2259,10 +2563,11 @@ class StableBrowser {
2259
2563
  highlight: false,
2260
2564
  type: Types.VERIFY_TEXT_WITH_RELATION,
2261
2565
  text: `Verify text with relation to another text`,
2566
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2262
2567
  operation: "verify_text_with_relation",
2263
2568
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2264
2569
  };
2265
- const timeout = this._getLoadTimeout(options);
2570
+ const timeout = this._getFindElementTimeout(options);
2266
2571
  await new Promise((resolve) => setTimeout(resolve, 2000));
2267
2572
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2268
2573
  if (newValue !== textAnchor) {
@@ -2280,8 +2585,16 @@ class StableBrowser {
2280
2585
  try {
2281
2586
  await _preCommand(state, this);
2282
2587
  state.info.text = textToVerify;
2588
+ let resultWithElementsFound = {
2589
+ length: 0,
2590
+ };
2283
2591
  while (true) {
2284
- 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
+ }
2285
2598
  if (resultWithElementsFound.length === 0) {
2286
2599
  if (Date.now() - state.startTime > timeout) {
2287
2600
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2289,51 +2602,56 @@ class StableBrowser {
2289
2602
  await new Promise((resolve) => setTimeout(resolve, 1000));
2290
2603
  continue;
2291
2604
  }
2292
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2293
- foundAncore = true;
2294
- const result = resultWithElementsFound[i];
2295
- const token = result.randomToken;
2296
- const frame = result.frame;
2297
- const css = `[data-blinq-id="blinq-id-${token}"]`;
2298
- const findResult = await frame.evaluate(([css, climb, textToVerify, token]) => {
2299
- const elements = Array.from(document.querySelectorAll(css));
2300
- for (let i = 0; i < elements.length; i++) {
2301
- const element = elements[i];
2302
- let climbParent = element;
2303
- for (let j = 0; j < climb; j++) {
2304
- climbParent = climbParent.parentElement;
2305
- if (!climbParent) {
2306
- 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");
2307
2645
  }
2308
- }
2309
- if (!climbParent) {
2310
- continue;
2311
- }
2312
- const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
2313
- if (foundElements.length > 0) {
2314
- // set the container element attribute
2315
- element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
2316
- climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
2317
- foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
2318
- return { found: true };
2646
+ await _screenshot(state, this);
2647
+ return state.info;
2319
2648
  }
2320
2649
  }
2321
- return { found: false };
2322
- }, [css, climb, textToVerify, result.randomToken]);
2323
- if (findResult.found === true) {
2324
- const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
2325
- const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2326
- await this._highlightElements(frame, dataAttribute);
2327
- await this._highlightElements(frame, cssAnchor);
2328
- const element = await frame.locator(dataAttribute).first();
2329
- if (element) {
2330
- await this.scrollIfNeeded(element, state.info);
2331
- await element.dispatchEvent("bvt_verify_page_contains_text");
2332
- }
2333
- await _screenshot(state, this);
2334
- return state.info;
2335
2650
  }
2336
2651
  }
2652
+ catch (error) {
2653
+ console.error(error);
2654
+ }
2337
2655
  }
2338
2656
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2339
2657
  }
@@ -2344,6 +2662,30 @@ class StableBrowser {
2344
2662
  _commandFinally(state, this);
2345
2663
  }
2346
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
+ }
2347
2689
  async visualVerification(text, options = {}, world = null) {
2348
2690
  const startTime = Date.now();
2349
2691
  let error = null;
@@ -2362,10 +2704,13 @@ class StableBrowser {
2362
2704
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2363
2705
  info.screenshotPath = screenshotPath;
2364
2706
  const screenshot = await this.takeScreenshot();
2365
- const request = {
2366
- method: "POST",
2707
+ let request = {
2708
+ method: "post",
2709
+ maxBodyLength: Infinity,
2367
2710
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2368
2711
  headers: {
2712
+ "x-bvt-project-id": path.basename(this.project_path),
2713
+ "x-source": "aaa",
2369
2714
  "Content-Type": "application/json",
2370
2715
  Authorization: `Bearer ${process.env.TOKEN}`,
2371
2716
  },
@@ -2374,7 +2719,7 @@ class StableBrowser {
2374
2719
  screenshot: screenshot,
2375
2720
  }),
2376
2721
  };
2377
- let result = await this.context.api.request(request);
2722
+ const result = await axios.request(request);
2378
2723
  if (result.data.status !== true) {
2379
2724
  throw new Error("Visual validation failed");
2380
2725
  }
@@ -2402,6 +2747,7 @@ class StableBrowser {
2402
2747
  _reportToWorld(world, {
2403
2748
  type: Types.VERIFY_VISUAL,
2404
2749
  text: "Visual verification",
2750
+ _text: "Visual verification of " + text,
2405
2751
  screenshotId,
2406
2752
  result: error
2407
2753
  ? {
@@ -2668,6 +3014,32 @@ class StableBrowser {
2668
3014
  }
2669
3015
  return timeout;
2670
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
+ }
2671
3043
  async waitForPageLoad(options = {}, world = null) {
2672
3044
  let timeout = this._getLoadTimeout(options);
2673
3045
  const promiseArray = [];
@@ -2735,6 +3107,7 @@ class StableBrowser {
2735
3107
  highlight: false,
2736
3108
  type: Types.CLOSE_PAGE,
2737
3109
  text: `Close page`,
3110
+ _text: `Close the page`,
2738
3111
  operation: "closePage",
2739
3112
  log: "***** close page *****\n",
2740
3113
  throwError: false,
@@ -2751,8 +3124,95 @@ class StableBrowser {
2751
3124
  _commandFinally(state, this);
2752
3125
  }
2753
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
+ }
2754
3214
  saveTestDataAsGlobal(options, world) {
2755
- const dataFile = this._getDataFile(world);
3215
+ const dataFile = _getDataFile(world, this.context, this);
2756
3216
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2757
3217
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2758
3218
  }
@@ -2782,6 +3242,7 @@ class StableBrowser {
2782
3242
  _reportToWorld(world, {
2783
3243
  type: Types.SET_VIEWPORT,
2784
3244
  text: "set viewport size to " + width + "x" + hight,
3245
+ _text: "Set the viewport size to " + width + "x" + hight,
2785
3246
  screenshotId,
2786
3247
  result: error
2787
3248
  ? {
@@ -2869,6 +3330,9 @@ class StableBrowser {
2869
3330
  else {
2870
3331
  this.stepName = "step " + this.stepIndex;
2871
3332
  }
3333
+ if (this.context) {
3334
+ this.context.examplesRow = extractStepExampleParameters(step);
3335
+ }
2872
3336
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2873
3337
  if (this.context.browserObject.context) {
2874
3338
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2881,6 +3345,41 @@ class StableBrowser {
2881
3345
  this.saveTestDataAsGlobal({}, world);
2882
3346
  }
2883
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;
2884
3383
  }
2885
3384
  async afterStep(world, step) {
2886
3385
  this.stepName = null;
@@ -2889,6 +3388,23 @@ class StableBrowser {
2889
3388
  await this.context.browserObject.context.tracing.stopChunk({
2890
3389
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
2891
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");
2892
3408
  }
2893
3409
  }
2894
3410
  }