automation_model 1.0.573-dev → 1.0.573-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,66 +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
- if (text.startsWith("/") && text.endsWith("/")) {
351
- if (text.length < 3) {
352
- throw new Error("Invalid regex pattern: empty pattern");
353
- }
354
- try {
355
- const pattern = text.slice(1, -1);
356
- new RegExp(pattern); // Validate pattern
357
- text = pattern;
358
- options.singleRegex = true;
359
- }
360
- catch (e) {
361
- throw new Error(`Invalid regex pattern: ${e.message}`);
362
- }
363
- }
364
- const elements = window.findMatchingElements(text, options);
365
- let randomToken = null;
366
- const foundElements = [];
367
- let elementCount = 0;
368
- if (elements.length > 0) {
369
- for (let i = 0; i < elements.length; i++) {
370
- if (randomToken === null) {
371
- 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;
372
374
  }
373
- let element = elements[i];
374
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
375
- elementCount++;
376
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;
377
389
  }
378
- return { elementCount: elementCount, randomToken: randomToken };
379
- }, [text1, tag1, regex1, partial1]);
390
+ tagCount++;
391
+ }
392
+ return { elementCount: tagCount, randomToken };
380
393
  }
381
- 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) {
382
395
  if (!info) {
383
396
  info = {};
384
397
  }
@@ -401,30 +414,31 @@ class StableBrowser {
401
414
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
402
415
  let locator = null;
403
416
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
404
- 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);
405
419
  if (!locatorString) {
406
420
  info.failCause.textNotFound = true;
407
- 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}`;
408
422
  return;
409
423
  }
410
- locator = this._getLocator({ css: locatorString }, scope, _params);
424
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
411
425
  }
412
426
  else if (locatorSearch.text) {
413
427
  let text = _fixUsingParams(locatorSearch.text, _params);
414
- 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);
415
429
  if (result.elementCount === 0) {
416
430
  info.failCause.textNotFound = true;
417
- info.failCause.lastError = "failed to locate element by text: " + text;
431
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
418
432
  return;
419
433
  }
420
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
434
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
421
435
  if (locatorSearch.childCss) {
422
436
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
423
437
  }
424
- locator = this._getLocator(locatorSearch, scope, _params);
438
+ locator = await this._getLocator(locatorSearch, scope, _params);
425
439
  }
426
440
  else {
427
- locator = this._getLocator(locatorSearch, scope, _params);
441
+ locator = await this._getLocator(locatorSearch, scope, _params);
428
442
  }
429
443
  // let cssHref = false;
430
444
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
@@ -453,7 +467,7 @@ class StableBrowser {
453
467
  if (!visibleOnly) {
454
468
  visible = true;
455
469
  }
456
- if (visible && enabled) {
470
+ if (visible && (allowDisabled || enabled)) {
457
471
  foundLocators.push(locator.nth(j));
458
472
  if (info.locatorLog) {
459
473
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
@@ -466,9 +480,11 @@ class StableBrowser {
466
480
  info.printMessages = {};
467
481
  }
468
482
  if (info.locatorLog && !visible) {
483
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
469
484
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
470
485
  }
471
486
  if (info.locatorLog && !enabled) {
487
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
472
488
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
473
489
  }
474
490
  if (!info.printMessages[j.toString()]) {
@@ -536,7 +552,7 @@ class StableBrowser {
536
552
  }
537
553
  return { rerun: false };
538
554
  }
539
- async _locate(selectors, info, _params, timeout) {
555
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
540
556
  if (!timeout) {
541
557
  timeout = 30000;
542
558
  }
@@ -546,9 +562,30 @@ class StableBrowser {
546
562
  let selector = selectors.locators[j];
547
563
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
548
564
  }
549
- let element = await this._locate_internal(selectors, info, _params, timeout);
565
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
550
566
  if (!element.rerun) {
551
- 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);
552
589
  }
553
590
  }
554
591
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -621,7 +658,7 @@ class StableBrowser {
621
658
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
622
659
  if (Date.now() - startTime > timeout) {
623
660
  info.failCause.iframeNotFound = true;
624
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
661
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
625
662
  throw new Error("unable to locate iframe " + selectors.iframe_src);
626
663
  }
627
664
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -646,7 +683,7 @@ class StableBrowser {
646
683
  return bodyContent;
647
684
  });
648
685
  }
649
- async _locate_internal(selectors, info, _params, timeout = 30000) {
686
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
650
687
  if (!info) {
651
688
  info = {};
652
689
  info.failCause = {};
@@ -695,18 +732,13 @@ class StableBrowser {
695
732
  }
696
733
  // info.log += "scanning locators in priority 1" + "\n";
697
734
  let onlyPriority3 = selectorsLocators[0].priority === 3;
698
- 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);
699
736
  if (result.foundElements.length === 0) {
700
737
  // info.log += "scanning locators in priority 2" + "\n";
701
- 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);
702
739
  }
703
- if (result.foundElements.length === 0 && onlyPriority3) {
704
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
705
- }
706
- else {
707
- if (result.foundElements.length === 0 && !highPriorityOnly) {
708
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
709
- }
740
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
741
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
710
742
  }
711
743
  let foundElements = result.foundElements;
712
744
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -750,7 +782,7 @@ class StableBrowser {
750
782
  break;
751
783
  }
752
784
  if (Date.now() - startTime > highPriorityTimeout) {
753
- info.log += "high priority timeout, will try all elements" + "\n";
785
+ //info.log += "high priority timeout, will try all elements" + "\n";
754
786
  highPriorityOnly = false;
755
787
  if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
756
788
  lazy_scroll = true;
@@ -758,10 +790,15 @@ class StableBrowser {
758
790
  }
759
791
  }
760
792
  if (Date.now() - startTime > visibleOnlyTimeout) {
761
- info.log += "visible only timeout, will try all elements" + "\n";
793
+ //info.log += "visible only timeout, will try all elements" + "\n";
762
794
  visibleOnly = false;
763
795
  }
764
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
+ }
765
802
  }
766
803
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
767
804
  // if (info.locatorLog) {
@@ -772,10 +809,12 @@ class StableBrowser {
772
809
  // }
773
810
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
774
811
  info.failCause.locatorNotFound = true;
775
- 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
+ }
776
815
  throw new Error("failed to locate first element no elements found, " + info.log);
777
816
  }
778
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
817
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
779
818
  let foundElements = [];
780
819
  const result = {
781
820
  foundElements: foundElements,
@@ -783,7 +822,7 @@ class StableBrowser {
783
822
  for (let i = 0; i < locatorsGroup.length; i++) {
784
823
  let foundLocators = [];
785
824
  try {
786
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
825
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
787
826
  }
788
827
  catch (e) {
789
828
  // this call can fail it the browser is navigating
@@ -791,7 +830,7 @@ class StableBrowser {
791
830
  // this.logger.debug(e);
792
831
  foundLocators = [];
793
832
  try {
794
- 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);
795
834
  }
796
835
  catch (e) {
797
836
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -806,9 +845,40 @@ class StableBrowser {
806
845
  result.locatorIndex = i;
807
846
  }
808
847
  if (foundLocators.length > 1) {
809
- info.failCause.foundMultiple = true;
810
- if (info.locatorLog) {
811
- 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
+ }
812
882
  }
813
883
  }
814
884
  }
@@ -919,25 +989,14 @@ class StableBrowser {
919
989
  options,
920
990
  world,
921
991
  text: "Click element",
992
+ _text: "Click on " + selectors.element_name,
922
993
  type: Types.CLICK,
923
994
  operation: "click",
924
995
  log: "***** click on " + selectors.element_name + " *****\n",
925
996
  };
926
997
  try {
927
998
  await _preCommand(state, this);
928
- if (state.options && state.options.context) {
929
- state.selectors.locators[0].text = state.options.context;
930
- }
931
- try {
932
- await state.element.click();
933
- // await new Promise((resolve) => setTimeout(resolve, 1000));
934
- }
935
- catch (e) {
936
- // await this.closeUnexpectedPopups();
937
- state.element = await this._locate(selectors, state.info, _params);
938
- await state.element.dispatchEvent("click");
939
- // await new Promise((resolve) => setTimeout(resolve, 1000));
940
- }
999
+ await performAction("click", state.element, options, this, state, _params);
941
1000
  await this.waitForPageLoad();
942
1001
  return state.info;
943
1002
  }
@@ -948,6 +1007,38 @@ class StableBrowser {
948
1007
  _commandFinally(state, this);
949
1008
  }
950
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
+ }
951
1042
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
952
1043
  const state = {
953
1044
  selectors,
@@ -956,6 +1047,7 @@ class StableBrowser {
956
1047
  world,
957
1048
  type: checked ? Types.CHECK : Types.UNCHECK,
958
1049
  text: checked ? `Check element` : `Uncheck element`,
1050
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
959
1051
  operation: "setCheck",
960
1052
  log: "***** check " + selectors.element_name + " *****\n",
961
1053
  };
@@ -965,9 +1057,15 @@ class StableBrowser {
965
1057
  // let element = await this._locate(selectors, info, _params);
966
1058
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
967
1059
  try {
968
- // 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);
969
1063
  await state.element.setChecked(checked);
970
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);
971
1069
  }
972
1070
  catch (e) {
973
1071
  if (e.message && e.message.includes("did not change its state")) {
@@ -999,22 +1097,13 @@ class StableBrowser {
999
1097
  world,
1000
1098
  type: Types.HOVER,
1001
1099
  text: `Hover element`,
1100
+ _text: `Hover on ${selectors.element_name}`,
1002
1101
  operation: "hover",
1003
1102
  log: "***** hover " + selectors.element_name + " *****\n",
1004
1103
  };
1005
1104
  try {
1006
1105
  await _preCommand(state, this);
1007
- try {
1008
- await state.element.hover();
1009
- await new Promise((resolve) => setTimeout(resolve, 1000));
1010
- }
1011
- catch (e) {
1012
- //await this.closeUnexpectedPopups();
1013
- state.info.log += "hover failed, will try again" + "\n";
1014
- state.element = await this._locate(selectors, state.info, _params);
1015
- await state.element.hover({ timeout: 10000 });
1016
- await new Promise((resolve) => setTimeout(resolve, 1000));
1017
- }
1106
+ await performAction("hover", state.element, options, this, state, _params);
1018
1107
  await _screenshot(state, this);
1019
1108
  await this.waitForPageLoad();
1020
1109
  return state.info;
@@ -1038,6 +1127,7 @@ class StableBrowser {
1038
1127
  value: values.toString(),
1039
1128
  type: Types.SELECT,
1040
1129
  text: `Select option: ${values}`,
1130
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1041
1131
  operation: "selectOption",
1042
1132
  log: "***** select option " + selectors.element_name + " *****\n",
1043
1133
  };
@@ -1072,6 +1162,7 @@ class StableBrowser {
1072
1162
  highlight: false,
1073
1163
  type: Types.TYPE_PRESS,
1074
1164
  text: `Type value: ${_value}`,
1165
+ _text: `Type value: ${_value}`,
1075
1166
  operation: "type",
1076
1167
  log: "",
1077
1168
  };
@@ -1151,6 +1242,7 @@ class StableBrowser {
1151
1242
  world,
1152
1243
  type: Types.SET_DATE_TIME,
1153
1244
  text: `Set date time value: ${value}`,
1245
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1154
1246
  operation: "setDateTime",
1155
1247
  log: "***** set date time value " + selectors.element_name + " *****\n",
1156
1248
  throwError: false,
@@ -1158,7 +1250,7 @@ class StableBrowser {
1158
1250
  try {
1159
1251
  await _preCommand(state, this);
1160
1252
  try {
1161
- await state.element.click();
1253
+ await performAction("click", state.element, options, this, state, _params);
1162
1254
  await new Promise((resolve) => setTimeout(resolve, 500));
1163
1255
  if (format) {
1164
1256
  state.value = dayjs(state.value).format(format);
@@ -1222,9 +1314,13 @@ class StableBrowser {
1222
1314
  world,
1223
1315
  type: Types.FILL,
1224
1316
  text: `Click type input with value: ${_value}`,
1317
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1225
1318
  operation: "clickType",
1226
1319
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1227
1320
  };
1321
+ if (!options) {
1322
+ options = {};
1323
+ }
1228
1324
  if (newValue !== _value) {
1229
1325
  //this.logger.info(_value + "=" + newValue);
1230
1326
  _value = newValue;
@@ -1232,7 +1328,7 @@ class StableBrowser {
1232
1328
  try {
1233
1329
  await _preCommand(state, this);
1234
1330
  state.info.value = _value;
1235
- if (options === null || options === undefined || !options.press) {
1331
+ if (!options.press) {
1236
1332
  try {
1237
1333
  let currentValue = await state.element.inputValue();
1238
1334
  if (currentValue) {
@@ -1243,13 +1339,9 @@ class StableBrowser {
1243
1339
  this.logger.info("unable to clear input value");
1244
1340
  }
1245
1341
  }
1246
- if (options === null || options === undefined || options.press) {
1247
- try {
1248
- await state.element.click({ timeout: 5000 });
1249
- }
1250
- catch (e) {
1251
- await state.element.dispatchEvent("click");
1252
- }
1342
+ if (options.press) {
1343
+ options.timeout = 5000;
1344
+ await performAction("click", state.element, options, this, state, _params);
1253
1345
  }
1254
1346
  else {
1255
1347
  try {
@@ -1287,7 +1379,12 @@ class StableBrowser {
1287
1379
  await this.waitForPageLoad();
1288
1380
  }
1289
1381
  else if (enter === false) {
1290
- await state.element.dispatchEvent("change");
1382
+ try {
1383
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1384
+ }
1385
+ catch (e) {
1386
+ // ignore
1387
+ }
1291
1388
  //await this.page.keyboard.press("Tab");
1292
1389
  }
1293
1390
  else {
@@ -1339,6 +1436,7 @@ class StableBrowser {
1339
1436
  return await this._getText(selectors, 0, _params, options, info, world);
1340
1437
  }
1341
1438
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1439
+ const timeout = this._getFindElementTimeout(options);
1342
1440
  _validateSelectors(selectors);
1343
1441
  let screenshotId = null;
1344
1442
  let screenshotPath = null;
@@ -1348,7 +1446,7 @@ class StableBrowser {
1348
1446
  }
1349
1447
  info.operation = "getText";
1350
1448
  info.selectors = selectors;
1351
- let element = await this._locate(selectors, info, _params);
1449
+ let element = await this._locate(selectors, info, _params, timeout);
1352
1450
  if (climb > 0) {
1353
1451
  const climbArray = [];
1354
1452
  for (let i = 0; i < climb; i++) {
@@ -1367,6 +1465,18 @@ class StableBrowser {
1367
1465
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1368
1466
  try {
1369
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
+ // }
1370
1480
  const elementText = await element.innerText();
1371
1481
  return {
1372
1482
  text: elementText,
@@ -1378,7 +1488,7 @@ class StableBrowser {
1378
1488
  }
1379
1489
  catch (e) {
1380
1490
  //await this.closeUnexpectedPopups();
1381
- this.logger.info("no innerText will use textContent");
1491
+ this.logger.info("no innerText, will use textContent");
1382
1492
  const elementText = await element.textContent();
1383
1493
  return { text: elementText, screenshotId, screenshotPath, value: value };
1384
1494
  }
@@ -1403,6 +1513,7 @@ class StableBrowser {
1403
1513
  highlight: false,
1404
1514
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1405
1515
  text: `Verify element contains pattern: ${pattern}`,
1516
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1406
1517
  operation: "containsPattern",
1407
1518
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1408
1519
  };
@@ -1438,6 +1549,8 @@ class StableBrowser {
1438
1549
  }
1439
1550
  }
1440
1551
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1552
+ const timeout = this._getFindElementTimeout(options);
1553
+ const startTime = Date.now();
1441
1554
  const state = {
1442
1555
  selectors,
1443
1556
  _params,
@@ -1464,62 +1577,54 @@ class StableBrowser {
1464
1577
  }
1465
1578
  let foundObj = null;
1466
1579
  try {
1467
- await _preCommand(state, this);
1468
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1469
- if (foundObj && foundObj.element) {
1470
- await this.scrollIfNeeded(foundObj.element, state.info);
1471
- }
1472
- await _screenshot(state, this);
1473
- const dateAlternatives = findDateAlternatives(text);
1474
- const numberAlternatives = findNumberAlternatives(text);
1475
- if (dateAlternatives.date) {
1476
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1477
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1478
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1479
- 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);
1480
1586
  }
1481
- }
1482
- throw new Error("element doesn't contain text " + text);
1483
- }
1484
- else if (numberAlternatives.number) {
1485
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1486
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1487
- 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)) {
1488
1607
  return state.info;
1489
1608
  }
1490
1609
  }
1491
- throw new Error("element doesn't contain text " + text);
1492
- }
1493
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1494
- state.info.foundText = foundObj?.text;
1495
- state.info.value = foundObj?.value;
1496
- 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
1497
1615
  }
1498
- 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);
1499
1619
  }
1500
1620
  catch (e) {
1501
1621
  await _commandError(state, e, this);
1622
+ throw e;
1502
1623
  }
1503
1624
  finally {
1504
1625
  _commandFinally(state, this);
1505
1626
  }
1506
1627
  }
1507
- _getDataFile(world = null) {
1508
- let dataFile = null;
1509
- if (world && world.reportFolder) {
1510
- dataFile = path.join(world.reportFolder, "data.json");
1511
- }
1512
- else if (this.reportFolder) {
1513
- dataFile = path.join(this.reportFolder, "data.json");
1514
- }
1515
- else if (this.context && this.context.reportFolder) {
1516
- dataFile = path.join(this.context.reportFolder, "data.json");
1517
- }
1518
- else {
1519
- dataFile = "data.json";
1520
- }
1521
- return dataFile;
1522
- }
1523
1628
  async waitForUserInput(message, world = null) {
1524
1629
  if (!message) {
1525
1630
  message = "# Wait for user input. Press any key to continue";
@@ -1548,13 +1653,22 @@ class StableBrowser {
1548
1653
  return;
1549
1654
  }
1550
1655
  // if data file exists, load it
1551
- const dataFile = this._getDataFile(world);
1656
+ const dataFile = _getDataFile(world, this.context, this);
1552
1657
  let data = this.getTestData(world);
1553
1658
  // merge the testData with the existing data
1554
1659
  Object.assign(data, testData);
1555
1660
  // save the data to the file
1556
1661
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1557
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
+ }
1558
1672
  _getDataFilePath(fileName) {
1559
1673
  let dataFile = path.join(this.project_path, "data", fileName);
1560
1674
  if (fs.existsSync(dataFile)) {
@@ -1651,7 +1765,7 @@ class StableBrowser {
1651
1765
  }
1652
1766
  }
1653
1767
  getTestData(world = null) {
1654
- const dataFile = this._getDataFile(world);
1768
+ const dataFile = _getDataFile(world, this.context, this);
1655
1769
  let data = {};
1656
1770
  if (fs.existsSync(dataFile)) {
1657
1771
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1738,6 +1852,15 @@ class StableBrowser {
1738
1852
  document.documentElement.clientWidth,
1739
1853
  ])));
1740
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
+ // }
1741
1864
  if (this.context.browserName === "chromium") {
1742
1865
  const client = await playContext.newCDPSession(this.page);
1743
1866
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1759,6 +1882,10 @@ class StableBrowser {
1759
1882
  else {
1760
1883
  screenshotBuffer = await this.page.screenshot();
1761
1884
  }
1885
+ // if (focusedElement) {
1886
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1887
+ // await this._unhighlightElements(focusedElement);
1888
+ // }
1762
1889
  let image = await Jimp.read(screenshotBuffer);
1763
1890
  // Get the image dimensions
1764
1891
  const { width, height } = image.bitmap;
@@ -1771,6 +1898,7 @@ class StableBrowser {
1771
1898
  else {
1772
1899
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1773
1900
  }
1901
+ return screenshotBuffer;
1774
1902
  }
1775
1903
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1776
1904
  const state = {
@@ -1806,8 +1934,10 @@ class StableBrowser {
1806
1934
  world,
1807
1935
  type: Types.EXTRACT,
1808
1936
  text: `Extract attribute from element`,
1937
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1809
1938
  operation: "extractAttribute",
1810
1939
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1940
+ allowDisabled: true,
1811
1941
  };
1812
1942
  await new Promise((resolve) => setTimeout(resolve, 2000));
1813
1943
  try {
@@ -1829,6 +1959,7 @@ class StableBrowser {
1829
1959
  state.info.value = state.value;
1830
1960
  this.setTestData({ [variable]: state.value }, world);
1831
1961
  this.logger.info("set test data: " + variable + "=" + state.value);
1962
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1832
1963
  return state.info;
1833
1964
  }
1834
1965
  catch (e) {
@@ -1847,14 +1978,21 @@ class StableBrowser {
1847
1978
  options,
1848
1979
  world,
1849
1980
  type: Types.VERIFY_ATTRIBUTE,
1981
+ highlight: true,
1982
+ screenshot: true,
1850
1983
  text: `Verify element attribute`,
1984
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1851
1985
  operation: "verifyAttribute",
1852
1986
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1987
+ allowDisabled: true,
1853
1988
  };
1854
1989
  await new Promise((resolve) => setTimeout(resolve, 2000));
1855
1990
  let val;
1991
+ let expectedValue;
1856
1992
  try {
1857
1993
  await _preCommand(state, this);
1994
+ expectedValue = state.value;
1995
+ state.info.expectedValue = expectedValue;
1858
1996
  switch (attribute) {
1859
1997
  case "innerText":
1860
1998
  val = String(await state.element.innerText());
@@ -1876,19 +2014,22 @@ class StableBrowser {
1876
2014
  val = String(await state.element.getAttribute(attribute));
1877
2015
  break;
1878
2016
  }
2017
+ state.info.value = val;
1879
2018
  let regex;
1880
- if (value.startsWith("/") && value.endsWith("/")) {
1881
- const patternBody = value.slice(1, -1);
2019
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2020
+ const patternBody = expectedValue.slice(1, -1);
1882
2021
  regex = new RegExp(patternBody, "g");
1883
2022
  }
1884
2023
  else {
1885
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2024
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1886
2025
  regex = new RegExp(escapedPattern, "g");
1887
2026
  }
1888
2027
  if (!val.match(regex)) {
1889
- 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);
1890
2032
  }
1891
- state.info.expectedValue = val;
1892
2033
  return state.info;
1893
2034
  }
1894
2035
  catch (e) {
@@ -1987,27 +2128,32 @@ class StableBrowser {
1987
2128
  async _highlightElements(scope, css) {
1988
2129
  try {
1989
2130
  if (!scope) {
2131
+ // console.log(`Scope is not defined`);
1990
2132
  return;
1991
2133
  }
1992
2134
  if (!css) {
1993
2135
  scope
1994
2136
  .evaluate((node) => {
1995
2137
  if (node && node.style) {
1996
- let originalBorder = node.style.border;
1997
- 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}`);
1998
2143
  if (window) {
1999
2144
  window.addEventListener("beforeunload", function (e) {
2000
- node.style.border = originalBorder;
2145
+ node.style.outline = originalOutline;
2001
2146
  });
2002
2147
  }
