automation_model 1.0.567-dev → 1.0.567-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;
@@ -235,16 +243,51 @@ class StableBrowser {
235
243
  // async closeUnexpectedPopups() {
236
244
  // await closeUnexpectedPopups(this.page);
237
245
  // }
238
- async goto(url) {
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
  }
242
- await this.page.goto(url, {
243
- timeout: 60000,
244
- });
253
+ const state = {
254
+ value: url,
255
+ world: world,
256
+ type: Types.NAVIGATE,
257
+ text: `Navigate Page to: ${url}`,
258
+ operation: "goto",
259
+ log: "***** navigate page to " + url + " *****\n",
260
+ info: {},
261
+ locate: false,
262
+ scroll: false,
263
+ screenshot: false,
264
+ highlight: false,
265
+ };
266
+ try {
267
+ await _preCommand(state, this);
268
+ await this.page.goto(url, {
269
+ timeout: 60000,
270
+ });
271
+ await _screenshot(state, this);
272
+ }
273
+ catch (error) {
274
+ console.error("Error on goto", error);
275
+ _commandError(state, error, this);
276
+ }
277
+ finally {
278
+ _commandFinally(state, this);
279
+ }
245
280
  }
246
- _getLocator(locator, scope, _params) {
281
+ async _getLocator(locator, scope, _params) {
247
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
+ }
248
291
  let locatorReturn;
249
292
  if (locator.role) {
250
293
  if (locator.role[1].nameReg) {
@@ -295,52 +338,60 @@ class StableBrowser {
295
338
  if (css && css.locator) {
296
339
  css = css.locator;
297
340
  }
298
- 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);
299
342
  if (result.elementCount === 0) {
300
343
  return;
301
344
  }
302
- let textElementCss = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
345
+ let textElementCss = "[data-blinq-id-" + result.randomToken + "]";
303
346
  // css climb to parent element
304
347
  const climbArray = [];
305
348
  for (let i = 0; i < climb; i++) {
306
349
  climbArray.push("..");
307
350
  }
308
351
  let climbXpath = "xpath=" + climbArray.join("/");
309
- return textElementCss + " >> " + climbXpath + " >> " + css;
310
- }
311
- async _locateElementByText(scope, text1, tag1, regex1 = false, partial1, _params) {
312
- //const stringifyText = JSON.stringify(text);
313
- return await scope.locator(":root").evaluate((_node, [text, tag, regex, partial]) => {
314
- const options = {
315
- innerText: true,
316
- };
317
- if (regex) {
318
- options.singleRegex = true;
319
- }
320
- if (tag) {
321
- options.tag = tag;
322
- }
323
- if (!(partial === true)) {
324
- options.exactMatch = true;
325
- }
326
- const elements = window.findMatchingElements(text, options);
327
- let randomToken = null;
328
- const foundElements = [];
329
- let elementCount = 0;
330
- if (elements.length > 0) {
331
- for (let i = 0; i < elements.length; i++) {
332
- if (randomToken === null) {
333
- 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;
334
374
  }
335
- let element = elements[i];
336
- element.setAttribute("data-blinq-id", "blinq-id-" + randomToken);
337
- elementCount++;
338
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;
339
389
  }
340
- return { elementCount: elementCount, randomToken: randomToken };
341
- }, [text1, tag1, regex1, partial1]);
390
+ tagCount++;
391
+ }
392
+ return { elementCount: tagCount, randomToken };
342
393
  }
343
- 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) {
344
395
  if (!info) {
345
396
  info = {};
346
397
  }
@@ -363,30 +414,31 @@ class StableBrowser {
363
414
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
364
415
  let locator = null;
365
416
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
366
- 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);
367
419
  if (!locatorString) {
368
420
  info.failCause.textNotFound = true;
369
- 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}`;
370
422
  return;
371
423
  }
372
- locator = this._getLocator({ css: locatorString }, scope, _params);
424
+ locator = await this._getLocator({ css: locatorString }, scope, _params);
373
425
  }
374
426
  else if (locatorSearch.text) {
375
427
  let text = _fixUsingParams(locatorSearch.text, _params);
376
- 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);
377
429
  if (result.elementCount === 0) {
378
430
  info.failCause.textNotFound = true;
379
- info.failCause.lastError = "failed to locate element by text: " + text;
431
+ info.failCause.lastError = `failed to locate ${formatElementName(element_name)} by text: ${text}`;
380
432
  return;
381
433
  }
382
- locatorSearch.css = "[data-blinq-id='blinq-id-" + result.randomToken + "']";
434
+ locatorSearch.css = "[data-blinq-id-" + result.randomToken + "]";
383
435
  if (locatorSearch.childCss) {
384
436
  locatorSearch.css = locatorSearch.css + " " + locatorSearch.childCss;
385
437
  }
386
- locator = this._getLocator(locatorSearch, scope, _params);
438
+ locator = await this._getLocator(locatorSearch, scope, _params);
387
439
  }
388
440
  else {
389
- locator = this._getLocator(locatorSearch, scope, _params);
441
+ locator = await this._getLocator(locatorSearch, scope, _params);
390
442
  }
391
443
  // let cssHref = false;
392
444
  // if (locatorSearch.css && locatorSearch.css.includes("href=")) {
@@ -415,7 +467,7 @@ class StableBrowser {
415
467
  if (!visibleOnly) {
416
468
  visible = true;
417
469
  }
418
- if (visible && enabled) {
470
+ if (visible && (allowDisabled || enabled)) {
419
471
  foundLocators.push(locator.nth(j));
420
472
  if (info.locatorLog) {
421
473
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND");
@@ -428,9 +480,11 @@ class StableBrowser {
428
480
  info.printMessages = {};
429
481
  }
430
482
  if (info.locatorLog && !visible) {
483
+ info.failCause.lastError = `${formatElementName(element_name)} is not visible, searching for ${originalLocatorSearch}`;
431
484
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_VISIBLE");
432
485
  }
433
486
  if (info.locatorLog && !enabled) {
487
+ info.failCause.lastError = `${formatElementName(element_name)} is disabled, searching for ${originalLocatorSearch}`;
434
488
  info.locatorLog.setLocatorSearchStatus(originalLocatorSearch, "FOUND_NOT_ENABLED");
435
489
  }
436
490
  if (!info.printMessages[j.toString()]) {
@@ -498,7 +552,7 @@ class StableBrowser {
498
552
  }
499
553
  return { rerun: false };
500
554
  }
501
- async _locate(selectors, info, _params, timeout) {
555
+ async _locate(selectors, info, _params, timeout, allowDisabled = false) {
502
556
  if (!timeout) {
503
557
  timeout = 30000;
504
558
  }
@@ -508,9 +562,30 @@ class StableBrowser {
508
562
  let selector = selectors.locators[j];
509
563
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
510
564
  }
511
- let element = await this._locate_internal(selectors, info, _params, timeout);
565
+ let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
512
566
  if (!element.rerun) {
513
- 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);
514
589
  }
515
590
  }
516
591
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -583,7 +658,7 @@ class StableBrowser {
583
658
  //info.log += "unable to locate iframe " + selectors.iframe_src + "\n";
584
659
  if (Date.now() - startTime > timeout) {
585
660
  info.failCause.iframeNotFound = true;
586
- info.failCause.lastError = "unable to locate iframe " + selectors.iframe_src;
661
+ info.failCause.lastError = `unable to locate iframe "${selectors.iframe_src}"`;
587
662
  throw new Error("unable to locate iframe " + selectors.iframe_src);
588
663
  }
589
664
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -608,7 +683,7 @@ class StableBrowser {
608
683
  return bodyContent;
609
684
  });
610
685
  }
611
- async _locate_internal(selectors, info, _params, timeout = 30000) {
686
+ async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
612
687
  if (!info) {
613
688
  info = {};
614
689
  info.failCause = {};
@@ -657,18 +732,13 @@ class StableBrowser {
657
732
  }
658
733
  // info.log += "scanning locators in priority 1" + "\n";
659
734
  let onlyPriority3 = selectorsLocators[0].priority === 3;
660
- 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);
661
736
  if (result.foundElements.length === 0) {
662
737
  // info.log += "scanning locators in priority 2" + "\n";
663
- result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly);
664
- }
665
- if (result.foundElements.length === 0 && onlyPriority3) {
666
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
738
+ result = await this._scanLocatorsGroup(locatorsByPriority["2"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
667
739
  }
668
- else {
669
- if (result.foundElements.length === 0 && !highPriorityOnly) {
670
- result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly);
671
- }
740
+ if (result.foundElements.length === 0 && (onlyPriority3 || !highPriorityOnly)) {
741
+ result = await this._scanLocatorsGroup(locatorsByPriority["3"], scope, _params, info, visibleOnly, allowDisabled, selectors?.element_name);
672
742
  }
673
743
  let foundElements = result.foundElements;
674
744
  if (foundElements.length === 1 && foundElements[0].unique) {
@@ -712,7 +782,7 @@ class StableBrowser {
712
782
  break;
713
783
  }
714
784
  if (Date.now() - startTime > highPriorityTimeout) {
715
- info.log += "high priority timeout, will try all elements" + "\n";
785
+ //info.log += "high priority timeout, will try all elements" + "\n";
716
786
  highPriorityOnly = false;
717
787
  if (this.configuration && this.configuration.load_all_lazy === true && !lazy_scroll) {
718
788
  lazy_scroll = true;
@@ -720,10 +790,15 @@ class StableBrowser {
720
790
  }
721
791
  }
722
792
  if (Date.now() - startTime > visibleOnlyTimeout) {
723
- info.log += "visible only timeout, will try all elements" + "\n";
793
+ //info.log += "visible only timeout, will try all elements" + "\n";
724
794
  visibleOnly = false;
725
795
  }
726
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
+ }
727
802
  }
728
803
  this.logger.debug("unable to locate unique element, total elements found " + locatorsCount);
729
804
  // if (info.locatorLog) {
@@ -734,10 +809,12 @@ class StableBrowser {
734
809
  // }
735
810
  //info.log += "failed to locate unique element, total elements found " + locatorsCount + "\n";
736
811
  info.failCause.locatorNotFound = true;
737
- 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
+ }
738
815
  throw new Error("failed to locate first element no elements found, " + info.log);
739
816
  }
740
- async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly) {
817
+ async _scanLocatorsGroup(locatorsGroup, scope, _params, info, visibleOnly, allowDisabled = false, element_name) {
741
818
  let foundElements = [];
742
819
  const result = {
743
820
  foundElements: foundElements,
@@ -745,7 +822,7 @@ class StableBrowser {
745
822
  for (let i = 0; i < locatorsGroup.length; i++) {
746
823
  let foundLocators = [];
747
824
  try {
748
- await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly);
825
+ await this._collectLocatorInformation(locatorsGroup, i, scope, foundLocators, _params, info, visibleOnly, allowDisabled, element_name);
749
826
  }
750
827
  catch (e) {
751
828
  // this call can fail it the browser is navigating
@@ -753,7 +830,7 @@ class StableBrowser {
753
830
  // this.logger.debug(e);
754
831
  foundLocators = [];
755
832
  try {
756
- 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);
757
834
  }
758
835
  catch (e) {
759
836
  this.logger.info("unable to use locator (second try) " + JSON.stringify(locatorsGroup[i]));
@@ -768,9 +845,40 @@ class StableBrowser {
768
845
  result.locatorIndex = i;
769
846
  }
770
847
  if (foundLocators.length > 1) {
771
- info.failCause.foundMultiple = true;
772
- if (info.locatorLog) {
773
- 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
+ }
774
882
  }
775
883
  }
776
884
  }
@@ -881,25 +989,14 @@ class StableBrowser {
881
989
  options,
882
990
  world,
883
991
  text: "Click element",
992
+ _text: "Click on " + selectors.element_name,
884
993
  type: Types.CLICK,
885
994
  operation: "click",
886
995
  log: "***** click on " + selectors.element_name + " *****\n",
887
996
  };
888
997
  try {
889
998
  await _preCommand(state, this);
890
- if (state.options && state.options.context) {
891
- state.selectors.locators[0].text = state.options.context;
892
- }
893
- try {
894
- await state.element.click();
895
- // await new Promise((resolve) => setTimeout(resolve, 1000));
896
- }
897
- catch (e) {
898
- // await this.closeUnexpectedPopups();
899
- state.element = await this._locate(selectors, state.info, _params);
900
- await state.element.dispatchEvent("click");
901
- // await new Promise((resolve) => setTimeout(resolve, 1000));
902
- }
999
+ await performAction("click", state.element, options, this, state, _params);
903
1000
  await this.waitForPageLoad();
904
1001
  return state.info;
905
1002
  }
@@ -910,6 +1007,38 @@ class StableBrowser {
910
1007
  _commandFinally(state, this);
911
1008
  }
912
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
+ }
913
1042
  async setCheck(selectors, checked = true, _params, options = {}, world = null) {
914
1043
  const state = {
915
1044
  selectors,
@@ -918,6 +1047,7 @@ class StableBrowser {
918
1047
  world,
919
1048
  type: checked ? Types.CHECK : Types.UNCHECK,
920
1049
  text: checked ? `Check element` : `Uncheck element`,
1050
+ _text: checked ? `Check ${selectors.element_name}` : `Uncheck ${selectors.element_name}`,
921
1051
  operation: "setCheck",
922
1052
  log: "***** check " + selectors.element_name + " *****\n",
923
1053
  };
@@ -927,9 +1057,15 @@ class StableBrowser {
927
1057
  // let element = await this._locate(selectors, info, _params);
928
1058
  // ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
929
1059
  try {
930
- // 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);
931
1063
  await state.element.setChecked(checked);
932
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);
933
1069
  }
934
1070
  catch (e) {
935
1071
  if (e.message && e.message.includes("did not change its state")) {
@@ -961,22 +1097,13 @@ class StableBrowser {
961
1097
  world,
962
1098
  type: Types.HOVER,
963
1099
  text: `Hover element`,
1100
+ _text: `Hover on ${selectors.element_name}`,
964
1101
  operation: "hover",
965
1102
  log: "***** hover " + selectors.element_name + " *****\n",
966
1103
  };
967
1104
  try {
968
1105
  await _preCommand(state, this);
969
- try {
970
- await state.element.hover();
971
- await new Promise((resolve) => setTimeout(resolve, 1000));
972
- }
973
- catch (e) {
974
- //await this.closeUnexpectedPopups();
975
- state.info.log += "hover failed, will try again" + "\n";
976
- state.element = await this._locate(selectors, state.info, _params);
977
- await state.element.hover({ timeout: 10000 });
978
- await new Promise((resolve) => setTimeout(resolve, 1000));
979
- }
1106
+ await performAction("hover", state.element, options, this, state, _params);
980
1107
  await _screenshot(state, this);
981
1108
  await this.waitForPageLoad();
982
1109
  return state.info;
@@ -1000,6 +1127,7 @@ class StableBrowser {
1000
1127
  value: values.toString(),
1001
1128
  type: Types.SELECT,
1002
1129
  text: `Select option: ${values}`,
1130
+ _text: `Select option: ${values} on ${selectors.element_name}`,
1003
1131
  operation: "selectOption",
1004
1132
  log: "***** select option " + selectors.element_name + " *****\n",
1005
1133
  };
@@ -1034,6 +1162,7 @@ class StableBrowser {
1034
1162
  highlight: false,
1035
1163
  type: Types.TYPE_PRESS,
1036
1164
  text: `Type value: ${_value}`,
1165
+ _text: `Type value: ${_value}`,
1037
1166
  operation: "type",
1038
1167
  log: "",
1039
1168
  };
@@ -1113,6 +1242,7 @@ class StableBrowser {
1113
1242
  world,
1114
1243
  type: Types.SET_DATE_TIME,
1115
1244
  text: `Set date time value: ${value}`,
1245
+ _text: `Set date time value: ${value} on ${selectors.element_name}`,
1116
1246
  operation: "setDateTime",
1117
1247
  log: "***** set date time value " + selectors.element_name + " *****\n",
1118
1248
  throwError: false,
@@ -1120,7 +1250,7 @@ class StableBrowser {
1120
1250
  try {
1121
1251
  await _preCommand(state, this);
1122
1252
  try {
1123
- await state.element.click();
1253
+ await performAction("click", state.element, options, this, state, _params);
1124
1254
  await new Promise((resolve) => setTimeout(resolve, 500));
1125
1255
  if (format) {
1126
1256
  state.value = dayjs(state.value).format(format);
@@ -1184,9 +1314,13 @@ class StableBrowser {
1184
1314
  world,
1185
1315
  type: Types.FILL,
1186
1316
  text: `Click type input with value: ${_value}`,
1317
+ _text: "Fill " + selectors.element_name + " with value " + maskValue(_value),
1187
1318
  operation: "clickType",
1188
1319
  log: "***** clickType on " + selectors.element_name + " with value " + maskValue(_value) + "*****\n",
1189
1320
  };
1321
+ if (!options) {
1322
+ options = {};
1323
+ }
1190
1324
  if (newValue !== _value) {
1191
1325
  //this.logger.info(_value + "=" + newValue);
1192
1326
  _value = newValue;
@@ -1194,7 +1328,7 @@ class StableBrowser {
1194
1328
  try {
1195
1329
  await _preCommand(state, this);
1196
1330
  state.info.value = _value;
1197
- if (options === null || options === undefined || !options.press) {
1331
+ if (!options.press) {
1198
1332
  try {
1199
1333
  let currentValue = await state.element.inputValue();
1200
1334
  if (currentValue) {
@@ -1205,13 +1339,9 @@ class StableBrowser {
1205
1339
  this.logger.info("unable to clear input value");
1206
1340
  }
1207
1341
  }
1208
- if (options === null || options === undefined || options.press) {
1209
- try {
1210
- await state.element.click({ timeout: 5000 });
1211
- }
1212
- catch (e) {
1213
- await state.element.dispatchEvent("click");
1214
- }
1342
+ if (options.press) {
1343
+ options.timeout = 5000;
1344
+ await performAction("click", state.element, options, this, state, _params);
1215
1345
  }
1216
1346
  else {
1217
1347
  try {
@@ -1249,7 +1379,12 @@ class StableBrowser {
1249
1379
  await this.waitForPageLoad();
1250
1380
  }
1251
1381
  else if (enter === false) {
1252
- await state.element.dispatchEvent("change");
1382
+ try {
1383
+ await state.element.dispatchEvent("change", null, { timeout: 5000 });
1384
+ }
1385
+ catch (e) {
1386
+ // ignore
1387
+ }
1253
1388
  //await this.page.keyboard.press("Tab");
1254
1389
  }
1255
1390
  else {
@@ -1301,6 +1436,7 @@ class StableBrowser {
1301
1436
  return await this._getText(selectors, 0, _params, options, info, world);
1302
1437
  }
1303
1438
  async _getText(selectors, climb, _params = null, options = {}, info = {}, world = null) {
1439
+ const timeout = this._getFindElementTimeout(options);
1304
1440
  _validateSelectors(selectors);
1305
1441
  let screenshotId = null;
1306
1442
  let screenshotPath = null;
@@ -1310,7 +1446,7 @@ class StableBrowser {
1310
1446
  }
1311
1447
  info.operation = "getText";
1312
1448
  info.selectors = selectors;
1313
- let element = await this._locate(selectors, info, _params);
1449
+ let element = await this._locate(selectors, info, _params, timeout);
1314
1450
  if (climb > 0) {
1315
1451
  const climbArray = [];
1316
1452
  for (let i = 0; i < climb; i++) {
@@ -1329,6 +1465,18 @@ class StableBrowser {
1329
1465
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
1330
1466
  try {
1331
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
+ // }
1332
1480
  const elementText = await element.innerText();
1333
1481
  return {
1334
1482
  text: elementText,
@@ -1340,7 +1488,7 @@ class StableBrowser {
1340
1488
  }
1341
1489
  catch (e) {
1342
1490
  //await this.closeUnexpectedPopups();
1343
- this.logger.info("no innerText will use textContent");
1491
+ this.logger.info("no innerText, will use textContent");
1344
1492
  const elementText = await element.textContent();
1345
1493
  return { text: elementText, screenshotId, screenshotPath, value: value };
1346
1494
  }
@@ -1365,6 +1513,7 @@ class StableBrowser {
1365
1513
  highlight: false,
1366
1514
  type: Types.VERIFY_ELEMENT_CONTAINS_TEXT,
1367
1515
  text: `Verify element contains pattern: ${pattern}`,
1516
+ _text: "Verify element " + selectors.element_name + " contains pattern " + pattern,
1368
1517
  operation: "containsPattern",
1369
1518
  log: "***** verify element " + selectors.element_name + " contains pattern " + pattern + " *****\n",
1370
1519
  };
@@ -1400,6 +1549,8 @@ class StableBrowser {
1400
1549
  }
1401
1550
  }
1402
1551
  async containsText(selectors, text, climb, _params = null, options = {}, world = null) {
1552
+ const timeout = this._getFindElementTimeout(options);
1553
+ const startTime = Date.now();
1403
1554
  const state = {
1404
1555
  selectors,
1405
1556
  _params,
@@ -1426,62 +1577,54 @@ class StableBrowser {
1426
1577
  }
1427
1578
  let foundObj = null;
1428
1579
  try {
1429
- await _preCommand(state, this);
1430
- foundObj = await this._getText(selectors, climb, _params, options, state.info, world);
1431
- if (foundObj && foundObj.element) {
1432
- await this.scrollIfNeeded(foundObj.element, state.info);
1433
- }
1434
- await _screenshot(state, this);
1435
- const dateAlternatives = findDateAlternatives(text);
1436
- const numberAlternatives = findNumberAlternatives(text);
1437
- if (dateAlternatives.date) {
1438
- for (let i = 0; i < dateAlternatives.dates.length; i++) {
1439
- if (foundObj?.text.includes(dateAlternatives.dates[i]) ||
1440
- foundObj?.value?.includes(dateAlternatives.dates[i])) {
1441
- 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);
1442
1586
  }
1443
- }
1444
- throw new Error("element doesn't contain text " + text);
1445
- }
1446
- else if (numberAlternatives.number) {
1447
- for (let i = 0; i < numberAlternatives.numbers.length; i++) {
1448
- if (foundObj?.text.includes(numberAlternatives.numbers[i]) ||
1449
- 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)) {
1450
1607
  return state.info;
1451
1608
  }
1452
1609
  }
1453
- throw new Error("element doesn't contain text " + text);
1454
- }
1455
- else if (!foundObj?.text.includes(text) && !foundObj?.value?.includes(text)) {
1456
- state.info.foundText = foundObj?.text;
1457
- state.info.value = foundObj?.value;
1458
- 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
1459
1615
  }
1460
- 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);
1461
1619
  }
1462
1620
  catch (e) {
1463
1621
  await _commandError(state, e, this);
1622
+ throw e;
1464
1623
  }
1465
1624
  finally {
1466
1625
  _commandFinally(state, this);
1467
1626
  }
1468
1627
  }
1469
- _getDataFile(world = null) {
1470
- let dataFile = null;
1471
- if (world && world.reportFolder) {
1472
- dataFile = path.join(world.reportFolder, "data.json");
1473
- }
1474
- else if (this.reportFolder) {
1475
- dataFile = path.join(this.reportFolder, "data.json");
1476
- }
1477
- else if (this.context && this.context.reportFolder) {
1478
- dataFile = path.join(this.context.reportFolder, "data.json");
1479
- }
1480
- else {
1481
- dataFile = "data.json";
1482
- }
1483
- return dataFile;
1484
- }
1485
1628
  async waitForUserInput(message, world = null) {
1486
1629
  if (!message) {
1487
1630
  message = "# Wait for user input. Press any key to continue";
@@ -1510,13 +1653,22 @@ class StableBrowser {
1510
1653
  return;
1511
1654
  }
1512
1655
  // if data file exists, load it
1513
- const dataFile = this._getDataFile(world);
1656
+ const dataFile = _getDataFile(world, this.context, this);
1514
1657
  let data = this.getTestData(world);
1515
1658
  // merge the testData with the existing data
1516
1659
  Object.assign(data, testData);
1517
1660
  // save the data to the file
1518
1661
  fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
1519
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
+ }
1520
1672
  _getDataFilePath(fileName) {
1521
1673
  let dataFile = path.join(this.project_path, "data", fileName);
1522
1674
  if (fs.existsSync(dataFile)) {
@@ -1613,7 +1765,7 @@ class StableBrowser {
1613
1765
  }
1614
1766
  }
1615
1767
  getTestData(world = null) {
1616
- const dataFile = this._getDataFile(world);
1768
+ const dataFile = _getDataFile(world, this.context, this);
1617
1769
  let data = {};
1618
1770
  if (fs.existsSync(dataFile)) {
1619
1771
  data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
@@ -1700,6 +1852,15 @@ class StableBrowser {
1700
1852
  document.documentElement.clientWidth,
1701
1853
  ])));
1702
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
+ // }
1703
1864
  if (this.context.browserName === "chromium") {
1704
1865
  const client = await playContext.newCDPSession(this.page);
1705
1866
  const { data } = await client.send("Page.captureScreenshot", {
@@ -1721,6 +1882,10 @@ class StableBrowser {
1721
1882
  else {
1722
1883
  screenshotBuffer = await this.page.screenshot();
1723
1884
  }
1885
+ // if (focusedElement) {
1886
+ // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
1887
+ // await this._unhighlightElements(focusedElement);
1888
+ // }
1724
1889
  let image = await Jimp.read(screenshotBuffer);
1725
1890
  // Get the image dimensions
1726
1891
  const { width, height } = image.bitmap;
@@ -1733,6 +1898,7 @@ class StableBrowser {
1733
1898
  else {
1734
1899
  fs.writeFileSync(screenshotPath, screenshotBuffer);
1735
1900
  }
1901
+ return screenshotBuffer;
1736
1902
  }
1737
1903
  async verifyElementExistInPage(selectors, _params = null, options = {}, world = null) {
1738
1904
  const state = {
@@ -1768,8 +1934,10 @@ class StableBrowser {
1768
1934
  world,
1769
1935
  type: Types.EXTRACT,
1770
1936
  text: `Extract attribute from element`,
1937
+ _text: `Extract attribute ${attribute} from ${selectors.element_name}`,
1771
1938
  operation: "extractAttribute",
1772
1939
  log: "***** extract attribute " + attribute + " from " + selectors.element_name + " *****\n",
1940
+ allowDisabled: true,
1773
1941
  };
1774
1942
  await new Promise((resolve) => setTimeout(resolve, 2000));
1775
1943
  try {
@@ -1791,6 +1959,7 @@ class StableBrowser {
1791
1959
  state.info.value = state.value;
1792
1960
  this.setTestData({ [variable]: state.value }, world);
1793
1961
  this.logger.info("set test data: " + variable + "=" + state.value);
1962
+ // await new Promise((resolve) => setTimeout(resolve, 500));
1794
1963
  return state.info;
1795
1964
  }
1796
1965
  catch (e) {
@@ -1809,14 +1978,21 @@ class StableBrowser {
1809
1978
  options,
1810
1979
  world,
1811
1980
  type: Types.VERIFY_ATTRIBUTE,
1981
+ highlight: true,
1982
+ screenshot: true,
1812
1983
  text: `Verify element attribute`,
1984
+ _text: `Verify attribute ${attribute} from ${selectors.element_name} is ${value}`,
1813
1985
  operation: "verifyAttribute",
1814
1986
  log: "***** verify attribute " + attribute + " from " + selectors.element_name + " *****\n",
1987
+ allowDisabled: true,
1815
1988
  };
1816
1989
  await new Promise((resolve) => setTimeout(resolve, 2000));
1817
1990
  let val;
1991
+ let expectedValue;
1818
1992
  try {
1819
1993
  await _preCommand(state, this);
1994
+ expectedValue = state.value;
1995
+ state.info.expectedValue = expectedValue;
1820
1996
  switch (attribute) {
1821
1997
  case "innerText":
1822
1998
  val = String(await state.element.innerText());
@@ -1830,7 +2006,7 @@ class StableBrowser {
1830
2006
  case "disabled":
1831
2007
  val = String(await state.element.isDisabled());
1832
2008
  break;
1833
- case "readonly":
2009
+ case "readOnly":
1834
2010
  const isEditable = await state.element.isEditable();
1835
2011
  val = String(!isEditable);
1836
2012
  break;
@@ -1838,19 +2014,22 @@ class StableBrowser {
1838
2014
  val = String(await state.element.getAttribute(attribute));
1839
2015
  break;
1840
2016
  }
2017
+ state.info.value = val;
1841
2018
  let regex;
1842
- if (value.startsWith("/") && value.endsWith("/")) {
1843
- const patternBody = value.slice(1, -1);
2019
+ if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2020
+ const patternBody = expectedValue.slice(1, -1);
1844
2021
  regex = new RegExp(patternBody, "g");
1845
2022
  }
1846
2023
  else {
1847
- const escapedPattern = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2024
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1848
2025
  regex = new RegExp(escapedPattern, "g");
1849
2026
  }
1850
2027
  if (!val.match(regex)) {
1851
- 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);
1852
2032
  }
1853
- state.info.value = val;
1854
2033
  return state.info;
1855
2034
  }
1856
2035
  catch (e) {
@@ -1949,27 +2128,32 @@ class StableBrowser {
1949
2128
  async _highlightElements(scope, css) {
1950
2129
  try {
1951
2130
  if (!scope) {
2131
+ // console.log(`Scope is not defined`);
1952
2132
  return;
1953
2133
  }
1954
2134
  if (!css) {
1955
2135
  scope
1956
2136
  .evaluate((node) => {
1957
2137
  if (node && node.style) {
1958
- let originalBorder = node.style.border;
1959
- 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}`);
1960
2143
  if (window) {
1961
2144
  window.addEventListener("beforeunload", function (e) {
1962
- node.style.border = originalBorder;
2145
+ node.style.outline = originalOutline;
1963
2146
  });
1964
2147
  }
1965
2148
  setTimeout(function () {
1966
- node.style.border = originalBorder;
2149
+ node.style.outline = originalOutline;
1967
2150
  }, 2000);
1968
2151
  }
