automation_model 1.0.568-dev → 1.0.568-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
  }
@@ -267,8 +278,16 @@ class StableBrowser {
267
278
  _commandFinally(state, this);
268
279
  }
269
280
  }
270
- _getLocator(locator, scope, _params) {
281
+ async _getLocator(locator, scope, _params) {
271
282
  locator = _fixLocatorUsingParams(locator, _params);
283
+ // locator = await this._replaceWithLocalData(locator);
284
+ for (let key in locator) {
285
+ if (typeof locator[key] !== "string")
286
+ continue;
287
+ if (locator[key].includes("{{") && locator[key].includes("}}")) {
288
+ locator[key] = await this._replaceWithLocalData(locator[key], this.world);
289
+ }
290
+ }
272
291
  let locatorReturn;
273
292
  if (locator.role) {
274
293
  if (locator.role[1].nameReg) {
@@ -319,52 +338,60 @@ class StableBrowser {
319
338
  if (css && css.locator) {
320
339
  css = css.locator;
321
340
  }
322
- 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);
323
342
  if (result.elementCount === 0) {
324
343
  return;
325
344
  }
326
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
345
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
327
346
  // css climb to parent element
328
347
  const climbArray = [];
329
348
  for (let i = 0; i < climb; i++) {
330
349
  climbArray.push("..");
331
350
  }
332
351
  let climbXpath = "xpath=" + climbArray.join("/");
333
- return textElementCss + " >> " + climbXpath + " >> " + css;
334
- }
335
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
336
- //const stringifyText = JSON.stringify(text);
337
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
338
- const options = {
339
- innerText: true,
340
- };
341
- if (regex) {
342
- options.singleRegex = true;
343
- }
344
- if (tag) {
345
- options.tag = tag;
346
- }
347
- if (!(partial === true)) {
348
- options.exactMatch = true;
349
- }
350
- const elements = window.findMatchingElements(text, options);
351
- let randomToken = null;
352
- const foundElements = [];
353
- let elementCount = 0;
354
- if (elements.length > 0) {
355
- for (let i = 0; i < elements.length; i++) {
356
- if (randomToken === null) {
357
- 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;
358
374
  }
359
- let element = elements[i];
360
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
361
- elementCount++;
362
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;
363
389
  }
364
- return { elementCount: elementCount, randomToken: randomToken };
365
- }, [text1, tag1, regex1, partial1]);
390
+ tagCount++;
391
+ }
392
+ return { elementCount: tagCount, randomToken };
366
393
  }
