automation_model 1.0.772-dev → 1.0.772-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,6 +139,7 @@ class StableBrowser {
131
139
  this.fastMode = true;
132
140
  }
133
141
  if (process.env.FAST_MODE === "true") {
142
+ // console.log("Fast mode enabled from environment variable");
134
143
  this.fastMode = true;
135
144
  }
136
145
  if (process.env.FAST_MODE === "false") {
@@ -173,6 +182,7 @@ class StableBrowser {
173
182
  registerNetworkEvents(this.world, this, context, this.page);
174
183
  registerDownloadEvent(this.page, this.world, context);
175
184
  page.on("close", async () => {
185
+ // return if browser context is already closed
176
186
  if (this.context && this.context.pages && this.context.pages.length > 1) {
177
187
  this.context.pages.pop();
178
188
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -182,7 +192,12 @@ class StableBrowser {
182
192
  console.log("Switched to page " + title);
183
193
  }
184
194
  catch (error) {
185
- 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
+ }
186
201
  }
187
202
  }
188
203
  });
@@ -191,7 +206,12 @@ class StableBrowser {
191
206
  console.log("Switch page: " + (await page.title()));
192
207
  }
193
208
  catch (e) {
194
- 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
+ }
195
215
  }
196
216
  context.pageLoading.status = false;
197
217
  }.bind(this));
@@ -219,7 +239,7 @@ class StableBrowser {
219
239
  if (newContextCreated) {
220
240
  this.registerEventListeners(this.context);
221
241
  await this.goto(this.context.environment.baseUrl);
222
- if (!this.fastMode) {
242
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
223
243
  await this.waitForPageLoad();
224
244
  }
225
245
  }
@@ -311,7 +331,7 @@ class StableBrowser {
311
331
  // async closeUnexpectedPopups() {
312
332
  // await closeUnexpectedPopups(this.page);
313
333
  // }
314
- async goto(url, world = null) {
334
+ async goto(url, world = null, options = {}) {
315
335
  if (!url) {
316
336
  throw new Error("url is null, verify that the environment file is correct");
317
337
  }
@@ -332,10 +352,14 @@ class StableBrowser {
332
352
  screenshot: false,
333
353
  highlight: false,
334
354
  };
355
+ let timeout = 60000;
356
+ if (options && options["timeout"]) {
357
+ timeout = options["timeout"];
358
+ }
335
359
  try {
336
360
  await _preCommand(state, this);
337
361
  await this.page.goto(url, {
338
- timeout: 60000,
362
+ timeout: timeout,
339
363
  });
340
364
  await _screenshot(state, this);
341
365
  }
@@ -503,12 +527,6 @@ class StableBrowser {
503
527
  if (!el.setAttribute) {
504
528
  el = el.parentElement;
505
529
  }
506
- // remove any attributes start with data-blinq-id
507
- // for (let i = 0; i < el.attributes.length; i++) {
508
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
509
- // el.removeAttribute(el.attributes[i].name);
510
- // }
511
- // }
512
530
  el.setAttribute("data-blinq-id-" + randomToken, "");
513
531
  return true;
514
532
  }, [tag1, randomToken]))) {
@@ -678,40 +696,186 @@ class StableBrowser {
678
696
  }
679
697
  return { rerun: false };
680
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
+ }
681
763
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
682
764
  if (!timeout) {
683
765
  timeout = 30000;
684
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
+ }
685
774
  for (let i = 0; i < 3; i++) {
686
775
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
687
776
  for (let j = 0; j < selectors.locators.length; j++) {
688
777
  let selector = selectors.locators[j];
689
778
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
690
779
  }
691
- 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
+ }
692
823
  if (!element.rerun) {
693
- const randomToken = Math.random().toString(36).substring(7);
694
- await element.evaluate((el, randomToken) => {
695
- el.setAttribute("data-blinq-id-" + randomToken, "");
696
- }, randomToken);
697
- // if (element._frame) {
698
- // return element;
699
- // }
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
+ }
700
865
  const scope = element._frame ?? element.page();
701
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
702
866
  let prefixSelector = "";
703
867
  const frameControlSelector = " >> internal:control=enter-frame";
704
868
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
705
869
  if (frameSelectorIndex !== -1) {
706
870
  // remove everything after the >> internal:control=enter-frame
707
871
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
708
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
872
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
709
873
  }
710
874
  // if (element?._frame?._selector) {
711
875
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
712
876
  // }
713
877
  const newSelector = prefixSelector + newElementSelector;
714
- return scope.locator(newSelector);
878
+ return scope.locator(newSelector).first();
715
879
  }