2003
2148
  setTimeout(function () {
2004
- node.style.border = originalBorder;
2149
+ node.style.outline = originalOutline;
2005
2150
  }, 2000);
2006
2151
  }
2007
2152
  })
2008
2153
  .then(() => { })
2009
2154
  .catch((e) => {
2010
2155
  // ignore
2156
+ // console.error(`Could not highlight node : ${e}`);
2011
2157
  });
2012
2158
  }
2013
2159
  else {
@@ -2023,17 +2169,18 @@ class StableBrowser {
2023
2169
  if (!element.style) {
2024
2170
  return;
2025
2171
  }
2026
- var originalBorder = element.style.border;
2172
+ let originalOutline = element.style.outline;
2173
+ element.__previousOutline = originalOutline;
2027
2174
  // Set the new border to be red and 2px solid
2028
- element.style.border = "2px solid red";
2175
+ element.style.outline = "2px solid red";
2029
2176
  if (window) {
2030
2177
  window.addEventListener("beforeunload", function (e) {
2031
- element.style.border = originalBorder;
2178
+ element.style.outline = originalOutline;
2032
2179
  });
2033
2180
  }
2034
2181
  // Set a timeout to revert to the original border after 2 seconds
2035
2182
  setTimeout(function () {
2036
- element.style.border = originalBorder;
2183
+ element.style.outline = originalOutline;
2037
2184
  }, 2000);
2038
2185
  }
2039
2186
  return;
@@ -2041,6 +2188,7 @@ class StableBrowser {
2041
2188
  .then(() => { })
2042
2189
  .catch((e) => {
2043
2190
  // ignore
2191
+ // console.error(`Could not highlight css: ${e}`);
2044
2192
  });
2045
2193
  }
2046
2194
  }