367
- 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) {
368
395
  if (!info) {
369
396
  info = {};
370
397
  }
@@ -387,30 +414,31 @@ class StableBrowser {
387
414
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
388
415
  let locator = null;
389
416
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
390
- 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);
391
419
  if (!locatorString) {
392
420
  info.failCause.textNotFound = true;
393
- 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}`;
394
422
  return;
395
423
  }
396
- locator = this._getLocator({ css: locatorString }, scope, _params);
424
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
397
425
  }
398
426
  else if (locatorSearch.text) {
399
427
  let text = _fixUsingParams(locatorSearch.text, _params);
400
- 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);
401
429
  if (result.elementCount === 0) {
402
430
  info.failCause.textNotFound = true;
403
- info.failCause.lastError = "failed to locate element by text: " + text;
431
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
404
432
  return;
405
433
  }
406
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
434
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
407
435
  if (locatorSearch.childCss) {
408
436
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
409
437
  }
410
- locator = this._getLocator(locatorSearch, scope, _params);
438
+ locator = await this._getLocator(locatorSearch, scope, _params);
411
439
  }
412
440
  else {
413
- locator = this._getLocator(locatorSearch, scope, _params);
441
+ locator = await this._getLocator(locatorSearch, scope, _params);
414
442
  }
415
443
  // let cssHref = false;
416
444
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
@@ -439,7 +467,7 @@ class StableBrowser {
439
467
  if (!visibleOnly) {
440
468
  visible = true;
441
469
  }
442
- if (visible && enabled) {
470
+ if (visible && (allowDisabled || enabled)) {
443
471
  foundLocators.push(locator.nth(j));
444
472
  if (info.locatorLog) {
445
473
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
@@ -452,9 +480,11 @@ class StableBrowser {
452
480
  info.printMessages = {};
453
481
  }
454
482
  if (info.locatorLog && !visible) {
483
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
455
484
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
456
485
  }
457
486
  if (info.locatorLog && !enabled) {
487
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
458
488
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
459
489
  }
460
490
  if (!info.printMessages[j.toString()]) {
@@ -522,7 +552,7 @@ class StableBrowser {
522
552
  }
523
553
  return { rerun: false };
524
554
  }
525
- async _locate(selectors, info, _params, timeout) {
555
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
526
556
  if (!timeout) {
527
557
  timeout = 30000;
528
558
  }
@@ -532,9 +562,30 @@ class StableBrowser {
532
562
  let selector = selectors.locators[j];
533
563
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
534
564
  }
535
- let element = await this._locate_internal(selectors, info, _params, timeout);
565
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
536
566
  if (!element.rerun) {
537
- 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);
538
589
  }
539
590
  }
540
591
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -607,7 +658,7 @@ class StableBrowser {
607
658
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
608
659
  if (Date.now() - startTime > timeout) {
609
660
  info.failCause.iframeNotFound = true;
610
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
661
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
611
662
  throw new Error("unable to locate iframe " + selectors.iframe_src);
612
663
  }
613
664
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -632,7 +683,7 @@ class StableBrowser {
632
683
  return bodyContent;
633
684
  });
634
685
  }
635
- async _locate_internal(selectors, info, _params, timeout = 30000) {
686
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
636
687
  if (!info) {
637
688
  info = {};
638
689
  info.failCause = {};
@@ -681,18 +732,13 @@ class StableBrowser {
681
732
  }
682
733
  // info.log += "scanning locators in priority 1" + "\n";
683
734
  let onlyPriority3 = selectorsLocators[0].priority === 3;
684
- 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);
685
736
  if (result.foundElements.length === 0) {
686
737
  // info.log += "scanning locators in priority 2" + "\n";
687
- 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);
688
739
  }
689
- if (result.foundElements.length === 0 && onlyPriority3) {
690
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
691
- }
692
- else {
693
- if (result.foundElements.length === 0 && !highPriorityOnly) {
694
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
695
- }
740
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
741
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
696
742
  }
697
743
  let foundElements = result.foundElements;
698
744
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -736,7 +782,7 @@ class StableBrowser {
736
782
  break;
737
783
  }
738
784
  if (Date.now() - startTime > highPriorityTimeout) {
739
- info.log += "high priority timeout, will try all elements" + "\n";
785
+ //info.log += "high priority timeout, will try all elements" + "\n";
740
786
  highPriorityOnly = false;
741
787
  if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
742
788
  lazy_scroll = true;
@@ -744,10 +790,15 @@ class StableBrowser {
744
790
  }
745
791
  }
746
792
  if (Date.now() - startTime > visibleOnlyTimeout) {
747
- info.log += "visible only timeout, will try all elements" + "\n";
793
+ //info.log += "visible only timeout, will try all elements" + "\n";
748
794
  visibleOnly = false;
749
795
  }
750
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
+ }
751
802
  }
752
803
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
753
804
  // if (info.locatorLog) {
@@ -758,10 +809,12 @@ class StableBrowser {
758
809
  // }
759
810
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
760
811
  info.failCause.locatorNotFound = true;
761
- 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
+ }
762
815
  throw new Error("failed to locate first element no elements found, " + info.log);
763
816
  }
764
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
817
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
765
818
  let foundElements = [];
766
819
  const result = {
767
820
  foundElements: foundElements,
@@ -769,7 +822,7 @@ class StableBrowser {
769
822
  for (let i = 0; i < locatorsGroup.length; i++) {
770
823
  let foundLocators = [];
771
824
  try {
772
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
825
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
773
826
  }
774
827
  catch (e) {
775
828
  // this call can fail it the browser is navigating
@@ -777,7 +830,7 @@ class StableBrowser {
777
830
  // this.logger.debug(e);
778
831
  foundLocators = [];
779
832
  try {
780
- 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);
781
834
  }
782
835
  catch (e) {
783
836
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -792,9 +845,40 @@ class StableBrowser {
792
845
  result.locatorIndex = i;
793
846
  }
794
847
  if (foundLocators.length > 1) {
795
- info.failCause.foundMultiple = true;
796
- if (info.locatorLog) {
797
- info.locatorLog.setLocatorSearchStatus(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
+ }
798
882
  }
799
883
  }
800
884
  }
@@ -905,25 +989,14 @@ class StableBrowser {
905
989
  options,
906
990
  world,
907
991
  text: "Click element",
992
+ _text: "Click on " + selectors.element_name,
908
993
  type: Types.CLICK,
909
994
  operation: "click",
910
995
  log: "***** click on " + selectors.element_name + " *****\n",
911
996
  };
912
997
  try {
913
998
  await _preCommand(state, this);
914
- if (state.options && state.options.context) {
915
- state.selectors.locators[0].text = state.options.context;
916
- }
917
- try {
918
- await state.element.click();
919
- // await new Promise((resolve) => setTimeout(resolve, 1000));
920
- }
921
- catch (e) {
922
- // await this.closeUnexpectedPopups();
923
- state.element = await this._locate(selectors, state.info, _params);
924
- await state.element.dispatchEvent("click");
925
- // await new Promise((resolve) => setTimeout(resolve, 1000));
926
- }
999
+ await performAction("click", state.element, options, this, state, _params);
927
1000
  await this.waitForPageLoad();
928
1001
  return state.info;
929
1002
  }
@@ -934,6 +1007,38 @@ class StableBrowser {
934
1007
  _commandFinally(state, this);
935
1008
  }
936
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
+ }
937
1042
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
938
1043
  const state = {
939
1044
  selectors,
@@ -942,6 +1047,7 @@ class StableBrowser {
942
1047
  world,
943
1048
  type: checked ? Types.CHECK : Types.UNCHECK,
944
1049
  text: checked ? `Check element` : `Uncheck element`,
1050
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
945
1051
  operation: "setCheck",
946
1052
  log: "***** check " + selectors.element_name + " *****\n",
947
1053
  };
@@ -951,9 +1057,15 @@ class StableBrowser {
951
1057
  // let element = await this._locate(selectors, info, _params);
952
1058
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
953
1059
  try {
954
- // 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);
955
1063
  await state.element.setChecked(checked);
956
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);
957
1069
  }
958
1070
  catch (e) {
959
1071
  if (e.message && e.message.includes("did not change its state")) {
@@ -985,22 +1097,13 @@ class StableBrowser {
985
1097
  world,
986
1098
  type: Types.HOVER,
987
1099
  text: `Hover element`,
1100
+ _text: `Hover on ${selectors.element_name}`,
988
1101
  operation: "hover",
989
1102
  log: "***** hover " + selectors.element_name + " *****\n",
990
1103
  };
991
1104
  try {
992
1105
  await _preCommand(state, this);
993
- try {
994
- await state.element.hover();
995
- await new Promise((resolve) => setTimeout(resolve, 1000));
996
- }
997
- catch (e) {
998
- //await this.closeUnexpectedPopups();
999
- state.info.log += "hover failed, will try again" + "\n";
1000
- state.element = await this._locate(selectors, state.info, _params);
1001
- await state.element.hover({ timeout: 10000 });
1002
- await new Promise((resolve) => setTimeout(resolve, 1000));
1003
- }
1106
+ await performAction("hover", state.element, options, this, state, _params);
1004
1107
  await _screenshot(state, this);
1005
1108
  await this.waitForPageLoad();
1006
1109
  return state.info;
@@ -1024,6 +1127,7 @@ class StableBrowser {
1024
1127
  value: values.toString(),
1025
1128
  type: Types.SELECT,
1026
1129
  text: `Select option: ${values}`,
1130
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1027
1131
  operation: "selectOption",
1028
1132
  log: "***** select option " + selectors.element_name + " *****\n",
1029
1133
  };
@@ -1058,6 +1162,7 @@ class StableBrowser {
1058
1162
  highlight: false,
1059
1163
  type: Types.TYPE_PRESS,
1060
1164
  text: `Type value: ${_value}`,
1165
+ _text: `Type value: ${_value}`,
1061
1166
  operation: "type",
1062
1167
  log: "",
1063
1168
  };
@@ -1137,6 +1242,7 @@ class StableBrowser {
1137
1242
  world,
1138
1243
  type: Types.SET_DATE_TIME,
1139
1244
  text: `Set date time value: ${value}`,
1245
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1140
1246
  operation: "setDateTime",
1141
1247
  log: "***** set date time value " + selectors.element_name + " *****\n",
1142
1248
  throwError: false,
@@ -1144,7 +1250,7 @@ class StableBrowser {
1144
1250
  try {
1145
1251
  await _preCommand(state, this);
1146
1252
  try {
1147
- await state.element.click();
1253
+ await performAction("click", state.element, options, this, state, _params);
1148
1254
  await new Promise((resolve) => setTimeout(resolve, 500));
1149
1255
  if (format) {
1150
1256
  state.value = dayjs(state.value).format(format);
@@ -1208,9 +1314,13 @@ class StableBrowser {
1208
1314
  world,
1209
1315
  type: Types.FILL,
1210
1316
  text: `Click type input with value: ${_value}`,
1317
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1211
1318
  operation: "clickType",
1212
1319
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1213
1320
  };
1321
+ if (!options) {
1322
+ options = {};
1323
+ }
1214
1324
  if (newValue !== _value) {
1215
1325
  //this.logger.info(_value + "=" + newValue);
1216
1326
  _value = newValue;
@@ -1218,7 +1328,7 @@ class StableBrowser {
1218
1328
  try {
1219
1329
  await _preCommand(state, this);
1220
1330
  state.info.value = _value;
1221
- if (options === null || options === undefined || !options.press) {
1331
+ if (!options.press) {
1222
1332
  try {
1223
1333
  let currentValue = await state.element.inputValue();
1224
1334
  if (currentValue) {
@@ -1229,13 +1339,9 @@ class StableBrowser {
1229
1339
  this.logger.info("unable to clear input value");
1230
1340
  }
1231
1341
  }
1232
- if (options === null || options === undefined || options.press) {
1233
- try {
1234
- await state.element.click({ timeout: 5000 });
1235
- }
1236
- catch (e) {
1237
- await state.element.dispatchEvent("click");
1238
- }
1342
+ if (options.press) {
1343
+ options.timeout = 5000;
1344
+ await performAction("click", state.element, options, this, state, _params);
1239
1345
  }
1240
1346
  else {
1241
1347
  try {
@@ -1273,7 +1379,12 @@ class StableBrowser {
1273
1379
  await this.waitForPageLoad();
1274
1380
  }
1275
1381
  else if (enter === false) {
1276
- await state.element.dispatchEvent("change");
1382
+ try {
1383
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1384
+ }
1385
+ catch (e) {
1386
+ // ignore
1387
+ }
1277
1388
  //await this.page.keyboard.press("Tab");
1278
1389
  }
1279
1390
  else {
@@ -1325,6 +1436,7 @@ class StableBrowser {
1325
1436
  return await this._getText(selectors, 0, _params, options, info, world);
1326
1437
  }
1327
1438
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1439
+ const timeout = this._getFindElementTimeout(options);
1328
1440
  _validateSelectors(selectors);
1329
1441
  let screenshotId = null;
1330
1442
  let screenshotPath = null;
@@ -1334,7 +1446,7 @@ class StableBrowser {
1334
1446
  }
1335
1447
  info.operation = "getText";
1336
1448
  info.selectors = selectors;
1337
- let element = await this._locate(selectors, info, _params);
1449
+ let element = await this._locate(selectors, info, _params, timeout);
1338
1450
  if (climb > 0) {
1339
1451
  const climbArray = [];
1340
1452
  for (let i = 0; i < climb; i++) {
@@ -1353,6 +1465,18 @@ class StableBrowser {
1353
1465
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1354
1466
  try {
1355
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
+ // }
1356
1480
  const elementText = await element.innerText();
1357
1481
  return {
1358
1482
  text: elementText,
@@ -1364,7 +1488,7 @@ class StableBrowser {
1364
1488
  }
1365
1489
  catch (e) {
1366
1490
  //await this.closeUnexpectedPopups();
1367
- this.logger.info("no innerText will use textContent");
1491
+ this.logger.info("no innerText, will use textContent");
1368
1492
  const elementText = await element.textContent();
1369
1493
  return { text: elementText, screenshotId, screenshotPath, value: value };
1370
1494
  }
@@ -1389,6 +1513,7 @@ class StableBrowser {
1389
1513
  highlight: false,
1390
1514
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1391
1515
  text: `Verify element contains pattern: ${pattern}`,
1516
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1392
1517
  operation: "containsPattern",
1393
1518
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1394
1519
  };
@@ -1424,6 +1549,8 @@ class StableBrowser {
1424
1549
  }
1425
1550
  }
1426
1551
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1552
+ const timeout = this._getFindElementTimeout(options);
1553
+ const startTime = Date.now();
1427
1554
  const state = {
1428
1555
  selectors,
1429
1556
  _params,
@@ -1450,62 +1577,54 @@ class StableBrowser {
1450
1577
  }
1451
1578
  let foundObj = null;
1452
1579
  try {
1453
- await _preCommand(state, this);
1454
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1455
- if (foundObj && foundObj.element) {
1456
- await this.scrollIfNeeded(foundObj.element, state.info);
1457
- }
1458
- await _screenshot(state, this);
1459
- const dateAlternatives = findDateAlternatives(text);
1460
- const numberAlternatives = findNumberAlternatives(text);
1461
- if (dateAlternatives.date) {
1462
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1463
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1464
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1465
- 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);
1466
1586
  }
1467
- }
1468
- throw new Error("element doesn't contain text " + text);
1469
- }
1470
- else if (numberAlternatives.number) {
1471
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1472
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1473
- 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)) {
1474
1607
  return state.info;
1475
1608
  }
1476
1609
  }
1477
- throw new Error("element doesn't contain text " + text);
1478
- }
1479
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1480
- state.info.foundText = foundObj?.text;
1481
- state.info.value = foundObj?.value;
1482
- 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
1483
1615
  }
1484
- 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);
1485
1619
  }
1486
1620
  catch (e) {
1487
1621
  await _commandError(state, e, this);
1622
+ throw e;
1488
1623
  }
1489
1624
  finally {
1490
1625
  _commandFinally(state, this);
1491
1626
  }
1492
1627
  }
1493
- _getDataFile(world = null) {
1494
- let dataFile = null;
1495
- if (world && world.reportFolder) {
1496
- dataFile = path.join(world.reportFolder, "data.json");
1497
- }
1498
- else if (this.reportFolder) {
1499
- dataFile = path.join(this.reportFolder, "data.json");
1500
- }
1501
- else if (this.context && this.context.reportFolder) {
1502
- dataFile = path.join(this.context.reportFolder, "data.json");
1503
- }
1504
- else {
1505
- dataFile = "data.json";
1506
- }
1507
- return dataFile;
1508
- }
1509
1628
  async waitForUserInput(message, world = null) {
1510
1629
  if (!message) {
1511
1630
  message = "# Wait for user input. Press any key to continue";
@@ -1534,13 +1653,22 @@ class StableBrowser {
1534
1653
  return;
1535
1654
  }
1536
1655
  // if data file exists, load it
1537
- const dataFile = this._getDataFile(world);
1656
+ const dataFile = _getDataFile(world, this.context, this);
1538
1657
  let data = this.getTestData(world);
1539
1658
  // merge the testData with the existing data
1540
1659
  Object.assign(data, testData);
1541
1660
  // save the data to the file
1542
1661
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1543
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
+ }
1544
1672
  _getDataFilePath(fileName) {
1545
1673
  let dataFile = path.join(this.project_path, "data", fileName);
1546
1674
  if (fs.existsSync(dataFile)) {
@@ -1637,7 +1765,7 @@ class StableBrowser {
1637
1765
  }
1638
1766
  }
1639
1767
  getTestData(world = null) {
1640
- const dataFile = this._getDataFile(world);
1768
+ const dataFile = _getDataFile(world, this.context, this);
1641
1769
  let data = {};
1642
1770
  if (fs.existsSync(dataFile)) {
1643
1771
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1724,6 +1852,15 @@ class StableBrowser {
1724
1852
  document.documentElement.clientWidth,
1725
1853
  ])));
1726
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
+ // }
1727
1864
  if (this.context.browserName === "chromium") {
1728
1865
  const client = await playContext.newCDPSession(this.page);
1729
1866
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1745,6 +1882,10 @@ class StableBrowser {
1745
1882
  else {
1746
1883
  screenshotBuffer = await this.page.screenshot();
1747
1884
  }
1885
+ // if (focusedElement) {
1886
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1887
+ // await this._unhighlightElements(focusedElement);
1888
+ // }
1748
1889
  let image = await Jimp.read(screenshotBuffer);
1749
1890
  // Get the image dimensions
1750
1891
  const { width, height } = image.bitmap;
@@ -1757,6 +1898,7 @@ class StableBrowser {
1757
1898
  else {
1758
1899
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1759
1900
  }
1901
+ return screenshotBuffer;
1760
1902
  }
1761
1903
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1762
1904
  const state = {
@@ -1792,8 +1934,10 @@ class StableBrowser {
1792
1934
  world,
1793
1935
  type: Types.EXTRACT,
1794
1936
  text: `Extract attribute from element`,
1937
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1795
1938
  operation: "extractAttribute",
1796
1939
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1940
+ allowDisabled: true,
1797
1941
  };
1798
1942
  await new Promise((resolve) => setTimeout(resolve, 2000));
1799
1943
  try {
@@ -1815,6 +1959,7 @@ class StableBrowser {
1815
1959
  state.info.value = state.value;
1816
1960
  this.setTestData({ [variable]: state.value }, world);
1817
1961
  this.logger.info("set test data: " + variable + "=" + state.value);
1962
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1818
1963
  return state.info;
1819
1964
  }
1820
1965
  catch (e) {
@@ -1833,14 +1978,21 @@ class StableBrowser {
1833
1978
  options,
1834
1979
  world,
1835
1980
  type: Types.VERIFY_ATTRIBUTE,
1981
+ highlight: true,
1982
+ screenshot: true,
1836
1983
  text: `Verify element attribute`,
1984
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1837
1985
  operation: "verifyAttribute",
1838
1986
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1987
+ allowDisabled: true,
1839
1988
  };
1840
1989
  await new Promise((resolve) => setTimeout(resolve, 2000));
1841
1990
  let val;
1991
+ let expectedValue;
1842
1992
  try {
1843
1993
  await _preCommand(state, this);
1994
+ expectedValue = state.value;
1995
+ state.info.expectedValue = expectedValue;
1844
1996
  switch (attribute) {
1845
1997
  case "innerText":
1846
1998
  val = String(await state.element.innerText());
@@ -1854,7 +2006,7 @@ class StableBrowser {
1854
2006
  case "disabled":
1855
2007
  val = String(await state.element.isDisabled());
1856
2008
  break;
1857
- case "readonly":
2009
+ case "readOnly":
1858
2010
  const isEditable = await state.element.isEditable();
1859
2011
  val = String(!isEditable);
1860
2012
  break;
@@ -1862,19 +2014,22 @@ class StableBrowser {
1862
2014
  val = String(await state.element.getAttribute(attribute));
1863
2015
  break;
1864
2016
  }
2017
+ state.info.value = val;
1865
2018
  let regex;
1866
- if (value.startsWith("/") && value.endsWith("/")) {
1867
- const patternBody = value.slice(1, -1);
2019
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2020
+ const patternBody = expectedValue.slice(1, -1);
1868
2021
  regex = new RegExp(patternBody, "g");
1869
2022
  }
1870
2023
  else {
1871
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2024
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1872
2025
  regex = new RegExp(escapedPattern, "g");
1873
2026
  }
1874
2027
  if (!val.match(regex)) {
1875
- throw new Error(`The ${attribute} attribute has a value of "${val}", but the expected value is "${value}"`);
2028
+ let errorMessage = `The ${attribute} attribute has a value of "${val}", but the expected value is "${expectedValue}"`;
2029
+ state.info.failCause.assertionFailed = true;
2030
+ state.info.failCause.lastError = errorMessage;
2031
+ throw new Error(errorMessage);
1876
2032
  }
1877
- state.info.value = val;
1878
2033
  return state.info;
1879
2034
  }
1880
2035
  catch (e) {
@@ -1973,27 +2128,32 @@ class StableBrowser {
1973
2128
  async _highlightElements(scope, css) {
1974
2129
  try {
1975
2130
  if (!scope) {
2131
+ // console.log(`Scope is not defined`);
1976
2132
  return;
1977
2133
  }
1978
2134
  if (!css) {
1979
2135
  scope
1980
2136
  .evaluate((node) => {
1981
2137
  if (node && node.style) {
1982
- let originalBorder = node.style.border;
1983
- node.style.border = "2px solid red";
2138
+ let originalOutline = node.style.outline;
2139
+ // console.log(`Original outline was: ${originalOutline}`);
2140
+ // node.__previousOutline = originalOutline;
2141
+ node.style.outline = "2px solid red";
2142
+ // console.log(`New outline is: ${node.style.outline}`);
1984
2143
  if (window) {
1985
2144
  window.addEventListener("beforeunload", function (e) {
1986
- node.style.border = originalBorder;
2145
+ node.style.outline = originalOutline;
1987
2146
  });
1988
2147
  }
1989
2148
  setTimeout(function () {
1990
- node.style.border = originalBorder;
2149
+ node.style.outline = originalOutline;
1991
2150
  }, 2000);
1992
2151
  }
1993
2152
  })
1994
2153
  .then(() => { })
1995
2154
  .catch((e) => {
1996
2155
  // ignore
2156
+ // console.error(`Could not highlight node : ${e}`);
1997
2157
  });
1998
2158
  }
1999
2159
  else {
@@ -2009,17 +2169,18 @@ class StableBrowser {
2009
2169
  if (!element.style) {
2010
2170
  return;
2011
2171
  }
2012
- var originalBorder = element.style.border;
2172
+ let originalOutline = element.style.outline;
2173
+ element.__previousOutline = originalOutline;
2013
2174
  // Set the new border to be red and 2px solid
2014
- element.style.border = "2px solid red";
2175
+ element.style.outline = "2px solid red";
2015
2176
  if (window) {
2016
2177
  window.addEventListener("beforeunload", function (e) {
2017
- element.style.border = originalBorder;
2178
+ element.style.outline = originalOutline;
2018
2179
  });
2019
2180
  }
2020
2181
  // Set a timeout to revert to the original border after 2 seconds
2021
2182
  setTimeout(function () {
2022
- element.style.border = originalBorder;
2183
+ element.style.outline = originalOutline;
2023
2184
  }, 2000);
2024
2185
  }
2025
2186
  return;
@@ -2027,6 +2188,7 @@ class StableBrowser {
2027
2188
  .then(() => { })
2028
2189
  .catch((e) => {
2029
2190
  // ignore
2191
+ // console.error(`Could not highlight css: ${e}`);
2030
2192
  });
2031
2193
  }
2032
2194
  }
@@ -2034,6 +2196,54 @@ class StableBrowser {
2034
2196
  console.debug(error);
2035
2197
  }
2036
2198
  }
2199
+ // async _unhighlightElements(scope, css) {
2200
+ // try {
2201
+ // if (!scope) {
2202
+ // return;
2203
+ // }
2204
+ // if (!css) {
2205
+ // scope
2206
+ // .evaluate((node) => {
2207
+ // if (node && node.style) {
2208
+ // if (!node.__previousOutline) {
2209
+ // node.style.outline = "";
2210
+ // } else {
2211
+ // node.style.outline = node.__previousOutline;
2212
+ // }
2213
+ // }
2214
+ // })
2215
+ // .then(() => {})
2216
+ // .catch((e) => {
2217
+ // // console.log(`Error while unhighlighting node ${JSON.stringify(scope)}: ${e}`);
2218
+ // });
2219
+ // } else {
2220
+ // scope
2221
+ // .evaluate(([css]) => {
2222
+ // if (!css) {
2223
+ // return;
2224
+ // }
2225
+ // let elements = Array.from(document.querySelectorAll(css));
2226
+ // for (i = 0; i < elements.length; i++) {
2227
+ // let element = elements[i];
2228
+ // if (!element.style) {
2229
+ // return;
2230
+ // }
2231
+ // if (!element.__previousOutline) {
2232
+ // element.style.outline = "";
2233
+ // } else {
2234
+ // element.style.outline = element.__previousOutline;
2235
+ // }
2236
+ // }
2237
+ // })
2238
+ // .then(() => {})
2239
+ // .catch((e) => {
2240
+ // // console.error(`Error while unhighlighting element in css: ${e}`);
2241
+ // });
2242
+ // }
2243
+ // } catch (error) {
2244
+ // // console.debug(error);
2245
+ // }
2246
+ // }
2037
2247
  async verifyPagePath(pathPart, options = {}, world = null) {
2038
2248
  const startTime = Date.now();
2039
2249
  let error = null;
@@ -2078,6 +2288,7 @@ class StableBrowser {
2078
2288
  _reportToWorld(world, {
2079
2289
  type: Types.VERIFY_PAGE_PATH,
2080
2290
  text: "Verify page path",
2291
+ _text: "Verify the page path contains " + pathPart,
2081
2292
  screenshotId,
2082
2293
  result: error
2083
2294
  ? {
@@ -2095,26 +2306,89 @@ class StableBrowser {
2095
2306
  });
2096
2307
  }
2097
2308
  }
2098
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2309
+ async verifyPageTitle(title, options = {}, world = null) {
2310
+ const startTime = Date.now();
2311
+ let error = null;
2312
+ let screenshotId = null;
2313
+ let screenshotPath = null;
2314
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2315
+ const info = {};
2316
+ info.log = "***** verify page title " + title + " *****\n";
2317
+ info.operation = "verifyPageTitle";
2318
+ const newValue = await this._replaceWithLocalData(title, world);
2319
+ if (newValue !== title) {
2320
+ this.logger.info(title + "=" + newValue);
2321
+ title = newValue;
2322
+ }
2323
+ info.title = title;
2324
+ try {
2325
+ for (let i = 0; i < 30; i++) {
2326
+ const foundTitle = await this.page.title();
2327
+ if (!foundTitle.includes(title)) {
2328
+ if (i === 29) {
2329
+ throw new Error(`url ${foundTitle} doesn't contain ${title}`);
2330
+ }
2331
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2332
+ continue;
2333
+ }
2334
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2335
+ return info;
2336
+ }
2337
+ }
2338
+ catch (e) {
2339
+ //await this.closeUnexpectedPopups();
2340
+ this.logger.error("verify page title failed " + info.log);
2341
+ ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2342
+ info.screenshotPath = screenshotPath;
2343
+ Object.assign(e, { info: info });
2344
+ error = e;
2345
+ // throw e;
2346
+ await _commandError({ text: "verifyPageTitle", operation: "verifyPageTitle", title, info, throwError: true }, e, this);
2347
+ }
2348
+ finally {
2349
+ const endTime = Date.now();
2350
+ _reportToWorld(world, {
2351
+ type: Types.VERIFY_PAGE_PATH,
2352
+ text: "Verify page title",
2353
+ _text: "Verify the page title contains " + title,
2354
+ screenshotId,
2355
+ result: error
2356
+ ? {
2357
+ status: "FAILED",
2358
+ startTime,
2359
+ endTime,
2360
+ message: error?.message,
2361
+ }
2362
+ : {
2363
+ status: "PASSED",
2364
+ startTime,
2365
+ endTime,
2366
+ },
2367
+ info: info,
2368
+ });
2369
+ }
2370
+ }
2371
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2099
2372
  const frames = this.page.frames();