716
880
  }
717
881
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -732,7 +896,7 @@ class StableBrowser {
732
896
  for (let i = 0; i < frame.selectors.length; i++) {
733
897
  let frameLocator = frame.selectors[i];
734
898
  if (frameLocator.css) {
735
- let testframescope = framescope.frameLocator(frameLocator.css);
899
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
736
900
  if (frameLocator.index) {
737
901
  testframescope = framescope.nth(frameLocator.index);
738
902
  }
@@ -744,7 +908,7 @@ class StableBrowser {
744
908
  break;
745
909
  }
746
910
  catch (error) {
747
- console.error("frame not found " + frameLocator.css);
911
+ // console.error("frame not found " + frameLocator.css);
748
912
  }
749
913
  }
750
914
  }
@@ -810,6 +974,15 @@ class StableBrowser {
810
974
  });
811
975
  }
812
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
+ }
813
986
  if (!info) {
814
987
  info = {};
815
988
  info.failCause = {};
@@ -822,7 +995,6 @@ class StableBrowser {
822
995
  let locatorsCount = 0;
823
996
  let lazy_scroll = false;
824
997
  //let arrayMode = Array.isArray(selectors);
825
- let scope = await this._findFrameScope(selectors, timeout, info);
826
998
  let selectorsLocators = null;
827
999
  selectorsLocators = selectors.locators;
828
1000
  // group selectors by priority
@@ -850,6 +1022,7 @@ class StableBrowser {
850
1022
  let highPriorityOnly = true;
851
1023
  let visibleOnly = true;
852
1024
  while (true) {
1025
+ let scope = await this._findFrameScope(selectors, timeout, info);
853
1026
  locatorsCount = 0;
854
1027
  let result = [];
855
1028
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -965,9 +1138,13 @@ class StableBrowser {
965
1138
  }
966
1139
  }
967
1140
  if (foundLocators.length === 1) {
1141
+ let box = null;
1142
+ if (!this.onlyFailuresScreenshot) {
1143
+ box = await foundLocators[0].boundingBox();
1144
+ }
968
1145
  result.foundElements.push({
969
1146
  locator: foundLocators[0],
970
- box: await foundLocators[0].boundingBox(),
1147
+ box: box,
971
1148
  unique: true,
972
1149
  });
973
1150
  result.locatorIndex = i;
@@ -1122,11 +1299,22 @@ class StableBrowser {
1122
1299
  operation: "click",
1123
1300
  log: "***** click on " + selectors.element_name + " *****\n",
1124
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
+ }
1125
1309
  try {
1310
+ check_performance("click_preCommand", this.context, true);
1126
1311
  await _preCommand(state, this);
1312
+ check_performance("click_preCommand", this.context, false);
1127
1313
  await performAction("click", state.element, options, this, state, _params);
1128
- if (!this.fastMode) {
1129
- 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);
1130
1318
  }
1131
1319
  return state.info;
1132
1320
  }
@@ -1134,7 +1322,13 @@ class StableBrowser {
1134
1322
  await _commandError(state, e, this);
1135
1323
  }
1136
1324
  finally {
1325
+ check_performance("click_commandFinally", this.context, true);
1137
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
+ }
1138
1332
  }
1139
1333
  }
1140
1334
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1226,7 +1420,7 @@ class StableBrowser {
1226
1420
  }
1227
1421
  }
1228
1422
  }
1229
- await this.waitForPageLoad();
1423
+ //await this.waitForPageLoad();
1230
1424
  return state.info;
1231
1425
  }
