automation_model 1.0.778-dev → 1.0.778-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.
@@ -1,4 +1,5 @@
1
1
  // @ts-nocheck
2
+ import { check_performance } from "./check_performance.js";
2
3
  import { expect } from "@playwright/test";
3
4
  import dayjs from "dayjs";
4
5
  import fs from "fs";
@@ -10,6 +11,7 @@ import { getDateTimeValue } from "./date_time.js";
10
11
  import drawRectangle from "./drawRect.js";
11
12
  //import { closeUnexpectedPopups } from "./popups.js";
12
13
  import { getTableCells, getTableData } from "./table_analyze.js";
14
+ import errorStackParser from "error-stack-parser";
13
15
  import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
14
16
  import csv from "csv-parser";
15
17
  import { Readable } from "node:stream";
@@ -26,6 +28,7 @@ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
28
  import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
27
29
  import { loadBrunoParams } from "./bruno.js";
28
30
  import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
31
+ import { existsSync } from "node:fs";
29
32
  export const Types = {
30
33
  CLICK: "click_element",
31
34
  WAIT_ELEMENT: "wait_element",
@@ -44,6 +47,7 @@ export const Types = {
44
47
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
45
48
  ANALYZE_TABLE: "analyze_table",
46
49
  SELECT: "select_combobox", //
50
+ VERIFY_PROPERTY: "verify_element_property",
47
51
  VERIFY_PAGE_PATH: "verify_page_path",
48
52
  VERIFY_PAGE_TITLE: "verify_page_title",
49
53
  TYPE_PRESS: "type_press",
@@ -62,12 +66,11 @@ export const Types = {
62
66
  SET_INPUT: "set_input",
63
67
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
64
68
  VERIFY_ATTRIBUTE: "verify_element_attribute",
65
- VERIFY_PROPERTY: "verify_element_property",
66
69
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
67
70
  BRUNO: "bruno",
68
- SNAPSHOT_VALIDATION: "snapshot_validation",
69
71
  VERIFY_FILE_EXISTS: "verify_file_exists",
70
72
  SET_INPUT_FILES: "set_input_files",
73
+ SNAPSHOT_VALIDATION: "snapshot_validation",
71
74
  REPORT_COMMAND: "report_command",
72
75
  STEP_COMPLETE: "step_complete",
73
76
  SLEEP: "sleep",
@@ -84,6 +87,7 @@ class StableBrowser {
84
87
  context;
85
88
  world;
86
89
  fastMode;
90
+ stepTags;
87
91
  project_path = null;
88
92
  webLogFile = null;
89
93
  networkLogger = null;
@@ -92,13 +96,17 @@ class StableBrowser {
92
96
  tags = null;
93
97
  isRecording = false;
94
98
  initSnapshotTaken = false;
95
- constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
99
+ onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
100
+ // set to true if the step issue a report
101
+ inStepReport = false;
102
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
96
103
  this.browser = browser;
97
104
  this.page = page;
98
105
  this.logger = logger;
99
106
  this.context = context;
100
107
  this.world = world;
101
108
  this.fastMode = fastMode;
109
+ this.stepTags = stepTags;
102
110
  if (!this.logger) {
103
111
  this.logger = console;
104
112
  }
@@ -131,7 +139,7 @@ class StableBrowser {
131
139
  this.fastMode = true;
132
140
  }
133
141
  if (process.env.FAST_MODE === "true") {
134
- console.log("Fast mode enabled from environment variable");
142
+ // console.log("Fast mode enabled from environment variable");
135
143
  this.fastMode = true;
136
144
  }
137
145
  if (process.env.FAST_MODE === "false") {
@@ -174,6 +182,7 @@ class StableBrowser {
174
182
  registerNetworkEvents(this.world, this, context, this.page);
175
183
  registerDownloadEvent(this.page, this.world, context);
176
184
  page.on("close", async () => {
185
+ // return if browser context is already closed
177
186
  if (this.context && this.context.pages && this.context.pages.length > 1) {
178
187
  this.context.pages.pop();
179
188
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -183,7 +192,12 @@ class StableBrowser {
183
192
  console.log("Switched to page " + title);
184
193
  }
185
194
  catch (error) {
186
- console.error("Error on page close", error);
195
+ if (error?.message?.includes("Target page, context or browser has been closed")) {
196
+ // Ignore this error
197
+ }
198
+ else {
199
+ console.error("Error on page close", error);
200
+ }
187
201
  }
188
202
  }
189
203
  });
@@ -192,7 +206,12 @@ class StableBrowser {
192
206
  console.log("Switch page: " + (await page.title()));
193
207
  }
194
208
  catch (e) {
195
- this.logger.error("error on page load " + e);
209
+ if (e?.message?.includes("Target page, context or browser has been closed")) {
210
+ // Ignore this error
211
+ }
212
+ else {
213
+ this.logger.error("error on page load " + e);
214
+ }
196
215
  }
197
216
  context.pageLoading.status = false;
198
217
  }.bind(this));
@@ -220,7 +239,7 @@ class StableBrowser {
220
239
  if (newContextCreated) {
221
240
  this.registerEventListeners(this.context);
222
241
  await this.goto(this.context.environment.baseUrl);
223
- if (!this.fastMode) {
242
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
224
243
  await this.waitForPageLoad();
225
244
  }
226
245
  }
@@ -312,7 +331,7 @@ class StableBrowser {
312
331
  // async closeUnexpectedPopups() {
313
332
  // await closeUnexpectedPopups(this.page);
314
333
  // }
315
- async goto(url, world = null) {
334
+ async goto(url, world = null, options = {}) {
316
335
  if (!url) {
317
336
  throw new Error("url is null, verify that the environment file is correct");
318
337
  }
@@ -333,10 +352,14 @@ class StableBrowser {
333
352
  screenshot: false,
334
353
  highlight: false,
335
354
  };
355
+ let timeout = 60000;
356
+ if (options && options["timeout"]) {
357
+ timeout = options["timeout"];
358
+ }
336
359
  try {
337
360
  await _preCommand(state, this);
338
361
  await this.page.goto(url, {
339
- timeout: 60000,
362
+ timeout: timeout,
340
363
  });
341
364
  await _screenshot(state, this);
342
365
  }
@@ -504,12 +527,6 @@ class StableBrowser {
504
527
  if (!el.setAttribute) {
505
528
  el = el.parentElement;
506
529
  }
507
- // remove any attributes start with data-blinq-id
508
- // for (let i = 0; i < el.attributes.length; i++) {
509
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
510
- // el.removeAttribute(el.attributes[i].name);
511
- // }
512
- // }
513
530
  el.setAttribute("data-blinq-id-" + randomToken, "");
514
531
  return true;
515
532
  }, [tag1, randomToken]))) {
@@ -679,40 +696,186 @@ class StableBrowser {
679
696
  }
680
697
  return { rerun: false };
681
698
  }
699
+ getFilePath() {
700
+ const stackFrames = errorStackParser.parse(new Error());
701
+ const stackFrame = stackFrames.findLast((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
702
+ // return stackFrame?.fileName || null;
703
+ const filepath = stackFrame?.fileName;
704
+ if (filepath) {
705
+ let jsonFilePath = filepath.replace(".mjs", ".json");
706
+ if (existsSync(jsonFilePath)) {
707
+ return jsonFilePath;
708
+ }
709
+ const config = this.configuration ?? {};
710
+ if (!config?.locatorsMetadataDir) {
711
+ config.locatorsMetadataDir = "features/step_definitions/locators";
712
+ }
713
+ if (config && config.locatorsMetadataDir) {
714
+ jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
715
+ }
716
+ if (existsSync(jsonFilePath)) {
717
+ return jsonFilePath;
718
+ }
719
+ return null;
720
+ }
721
+ return null;
722
+ }
723
+ getFullElementLocators(selectors, filePath) {
724
+ if (!filePath || !existsSync(filePath)) {
725
+ return null;
726
+ }
727
+ const content = fs.readFileSync(filePath, "utf8");
728
+ try {
729
+ const allElements = JSON.parse(content);
730
+ const element_key = selectors?.element_key;
731
+ if (element_key && allElements[element_key]) {
732
+ return allElements[element_key];
733
+ }
734
+ for (const elementKey in allElements) {
735
+ const element = allElements[elementKey];
736
+ let foundStrategy = null;
737
+ for (const key in element) {
738
+ if (key === "strategy") {
739
+ continue;
740
+ }
741
+ const locators = element[key];
742
+ if (!locators || !locators.length) {
743
+ continue;
744
+ }
745
+ for (const locator of locators) {
746
+ delete locator.score;
747
+ }
748
+ if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
749
+ foundStrategy = key;
750
+ break;
751
+ }
752
+ }
753
+ if (foundStrategy) {
754
+ return element;
755
+ }
756
+ }
757
+ }
758
+ catch (error) {
759
+ console.error("Error parsing locators from file: " + filePath, error);
760
+ }
761
+ return null;
762
+ }
682
763
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
683
764
  if (!timeout) {
684
765
  timeout = 30000;
685
766
  }
767
+ let element = null;
768
+ let allStrategyLocators = null;
769
+ let selectedStrategy = null;
770
+ if (this.tryAllStrategies) {
771
+ allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
772
+ selectedStrategy = allStrategyLocators?.strategy;
773
+ }
686
774
  for (let i = 0; i < 3; i++) {
687
775
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
688
776
  for (let j = 0; j < selectors.locators.length; j++) {
689
777
  let selector = selectors.locators[j];
690
778
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
691
779
  }
692
- let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
780
+ if (this.tryAllStrategies && selectedStrategy) {
781
+ const strategyLocators = allStrategyLocators[selectedStrategy];
782
+ let err;
783
+ if (strategyLocators && strategyLocators.length) {
784
+ try {
785
+ selectors.locators = strategyLocators;
786
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
787
+ info.selectedStrategy = selectedStrategy;
788
+ info.log += "element found using strategy " + selectedStrategy + "\n";
789
+ }
790
+ catch (error) {
791
+ err = error;
792
+ }
793
+ }
794
+ if (!element) {
795
+ for (const key in allStrategyLocators) {
796
+ if (key === "strategy" || key === selectedStrategy) {
797
+ continue;
798
+ }
799
+ const strategyLocators = allStrategyLocators[key];
800
+ if (strategyLocators && strategyLocators.length) {
801
+ try {
802
+ info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
803
+ selectors.locators = strategyLocators;
804
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
805
+ err = null;
806
+ info.selectedStrategy = key;
807
+ info.log += "element found using strategy " + key + "\n";
808
+ break;
809
+ }
810
+ catch (error) {
811
+ err = error;
812
+ }
813
+ }
814
+ }
815
+ }
816
+ if (err) {
817
+ throw err;
818
+ }
819
+ }
820
+ else {
821
+ element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
822
+ }
693
823
  if (!element.rerun) {
694
- const randomToken = Math.random().toString(36).substring(7);
695
- await element.evaluate((el, randomToken) => {
696
- el.setAttribute("data-blinq-id-" + randomToken, "");
697
- }, randomToken);
698
- // if (element._frame) {
699
- // return element;
700
- // }
824
+ let newElementSelector = "";
825
+ if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
826
+ const cssSelector = await element.evaluate((el) => {
827
+ function getCssSelector(el) {
828
+ if (!el || el.nodeType !== 1 || el === document.body)
829
+ return el.tagName.toLowerCase();
830
+ const parent = el.parentElement;
831
+ const tag = el.tagName.toLowerCase();
832
+ // Find the index of the element among its siblings of the same tag
833
+ let index = 1;
834
+ for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
835
+ if (sibling.tagName === el.tagName) {
836
+ index++;
837
+ }
838
+ }
839
+ // Use nth-child if necessary (i.e., if there's more than one of the same tag)
840
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
841
+ const needsNthChild = siblings.length > 1;
842
+ const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
843
+ return getCssSelector(parent) + " > " + selector;
844
+ }
845
+ const cssSelector = getCssSelector(el);
846
+ return cssSelector;
847
+ });
848
+ newElementSelector = cssSelector;
849
+ }
850
+ else {
851
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
852
+ if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
853
+ const dataAttribute = "data-blinq-id";
854
+ await element.evaluate((el, [dataAttribute, randomToken]) => {
855
+ el.setAttribute(dataAttribute, randomToken);
856
+ }, [dataAttribute, randomToken]);
857
+ newElementSelector = `[${dataAttribute}="${randomToken}"]`;
858
+ }
859
+ else {
860
+ // the default case just return the located element
861
+ // will not work for click and type if the locator is placeholder and the placeholder change due to the click event
862
+ return element;
863
+ }
864
+ }
701
865
  const scope = element._frame ?? element.page();
702
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
703
866
  let prefixSelector = "";
704
867
  const frameControlSelector = " >> internal:control=enter-frame";
705
868
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
706
869
  if (frameSelectorIndex !== -1) {
707
870
  // remove everything after the >> internal:control=enter-frame
708
871
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
709
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
872
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
710
873
  }
711
874
  // if (element?._frame?._selector) {
712
875
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
713
876
  // }
714
877
  const newSelector = prefixSelector + newElementSelector;
715
- return scope.locator(newSelector);
878
+ return scope.locator(newSelector).first();
716
879
  }
717
880
  }
718
881
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -733,7 +896,7 @@ class StableBrowser {
733
896
  for (let i = 0; i < frame.selectors.length; i++) {
734
897
  let frameLocator = frame.selectors[i];
735
898
  if (frameLocator.css) {
736
- let testframescope = framescope.frameLocator(frameLocator.css);
899
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
737
900
  if (frameLocator.index) {
738
901
  testframescope = framescope.nth(frameLocator.index);
739
902
  }
@@ -811,6 +974,15 @@ class StableBrowser {
811
974
  });
812
975
  }
813
976
  async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
977
+ if (selectors.locators && Array.isArray(selectors.locators)) {
978
+ selectors.locators.forEach((locator) => {
979
+ locator.index = locator.index ?? 0;
980
+ locator.visible = locator.visible ?? true;
981
+ if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
982
+ locator.css = locator.css + " >> visible=true";
983
+ }
984
+ });
985
+ }
814
986
  if (!info) {
815
987
  info = {};
816
988
  info.failCause = {};
@@ -823,7 +995,6 @@ class StableBrowser {
823
995
  let locatorsCount = 0;
824
996
  let lazy_scroll = false;
825
997
  //let arrayMode = Array.isArray(selectors);
826
- let scope = await this._findFrameScope(selectors, timeout, info);
827
998
  let selectorsLocators = null;
828
999
  selectorsLocators = selectors.locators;
829
1000
  // group selectors by priority
@@ -851,6 +1022,7 @@ class StableBrowser {
851
1022
  let highPriorityOnly = true;
852
1023
  let visibleOnly = true;
853
1024
  while (true) {
1025
+ let scope = await this._findFrameScope(selectors, timeout, info);
854
1026
  locatorsCount = 0;
855
1027
  let result = [];
856
1028
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -966,9 +1138,13 @@ class StableBrowser {
966
1138
  }
967
1139
  }
968
1140
  if (foundLocators.length === 1) {
1141
+ let box = null;
1142
+ if (!this.onlyFailuresScreenshot) {
1143
+ box = await foundLocators[0].boundingBox();
1144
+ }
969
1145
  result.foundElements.push({
970
1146
  locator: foundLocators[0],
971
- box: await foundLocators[0].boundingBox(),
1147
+ box: box,
972
1148
  unique: true,
973
1149
  });
974
1150
  result.locatorIndex = i;
@@ -1123,11 +1299,22 @@ class StableBrowser {
1123
1299
  operation: "click",
1124
1300
  log: "***** click on " + selectors.element_name + " *****\n",
1125
1301
  };
1302
+ check_performance("click_all ***", this.context, true);
1303
+ let stepFastMode = this.stepTags.includes("fast-mode");
1304
+ if (stepFastMode) {
1305
+ state.onlyFailuresScreenshot = true;
1306
+ state.scroll = false;
1307
+ state.highlight = false;
1308
+ }
1126
1309
  try {
1310
+ check_performance("click_preCommand", this.context, true);
1127
1311
  await _preCommand(state, this);
1312
+ check_performance("click_preCommand", this.context, false);
1128
1313
  await performAction("click", state.element, options, this, state, _params);
1129
- if (!this.fastMode) {
1130
- await this.waitForPageLoad();
1314
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1315
+ check_performance("click_waitForPageLoad", this.context, true);
1316
+ await this.waitForPageLoad({ noSleep: true });
1317
+ check_performance("click_waitForPageLoad", this.context, false);
1131
1318
  }
1132
1319
  return state.info;
1133
1320
  }
@@ -1135,7 +1322,13 @@ class StableBrowser {
1135
1322
  await _commandError(state, e, this);
1136
1323
  }
1137
1324
  finally {
1325
+ check_performance("click_commandFinally", this.context, true);
1138
1326
  await _commandFinally(state, this);
1327
+ check_performance("click_commandFinally", this.context, false);
1328
+ check_performance("click_all ***", this.context, false);
1329
+ if (this.context.profile) {
1330
+ console.log(JSON.stringify(this.context.profile, null, 2));
1331
+ }
1139
1332
  }
1140
1333
  }
1141
1334
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1227,7 +1420,7 @@ class StableBrowser {
1227
1420
  }
1228
1421
  }
1229
1422
  }
1230
- await this.waitForPageLoad();
1423
+ //await this.waitForPageLoad();
1231
1424
  return state.info;
1232
1425
  }
1233
1426
  catch (e) {
@@ -1253,7 +1446,7 @@ class StableBrowser {
1253
1446
  await _preCommand(state, this);
1254
1447
  await performAction("hover", state.element, options, this, state, _params);
1255
1448
  await _screenshot(state, this);
1256
- await this.waitForPageLoad();
1449
+ //await this.waitForPageLoad();
1257
1450
  return state.info;
1258
1451
  }
1259
1452
  catch (e) {
@@ -1289,7 +1482,7 @@ class StableBrowser {
1289
1482
  state.info.log += "selectOption failed, will try force" + "\n";
1290
1483
  await state.element.selectOption(values, { timeout: 10000, force: true });
1291
1484
  }
1292
- await this.waitForPageLoad();
1485
+ //await this.waitForPageLoad();
1293
1486
  return state.info;
1294
1487
  }
1295
1488
  catch (e) {
@@ -1475,6 +1668,14 @@ class StableBrowser {
1475
1668
  }
1476
1669
  try {
1477
1670
  await _preCommand(state, this);
1671
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1672
+ // tag the element
1673
+ let newElementSelector = await state.element.evaluate((el, token) => {
1674
+ // use attribute and not id
1675
+ const attrName = `data-blinq-id-${token}`;
1676
+ el.setAttribute(attrName, "");
1677
+ return `[${attrName}]`;
1678
+ }, randomToken);
1478
1679
  state.info.value = _value;
1479
1680
  if (!options.press) {
1480
1681
  try {
@@ -1500,6 +1701,25 @@ class StableBrowser {
1500
1701
  }
1501
1702
  }
1502
1703
  await new Promise((resolve) => setTimeout(resolve, 500));
1704
+ // check if the element exist after the click (no wait)
1705
+ const count = await state.element.count({ timeout: 0 });
1706
+ if (count === 0) {
1707
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1708
+ const scope = state.element._frame ?? element.page();
1709
+ let prefixSelector = "";
1710
+ const frameControlSelector = " >> internal:control=enter-frame";
1711
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1712
+ if (frameSelectorIndex !== -1) {
1713
+ // remove everything after the >> internal:control=enter-frame
1714
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1715
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1716
+ }
1717
+ // if (element?._frame?._selector) {
1718
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1719
+ // }
1720
+ const newSelector = prefixSelector + newElementSelector;
1721
+ state.element = scope.locator(newSelector).first();
1722
+ }
1503
1723
  const valueSegment = state.value.split("&&");
1504
1724
  for (let i = 0; i < valueSegment.length; i++) {
1505
1725
  if (i > 0) {
@@ -1571,8 +1791,8 @@ class StableBrowser {
1571
1791
  if (enter) {
1572
1792
  await new Promise((resolve) => setTimeout(resolve, 2000));
1573
1793
  await this.page.keyboard.press("Enter");
1794
+ await this.waitForPageLoad();
1574
1795
  }
1575
- await this.waitForPageLoad();
1576
1796
  return state.info;
1577
1797
  }
1578
1798
  catch (e) {
@@ -1831,11 +2051,12 @@ class StableBrowser {
1831
2051
  throw new Error("referanceSnapshot is null");
1832
2052
  }
1833
2053
  let text = null;
1834
- if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1835
- text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
2054
+ const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
2055
+ if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
2056
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
1836
2057
  }
1837
- else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1838
- text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
2058
+ else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
2059
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
1839
2060
  }
1840
2061
  else if (referanceSnapshot.startsWith("yaml:")) {
1841
2062
  text = referanceSnapshot.substring(5);
@@ -2030,6 +2251,9 @@ class StableBrowser {
2030
2251
  return _getTestData(world, this.context, this);
2031
2252
  }
2032
2253
  async _screenShot(options = {}, world = null, info = null) {
2254
+ if (!options) {
2255
+ options = {};
2256
+ }
2033
2257
  // collect url/path/title
2034
2258
  if (info) {
2035
2259
  if (!info.title) {
@@ -2058,7 +2282,7 @@ class StableBrowser {
2058
2282
  const uuidStr = "id_" + randomUUID();
2059
2283
  const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
2060
2284
  try {
2061
- await this.takeScreenshot(screenshotPath);
2285
+ await this.takeScreenshot(screenshotPath, options.fullPage === true);
2062
2286
  // let buffer = await this.page.screenshot({ timeout: 4000 });
2063
2287
  // // save the buffer to the screenshot path asynchrously
2064
2288
  // fs.writeFile(screenshotPath, buffer, (err) => {
@@ -2079,7 +2303,7 @@ class StableBrowser {
2079
2303
  else if (options && options.screenshot) {
2080
2304
  result.screenshotPath = options.screenshotPath;
2081
2305
  try {
2082
- await this.takeScreenshot(options.screenshotPath);
2306
+ await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
2083
2307
  // let buffer = await this.page.screenshot({ timeout: 4000 });
2084
2308
  // // save the buffer to the screenshot path asynchrously
2085
2309
  // fs.writeFile(options.screenshotPath, buffer, (err) => {
@@ -2097,7 +2321,7 @@ class StableBrowser {
2097
2321
  }
2098
2322
  return result;
2099
2323
  }
2100
- async takeScreenshot(screenshotPath) {
2324
+ async takeScreenshot(screenshotPath, fullPage = false) {
2101
2325
  const playContext = this.context.playContext;
2102
2326
  // Using CDP to capture the screenshot
2103
2327
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
@@ -2122,13 +2346,7 @@ class StableBrowser {
2122
2346
  const client = await playContext.newCDPSession(this.page);
2123
2347
  const { data } = await client.send("Page.captureScreenshot", {
2124
2348
  format: "png",
2125
- // clip: {
2126
- // x: 0,
2127
- // y: 0,
2128
- // width: viewportWidth,
2129
- // height: viewportHeight,
2130
- // scale: 1,
2131
- // },
2349
+ captureBeyondViewport: fullPage,
2132
2350
  });
2133
2351
  await client.detach();
2134
2352
  if (!screenshotPath) {
@@ -2137,7 +2355,7 @@ class StableBrowser {
2137
2355
  screenshotBuffer = Buffer.from(data, "base64");
2138
2356
  }
2139
2357
  else {
2140
- screenshotBuffer = await this.page.screenshot();
2358
+ screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
2141
2359
  }
2142
2360
  // if (focusedElement) {
2143
2361
  // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
@@ -2438,7 +2656,7 @@ class StableBrowser {
2438
2656
  let expectedValue;
2439
2657
  try {
2440
2658
  await _preCommand(state, this);
2441
- expectedValue = await replaceWithLocalTestData(state.value, world);
2659
+ expectedValue = await this._replaceWithLocalData(value, world);
2442
2660
  state.info.expectedValue = expectedValue;
2443
2661
  switch (property) {
2444
2662
  case "innerText":
@@ -2486,47 +2704,54 @@ class StableBrowser {
2486
2704
  }
2487
2705
  state.info.value = val;
2488
2706
  let regex;
2489
- if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2490
- const patternBody = expectedValue.slice(1, -1);
2491
- const processedPattern = patternBody.replace(/\n/g, ".*");
2492
- regex = new RegExp(processedPattern, "gs");
2493
- state.info.regex = true;
2494
- }
2495
- else {
2496
- const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2497
- regex = new RegExp(escapedPattern, "g");
2498
- }
2499
- if (property === "innerText") {
2500
- if (state.info.regex) {
2501
- if (!regex.test(val)) {
2502
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2503
- state.info.failCause.assertionFailed = true;
2504
- state.info.failCause.lastError = errorMessage;
2505
- throw new Error(errorMessage);
2506
- }
2707
+ state.info.value = val;
2708
+ const isRegex = expectedValue.startsWith("regex:");
2709
+ const isContains = expectedValue.startsWith("contains:");
2710
+ const isExact = expectedValue.startsWith("exact:");
2711
+ let matchPassed = false;
2712
+ if (isRegex) {
2713
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2714
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2715
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2716
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2717
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2718
+ const regex = new RegExp(patternBody, flags);
2719
+ state.info.regex = true;
2720
+ matchPassed = regex.test(val);
2507
2721
  }
2508
2722
  else {
2509
- // Fix: Replace escaped newlines with actual newlines before splitting
2510
- const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2511
- const valLines = val.split("\n");
2512
- const expectedLines = normalizedExpectedValue.split("\n");
2513
- // Check if all expected lines are present in the actual lines
2514
- const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2515
- if (!isPart) {
2516
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2517
- state.info.failCause.assertionFailed = true;
2518
- state.info.failCause.lastError = errorMessage;
2519
- throw new Error(errorMessage);
2520
- }
2723
+ // Fallback: treat as literal
2724
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2725
+ const regex = new RegExp(escapedPattern, "g");
2726
+ matchPassed = regex.test(val);
2521
2727
  }
2522
2728
  }
2729
+ else if (isContains) {
2730
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2731
+ matchPassed = val.includes(containsValue);
2732
+ }
2733
+ else if (isExact) {
2734
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2735
+ matchPassed = val === exactValue;
2736
+ }
2737
+ else if (property === "innerText") {
2738
+ // Default innerText logic
2739
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2740
+ const valLines = val.split("\n");
2741
+ const expectedLines = normalizedExpectedValue.split("\n");
2742
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2743
+ }
2523
2744
  else {
2524
- if (!val.match(regex)) {
2525
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2526
- state.info.failCause.assertionFailed = true;
2527
- state.info.failCause.lastError = errorMessage;
2528
- throw new Error(errorMessage);
2529
- }
2745
+ // Fallback exact or loose match
2746
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2747
+ const regex = new RegExp(escapedPattern, "g");
2748
+ matchPassed = regex.test(val);
2749
+ }
2750
+ if (!matchPassed) {
2751
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2752
+ state.info.failCause.assertionFailed = true;
2753
+ state.info.failCause.lastError = errorMessage;
2754
+ throw new Error(errorMessage);
2530
2755
  }
2531
2756
  return state.info;
2532
2757
  }
@@ -2557,6 +2782,7 @@ class StableBrowser {
2557
2782
  allowDisabled: true,
2558
2783
  info: {},
2559
2784
  };
2785
+ state.options ??= { timeout: timeoutMs };
2560
2786
  // Initialize startTime outside try block to ensure it's always accessible
2561
2787
  const startTime = Date.now();
2562
2788
  let conditionMet = false;
@@ -3132,7 +3358,16 @@ class StableBrowser {
3132
3358
  text = text.replace(/\\"/g, '"');
3133
3359
  }
3134
3360
  const timeout = this._getFindElementTimeout(options);
3135
- await new Promise((resolve) => setTimeout(resolve, 2000));
3361
+ //if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
3362
+ let stepFastMode = this.stepTags.includes("fast-mode");
3363
+ if (!stepFastMode) {
3364
+ if (!this.fastMode) {
3365
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3366
+ }
3367
+ else {
3368
+ await new Promise((resolve) => setTimeout(resolve, 500));
3369
+ }
3370
+ }
3136
3371
  const newValue = await this._replaceWithLocalData(text, world);
3137
3372
  if (newValue !== text) {
3138
3373
  this.logger.info(text + "=" + newValue);
@@ -3140,6 +3375,11 @@ class StableBrowser {
3140
3375
  }
3141
3376
  let dateAlternatives = findDateAlternatives(text);
3142
3377
  let numberAlternatives = findNumberAlternatives(text);
3378
+ if (stepFastMode) {
3379
+ state.onlyFailuresScreenshot = true;
3380
+ state.scroll = false;
3381
+ state.highlight = false;
3382
+ }
3143
3383
  try {
3144
3384
  await _preCommand(state, this);
3145
3385
  state.info.text = text;
@@ -3259,6 +3499,8 @@ class StableBrowser {
3259
3499
  operation: "verify_text_with_relation",
3260
3500
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3261
3501
  };
3502
+ const cmdStartTime = Date.now();
3503
+ let cmdEndTime = null;
3262
3504
  const timeout = this._getFindElementTimeout(options);
3263
3505
  await new Promise((resolve) => setTimeout(resolve, 2000));
3264
3506
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3294,6 +3536,17 @@ class StableBrowser {
3294
3536
  await new Promise((resolve) => setTimeout(resolve, 1000));
3295
3537
  continue;
3296
3538
  }
3539
+ else {
3540
+ cmdEndTime = Date.now();
3541
+ if (cmdEndTime - cmdStartTime > 55000) {
3542
+ if (foundAncore) {
3543
+ throw new Error(`Text ${textToVerify} not found in page`);
3544
+ }
3545
+ else {
3546
+ throw new Error(`Text ${textAnchor} not found in page`);
3547
+ }
3548
+ }
3549
+ }
3297
3550
  try {
3298
3551
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3299
3552
  foundAncore = true;
@@ -3432,7 +3685,7 @@ class StableBrowser {
3432
3685
  Object.assign(e, { info: info });
3433
3686
  error = e;
3434
3687
  // throw e;
3435
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3688
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3436
3689
  }
3437
3690
  finally {
3438
3691
  const endTime = Date.now();
@@ -3781,6 +4034,22 @@ class StableBrowser {
3781
4034
  }
3782
4035
  }
3783
4036
  async waitForPageLoad(options = {}, world = null) {
4037
+ // try {
4038
+ // let currentPagePath = null;
4039
+ // currentPagePath = new URL(this.page.url()).pathname;
4040
+ // if (this.latestPagePath) {
4041
+ // // get the currect page path and compare with the latest page path
4042
+ // if (this.latestPagePath === currentPagePath) {
4043
+ // // if the page path is the same, do not wait for page load
4044
+ // console.log("No page change: " + currentPagePath);
4045
+ // return;
4046
+ // }
4047
+ // }
4048
+ // this.latestPagePath = currentPagePath;
4049
+ // } catch (e) {
4050
+ // console.debug("Error getting current page path: ", e);
4051
+ // }
4052
+ //console.log("Waiting for page load");
3784
4053
  let timeout = this._getLoadTimeout(options);
3785
4054
  const promiseArray = [];
3786
4055
  // let waitForNetworkIdle = true;
@@ -3815,7 +4084,10 @@ class StableBrowser {
3815
4084
  }
3816
4085
  }
3817
4086
  finally {
3818
- await new Promise((resolve) => setTimeout(resolve, 2000));
4087
+ await new Promise((resolve) => setTimeout(resolve, 500));
4088
+ if (options && !options.noSleep) {
4089
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4090
+ }
3819
4091
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3820
4092
  const endTime = Date.now();
3821
4093
  _reportToWorld(world, {
@@ -3869,7 +4141,7 @@ class StableBrowser {
3869
4141
  }
3870
4142
  operation = options.operation;
3871
4143
  // validate operation is one of the supported operations
3872
- if (operation != "click" && operation != "hover+click") {
4144
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3873
4145
  throw new Error("operation is not supported");
3874
4146
  }
3875
4147
  const state = {
@@ -3938,6 +4210,17 @@ class StableBrowser {
3938
4210
  state.element = results[0];
3939
4211
  await performAction("hover+click", state.element, options, this, state, _params);
3940
4212
  break;
4213
+ case "hover":
4214
+ if (!options.css) {
4215
+ throw new Error("css is not defined");
4216
+ }
4217
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4218
+ if (result1.length === 0) {
4219
+ throw new Error(`Element not found in cell area`);
4220
+ }
4221
+ state.element = result1[0];
4222
+ await performAction("hover", state.element, options, this, state, _params);
4223
+ break;
3941
4224
  default:
3942
4225
  throw new Error("operation is not supported");
3943
4226
  }
@@ -3951,6 +4234,12 @@ class StableBrowser {
3951
4234
  }
3952
4235
  saveTestDataAsGlobal(options, world) {
3953
4236
  const dataFile = _getDataFile(world, this.context, this);
4237
+ if (process.env.MODE === "executions") {
4238
+ const globalDataFile = path.join(this.project_path, "global_test_data.json");
4239
+ fs.copyFileSync(dataFile, globalDataFile);
4240
+ this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
4241
+ return;
4242
+ }
3954
4243
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
3955
4244
  this.logger.info("Save the scenario test data as global for the following scenarios.");
3956
4245
  }
@@ -4053,6 +4342,7 @@ class StableBrowser {
4053
4342
  if (world && world.attach) {
4054
4343
  world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4055
4344
  }
4345
+ this.context.loadedRoutes = null;
4056
4346
  this.beforeScenarioCalled = true;
4057
4347
  if (scenario && scenario.pickle && scenario.pickle.name) {
4058
4348
  this.scenarioName = scenario.pickle.name;
@@ -4082,9 +4372,10 @@ class StableBrowser {
4082
4372
  }
4083
4373
  async afterScenario(world, scenario) { }
4084
4374
  async beforeStep(world, step) {
4085
- console.log("Inside beforeStep");
4375
+ this.stepTags = [];
4086
4376
  if (!this.beforeScenarioCalled) {
4087
4377
  this.beforeScenario(world, step);
4378
+ this.context.loadedRoutes = null;
4088
4379
  }
4089
4380
  if (this.stepIndex === undefined) {
4090
4381
  this.stepIndex = 0;
@@ -4094,7 +4385,12 @@ class StableBrowser {
4094
4385
  }
4095
4386
  if (step && step.pickleStep && step.pickleStep.text) {
4096
4387
  this.stepName = step.pickleStep.text;
4097
- this.logger.info("step: " + this.stepName);
4388
+ let printableStepName = this.stepName;
4389
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4390
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4391
+ return `\x1b[33m"${p1}"\x1b[0m`;
4392
+ });
4393
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4098
4394
  }
4099
4395
  else if (step && step.text) {
4100
4396
  this.stepName = step.text;
@@ -4109,7 +4405,10 @@ class StableBrowser {
4109
4405
  }
4110
4406
  if (this.initSnapshotTaken === false) {
4111
4407
  this.initSnapshotTaken = true;
4112
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4408
+ if (world &&
4409
+ world.attach &&
4410
+ !process.env.DISABLE_SNAPSHOT &&
4411
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4113
4412
  const snapshot = await this.getAriaSnapshot();
4114
4413
  if (snapshot) {
4115
4414
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -4117,8 +4416,13 @@ class StableBrowser {
4117
4416
  }
4118
4417
  }
4119
4418
  this.context.routeResults = null;
4120
- await registerBeforeStepRoutes(this.context, this.stepName);
4121
- networkBeforeStep(this.stepName);
4419
+ this.context.loadedRoutes = null;
4420
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4421
+ networkBeforeStep(this.stepName, this.context);
4422
+ this.inStepReport = false;
4423
+ }
4424
+ setStepTags(tags) {
4425
+ this.stepTags = tags;
4122
4426
  }
4123
4427
  async getAriaSnapshot() {
4124
4428
  try {
@@ -4192,7 +4496,7 @@ class StableBrowser {
4192
4496
  state.payload = payload;
4193
4497
  if (commandStatus === "FAILED") {
4194
4498
  state.throwError = true;
4195
- throw new Error("Command failed");
4499
+ throw new Error(commandText);
4196
4500
  }
4197
4501
  }
4198
4502
  catch (e) {
@@ -4202,7 +4506,7 @@ class StableBrowser {
4202
4506
  await _commandFinally(state, this);
4203
4507
  }
4204
4508
  }
4205
- async afterStep(world, step) {
4509
+ async afterStep(world, step, result) {
4206
4510
  this.stepName = null;
4207
4511
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4208
4512
  if (this.context.browserObject.context) {
@@ -4221,7 +4525,17 @@ class StableBrowser {
4221
4525
  if (this.context) {
4222
4526
  this.context.examplesRow = null;
4223
4527
  }
4224
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4528
+ if (!this.inStepReport) {
4529
+ // check the step result
4530
+ if (result && result.status === "FAILED" && world && world.attach) {
4531
+ await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
4532
+ }
4533
+ }
4534
+ if (world &&
4535
+ world.attach &&
4536
+ !process.env.DISABLE_SNAPSHOT &&
4537
+ !this.fastMode &&
4538
+ !this.stepTags.includes("fast-mode")) {
4225
4539
  const snapshot = await this.getAriaSnapshot();
4226
4540
  if (snapshot) {
4227
4541
  const obj = {};
@@ -4230,7 +4544,6 @@ class StableBrowser {
4230
4544
  }
4231
4545
  this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4232
4546
  if (this.context.routeResults) {
4233
- this.logger.info("Route results after step: " + JSON.stringify(this.context.routeResults));
4234
4547
  if (world && world.attach) {
4235
4548
  await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4236
4549
  }
@@ -4258,7 +4571,13 @@ class StableBrowser {
4258
4571
  await _commandFinally(state, this);
4259
4572
  }
4260
4573
  }
4261
- networkAfterStep(this.stepName);
4574
+ networkAfterStep(this.stepName, this.context);
4575
+ if (process.env.TEMP_RUN === "true") {
4576
+ // Put a sleep for some time to allow the browser to finish processing
4577
+ if (!this.stepTags.includes("fast-mode")) {
4578
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4579
+ }
4580
+ }
4262
4581
  }
4263
4582
  }
4264
4583
  function createTimedPromise(promise, label) {