2100
2373
  let results = [];
2374
+ // let ignoreCase = false;
2101
2375
  for (let i = 0; i < frames.length; i++) {
2102
2376
  if (dateAlternatives.date) {
2103
2377
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2104
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, {});
2378
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2105
2379
  result.frame = frames[i];
2106
2380
  results.push(result);
2107
2381
  }
2108
2382
  }
2109
2383
  else if (numberAlternatives.number) {
2110
2384
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2111
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, {});
2385
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2112
2386
  result.frame = frames[i];
2113
2387
  results.push(result);
2114
2388
  }
2115
2389
  }
2116
2390
  else {
2117
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, {});
2391
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2118
2392
  result.frame = frames[i];
2119
2393
  results.push(result);
2120
2394
  }
@@ -2133,11 +2407,15 @@ class StableBrowser {
2133
2407
  scroll: false,
2134
2408
  highlight: false,
2135
2409
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2136
- text: `Verify text exists in page`,
2410
+ text: `Verify the text '${text}' exists in page`,
2411
+ _text: `Verify the text '${text}' exists in page`,
2137
2412
  operation: "verifyTextExistInPage",
2138
2413
  log: "***** verify text " + text + " exists in page *****\n",
2139
2414
  };
2140
- const timeout = this._getLoadTimeout(options);
2415
+ if (testForRegex(text)) {
2416
+ text = text.replace(/\\"/g, '"');
2417
+ }
2418
+ const timeout = this._getFindElementTimeout(options);
2141
2419
  await new Promise((resolve) => setTimeout(resolve, 2000));
2142
2420
  const newValue = await this._replaceWithLocalData(text, world);
2143
2421
  if (newValue !== text) {
@@ -2150,7 +2428,15 @@ class StableBrowser {
2150
2428
  await _preCommand(state, this);
2151
2429
  state.info.text = text;
2152
2430
  while (true) {
2153
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2431
+ let resultWithElementsFound = {
2432
+ length: 0,
2433
+ };
2434
+ try {
2435
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2436
+ }
2437
+ catch (error) {
2438
+ // ignore
2439
+ }
2154
2440
  if (resultWithElementsFound.length === 0) {
2155
2441
  if (Date.now() - state.startTime > timeout) {
2156
2442
  throw new Error(`Text ${text} not found in page`);
@@ -2158,18 +2444,40 @@ class StableBrowser {
2158
2444
  await new Promise((resolve) => setTimeout(resolve, 1000));
2159
2445
  continue;
2160
2446
  }
2161
- if (resultWithElementsFound[0].randomToken) {
2162
- const frame = resultWithElementsFound[0].frame;
2163
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2164
- await this._highlightElements(frame, dataAttribute);
2165
- const element = await frame.$(dataAttribute);
2166
- if (element) {
2167
- await this.scrollIfNeeded(element, state.info);
2168
- await element.dispatchEvent("bvt_verify_page_contains_text");
2447
+ try {
2448
+ if (resultWithElementsFound[0].randomToken) {
2449
+ const frame = resultWithElementsFound[0].frame;
2450
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2451
+ await this._highlightElements(frame, dataAttribute);
2452
+ // if (world && world.screenshot && !world.screenshotPath) {
2453
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2454
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2455
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2456
+ // this._unhighlightElements(frame, dataAttribute)
2457
+ // .then(async () => {
2458
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2459
+ // })
2460
+ // .catch(
2461
+ // (e) => {}
2462
+ // console.error(e)
2463
+ // );
2464
+ // });
2465
+ // }
2466
+ const element = await frame.locator(dataAttribute).first();
2467
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2468
+ // await this._unhighlightElements(frame, dataAttribute);
2469
+ if (element) {
2470
+ await this.scrollIfNeeded(element, state.info);
2471
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2472
+ // await _screenshot(state, this, element);
2473
+ }
2169
2474
  }
2475
+ await _screenshot(state, this);
2476
+ return state.info;
2477
+ }
2478
+ catch (error) {
2479
+ console.error(error);
2170
2480
  }
2171
- await _screenshot(state, this);
2172
- return state.info;
2173
2481
  }
2174
2482
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2175
2483
  }
@@ -2190,11 +2498,15 @@ class StableBrowser {
2190
2498
  scroll: false,
2191
2499
  highlight: false,
2192
2500
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2193
- text: `Verify text does not exist in page`,
2501
+ text: `Verify the text '${text}' does not exist in page`,
2502
+ _text: `Verify the text '${text}' does not exist in page`,
2194
2503
  operation: "verifyTextNotExistInPage",
2195
2504
  log: "***** verify text " + text + " does not exist in page *****\n",
2196
2505
  };
2197
- const timeout = this._getLoadTimeout(options);
2506
+ if (testForRegex(text)) {
2507
+ text = text.replace(/\\"/g, '"');
2508
+ }
2509
+ const timeout = this._getFindElementTimeout(options);
2198
2510
  await new Promise((resolve) => setTimeout(resolve, 2000));
2199
2511
  const newValue = await this._replaceWithLocalData(text, world);
2200
2512
  if (newValue !== text) {
@@ -2206,8 +2518,16 @@ class StableBrowser {
2206
2518
  try {
2207
2519
  await _preCommand(state, this);
2208
2520
  state.info.text = text;
2521
+ let resultWithElementsFound = {
2522
+ length: null, // initial cannot be 0
2523
+ };
2209
2524
  while (true) {
2210
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2525
+ try {
2526
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2527
+ }
2528
+ catch (error) {
2529
+ // ignore
2530
+ }
2211
2531
  if (resultWithElementsFound.length === 0) {
2212
2532
  await _screenshot(state, this);
2213
2533
  return state.info;
@@ -2237,10 +2557,11 @@ class StableBrowser {
2237
2557
  highlight: false,
2238
2558
  type: Types.VERIFY_TEXT_WITH_RELATION,
2239
2559
  text: `Verify text with relation to another text`,
2560
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2240
2561
  operation: "verify_text_with_relation",
2241
2562
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2242
2563
  };
2243
- const timeout = this._getLoadTimeout(options);
2564
+ const timeout = this._getFindElementTimeout(options);
2244
2565
  await new Promise((resolve) => setTimeout(resolve, 2000));
2245
2566
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2246
2567
  if (newValue !== textAnchor) {
@@ -2258,8 +2579,16 @@ class StableBrowser {
2258
2579
  try {
2259
2580
  await _preCommand(state, this);
2260
2581
  state.info.text = textToVerify;
2582
+ let resultWithElementsFound = {
2583
+ length: 0,
2584
+ };
2261
2585
  while (true) {
2262
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2586
+ try {
2587
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2588
+ }
2589
+ catch (error) {
2590
+ // ignore
2591
+ }
2263
2592
  if (resultWithElementsFound.length === 0) {
2264
2593
  if (Date.now() - state.startTime > timeout) {
2265
2594
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2267,51 +2596,56 @@ class StableBrowser {
2267
2596
  await new Promise((resolve) => setTimeout(resolve, 1000));
2268
2597
  continue;
2269
2598
  }
2270
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2271
- foundAncore = true;
2272
- const result = resultWithElementsFound[i];
2273
- const token = result.randomToken;
2274
- const frame = result.frame;
2275
- const css = `[data-blinq-id="blinq-id-${token}"]`;
2276
- const findResult = await frame.evaluate(([css, climb, textToVerify, token]) => {
2277
- const elements = Array.from(document.querySelectorAll(css));
2278
- for (let i = 0; i < elements.length; i++) {
2279
- const element = elements[i];
2280
- let climbParent = element;
2281
- for (let j = 0; j < climb; j++) {
2282
- climbParent = climbParent.parentElement;
2283
- if (!climbParent) {
2284
- break;
2599
+ try {
2600
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2601
+ foundAncore = true;
2602
+ const result = resultWithElementsFound[i];
2603
+ const token = result.randomToken;
2604
+ const frame = result.frame;
2605
+ let css = `[data-blinq-id-${token}]`;
2606
+ const climbArray1 = [];
2607
+ for (let i = 0; i < climb; i++) {
2608
+ climbArray1.push("..");
2609
+ }
2610
+ let climbXpath = "xpath=" + climbArray1.join("/");
2611
+ css = css + " >> " + climbXpath;
2612
+ const count = await frame.locator(css).count();
2613
+ for (let j = 0; j < count; j++) {
2614
+ const continer = await frame.locator(css).nth(j);
2615
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
2616
+ if (result.elementCount > 0) {
2617
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2618
+ await this._highlightElements(frame, dataAttribute);
2619
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2620
+ // if (world && world.screenshot && !world.screenshotPath) {
2621
+ // console.log(`Highlighting for vtrt while running from recorder`);
2622
+ // this._highlightElements(frame, dataAttribute)
2623
+ // .then(async () => {
2624
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2625
+ // this._unhighlightElements(frame, dataAttribute).then(
2626
+ // () => {}
2627
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2628
+ // );
2629
+ // })
2630
+ // .catch(e);
2631
+ // }
2632
+ //await this._highlightElements(frame, cssAnchor);
2633
+ const element = await frame.locator(dataAttribute).first();
2634
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2635
+ // await this._unhighlightElements(frame, dataAttribute);
2636
+ if (element) {
2637
+ await this.scrollIfNeeded(element, state.info);
2638
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2285
2639
  }
2640
+ await _screenshot(state, this);
2641
+ return state.info;
2286
2642
  }
2287
- if (!climbParent) {
2288
- continue;
2289
- }
2290
- const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
2291
- if (foundElements.length > 0) {
2292
- // set the container element attribute
2293
- element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
2294
- climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
2295
- foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
2296
- return { found: true };
2297
- }
2298
- }
2299
- return { found: false };
2300
- }, [css, climb, textToVerify, result.randomToken]);
2301
- if (findResult.found === true) {
2302
- const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
2303
- const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2304
- await this._highlightElements(frame, dataAttribute);
2305
- await this._highlightElements(frame, cssAnchor);
2306
- const element = await frame.$(dataAttribute);
2307
- if (element) {
2308
- await this.scrollIfNeeded(element, state.info);
2309
- await element.dispatchEvent("bvt_verify_page_contains_text");
2310
2643
  }
2311
- await _screenshot(state, this);
2312
- return state.info;
2313
2644
  }
2314
2645
  }
2646
+ catch (error) {
2647
+ console.error(error);
2648
+ }
2315
2649
  }
2316
2650
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2317
2651
  }
@@ -2322,6 +2656,30 @@ class StableBrowser {
2322
2656
  _commandFinally(state, this);
2323
2657
  }
2324
2658
  }
2659
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2660
+ const frames = this.page.frames();
2661
+ let results = [];
2662
+ let ignoreCase = false;
2663
+ for (let i = 0; i < frames.length; i++) {
2664
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2665
+ result.frame = frames[i];
2666
+ const climbArray = [];
2667
+ for (let i = 0; i < climb; i++) {
2668
+ climbArray.push("..");
2669
+ }
2670
+ let climbXpath = "xpath=" + climbArray.join("/");
2671
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2672
+ const count = await frames[i].locator(newLocator).count();
2673
+ if (count > 0) {
2674
+ result.elementCount = count;
2675
+ result.locator = newLocator;
2676
+ results.push(result);
2677
+ }
2678
+ }
2679
+ // state.info.results = results;
2680
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2681
+ return resultWithElementsFound;
2682
+ }
2325
2683
  async visualVerification(text, options = {}, world = null) {
2326
2684
  const startTime = Date.now();
2327
2685
  let error = null;
@@ -2340,10 +2698,13 @@ class StableBrowser {
2340
2698
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2341
2699
  info.screenshotPath = screenshotPath;
2342
2700
  const screenshot = await this.takeScreenshot();
2343
- const request = {
2344
- method: "POST",
2701
+ let request = {
2702
+ method: "post",
2703
+ maxBodyLength: Infinity,
2345
2704
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2346
2705
  headers: {
2706
+ "x-bvt-project-id": path.basename(this.project_path),
2707
+ "x-source": "aaa",
2347
2708
  "Content-Type": "application/json",
2348
2709
  Authorization: `Bearer ${process.env.TOKEN}`,
2349
2710
  },
@@ -2352,7 +2713,7 @@ class StableBrowser {
2352
2713
  screenshot: screenshot,
2353
2714
  }),
2354
2715
  };
2355
- let result = await this.context.api.request(request);
2716
+ const result = await axios.request(request);
2356
2717
  if (result.data.status !== true) {
2357
2718
  throw new Error("Visual validation failed");
2358
2719
  }
@@ -2380,6 +2741,7 @@ class StableBrowser {
2380
2741
  _reportToWorld(world, {
2381
2742
  type: Types.VERIFY_VISUAL,
2382
2743
  text: "Visual verification",
2744
+ _text: "Visual verification of " + text,
2383
2745
  screenshotId,
2384
2746
  result: error
2385
2747
  ? {
@@ -2646,6 +3008,32 @@ class StableBrowser {
2646
3008
  }
2647
3009
  return timeout;
2648
3010
  }
3011
+ _getFindElementTimeout(options) {
3012
+ if (options && options.timeout) {
3013
+ return options.timeout;
3014
+ }
3015
+ if (this.configuration.find_element_timeout) {
3016
+ return this.configuration.find_element_timeout;
3017
+ }
3018
+ return 30000;
3019
+ }
3020
+ async saveStoreState(path = null, world = null) {
3021
+ const storageState = await this.page.context().storageState();
3022
+ //const testDataFile = _getDataFile(world, this.context, this);
3023
+ if (path) {
3024
+ // save { storageState: storageState } into the path
3025
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
3026
+ }
3027
+ else {
3028
+ await this.setTestData({ storageState: storageState }, world);
3029
+ }
3030
+ }
3031
+ async restoreSaveState(path = null, world = null) {
3032
+ await refreshBrowser(this, path, world);
3033
+ this.registerEventListeners(this.context);
3034
+ registerNetworkEvents(this.world, this, this.context, this.page);
3035
+ registerDownloadEvent(this.page, this.world, this.context);
3036
+ }
2649
3037
  async waitForPageLoad(options = {}, world = null) {
2650
3038
  let timeout = this._getLoadTimeout(options);
2651
3039
  const promiseArray = [];
@@ -2713,6 +3101,7 @@ class StableBrowser {
2713
3101
  highlight: false,
2714
3102
  type: Types.CLOSE_PAGE,
2715
3103
  text: `Close page`,
3104
+ _text: `Close the page`,
2716
3105
  operation: "closePage",
2717
3106
  log: "***** close page *****\n",
2718
3107
  throwError: false,
@@ -2729,8 +3118,95 @@ class StableBrowser {
2729
3118
  _commandFinally(state, this);
2730
3119
  }
2731
3120
  }
3121
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3122
+ let operation = null;
3123
+ if (!options || !options.operation) {
3124
+ throw new Error("operation is not defined");
3125
+ }
3126
+ operation = options.operation;
3127
+ // validate operation is one of the supported operations
3128
+ if (operation != "click" && operation != "hover+click") {
3129
+ throw new Error("operation is not supported");
3130
+ }
3131
+ const state = {
3132
+ options,
3133
+ world,
3134
+ locate: false,
3135
+ scroll: false,
3136
+ highlight: false,
3137
+ type: Types.TABLE_OPERATION,
3138
+ text: `Table operation`,
3139
+ _text: `Table ${operation} operation`,
3140
+ operation: operation,
3141
+ log: "***** Table operation *****\n",
3142
+ };
3143
+ const timeout = this._getFindElementTimeout(options);
3144
+ try {
3145
+ await _preCommand(state, this);
3146
+ const start = Date.now();
3147
+ let cellArea = null;
3148
+ while (true) {
3149
+ try {
3150
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3151
+ if (cellArea) {
3152
+ break;
3153
+ }
3154
+ }
3155
+ catch (e) {
3156
+ // ignore
3157
+ }
3158
+ if (Date.now() - start > timeout) {
3159
+ throw new Error(`Cell not found in table`);
3160
+ }
3161
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3162
+ }
3163
+ switch (operation) {
3164
+ case "click":
3165
+ if (!options.css) {
3166
+ // will click in the center of the cell
3167
+ let xOffset = 0;
3168
+ let yOffset = 0;
3169
+ if (options.xOffset) {
3170
+ xOffset = options.xOffset;
3171
+ }
3172
+ if (options.yOffset) {
3173
+ yOffset = options.yOffset;
3174
+ }
3175
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3176
+ }
3177
+ else {
3178
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3179
+ if (results.length === 0) {
3180
+ throw new Error(`Element not found in cell area`);
3181
+ }
3182
+ state.element = results[0];
3183
+ await performAction("click", state.element, options, this, state, _params);
3184
+ }
3185
+ break;
3186
+ case "hover+click":
3187
+ if (!options.css) {
3188
+ throw new Error("css is not defined");
3189
+ }
3190
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3191
+ if (results.length === 0) {
3192
+ throw new Error(`Element not found in cell area`);
3193
+ }
3194
+ state.element = results[0];
3195
+ await performAction("hover+click", state.element, options, this, state, _params);
3196
+ break;
3197
+ default:
3198
+ throw new Error("operation is not supported");
3199
+ }
3200
+ }
3201
+ catch (e) {
3202
+ await _commandError(state, e, this);
3203
+ }
3204
+ finally {
3205
+ _commandFinally(state, this);
3206
+ }
3207
+ }
2732
3208
  saveTestDataAsGlobal(options, world) {
2733
- const dataFile = this._getDataFile(world);
3209
+ const dataFile = _getDataFile(world, this.context, this);
2734
3210
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2735
3211
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2736
3212
  }
@@ -2760,6 +3236,7 @@ class StableBrowser {
2760
3236
  _reportToWorld(world, {
2761
3237
  type: Types.SET_VIEWPORT,
2762
3238
  text: "set viewport size to " + width + "x" + hight,
3239
+ _text: "Set the viewport size to " + width + "x" + hight,
2763
3240
  screenshotId,
2764
3241
  result: error
2765
3242
  ? {
@@ -2831,14 +3308,25 @@ class StableBrowser {
2831
3308
  }
2832
3309
  }
2833
3310
  async beforeStep(world, step) {
2834
- this.stepName = step.pickleStep.text;
2835
- this.logger.info("step: " + this.stepName);
2836
3311
  if (this.stepIndex === undefined) {
2837
3312
  this.stepIndex = 0;
2838
3313
  }
2839
3314
  else {
2840
3315
  this.stepIndex++;
2841
3316
  }
3317
+ if (step && step.pickleStep && step.pickleStep.text) {
3318
+ this.stepName = step.pickleStep.text;
3319
+ this.logger.info("step: " + this.stepName);
3320
+ }
3321
+ else if (step && step.text) {
3322
+ this.stepName = step.text;
3323
+ }
3324
+ else {
3325
+ this.stepName = "step " + this.stepIndex;
3326
+ }
3327
+ if (this.context) {
3328
+ this.context.examplesRow = extractStepExampleParameters(step);
3329
+ }
2842
3330
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2843
3331
  if (this.context.browserObject.context) {
2844
3332
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2851,6 +3339,41 @@ class StableBrowser {
2851
3339
  this.saveTestDataAsGlobal({}, world);
2852
3340
  }
2853
3341
  }
3342
+ if (this.initSnapshotTaken === false) {
3343
+ this.initSnapshotTaken = true;
3344
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3345
+ const snapshot = await this.getAriaSnapshot();
3346
+ if (snapshot) {
3347
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3348
+ }
3349
+ }
3350
+ }
3351
+ }
3352
+ async getAriaSnapshot() {
3353
+ try {
3354
+ // find the page url
3355
+ const url = await this.page.url();
3356
+ // extract the path from the url
3357
+ const path = new URL(url).pathname;
3358
+ // get the page title
3359
+ const title = await this.page.title();
3360
+ // go over other frams
3361
+ const frames = this.page.frames();
3362
+ const snapshots = [];
3363
+ const content = [`- path: ${path}`, `- title: ${title}`];
3364
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3365
+ for (let i = 0; i < frames.length; i++) {
3366
+ content.push(`- frame: ${i}`);
3367
+ const frame = frames[i];
3368
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3369
+ content.push(snapshot);
3370
+ }
3371
+ return content.join("\n");
3372
+ }
3373
+ catch (e) {
3374
+ console.error(e);
3375
+ }
3376
+ return null;
2854
3377
  }
2855
3378
  async afterStep(world, step) {
2856
3379
  this.stepName = null;
@@ -2861,6 +3384,16 @@ class StableBrowser {
2861
3384
  });
2862
3385
  }
2863
3386
  }
3387
+ if (this.context) {
3388
+ this.context.examplesRow = null;
3389
+ }
3390
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3391
+ const snapshot = await this.getAriaSnapshot();
3392
+ if (snapshot) {
3393
+ const obj = {};
3394
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3395
+ }
3396
+ }
2864
3397
  }
2865
3398
  }
2866
3399
  function createTimedPromise(promise, label) {