1969
2152
  })
1970
2153
  .then(() => { })
1971
2154
  .catch((e) => {
1972
2155
  // ignore
2156
+ // console.error(`Could not highlight node : ${e}`);
1973
2157
  });
1974
2158
  }
1975
2159
  else {
@@ -1985,17 +2169,18 @@ class StableBrowser {
1985
2169
  if (!element.style) {
1986
2170
  return;
1987
2171
  }
1988
- var originalBorder = element.style.border;
2172
+ let originalOutline = element.style.outline;
2173
+ element.__previousOutline = originalOutline;
1989
2174
  // Set the new border to be red and 2px solid
1990
- element.style.border = "2px solid red";
2175
+ element.style.outline = "2px solid red";
1991
2176
  if (window) {
1992
2177
  window.addEventListener("beforeunload", function (e) {
1993
- element.style.border = originalBorder;
2178
+ element.style.outline = originalOutline;
1994
2179
  });
1995
2180
  }
1996
2181
  // Set a timeout to revert to the original border after 2 seconds
1997
2182
  setTimeout(function () {
1998
- element.style.border = originalBorder;
2183
+ element.style.outline = originalOutline;
1999
2184
  }, 2000);
2000
2185
  }
2001
2186
  return;
@@ -2003,6 +2188,7 @@ class StableBrowser {
2003
2188
  .then(() => { })
2004
2189
  .catch((e) => {
2005
2190
  // ignore
2191
+ // console.error(`Could not highlight css: ${e}`);
2006
2192
  });
2007
2193
  }
2008
2194
  }