1232
1426
  catch (e) {
@@ -1252,7 +1446,7 @@ class StableBrowser {
1252
1446
  await _preCommand(state, this);
1253
1447
  await performAction("hover", state.element, options, this, state, _params);
1254
1448
  await _screenshot(state, this);
1255
- await this.waitForPageLoad();
1449
+ //await this.waitForPageLoad();
1256
1450
  return state.info;
1257
1451
  }
1258
1452
  catch (e) {
@@ -1288,7 +1482,7 @@ class StableBrowser {
1288
1482
  state.info.log += "selectOption failed, will try force" + "\n";
1289
1483
  await state.element.selectOption(values, { timeout: 10000, force: true });
1290
1484
  }
1291
- await this.waitForPageLoad();
1485
+ //await this.waitForPageLoad();
1292
1486
  return state.info;
1293
1487
  }
1294
1488
  catch (e) {
@@ -1474,6 +1668,14 @@ class StableBrowser {
1474
1668
  }
1475
1669
  try {
1476
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);
1477
1679
  state.info.value = _value;
1478
1680
  if (!options.press) {
1479
1681
  try {
@@ -1499,6 +1701,25 @@ class StableBrowser {
1499
1701
  }
1500
1702
  }
1501
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
+ }
1502
1723
  const valueSegment = state.value.split("&&");
1503
1724
  for (let i = 0; i < valueSegment.length; i++) {
1504
1725
  if (i > 0) {
@@ -1570,8 +1791,8 @@ class StableBrowser {
1570
1791
  if (enter) {
1571
1792
  await new Promise((resolve) => setTimeout(resolve, 2000));
1572
1793
  await this.page.keyboard.press("Enter");
1794
+ await this.waitForPageLoad();
1573
1795
  }
1574
- await this.waitForPageLoad();
1575
1796
  return state.info;
1576
1797
  }
1577
1798
  catch (e) {
@@ -2437,7 +2658,7 @@ class StableBrowser {
2437
2658
  let expectedValue;
2438
2659
  try {
2439
2660
  await _preCommand(state, this);
2440
- expectedValue = await replaceWithLocalTestData(state.value, world);
2661
+ expectedValue = await this._replaceWithLocalData(value, world);
2441
2662
  state.info.expectedValue = expectedValue;
2442
2663
  switch (property) {
2443
2664
  case "innerText":
@@ -2485,47 +2706,54 @@ class StableBrowser {
2485
2706
  }
2486
2707
  state.info.value = val;
2487
2708
  let regex;
2488
- if (expectedValue.startsWith("/") && expectedValue.endsWith("/")) {
2489
- const patternBody = expectedValue.slice(1, -1);
2490
- const processedPattern = patternBody.replace(/\n/g, ".*");
2491
- regex = new RegExp(processedPattern, "gs");
2492
- state.info.regex = true;
2493
- }
2494
- else {
2495
- const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2496
- regex = new RegExp(escapedPattern, "g");
2497
- }
2498
- if (property === "innerText") {
2499
- if (state.info.regex) {
2500
- if (!regex.test(val)) {
2501
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2502
- state.info.failCause.assertionFailed = true;
2503
- state.info.failCause.lastError = errorMessage;
2504
- throw new Error(errorMessage);
2505
- }
2709
+ state.info.value = val;
2710
+ const isRegex = expectedValue.startsWith("regex:");
2711
+ const isContains = expectedValue.startsWith("contains:");
2712
+ const isExact = expectedValue.startsWith("exact:");
2713
+ let matchPassed = false;
2714
+ if (isRegex) {
2715
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2716
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2717
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2718
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2719
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2720
+ const regex = new RegExp(patternBody, flags);
2721
+ state.info.regex = true;
2722
+ matchPassed = regex.test(val);
2506
2723
  }
2507
2724
  else {
2508
- // Fix: Replace escaped newlines with actual newlines before splitting
2509
- const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2510
- const valLines = val.split("\n");
2511
- const expectedLines = normalizedExpectedValue.split("\n");
2512
- // Check if all expected lines are present in the actual lines
2513
- const isPart = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2514
- if (!isPart) {
2515
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2516
- state.info.failCause.assertionFailed = true;
2517
- state.info.failCause.lastError = errorMessage;
2518
- throw new Error(errorMessage);
2519
- }
2725
+ // Fallback: treat as literal
2726
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2727
+ const regex = new RegExp(escapedPattern, "g");
2728
+ matchPassed = regex.test(val);
2520
2729
  }
2521
2730
  }
2731
+ else if (isContains) {
2732
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2733
+ matchPassed = val.includes(containsValue);
2734
+ }
2735
+ else if (isExact) {
2736
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2737
+ matchPassed = val === exactValue;
2738
+ }
2739
+ else if (property === "innerText") {
2740
+ // Default innerText logic
2741
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2742
+ const valLines = val.split("\n");
2743
+ const expectedLines = normalizedExpectedValue.split("\n");
2744
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2745
+ }
2522
2746
  else {
2523
- if (!val.match(regex)) {
2524
- let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2525
- state.info.failCause.assertionFailed = true;
2526
- state.info.failCause.lastError = errorMessage;
2527
- throw new Error(errorMessage);
2528
- }
2747
+ // Fallback exact or loose match
2748
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2749
+ const regex = new RegExp(escapedPattern, "g");
2750
+ matchPassed = regex.test(val);
2751
+ }
2752
+ if (!matchPassed) {
2753
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2754
+ state.info.failCause.assertionFailed = true;
2755
+ state.info.failCause.lastError = errorMessage;
2756
+ throw new Error(errorMessage);
2529
2757
  }
2530
2758
  return state.info;
2531
2759
  }
@@ -2556,6 +2784,7 @@ class StableBrowser {
2556
2784
  allowDisabled: true,
2557
2785
  info: {},
2558
2786
  };
2787
+ state.options ??= { timeout: timeoutMs };
2559
2788
  // Initialize startTime outside try block to ensure it's always accessible
2560
2789
  const startTime = Date.now();
2561
2790
  let conditionMet = false;
@@ -3131,7 +3360,16 @@ class StableBrowser {
3131
3360
  text = text.replace(/\\"/g, '"');
3132
3361
  }
3133
3362
  const timeout = this._getFindElementTimeout(options);
3134
- await new Promise((resolve) => setTimeout(resolve, 2000));
3363
+ //if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
3364
+ let stepFastMode = this.stepTags.includes("fast-mode");
3365
+ if (!stepFastMode) {
3366
+ if (!this.fastMode) {
3367
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3368
+ }
3369
+ else {
3370
+ await new Promise((resolve) => setTimeout(resolve, 500));
3371
+ }
3372
+ }
3135
3373
  const newValue = await this._replaceWithLocalData(text, world);
3136
3374
  if (newValue !== text) {
3137
3375
  this.logger.info(text + "=" + newValue);
@@ -3139,6 +3377,11 @@ class StableBrowser {
3139
3377
  }
3140
3378
  let dateAlternatives = findDateAlternatives(text);
3141
3379
  let numberAlternatives = findNumberAlternatives(text);
3380
+ if (stepFastMode) {
3381
+ state.onlyFailuresScreenshot = true;
3382
+ state.scroll = false;
3383
+ state.highlight = false;
3384
+ }
3142
3385
  try {
3143
3386
  await _preCommand(state, this);
3144
3387
  state.info.text = text;
@@ -3258,6 +3501,8 @@ class StableBrowser {
3258
3501
  operation: "verify_text_with_relation",
3259
3502
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3260
3503
  };
3504
+ const cmdStartTime = Date.now();
3505
+ let cmdEndTime = null;
3261
3506
  const timeout = this._getFindElementTimeout(options);
3262
3507
  await new Promise((resolve) => setTimeout(resolve, 2000));
3263
3508
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3293,6 +3538,17 @@ class StableBrowser {
3293
3538
  await new Promise((resolve) => setTimeout(resolve, 1000));
3294
3539
  continue;
3295
3540
  }
3541
+ else {
3542
+ cmdEndTime = Date.now();
3543
+ if (cmdEndTime - cmdStartTime > 55000) {
3544
+ if (foundAncore) {
3545
+ throw new Error(`Text ${textToVerify} not found in page`);
3546
+ }
3547
+ else {
3548
+ throw new Error(`Text ${textAnchor} not found in page`);
3549
+ }
3550
+ }
3551
+ }
3296
3552
  try {
3297
3553
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3298
3554
  foundAncore = true;
@@ -3431,7 +3687,7 @@ class StableBrowser {
3431
3687
  Object.assign(e, { info: info });
3432
3688
  error = e;
3433
3689
  // throw e;
3434
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3690
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3435
3691
  }
3436
3692
  finally {
3437
3693
  const endTime = Date.now();
@@ -3780,6 +4036,22 @@ class StableBrowser {
3780
4036
  }
3781
4037
  }
3782
4038
  async waitForPageLoad(options = {}, world = null) {
4039
+ // try {
4040
+ // let currentPagePath = null;
4041
+ // currentPagePath = new URL(this.page.url()).pathname;
4042
+ // if (this.latestPagePath) {
4043
+ // // get the currect page path and compare with the latest page path
4044
+ // if (this.latestPagePath === currentPagePath) {
4045
+ // // if the page path is the same, do not wait for page load
4046
+ // console.log("No page change: " + currentPagePath);
4047
+ // return;
4048
+ // }
4049
+ // }
4050
+ // this.latestPagePath = currentPagePath;
4051
+ // } catch (e) {
4052
+ // console.debug("Error getting current page path: ", e);
4053
+ // }
4054
+ //console.log("Waiting for page load");
3783
4055
  let timeout = this._getLoadTimeout(options);
3784
4056
  const promiseArray = [];
3785
4057
  // let waitForNetworkIdle = true;
@@ -3814,7 +4086,10 @@ class StableBrowser {
3814
4086
  }
3815
4087
  }
3816
4088
  finally {
3817
- await new Promise((resolve) => setTimeout(resolve, 2000));
4089
+ await new Promise((resolve) => setTimeout(resolve, 500));
4090
+ if (options && !options.noSleep) {
4091
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4092
+ }
3818
4093
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3819
4094
  const endTime = Date.now();
3820
4095
  _reportToWorld(world, {
@@ -3868,7 +4143,7 @@ class StableBrowser {
3868
4143
  }
3869
4144
  operation = options.operation;
3870
4145
  // validate operation is one of the supported operations
3871
- if (operation != "click" && operation != "hover+click") {
4146
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3872
4147
  throw new Error("operation is not supported");
3873
4148
  }
3874
4149
  const state = {
@@ -3937,6 +4212,17 @@ class StableBrowser {
3937
4212
  state.element = results[0];
3938
4213
  await performAction("hover+click", state.element, options, this, state, _params);
3939
4214
  break;
4215
+ case "hover":
4216
+ if (!options.css) {
4217
+ throw new Error("css is not defined");
4218
+ }
4219
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4220
+ if (result1.length === 0) {
4221
+ throw new Error(`Element not found in cell area`);
4222
+ }
4223
+ state.element = result1[0];
4224
+ await performAction("hover", state.element, options, this, state, _params);
4225
+ break;
3940
4226
  default:
3941
4227
  throw new Error("operation is not supported");
3942
4228
  }
@@ -3950,6 +4236,12 @@ class StableBrowser {
3950
4236
  }
3951
4237
  saveTestDataAsGlobal(options, world) {
3952
4238
  const dataFile = _getDataFile(world, this.context, this);
4239
+ if (process.env.MODE === "executions") {
4240
+ const globalDataFile = path.join(this.project_path, "global_test_data.json");
4241
+ fs.copyFileSync(dataFile, globalDataFile);
4242
+ this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
4243
+ return;
4244
+ }
3953
4245
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
3954
4246
  this.logger.info("Save the scenario test data as global for the following scenarios.");
3955
4247
  }
@@ -4052,6 +4344,7 @@ class StableBrowser {
4052
4344
  if (world && world.attach) {
4053
4345
  world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4054
4346
  }
4347
+ this.context.loadedRoutes = null;
4055
4348
  this.beforeScenarioCalled = true;
4056
4349
  if (scenario && scenario.pickle && scenario.pickle.name) {
4057
4350
  this.scenarioName = scenario.pickle.name;
@@ -4081,8 +4374,10 @@ class StableBrowser {
4081
4374
  }
4082
4375
  async afterScenario(world, scenario) { }
4083
4376
  async beforeStep(world, step) {
4377
+ this.stepTags = [];
4084
4378
  if (!this.beforeScenarioCalled) {
4085
4379
  this.beforeScenario(world, step);
4380
+ this.context.loadedRoutes = null;
4086
4381
  }
4087
4382
  if (this.stepIndex === undefined) {
4088
4383
  this.stepIndex = 0;
@@ -4092,7 +4387,12 @@ class StableBrowser {
4092
4387
  }
4093
4388
  if (step && step.pickleStep && step.pickleStep.text) {
4094
4389
  this.stepName = step.pickleStep.text;
4095
- this.logger.info("step: " + this.stepName);
4390
+ let printableStepName = this.stepName;
4391
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4392
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4393
+ return `\x1b[33m"${p1}"\x1b[0m`;
4394
+ });
4395
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4096
4396
  }
4097
4397
  else if (step && step.text) {
4098
4398
  this.stepName = step.text;
@@ -4107,7 +4407,10 @@ class StableBrowser {
4107
4407
  }
4108
4408
  if (this.initSnapshotTaken === false) {
4109
4409
  this.initSnapshotTaken = true;
4110
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4410
+ if (world &&
4411
+ world.attach &&
4412
+ !process.env.DISABLE_SNAPSHOT &&
4413
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4111
4414
  const snapshot = await this.getAriaSnapshot();
4112
4415
  if (snapshot) {
4113
4416
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -4115,8 +4418,13 @@ class StableBrowser {
4115
4418
  }
4116
4419
  }
4117
4420
  this.context.routeResults = null;
4118
- await registerBeforeStepRoutes(this.context, this.stepName);
4119
- networkBeforeStep(this.stepName);
4421
+ this.context.loadedRoutes = null;
4422
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4423
+ networkBeforeStep(this.stepName, this.context);
4424
+ this.inStepReport = false;
4425
+ }
4426
+ setStepTags(tags) {
4427
+ this.stepTags = tags;
4120
4428
  }
4121
4429
  async getAriaSnapshot() {
4122
4430
  try {
@@ -4190,7 +4498,7 @@ class StableBrowser {
4190
4498
  state.payload = payload;
4191
4499
  if (commandStatus === "FAILED") {
4192
4500
  state.throwError = true;
4193
- throw new Error("Command failed");
4501
+ throw new Error(commandText);
4194
4502
  }
4195
4503
  }
4196
4504
  catch (e) {
@@ -4200,7 +4508,7 @@ class StableBrowser {
4200
4508
  await _commandFinally(state, this);
4201
4509
  }
4202
4510
  }
4203
- async afterStep(world, step) {
4511
+ async afterStep(world, step, result) {
4204
4512
  this.stepName = null;
4205
4513
  if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4206
4514
  if (this.context.browserObject.context) {
@@ -4219,7 +4527,17 @@ class StableBrowser {
4219
4527
  if (this.context) {
4220
4528
  this.context.examplesRow = null;
4221
4529
  }
4222
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4530
+ if (!this.inStepReport) {
4531
+ // check the step result
4532
+ if (result && result.status === "FAILED" && world && world.attach) {
4533
+ await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
4534
+ }
4535
+ }
4536
+ if (world &&
4537
+ world.attach &&
4538
+ !process.env.DISABLE_SNAPSHOT &&
4539
+ !this.fastMode &&
4540
+ !this.stepTags.includes("fast-mode")) {
4223
4541
  const snapshot = await this.getAriaSnapshot();
4224
4542
  if (snapshot) {
4225
4543
  const obj = {};
@@ -4227,6 +4545,11 @@ class StableBrowser {
4227
4545
  }
4228
4546
  }
4229
4547
  this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4548
+ if (this.context.routeResults) {
4549
+ if (world && world.attach) {
4550
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4551
+ }
4552
+ }
4230
4553
  if (!process.env.TEMP_RUN) {
4231
4554
  const state = {
4232
4555
  world,
@@ -4250,7 +4573,13 @@ class StableBrowser {
4250
4573
  await _commandFinally(state, this);
4251
4574
  }
4252
4575
  }
4253
- networkAfterStep(this.stepName);
4576
+ networkAfterStep(this.stepName, this.context);
4577
+ if (process.env.TEMP_RUN === "true") {
4578
+ // Put a sleep for some time to allow the browser to finish processing
4579
+ if (!this.stepTags.includes("fast-mode")) {
4580
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4581
+ }
4582
+ }
4254
4583
  }
4255
4584
  }
4256
4585
  function createTimedPromise(promise, label) {