@@ -2048,6 +2196,54 @@ class StableBrowser {
2048
2196
  console.debug(error);
2049
2197
  }
2050
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
+ // }
2051
2247
  async verifyPagePath(pathPart, options = {}, world = null) {
2052
2248
  const startTime = Date.now();
2053
2249
  let error = null;
@@ -2092,6 +2288,7 @@ class StableBrowser {
2092
2288
  _reportToWorld(world, {
2093
2289
  type: Types.VERIFY_PAGE_PATH,
2094
2290
  text: "Verify page path",
2291
+ _text: "Verify the page path contains " + pathPart,
2095
2292
  screenshotId,
2096
2293
  result: error
2097
2294
  ? {
@@ -2109,26 +2306,89 @@ class StableBrowser {
2109
2306
  });
2110
2307
  }
2111
2308
  }
2112
- 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) {
2113
2372
  const frames = this.page.frames();
2114
2373
  let results = [];
2374
+ // let ignoreCase = false;
2115
2375
  for (let i = 0; i < frames.length; i++) {
2116
2376
  if (dateAlternatives.date) {
2117
2377
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2118
- 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, {});
2119
2379
  result.frame = frames[i];
2120
2380
  results.push(result);
2121
2381
  }
2122
2382
  }
2123
2383
  else if (numberAlternatives.number) {
2124
2384
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2125
- 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, {});
2126
2386
  result.frame = frames[i];
2127
2387
  results.push(result);
2128
2388
  }