@@ -2010,6 +2196,54 @@ class StableBrowser {
2010
2196
  console.debug(error);
2011
2197
  }
2012
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
+ // }
2013
2247
  async verifyPagePath(pathPart, options = {}, world = null) {
2014
2248
  const startTime = Date.now();
2015
2249
  let error = null;
@@ -2054,6 +2288,7 @@ class StableBrowser {
2054
2288
  _reportToWorld(world, {
2055
2289
  type: Types.VERIFY_PAGE_PATH,
2056
2290
  text: "Verify page path",
2291
+ _text: "Verify the page path contains " + pathPart,
2057
2292
  screenshotId,
2058
2293
  result: error
2059
2294
  ? {
@@ -2071,26 +2306,27 @@ class StableBrowser {
2071
2306
  });
2072
2307
  }
2073
2308
  }
2074
- async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state) {
2309
+ async findTextInAllFrames(dateAlternatives, numberAlternatives, text, state, partial = true, ignoreCase = false) {
2075
2310
  const frames = this.page.frames();
2076
2311
  let results = [];
2312
+ // let ignoreCase = false;
2077
2313
  for (let i = 0; i < frames.length; i++) {
2078
2314
  if (dateAlternatives.date) {
2079
2315
  for (let j = 0; j < dateAlternatives.dates.length; j++) {
2080
- const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, true, {});
2316
+ const result = await this._locateElementByText(frames[i], dateAlternatives.dates[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2081
2317
  result.frame = frames[i];
2082
2318
  results.push(result);
2083
2319
  }
2084
2320
  }
2085
2321
  else if (numberAlternatives.number) {
2086
2322
  for (let j = 0; j < numberAlternatives.numbers.length; j++) {
2087
- const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, true, {});
2323
+ const result = await this._locateElementByText(frames[i], numberAlternatives.numbers[j], "*:not(script, style, head)", false, partial, ignoreCase, {});
2088
2324
  result.frame = frames[i];
2089
2325
  results.push(result);
2090
2326
  }
2091
2327
  }
2092
2328
  else {
2093
- const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, true, {});
2329
+ const result = await this._locateElementByText(frames[i], text, "*:not(script, style, head)", false, partial, ignoreCase, {});
2094
2330
  result.frame = frames[i];
2095
2331
  results.push(result);
2096
2332
  }
@@ -2109,11 +2345,15 @@ class StableBrowser {
2109
2345
  scroll: false,
2110
2346
  highlight: false,
2111
2347
  type: Types.VERIFY_PAGE_CONTAINS_TEXT,
2112
- text: `Verify text exists in page`,
2348
+ text: `Verify the text '${text}' exists in page`,
2349
+ _text: `Verify the text '${text}' exists in page`,
2113
2350
  operation: "verifyTextExistInPage",
2114
2351
  log: "***** verify text " + text + " exists in page *****\n",
2115
2352
  };
2116
- const timeout = this._getLoadTimeout(options);
2353
+ if (testForRegex(text)) {
2354
+ text = text.replace(/\\"/g, '"');
2355
+ }
2356
+ const timeout = this._getFindElementTimeout(options);
2117
2357
  await new Promise((resolve) => setTimeout(resolve, 2000));
2118
2358
  const newValue = await this._replaceWithLocalData(text, world);
2119
2359
  if (newValue !== text) {
@@ -2126,7 +2366,15 @@ class StableBrowser {
2126
2366
  await _preCommand(state, this);
2127
2367
  state.info.text = text;
2128
2368
  while (true) {
2129
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2369
+ let resultWithElementsFound = {
2370
+ length: 0,
2371
+ };
2372
+ try {
2373
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2374
+ }
2375
+ catch (error) {
2376
+ // ignore
2377
+ }
2130
2378
  if (resultWithElementsFound.length === 0) {
2131
2379
  if (Date.now() - state.startTime > timeout) {
2132
2380
  throw new Error(`Text ${text} not found in page`);
@@ -2134,18 +2382,40 @@ class StableBrowser {
2134
2382
  await new Promise((resolve) => setTimeout(resolve, 1000));
2135
2383
  continue;
2136
2384
  }
2137
- if (resultWithElementsFound[0].randomToken) {
2138
- const frame = resultWithElementsFound[0].frame;
2139
- const dataAttribute = `[data-blinq-id="blinq-id-${resultWithElementsFound[0].randomToken}"]`;
2140
- await this._highlightElements(frame, dataAttribute);
2141
- const element = await frame.$(dataAttribute);
2142
- if (element) {
2143
- await this.scrollIfNeeded(element, state.info);
2144
- await element.dispatchEvent("bvt_verify_page_contains_text");
2385
+ try {
2386
+ if (resultWithElementsFound[0].randomToken) {
2387
+ const frame = resultWithElementsFound[0].frame;
2388
+ const dataAttribute = `[data-blinq-id-${resultWithElementsFound[0].randomToken}]`;
2389
+ await this._highlightElements(frame, dataAttribute);
2390
+ // if (world && world.screenshot && !world.screenshotPath) {
2391
+ // console.log(`Highlighting for verify text is found while running from recorder`);
2392
+ // this._highlightElements(frame, dataAttribute).then(async () => {
2393
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2394
+ // this._unhighlightElements(frame, dataAttribute)
2395
+ // .then(async () => {
2396
+ // console.log(`Unhighlighted frame dataAttribute successfully`);
2397
+ // })
2398
+ // .catch(
2399
+ // (e) => {}
2400
+ // console.error(e)
2401
+ // );
2402
+ // });
2403
+ // }
2404
+ const element = await frame.locator(dataAttribute).first();
2405
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2406
+ // await this._unhighlightElements(frame, dataAttribute);
2407
+ if (element) {
2408
+ await this.scrollIfNeeded(element, state.info);
2409
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2410
+ // await _screenshot(state, this, element);
2411
+ }
2145
2412
  }
2413
+ await _screenshot(state, this);
2414
+ return state.info;
2415
+ }
2416
+ catch (error) {
2417
+ console.error(error);
2146
2418
  }
2147
- await _screenshot(state, this);
2148
- return state.info;
2149
2419
  }
2150
2420
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2151
2421
  }
@@ -2166,11 +2436,15 @@ class StableBrowser {
2166
2436
  scroll: false,
2167
2437
  highlight: false,
2168
2438
  type: Types.WAIT_FOR_TEXT_TO_DISAPPEAR,
2169
- text: `Verify text does not exist in page`,
2439
+ text: `Verify the text '${text}' does not exist in page`,
2440
+ _text: `Verify the text '${text}' does not exist in page`,
2170
2441
  operation: "verifyTextNotExistInPage",
2171
2442
  log: "***** verify text " + text + " does not exist in page *****\n",
2172
2443
  };
2173
- const timeout = this._getLoadTimeout(options);
2444
+ if (testForRegex(text)) {
2445
+ text = text.replace(/\\"/g, '"');
2446
+ }
2447
+ const timeout = this._getFindElementTimeout(options);
2174
2448
  await new Promise((resolve) => setTimeout(resolve, 2000));
2175
2449
  const newValue = await this._replaceWithLocalData(text, world);
2176
2450
  if (newValue !== text) {
@@ -2182,8 +2456,16 @@ class StableBrowser {
2182
2456
  try {
2183
2457
  await _preCommand(state, this);
2184
2458
  state.info.text = text;
2459
+ let resultWithElementsFound = {
2460
+ length: null, // initial cannot be 0
2461
+ };
2185
2462
  while (true) {
2186
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2463
+ try {
2464
+ resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, text, state);
2465
+ }
2466
+ catch (error) {
2467
+ // ignore
2468
+ }
2187
2469
  if (resultWithElementsFound.length === 0) {
2188
2470
  await _screenshot(state, this);
2189
2471
  return state.info;
@@ -2213,10 +2495,11 @@ class StableBrowser {
2213
2495
  highlight: false,
2214
2496
  type: Types.VERIFY_TEXT_WITH_RELATION,
2215
2497
  text: `Verify text with relation to another text`,
2498
+ _text: "Search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found",
2216
2499
  operation: "verify_text_with_relation",
2217
2500
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2218
2501
  };
2219
- const timeout = this._getLoadTimeout(options);
2502
+ const timeout = this._getFindElementTimeout(options);
2220
2503
  await new Promise((resolve) => setTimeout(resolve, 2000));
2221
2504
  let newValue = await this._replaceWithLocalData(textAnchor, world);
2222
2505
  if (newValue !== textAnchor) {
@@ -2234,8 +2517,16 @@ class StableBrowser {
2234
2517
  try {
2235
2518
  await _preCommand(state, this);
2236
2519
  state.info.text = textToVerify;
2520
+ let resultWithElementsFound = {
2521
+ length: 0,
2522
+ };
2237
2523
  while (true) {
2238
- const resultWithElementsFound = await this.findTextInAllFrames(dateAlternatives, numberAlternatives, textAnchor, state);
2524
+ try {
2525
+ resultWithElementsFound = await this.findTextInAllFrames(findDateAlternatives(textAnchor), findNumberAlternatives(textAnchor), textAnchor, state, false);
2526
+ }
2527
+ catch (error) {
2528
+ // ignore
2529
+ }
2239
2530
  if (resultWithElementsFound.length === 0) {
2240
2531
  if (Date.now() - state.startTime > timeout) {
2241
2532
  throw new Error(`Text ${foundAncore ? textToVerify : textAnchor} not found in page`);
@@ -2243,51 +2534,56 @@ class StableBrowser {
2243
2534
  await new Promise((resolve) => setTimeout(resolve, 1000));
2244
2535
  continue;
2245
2536
  }
2246
- for (let i = 0; i < resultWithElementsFound.length; i++) {
2247
- foundAncore = true;
2248
- const result = resultWithElementsFound[i];
2249
- const token = result.randomToken;
2250
- const frame = result.frame;
2251
- const css = `[data-blinq-id="blinq-id-${token}"]`;
2252
- const findResult = await frame.evaluate(([css, climb, textToVerify, token]) => {
2253
- const elements = Array.from(document.querySelectorAll(css));
2254
- for (let i = 0; i < elements.length; i++) {
2255
- const element = elements[i];
2256
- let climbParent = element;
2257
- for (let j = 0; j < climb; j++) {
2258
- climbParent = climbParent.parentElement;
2259
- if (!climbParent) {
2260
- break;
2537
+ try {
2538
+ for (let i = 0; i < resultWithElementsFound.length; i++) {
2539
+ foundAncore = true;
2540
+ const result = resultWithElementsFound[i];
2541
+ const token = result.randomToken;
2542
+ const frame = result.frame;
2543
+ let css = `[data-blinq-id-${token}]`;
2544
+ const climbArray1 = [];
2545
+ for (let i = 0; i < climb; i++) {
2546
+ climbArray1.push("..");
2547
+ }
2548
+ let climbXpath = "xpath=" + climbArray1.join("/");
2549
+ css = css + " >> " + climbXpath;
2550
+ const count = await frame.locator(css).count();
2551
+ for (let j = 0; j < count; j++) {
2552
+ const continer = await frame.locator(css).nth(j);
2553
+ const result = await this._locateElementByText(continer, textToVerify, "*:not(script, style, head)", false, false, true, {});
2554
+ if (result.elementCount > 0) {
2555
+ const dataAttribute = "[data-blinq-id-" + result.randomToken + "]";
2556
+ await this._highlightElements(frame, dataAttribute);
2557
+ //const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2558
+ // if (world && world.screenshot && !world.screenshotPath) {
2559
+ // console.log(`Highlighting for vtrt while running from recorder`);
2560
+ // this._highlightElements(frame, dataAttribute)
2561
+ // .then(async () => {
2562
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
2563
+ // this._unhighlightElements(frame, dataAttribute).then(
2564
+ // () => {}
2565
+ // console.log(`Unhighlighting vrtr in recorder is successful`)
2566
+ // );
2567
+ // })
2568
+ // .catch(e);
2569
+ // }
2570
+ //await this._highlightElements(frame, cssAnchor);
2571
+ const element = await frame.locator(dataAttribute).first();
2572
+ // await new Promise((resolve) => setTimeout(resolve, 100));
2573
+ // await this._unhighlightElements(frame, dataAttribute);
2574
+ if (element) {
2575
+ await this.scrollIfNeeded(element, state.info);
2576
+ await element.dispatchEvent("bvt_verify_page_contains_text");
2261
2577
  }
2578
+ await _screenshot(state, this);
2579
+ return state.info;
2262
2580
  }
2263
- if (!climbParent) {
2264
- continue;
2265
- }
2266
- const foundElements = window.findMatchingElements(textToVerify, {}, climbParent);
2267
- if (foundElements.length > 0) {
2268
- // set the container element attribute
2269
- element.setAttribute("data-blinq-id", `blinq-id-${token}-anchor`);
2270
- climbParent.setAttribute("data-blinq-id", `blinq-id-${token}-container`);
2271
- foundElements[0].setAttribute("data-blinq-id", `blinq-id-${token}-verify`);
2272
- return { found: true };
2273
- }
2274
- }
2275
- return { found: false };
2276
- }, [css, climb, textToVerify, result.randomToken]);
2277
- if (findResult.found === true) {
2278
- const dataAttribute = `[data-blinq-id="blinq-id-${token}-verify"]`;
2279
- const cssAnchor = `[data-blinq-id="blinq-id-${token}-anchor"]`;
2280
- await this._highlightElements(frame, dataAttribute);
2281
- await this._highlightElements(frame, cssAnchor);
2282
- const element = await frame.$(dataAttribute);
2283
- if (element) {
2284
- await this.scrollIfNeeded(element, state.info);
2285
- await element.dispatchEvent("bvt_verify_page_contains_text");
2286
2581
  }
2287
- await _screenshot(state, this);
2288
- return state.info;
2289
2582
  }
2290
2583
  }
2584
+ catch (error) {
2585
+ console.error(error);
2586
+ }
2291
2587
  }
2292
2588
  // await expect(element).toHaveCount(1, { timeout: 10000 });
2293
2589
  }
@@ -2298,6 +2594,30 @@ class StableBrowser {
2298
2594
  _commandFinally(state, this);
2299
2595
  }
2300
2596
  }
2597
+ async findRelatedTextInAllFrames(textAnchor, climb, textToVerify, params = {}, options = {}, world = null) {
2598
+ const frames = this.page.frames();
2599
+ let results = [];
2600
+ let ignoreCase = false;
2601
+ for (let i = 0; i < frames.length; i++) {
2602
+ const result = await this._locateElementByText(frames[i], textAnchor, "*:not(script, style, head)", false, true, ignoreCase, {});
2603
+ result.frame = frames[i];
2604
+ const climbArray = [];
2605
+ for (let i = 0; i < climb; i++) {
2606
+ climbArray.push("..");
2607
+ }
2608
+ let climbXpath = "xpath=" + climbArray.join("/");
2609
+ const newLocator = `[data-blinq-id-${result.randomToken}] ${climb > 0 ? ">> " + climbXpath : ""} >> internal:text=${testForRegex(textToVerify) ? textToVerify : unEscapeString(textToVerify)}`;
2610
+ const count = await frames[i].locator(newLocator).count();
2611
+ if (count > 0) {
2612
+ result.elementCount = count;
2613
+ result.locator = newLocator;
2614
+ results.push(result);
2615
+ }
2616
+ }
2617
+ // state.info.results = results;
2618
+ const resultWithElementsFound = results.filter((result) => result.elementCount > 0);
2619
+ return resultWithElementsFound;
2620
+ }
2301
2621
  async visualVerification(text, options = {}, world = null) {
2302
2622
  const startTime = Date.now();
2303
2623
  let error = null;
@@ -2316,10 +2636,13 @@ class StableBrowser {
2316
2636
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world, info));
2317
2637
  info.screenshotPath = screenshotPath;
2318
2638
  const screenshot = await this.takeScreenshot();
2319
- const request = {
2320
- method: "POST",
2639
+ let request = {
2640
+ method: "post",
2641
+ maxBodyLength: Infinity,
2321
2642
  url: `${serviceUrl}/api/runs/screenshots/validate-screenshot`,
2322
2643
  headers: {
2644
+ "x-bvt-project-id": path.basename(this.project_path),
2645
+ "x-source": "aaa",
2323
2646
  "Content-Type": "application/json",
2324
2647
  Authorization: `Bearer ${process.env.TOKEN}`,
2325
2648
  },
@@ -2328,7 +2651,7 @@ class StableBrowser {
2328
2651
  screenshot: screenshot,
2329
2652
  }),
2330
2653
  };
2331
- let result = await this.context.api.request(request);
2654
+ const result = await axios.request(request);
2332
2655
  if (result.data.status !== true) {
2333
2656
  throw new Error("Visual validation failed");
2334
2657
  }
@@ -2356,6 +2679,7 @@ class StableBrowser {
2356
2679
  _reportToWorld(world, {
2357
2680
  type: Types.VERIFY_VISUAL,
2358
2681
  text: "Visual verification",
2682
+ _text: "Visual verification of " + text,
2359
2683
  screenshotId,
2360
2684
  result: error
2361
2685
  ? {
@@ -2622,6 +2946,32 @@ class StableBrowser {
2622
2946
  }
2623
2947
  return timeout;
2624
2948
  }
2949
+ _getFindElementTimeout(options) {
2950
+ if (options && options.timeout) {
2951
+ return options.timeout;
2952
+ }
2953
+ if (this.configuration.find_element_timeout) {
2954
+ return this.configuration.find_element_timeout;
2955
+ }
2956
+ return 30000;
2957
+ }
2958
+ async saveStoreState(path = null, world = null) {
2959
+ const storageState = await this.page.context().storageState();
2960
+ //const testDataFile = _getDataFile(world, this.context, this);
2961
+ if (path) {
2962
+ // save { storageState: storageState } into the path
2963
+ fs.writeFileSync(path, JSON.stringify({ storageState: storageState }, null, 2));
2964
+ }
2965
+ else {
2966
+ await this.setTestData({ storageState: storageState }, world);
2967
+ }
2968
+ }
2969
+ async restoreSaveState(path = null, world = null) {
2970
+ await refreshBrowser(this, path, world);
2971
+ this.registerEventListeners(this.context);
2972
+ registerNetworkEvents(this.world, this, this.context, this.page);
2973
+ registerDownloadEvent(this.page, this.world, this.context);
2974
+ }
2625
2975
  async waitForPageLoad(options = {}, world = null) {
2626
2976
  let timeout = this._getLoadTimeout(options);
2627
2977
  const promiseArray = [];
@@ -2689,6 +3039,7 @@ class StableBrowser {
2689
3039
  highlight: false,
2690
3040
  type: Types.CLOSE_PAGE,
2691
3041
  text: `Close page`,
3042
+ _text: `Close the page`,
2692
3043
  operation: "closePage",
2693
3044
  log: "***** close page *****\n",
2694
3045
  throwError: false,
@@ -2705,8 +3056,95 @@ class StableBrowser {
2705
3056
  _commandFinally(state, this);
2706
3057
  }
2707
3058
  }
3059
+ async tableCellOperation(headerText, rowText, options, _params, world = null) {
3060
+ let operation = null;
3061
+ if (!options || !options.operation) {
3062
+ throw new Error("operation is not defined");
3063
+ }
3064
+ operation = options.operation;
3065
+ // validate operation is one of the supported operations
3066
+ if (operation != "click" && operation != "hover+click") {
3067
+ throw new Error("operation is not supported");
3068
+ }
3069
+ const state = {
3070
+ options,
3071
+ world,
3072
+ locate: false,
3073
+ scroll: false,
3074
+ highlight: false,
3075
+ type: Types.TABLE_OPERATION,
3076
+ text: `Table operation`,
3077
+ _text: `Table ${operation} operation`,
3078
+ operation: operation,
3079
+ log: "***** Table operation *****\n",
3080
+ };
3081
+ const timeout = this._getFindElementTimeout(options);
3082
+ try {
3083
+ await _preCommand(state, this);
3084
+ const start = Date.now();
3085
+ let cellArea = null;
3086
+ while (true) {
3087
+ try {
3088
+ cellArea = await _findCellArea(headerText, rowText, this, state);
3089
+ if (cellArea) {
3090
+ break;
3091
+ }
3092
+ }
3093
+ catch (e) {
3094
+ // ignore
3095
+ }
3096
+ if (Date.now() - start > timeout) {
3097
+ throw new Error(`Cell not found in table`);
3098
+ }
3099
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3100
+ }
3101
+ switch (operation) {
3102
+ case "click":
3103
+ if (!options.css) {
3104
+ // will click in the center of the cell
3105
+ let xOffset = 0;
3106
+ let yOffset = 0;
3107
+ if (options.xOffset) {
3108
+ xOffset = options.xOffset;
3109
+ }
3110
+ if (options.yOffset) {
3111
+ yOffset = options.yOffset;
3112
+ }
3113
+ await this.page.mouse.click(cellArea.x + cellArea.width / 2 + xOffset, cellArea.y + cellArea.height / 2 + yOffset);
3114
+ }
3115
+ else {
3116
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3117
+ if (results.length === 0) {
3118
+ throw new Error(`Element not found in cell area`);
3119
+ }
3120
+ state.element = results[0];
3121
+ await performAction("click", state.element, options, this, state, _params);
3122
+ }
3123
+ break;
3124
+ case "hover+click":
3125
+ if (!options.css) {
3126
+ throw new Error("css is not defined");
3127
+ }
3128
+ const results = await findElementsInArea(options.css, cellArea, this, options);
3129
+ if (results.length === 0) {
3130
+ throw new Error(`Element not found in cell area`);
3131
+ }
3132
+ state.element = results[0];
3133
+ await performAction("hover+click", state.element, options, this, state, _params);
3134
+ break;
3135
+ default:
3136
+ throw new Error("operation is not supported");
3137
+ }
3138
+ }
3139
+ catch (e) {
3140
+ await _commandError(state, e, this);
3141
+ }
3142
+ finally {
3143
+ _commandFinally(state, this);
3144
+ }
3145
+ }
2708
3146
  saveTestDataAsGlobal(options, world) {
2709
- const dataFile = this._getDataFile(world);
3147
+ const dataFile = _getDataFile(world, this.context, this);
2710
3148
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
2711
3149
  this.logger.info("Save the scenario test data as global for the following scenarios.");
2712
3150
  }
@@ -2736,6 +3174,7 @@ class StableBrowser {
2736
3174
  _reportToWorld(world, {
2737
3175
  type: Types.SET_VIEWPORT,
2738
3176
  text: "set viewport size to " + width + "x" + hight,
3177
+ _text: "Set the viewport size to " + width + "x" + hight,
2739
3178
  screenshotId,
2740
3179
  result: error
2741
3180
  ? {
@@ -2807,14 +3246,25 @@ class StableBrowser {
2807
3246
  }
2808
3247
  }
2809
3248
  async beforeStep(world, step) {
2810
- this.stepName = step.pickleStep.text;
2811
- this.logger.info("step: " + this.stepName);
2812
3249
  if (this.stepIndex === undefined) {
2813
3250
  this.stepIndex = 0;
2814
3251
  }
2815
3252
  else {
2816
3253
  this.stepIndex++;
2817
3254
  }
3255
+ if (step && step.pickleStep && step.pickleStep.text) {
3256
+ this.stepName = step.pickleStep.text;
3257
+ this.logger.info("step: " + this.stepName);
3258
+ }
3259
+ else if (step && step.text) {
3260
+ this.stepName = step.text;
3261
+ }
3262
+ else {
3263
+ this.stepName = "step " + this.stepIndex;
3264
+ }
3265
+ if (this.context) {
3266
+ this.context.examplesRow = extractStepExampleParameters(step);
3267
+ }
2818
3268
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
2819
3269
  if (this.context.browserObject.context) {
2820
3270
  await this.context.browserObject.context.tracing.startChunk({ title: this.stepName });
@@ -2827,6 +3277,41 @@ class StableBrowser {
2827
3277
  this.saveTestDataAsGlobal({}, world);
2828
3278
  }
2829
3279
  }
3280
+ if (this.initSnapshotTaken === false) {
3281
+ this.initSnapshotTaken = true;
3282
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3283
+ const snapshot = await this.getAriaSnapshot();
3284
+ if (snapshot) {
3285
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3286
+ }
3287
+ }
3288
+ }
3289
+ }
3290
+ async getAriaSnapshot() {
3291
+ try {
3292
+ // find the page url
3293
+ const url = await this.page.url();
3294
+ // extract the path from the url
3295
+ const path = new URL(url).pathname;
3296
+ // get the page title
3297
+ const title = await this.page.title();
3298
+ // go over other frams
3299
+ const frames = this.page.frames();
3300
+ const snapshots = [];
3301
+ const content = [`- path: ${path}`, `- title: ${title}`];
3302
+ const timeout = this.configuration.ariaSnapshotTimeout ? this.configuration.ariaSnapshotTimeout : 3000;
3303
+ for (let i = 0; i < frames.length; i++) {
3304
+ content.push(`- frame: ${i}`);
3305
+ const frame = frames[i];
3306
+ const snapshot = await frame.locator("body").ariaSnapshot({ timeout });
3307
+ content.push(snapshot);
3308
+ }
3309
+ return content.join("\n");
3310
+ }
3311
+ catch (e) {
3312
+ console.error(e);
3313
+ }
3314
+ return null;
2830
3315
  }
2831
3316
  async afterStep(world, step) {
2832
3317
  this.stepName = null;
@@ -2837,6 +3322,16 @@ class StableBrowser {
2837
3322
  });
2838
3323
  }
2839
3324
  }
3325
+ if (this.context) {
3326
+ this.context.examplesRow = null;
3327
+ }
3328
+ if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
3329
+ const snapshot = await this.getAriaSnapshot();
3330
+ if (snapshot) {
3331
+ const obj = {};
3332
+ await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3333
+ }
3334
+ }
2840
3335
  }
2841
3336
  }
2842
3337
  function createTimedPromise(promise, label) {