2129
2389
  }
2130
2390
  else {
2131
- 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, {});
2132
2392
  result.frame = frames[i];
2133
2393
  results.push(result);
2134
2394
  }
@@ -2147,11 +2407,15 @@ class StableBrowser {
2147
2407
  scroll: false,
2148
2408
  highlight: false,
2149
2409
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2150
- text: `Verify text exists in page`,
2410
+ text: `Verify the text '${text}' exists in page`,
2411
+ _text: `Verify the text '${text}' exists in page`,
2151
2412
  operation: "verifyTextExistInPage",
2152
2413
  log: "***** verify text " + text + " exists in page *****\n",
2153
2414
  };
2154
- const timeout = this._getLoadTimeout(options);
2415
+ if (testForRegex(text)) {
2416
+ text = text.replace(/\\"/g, '"');
2417
+ }
2418
+ const timeout = this._getFindElementTimeout(options);
2155
2419
  await new Promise((resolve) => setTimeout(resolve, 2000));
2156
2420
  const newValue = await this._replaceWithLocalData(text, world);
2157
2421
  if (newValue !== text) {
@@ -2164,7 +2428,15 @@ class StableBrowser {
2164
2428
  await _preCommand(state, this);
2165
2429
  state.info.text = text;
2166
2430
  while (true) {
2167
- 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
+ }
2168
2440
  if (resultWithElementsFound.length === 0) {
2169
2441
  if (Date.now() - state.startTime > timeout) {
2170
2442
  throw new Error(`Text ${text} not found in page`);
@@ -2172,18 +2444,40 @@ class StableBrowser {
2172
2444
  await new Promise((resolve) => setTimeout(resolve, 1000));
2173
2445
  continue;
2174
2446
  }
2175
- if (resultWithElementsFound[0].randomToken) {
2176
- const frame = resultWithElementsFound[0].frame;
2177
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2178
- await this._highlightElements(frame, dataAttribute);
2179
- const element = await frame.$(dataAttribute);
2180
- if (element) {
2181
- await this.scrollIfNeeded(element, state.info);
2182
- 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
+ }
2183
2474
  }
2475
+ await _screenshot(state, this);
2476
+ return state.info;
2477
+ }
2478
+ catch (error) {
2479
+ console.error(error);
2184
2480
  }
2185
- await _screenshot(state, this);
2186
- return state.info;
2187
2481
  }
2188
2482
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2189
2483
  }
@@ -2204,11 +2498,15 @@ class StableBrowser {
2204
2498
  scroll: false,
2205
2499
  highlight: false,
2206
2500
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2207
- 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`,
2208
2503
  operation: "verifyTextNotExistInPage",
2209
2504
  log: "***** verify text " + text + " does not exist in page *****\n",
2210
2505
  };
2211
- const timeout = this._getLoadTimeout(options);
2506
+ if (testForRegex(text)) {
2507
+ text = text.replace(/\\"/g, '"');
2508
+ }
2509
+ const timeout = this._getFindElementTimeout(options);
2212
2510
  await new Promise((resolve) => setTimeout(resolve, 2000));
2213
2511
  const newValue = await this._replaceWithLocalData(text, world);
2214
2512
  if (newValue !== text) {
@@ -2220,8 +2518,16 @@ class StableBrowser {
2220
2518
  try {
2221
2519
  await _preCommand(state, this);
2222
2520
  state.info.text = text;
2521
+ let resultWithElementsFound = {
2522
+ length: null, // initial cannot be 0
2523
+ };
2223
2524
  while (true) {
2224
- 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
+ }
2225
2531
  if (resultWithElementsFound.length === 0) {
2226
2532
  await _screenshot(state, this);
2227
2533
  return state.info;
@@ -2251,10 +2557,11 @@ class StableBrowser {
2251
2557
  highlight: false,
2252
2558
  type: Types.VERIFY_TEXT_WITH_RELATION,
2253
2559
  text: `Verify text with relation to another text`,
2560
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2254
2561
  operation: "verify_text_with_relation",
2255
2562
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2256
2563
  };
2257
- const timeout = this._getLoadTimeout(options);
2564
+ const timeout = this._getFindElementTimeout(options);
2258
2565
  await new Promise((resolve) => setTimeout(resolve, 2000));
2259
2566
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2260
2567
  if (newValue !== textAnchor) {
@@ -2272,8 +2579,16 @@ class StableBrowser {
2272
2579
  try {
2273
2580
  await _preCommand(state, this);
2274
2581
  state.info.text = textToVerify;
2582
+ let resultWithElementsFound = {
2583
+ length: 0,
2584
+ };
2275
2585
  while (true) {
2276
- 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
+ }
2277
2592
  if (resultWithElementsFound.length === 0) {
2278
2593
  if (Date.now() - state.startTime > timeout) {
2279
2594
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2281,51 +2596,56 @@ class StableBrowser {
2281
2596
  await new Promise((resolve) => setTimeout(resolve, 1000));
2282
2597
  continue;
2283
2598
  }
2284
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2285
- foundAncore = true;
2286
- const result = resultWithElementsFound[i];
2287
- const token = result.randomToken;
2288
- const frame = result.frame;
2289
- const css = `[data-blinq-id="blinq-id-${token}"]`;
2290
- const findResult = await frame.evaluate(([css, climb, textToVerify, token]) => {
2291
- const elements = Array.from(document.querySelectorAll(css));
2292
- for (let i = 0; i < elements.length; i++) {
2293
- const element = elements[i];
2294
- let climbParent = element;
2295
- for (let j = 0; j < climb; j++) {
2296
- climbParent = climbParent.parentElement;
2297
- if (!climbParent) {
2298
- 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, true, 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");
2299
2639
  }
2300
- }
2301
- if (!climbParent) {
2302
- continue;
2303
- }
2304
- const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
2305
- if (foundElements.length > 0) {
2306
- // set the container element attribute
2307
- element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
2308
- climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
2309
- foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
2310
- return { found: true };
2640
+ await _screenshot(state, this);
2641
+ return state.info;
2311
2642
  }
2312
2643
  }
2313
- return { found: false };
2314
- }, [css, climb, textToVerify, result.randomToken]);
2315
- if (findResult.found === true) {
2316
- const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
2317
- const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2318
- await this._highlightElements(frame, dataAttribute);
2319
- await this._highlightElements(frame, cssAnchor);
2320
- const element = await frame.$(dataAttribute);
2321
- if (element) {
2322
- await this.scrollIfNeeded(element, state.info);
2323
- await element.dispatchEvent("bvt_verify_page_contains_text");
2324
- }
2325
- await _screenshot(state, this);
2326
- return state.info;
2327
2644
  }
2328
2645
  }
2646
+ catch (error) {
2647
+ console.error(error);
2648
+ }
2329
2649
  }
2330
2650
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2331
2651
  }
@@ -2336,6 +2656,30 @@ class StableBrowser {
2336
2656
  _commandFinally(state, this);
2337
2657
  }
2338
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
+ }
2339
2683
  async visualVerification(text, options = {}, world = null) {
2340
2684
  const startTime = Date.now();
2341
2685
  let error = null;
@@ -2354,10 +2698,13 @@ class StableBrowser {
2354
2698
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2355
2699
  info.screenshotPath = screenshotPath;
2356
2700
  const screenshot = await this.takeScreenshot();
2357
- const request = {
2358
- method: "POST",
2701
+ let request = {
2702
+ method: "post",
2703
+ maxBodyLength: Infinity,
2359
2704
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2360
2705
  headers: {
2706
+ "x-bvt-project-id": path.basename(this.project_path),
2707
+ "x-source": "aaa",
2361
2708
  "Content-Type": "application/json",
2362
2709
  Authorization: `Bearer ${process.env.TOKEN}`,
2363
2710
  },
@@ -2366,7 +2713,7 @@ class StableBrowser {
2366
2713
  screenshot: screenshot,
2367
2714
  }),
2368
2715
  };
2369
- let result = await this.context.api.request(request);
2716
+ const result = await axios.request(request);
2370
2717
  if (result.data.status !== true) {
2371
2718
  throw new Error("Visual validation failed");
2372
2719
  }
@@ -2394,6 +2741,7 @@ class StableBrowser {
2394
2741
  _reportToWorld(world, {
2395
2742
  type: Types.VERIFY_VISUAL,
2396
2743
  text: "Visual verification",
2744
+ _text: "Visual verification of " + text,
2397
2745
  screenshotId,
2398
2746
  result: error
2399
2747
  ? {
@@ -2660,6 +3008,32 @@ class StableBrowser {
2660
3008
  }
2661
3009
  return timeout;
2662
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
+ }
2663
3037
  async waitForPageLoad(options = {}, world = null) {
2664
3038
  let timeout = this._getLoadTimeout(options);
2665
3039
  const promiseArray = [];
@@ -2727,6 +3101,7 @@ class StableBrowser {
2727
3101
  highlight: false,
2728
3102
  type: Types.CLOSE_PAGE,
2729
3103
  text: `Close page`,
3104
+ _text: `Close the page`,
2730
3105
  operation: "closePage",
2731
3106
  log: "***** close page *****\n",
2732
3107
  throwError: false,
@@ -2743,8 +3118,95 @@ class StableBrowser {
2743
3118
  _commandFinally(state, this);
2744
3119
  }
2745
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
+ }
2746
3208
  saveTestDataAsGlobal(options, world) {
2747
- const dataFile = this._getDataFile(world);
3209
+ const dataFile = _getDataFile(world, this.context, this);
2748
3210
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2749
3211
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2750
3212
  }
@@ -2774,6 +3236,7 @@ class StableBrowser {
2774
3236
  _reportToWorld(world, {
2775
3237
  type: Types.SET_VIEWPORT,
2776
3238
  text: "set viewport size to " + width + "x" + hight,
3239
+ _text: "Set the viewport size to " + width + "x" + hight,
2777
3240
  screenshotId,
2778
3241
  result: error
2779
3242
  ? {
@@ -2861,6 +3324,9 @@ class StableBrowser {
2861
3324
  else {
2862
3325
  this.stepName = "step " + this.stepIndex;
2863
3326
  }
3327
+ if (this.context) {
3328
+ this.context.examplesRow = extractStepExampleParameters(step);
3329
+ }
2864
3330
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2865
3331
  if (this.context.browserObject.context) {
2866
3332
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2873,6 +3339,41 @@ class StableBrowser {
2873
3339
  this.saveTestDataAsGlobal({}, world);
2874
3340
  }
2875
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;
2876
3377
  }
2877
3378
  async afterStep(world, step) {
2878
3379
  this.stepName = null;
@@ -2881,6 +3382,21 @@ class StableBrowser {
2881
3382
  await this.context.browserObject.context.tracing.stopChunk({
2882
3383
  path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
2883
3384
  });
3385
+ await world.attach(JSON.stringify({
3386
+ type: "trace",
3387
+ traceFilePath: `trace-${this.stepIndex}.zip`,
3388
+ }), "application/json+trace");
3389
+ // console.log("trace file created", `trace-${this.stepIndex}.zip`);
3390
+ }
3391
+ }
3392
+ if (this.context) {
3393
+ this.context.examplesRow = null;
3394
+ }
3395
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3396
+ const snapshot = await this.getAriaSnapshot();
3397
+ if (snapshot) {
3398
+ const obj = {};
3399
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
2884
3400
  }
2885
3401
  }
2886
3402
  }