automation_model 1.0.766-dev → 1.0.766-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";
@@ -19,13 +21,14 @@ import { getTestData } from "./auto_page.js";
19
21
  import { locate_element } from "./locate_element.js";
20
22
  import { randomUUID } from "crypto";
21
23
  import { _commandError, _commandFinally, _preCommand, _validateSelectors, _screenshot, _reportToWorld, } from "./command_common.js";
22
- import { registerDownloadEvent, registerNetworkEvents } from "./network.js";
24
+ import { networkAfterStep, networkBeforeStep, registerDownloadEvent, registerNetworkEvents } from "./network.js";
23
25
  import { LocatorLog } from "./locator_log.js";
24
26
  import axios from "axios";
25
27
  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,15 @@ 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
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
96
101
  this.browser = browser;
97
102
  this.page = page;
98
103
  this.logger = logger;
99
104
  this.context = context;
100
105
  this.world = world;
101
106
  this.fastMode = fastMode;
107
+ this.stepTags = stepTags;
102
108
  if (!this.logger) {
103
109
  this.logger = console;
104
110
  }
@@ -131,6 +137,7 @@ class StableBrowser {
131
137
  this.fastMode = true;
132
138
  }
133
139
  if (process.env.FAST_MODE === "true") {
140
+ // console.log("Fast mode enabled from environment variable");
134
141
  this.fastMode = true;
135
142
  }
136
143
  if (process.env.FAST_MODE === "false") {
@@ -173,6 +180,7 @@ class StableBrowser {
173
180
  registerNetworkEvents(this.world, this, context, this.page);
174
181
  registerDownloadEvent(this.page, this.world, context);
175
182
  page.on("close", async () => {
183
+ // return if browser context is already closed
176
184
  if (this.context && this.context.pages && this.context.pages.length > 1) {
177
185
  this.context.pages.pop();
178
186
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -182,7 +190,12 @@ class StableBrowser {
182
190
  console.log("Switched to page " + title);
183
191
  }
184
192
  catch (error) {
185
- console.error("Error on page close", error);
193
+ if (error?.message?.includes("Target page, context or browser has been closed")) {
194
+ // Ignore this error
195
+ }
196
+ else {
197
+ console.error("Error on page close", error);
198
+ }
186
199
  }
187
200
  }
188
201
  });
@@ -191,7 +204,12 @@ class StableBrowser {
191
204
  console.log("Switch page: " + (await page.title()));
192
205
  }
193
206
  catch (e) {
194
- this.logger.error("error on page load " + e);
207
+ if (e?.message?.includes("Target page, context or browser has been closed")) {
208
+ // Ignore this error
209
+ }
210
+ else {
211
+ this.logger.error("error on page load " + e);
212
+ }
195
213
  }
196
214
  context.pageLoading.status = false;
197
215
  }.bind(this));
@@ -219,7 +237,7 @@ class StableBrowser {
219
237
  if (newContextCreated) {
220
238
  this.registerEventListeners(this.context);
221
239
  await this.goto(this.context.environment.baseUrl);
222
- if (!this.fastMode) {
240
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
223
241
  await this.waitForPageLoad();
224
242
  }
225
243
  }
@@ -503,12 +521,6 @@ class StableBrowser {
503
521
  if (!el.setAttribute) {
504
522
  el = el.parentElement;
505
523
  }
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
524
  el.setAttribute("data-blinq-id-" + randomToken, "");
513
525
  return true;
514
526
  }, [tag1, randomToken]))) {
@@ -678,40 +690,186 @@ class StableBrowser {
678
690
  }
679
691
  return { rerun: false };
680
692
  }
693
+ getFilePath() {
694
+ const stackFrames = errorStackParser.parse(new Error());
695
+ const stackFrame = stackFrames.findLast((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
696
+ // return stackFrame?.fileName || null;
697
+ const filepath = stackFrame?.fileName;
698
+ if (filepath) {
699
+ let jsonFilePath = filepath.replace(".mjs", ".json");
700
+ if (existsSync(jsonFilePath)) {
701
+ return jsonFilePath;
702
+ }
703
+ const config = this.configuration ?? {};
704
+ if (!config?.locatorsMetadataDir) {
705
+ config.locatorsMetadataDir = "features/step_definitions/locators";
706
+ }
707
+ if (config && config.locatorsMetadataDir) {
708
+ jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
709
+ }
710
+ if (existsSync(jsonFilePath)) {
711
+ return jsonFilePath;
712
+ }
713
+ return null;
714
+ }
715
+ return null;
716
+ }
717
+ getFullElementLocators(selectors, filePath) {
718
+ if (!filePath || !existsSync(filePath)) {
719
+ return null;
720
+ }
721
+ const content = fs.readFileSync(filePath, "utf8");
722
+ try {
723
+ const allElements = JSON.parse(content);
724
+ const element_key = selectors?.element_key;
725
+ if (element_key && allElements[element_key]) {
726
+ return allElements[element_key];
727
+ }
728
+ for (const elementKey in allElements) {
729
+ const element = allElements[elementKey];
730
+ let foundStrategy = null;
731
+ for (const key in element) {
732
+ if (key === "strategy") {
733
+ continue;
734
+ }
735
+ const locators = element[key];
736
+ if (!locators || !locators.length) {
737
+ continue;
738
+ }
739
+ for (const locator of locators) {
740
+ delete locator.score;
741
+ }
742
+ if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
743
+ foundStrategy = key;
744
+ break;
745
+ }
746
+ }
747
+ if (foundStrategy) {
748
+ return element;
749
+ }
750
+ }
751
+ }
752
+ catch (error) {
753
+ console.error("Error parsing locators from file: " + filePath, error);
754
+ }
755
+ return null;
756
+ }
681
757
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
682
758
  if (!timeout) {
683
759
  timeout = 30000;
684
760
  }
761
+ let element = null;
762
+ let allStrategyLocators = null;
763
+ let selectedStrategy = null;
764
+ if (this.tryAllStrategies) {
765
+ allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
766
+ selectedStrategy = allStrategyLocators?.strategy;
767
+ }
685
768
  for (let i = 0; i < 3; i++) {
686
769
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
687
770
  for (let j = 0; j < selectors.locators.length; j++) {
688
771
  let selector = selectors.locators[j];
689
772
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
690
773
  }
691
- let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
774
+ if (this.tryAllStrategies && selectedStrategy) {
775
+ const strategyLocators = allStrategyLocators[selectedStrategy];
776
+ let err;
777
+ if (strategyLocators && strategyLocators.length) {
778
+ try {
779
+ selectors.locators = strategyLocators;
780
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
781
+ info.selectedStrategy = selectedStrategy;
782
+ info.log += "element found using strategy " + selectedStrategy + "\n";
783
+ }
784
+ catch (error) {
785
+ err = error;
786
+ }
787
+ }
788
+ if (!element) {
789
+ for (const key in allStrategyLocators) {
790
+ if (key === "strategy" || key === selectedStrategy) {
791
+ continue;
792
+ }
793
+ const strategyLocators = allStrategyLocators[key];
794
+ if (strategyLocators && strategyLocators.length) {
795
+ try {
796
+ info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
797
+ selectors.locators = strategyLocators;
798
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
799
+ err = null;
800
+ info.selectedStrategy = key;
801
+ info.log += "element found using strategy " + key + "\n";
802
+ break;
803
+ }
804
+ catch (error) {
805
+ err = error;
806
+ }
807
+ }
808
+ }
809
+ }
810
+ if (err) {
811
+ throw err;
812
+ }
813
+ }
814
+ else {
815
+ element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
816
+ }
692
817
  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
- // }
818
+ let newElementSelector = "";
819
+ if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
820
+ const cssSelector = await element.evaluate((el) => {
821
+ function getCssSelector(el) {
822
+ if (!el || el.nodeType !== 1 || el === document.body)
823
+ return el.tagName.toLowerCase();
824
+ const parent = el.parentElement;
825
+ const tag = el.tagName.toLowerCase();
826
+ // Find the index of the element among its siblings of the same tag
827
+ let index = 1;
828
+ for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
829
+ if (sibling.tagName === el.tagName) {
830
+ index++;
831
+ }
832
+ }
833
+ // Use nth-child if necessary (i.e., if there's more than one of the same tag)
834
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
835
+ const needsNthChild = siblings.length > 1;
836
+ const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
837
+ return getCssSelector(parent) + " > " + selector;
838
+ }
839
+ const cssSelector = getCssSelector(el);
840
+ return cssSelector;
841
+ });
842
+ newElementSelector = cssSelector;
843
+ }
844
+ else {
845
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
846
+ if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
847
+ const dataAttribute = "data-blinq-id";
848
+ await element.evaluate((el, [dataAttribute, randomToken]) => {
849
+ el.setAttribute(dataAttribute, randomToken);
850
+ }, [dataAttribute, randomToken]);
851
+ newElementSelector = `[${dataAttribute}="${randomToken}"]`;
852
+ }
853
+ else {
854
+ // the default case just return the located element
855
+ // will not work for click and type if the locator is placeholder and the placeholder change due to the click event
856
+ return element;
857
+ }
858
+ }
700
859
  const scope = element._frame ?? element.page();
701
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
702
860
  let prefixSelector = "";
703
861
  const frameControlSelector = " >> internal:control=enter-frame";
704
862
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
705
863
  if (frameSelectorIndex !== -1) {
706
864
  // remove everything after the >> internal:control=enter-frame
707
865
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
708
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
866
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
709
867
  }
710
868
  // if (element?._frame?._selector) {
711
869
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
712
870
  // }
713
871
  const newSelector = prefixSelector + newElementSelector;
714
- return scope.locator(newSelector);
872
+ return scope.locator(newSelector).first();
715
873
  }
716
874
  }
717
875
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -732,7 +890,7 @@ class StableBrowser {
732
890
  for (let i = 0; i < frame.selectors.length; i++) {
733
891
  let frameLocator = frame.selectors[i];
734
892
  if (frameLocator.css) {
735
- let testframescope = framescope.frameLocator(frameLocator.css);
893
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
736
894
  if (frameLocator.index) {
737
895
  testframescope = framescope.nth(frameLocator.index);
738
896
  }
@@ -744,7 +902,7 @@ class StableBrowser {
744
902
  break;
745
903
  }
746
904
  catch (error) {
747
- console.error("frame not found " + frameLocator.css);
905
+ // console.error("frame not found " + frameLocator.css);
748
906
  }
749
907
  }
750
908
  }
@@ -810,6 +968,15 @@ class StableBrowser {
810
968
  });
811
969
  }
812
970
  async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
971
+ if (selectors.locators && Array.isArray(selectors.locators)) {
972
+ selectors.locators.forEach((locator) => {
973
+ locator.index = locator.index ?? 0;
974
+ locator.visible = locator.visible ?? true;
975
+ if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
976
+ locator.css = locator.css + " >> visible=true";
977
+ }
978
+ });
979
+ }
813
980
  if (!info) {
814
981
  info = {};
815
982
  info.failCause = {};
@@ -822,7 +989,6 @@ class StableBrowser {
822
989
  let locatorsCount = 0;
823
990
  let lazy_scroll = false;
824
991
  //let arrayMode = Array.isArray(selectors);
825
- let scope = await this._findFrameScope(selectors, timeout, info);
826
992
  let selectorsLocators = null;
827
993
  selectorsLocators = selectors.locators;
828
994
  // group selectors by priority
@@ -850,6 +1016,7 @@ class StableBrowser {
850
1016
  let highPriorityOnly = true;
851
1017
  let visibleOnly = true;
852
1018
  while (true) {
1019
+ let scope = await this._findFrameScope(selectors, timeout, info);
853
1020
  locatorsCount = 0;
854
1021
  let result = [];
855
1022
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -965,9 +1132,13 @@ class StableBrowser {
965
1132
  }
966
1133
  }
967
1134
  if (foundLocators.length === 1) {
1135
+ let box = null;
1136
+ if (!this.onlyFailuresScreenshot) {
1137
+ box = await foundLocators[0].boundingBox();
1138
+ }
968
1139
  result.foundElements.push({
969
1140
  locator: foundLocators[0],
970
- box: await foundLocators[0].boundingBox(),
1141
+ box: box,
971
1142
  unique: true,
972
1143
  });
973
1144
  result.locatorIndex = i;
@@ -1122,11 +1293,22 @@ class StableBrowser {
1122
1293
  operation: "click",
1123
1294
  log: "***** click on " + selectors.element_name + " *****\n",
1124
1295
  };
1296
+ check_performance("click_all ***", this.context, true);
1297
+ let stepFastMode = this.stepTags.includes("fast-mode");
1298
+ if (stepFastMode) {
1299
+ state.onlyFailuresScreenshot = true;
1300
+ state.scroll = false;
1301
+ state.highlight = false;
1302
+ }
1125
1303
  try {
1304
+ check_performance("click_preCommand", this.context, true);
1126
1305
  await _preCommand(state, this);
1306
+ check_performance("click_preCommand", this.context, false);
1127
1307
  await performAction("click", state.element, options, this, state, _params);
1128
- if (!this.fastMode) {
1129
- await this.waitForPageLoad();
1308
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1309
+ check_performance("click_waitForPageLoad", this.context, true);
1310
+ await this.waitForPageLoad({ noSleep: true });
1311
+ check_performance("click_waitForPageLoad", this.context, false);
1130
1312
  }
1131
1313
  return state.info;
1132
1314
  }
@@ -1134,7 +1316,13 @@ class StableBrowser {
1134
1316
  await _commandError(state, e, this);
1135
1317
  }
1136
1318
  finally {
1319
+ check_performance("click_commandFinally", this.context, true);
1137
1320
  await _commandFinally(state, this);
1321
+ check_performance("click_commandFinally", this.context, false);
1322
+ check_performance("click_all ***", this.context, false);
1323
+ if (this.context.profile) {
1324
+ console.log(JSON.stringify(this.context.profile, null, 2));
1325
+ }
1138
1326
  }
1139
1327
  }
1140
1328
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1226,7 +1414,7 @@ class StableBrowser {
1226
1414
  }
1227
1415
  }
1228
1416
  }
1229
- await this.waitForPageLoad();
1417
+ //await this.waitForPageLoad();
1230
1418
  return state.info;
1231
1419
  }
1232
1420
  catch (e) {
@@ -1252,7 +1440,7 @@ class StableBrowser {
1252
1440
  await _preCommand(state, this);
1253
1441
  await performAction("hover", state.element, options, this, state, _params);
1254
1442
  await _screenshot(state, this);
1255
- await this.waitForPageLoad();
1443
+ //await this.waitForPageLoad();
1256
1444
  return state.info;
1257
1445
  }
1258
1446
  catch (e) {
@@ -1288,7 +1476,7 @@ class StableBrowser {
1288
1476
  state.info.log += "selectOption failed, will try force" + "\n";
1289
1477
  await state.element.selectOption(values, { timeout: 10000, force: true });
1290
1478
  }
1291
- await this.waitForPageLoad();
1479
+ //await this.waitForPageLoad();
1292
1480
  return state.info;
1293
1481
  }
1294
1482
  catch (e) {
@@ -1474,6 +1662,14 @@ class StableBrowser {
1474
1662
  }
1475
1663
  try {
1476
1664
  await _preCommand(state, this);
1665
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1666
+ // tag the element
1667
+ let newElementSelector = await state.element.evaluate((el, token) => {
1668
+ // use attribute and not id
1669
+ const attrName = `data-blinq-id-${token}`;
1670
+ el.setAttribute(attrName, "");
1671
+ return `[${attrName}]`;
1672
+ }, randomToken);
1477
1673
  state.info.value = _value;
1478
1674
  if (!options.press) {
1479
1675
  try {
@@ -1499,6 +1695,25 @@ class StableBrowser {
1499
1695
  }
1500
1696
  }
1501
1697
  await new Promise((resolve) => setTimeout(resolve, 500));
1698
+ // check if the element exist after the click (no wait)
1699
+ const count = await state.element.count({ timeout: 0 });
1700
+ if (count === 0) {
1701
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1702
+ const scope = state.element._frame ?? element.page();
1703
+ let prefixSelector = "";
1704
+ const frameControlSelector = " >> internal:control=enter-frame";
1705
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1706
+ if (frameSelectorIndex !== -1) {
1707
+ // remove everything after the >> internal:control=enter-frame
1708
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1709
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1710
+ }
1711
+ // if (element?._frame?._selector) {
1712
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1713
+ // }
1714
+ const newSelector = prefixSelector + newElementSelector;
1715
+ state.element = scope.locator(newSelector).first();
1716
+ }
1502
1717
  const valueSegment = state.value.split("&&");
1503
1718
  for (let i = 0; i < valueSegment.length; i++) {
1504
1719
  if (i > 0) {
@@ -1570,8 +1785,8 @@ class StableBrowser {
1570
1785
  if (enter) {
1571
1786
  await new Promise((resolve) => setTimeout(resolve, 2000));
1572
1787
  await this.page.keyboard.press("Enter");
1788
+ await this.waitForPageLoad();
1573
1789
  }
1574
- await this.waitForPageLoad();
1575
1790
  return state.info;
1576
1791
  }
1577
1792
  catch (e) {
@@ -2437,7 +2652,7 @@ class StableBrowser {
2437
2652
  let expectedValue;
2438
2653
  try {
2439
2654
  await _preCommand(state, this);
2440
- expectedValue = await replaceWithLocalTestData(state.value, world);
2655
+ expectedValue = await this._replaceWithLocalData(value, world);
2441
2656
  state.info.expectedValue = expectedValue;
2442
2657
  switch (property) {
2443
2658
  case "innerText":
@@ -2485,47 +2700,54 @@ class StableBrowser {
2485
2700
  }
2486
2701
  state.info.value = val;
2487
2702
  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
- }
2703
+ state.info.value = val;
2704
+ const isRegex = expectedValue.startsWith("regex:");
2705
+ const isContains = expectedValue.startsWith("contains:");
2706
+ const isExact = expectedValue.startsWith("exact:");
2707
+ let matchPassed = false;
2708
+ if (isRegex) {
2709
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2710
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2711
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2712
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2713
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2714
+ const regex = new RegExp(patternBody, flags);
2715
+ state.info.regex = true;
2716
+ matchPassed = regex.test(val);
2506
2717
  }
2507
2718
  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
- }
2719
+ // Fallback: treat as literal
2720
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2721
+ const regex = new RegExp(escapedPattern, "g");
2722
+ matchPassed = regex.test(val);
2520
2723
  }
2521
2724
  }
2725
+ else if (isContains) {
2726
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2727
+ matchPassed = val.includes(containsValue);
2728
+ }
2729
+ else if (isExact) {
2730
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2731
+ matchPassed = val === exactValue;
2732
+ }
2733
+ else if (property === "innerText") {
2734
+ // Default innerText logic
2735
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2736
+ const valLines = val.split("\n");
2737
+ const expectedLines = normalizedExpectedValue.split("\n");
2738
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2739
+ }
2522
2740
  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
- }
2741
+ // Fallback exact or loose match
2742
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2743
+ const regex = new RegExp(escapedPattern, "g");
2744
+ matchPassed = regex.test(val);
2745
+ }
2746
+ if (!matchPassed) {
2747
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2748
+ state.info.failCause.assertionFailed = true;
2749
+ state.info.failCause.lastError = errorMessage;
2750
+ throw new Error(errorMessage);
2529
2751
  }
2530
2752
  return state.info;
2531
2753
  }
@@ -2556,6 +2778,7 @@ class StableBrowser {
2556
2778
  allowDisabled: true,
2557
2779
  info: {},
2558
2780
  };
2781
+ state.options ??= { timeout: timeoutMs };
2559
2782
  // Initialize startTime outside try block to ensure it's always accessible
2560
2783
  const startTime = Date.now();
2561
2784
  let conditionMet = false;
@@ -3131,7 +3354,16 @@ class StableBrowser {
3131
3354
  text = text.replace(/\\"/g, '"');
3132
3355
  }
3133
3356
  const timeout = this._getFindElementTimeout(options);
3134
- await new Promise((resolve) => setTimeout(resolve, 2000));
3357
+ //if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
3358
+ let stepFastMode = this.stepTags.includes("fast-mode");
3359
+ if (!stepFastMode) {
3360
+ if (!this.fastMode) {
3361
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3362
+ }
3363
+ else {
3364
+ await new Promise((resolve) => setTimeout(resolve, 500));
3365
+ }
3366
+ }
3135
3367
  const newValue = await this._replaceWithLocalData(text, world);
3136
3368
  if (newValue !== text) {
3137
3369
  this.logger.info(text + "=" + newValue);
@@ -3139,6 +3371,11 @@ class StableBrowser {
3139
3371
  }
3140
3372
  let dateAlternatives = findDateAlternatives(text);
3141
3373
  let numberAlternatives = findNumberAlternatives(text);
3374
+ if (stepFastMode) {
3375
+ state.onlyFailuresScreenshot = true;
3376
+ state.scroll = false;
3377
+ state.highlight = false;
3378
+ }
3142
3379
  try {
3143
3380
  await _preCommand(state, this);
3144
3381
  state.info.text = text;
@@ -3258,6 +3495,8 @@ class StableBrowser {
3258
3495
  operation: "verify_text_with_relation",
3259
3496
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3260
3497
  };
3498
+ const cmdStartTime = Date.now();
3499
+ let cmdEndTime = null;
3261
3500
  const timeout = this._getFindElementTimeout(options);
3262
3501
  await new Promise((resolve) => setTimeout(resolve, 2000));
3263
3502
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3293,6 +3532,17 @@ class StableBrowser {
3293
3532
  await new Promise((resolve) => setTimeout(resolve, 1000));
3294
3533
  continue;
3295
3534
  }
3535
+ else {
3536
+ cmdEndTime = Date.now();
3537
+ if (cmdEndTime - cmdStartTime > 55000) {
3538
+ if (foundAncore) {
3539
+ throw new Error(`Text ${textToVerify} not found in page`);
3540
+ }
3541
+ else {
3542
+ throw new Error(`Text ${textAnchor} not found in page`);
3543
+ }
3544
+ }
3545
+ }
3296
3546
  try {
3297
3547
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3298
3548
  foundAncore = true;
@@ -3431,7 +3681,7 @@ class StableBrowser {
3431
3681
  Object.assign(e, { info: info });
3432
3682
  error = e;
3433
3683
  // throw e;
3434
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3684
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3435
3685
  }
3436
3686
  finally {
3437
3687
  const endTime = Date.now();
@@ -3780,6 +4030,22 @@ class StableBrowser {
3780
4030
  }
3781
4031
  }
3782
4032
  async waitForPageLoad(options = {}, world = null) {
4033
+ // try {
4034
+ // let currentPagePath = null;
4035
+ // currentPagePath = new URL(this.page.url()).pathname;
4036
+ // if (this.latestPagePath) {
4037
+ // // get the currect page path and compare with the latest page path
4038
+ // if (this.latestPagePath === currentPagePath) {
4039
+ // // if the page path is the same, do not wait for page load
4040
+ // console.log("No page change: " + currentPagePath);
4041
+ // return;
4042
+ // }
4043
+ // }
4044
+ // this.latestPagePath = currentPagePath;
4045
+ // } catch (e) {
4046
+ // console.debug("Error getting current page path: ", e);
4047
+ // }
4048
+ //console.log("Waiting for page load");
3783
4049
  let timeout = this._getLoadTimeout(options);
3784
4050
  const promiseArray = [];
3785
4051
  // let waitForNetworkIdle = true;
@@ -3814,7 +4080,10 @@ class StableBrowser {
3814
4080
  }
3815
4081
  }
3816
4082
  finally {
3817
- await new Promise((resolve) => setTimeout(resolve, 2000));
4083
+ await new Promise((resolve) => setTimeout(resolve, 500));
4084
+ if (options && !options.noSleep) {
4085
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4086
+ }
3818
4087
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3819
4088
  const endTime = Date.now();
3820
4089
  _reportToWorld(world, {
@@ -3868,7 +4137,7 @@ class StableBrowser {
3868
4137
  }
3869
4138
  operation = options.operation;
3870
4139
  // validate operation is one of the supported operations
3871
- if (operation != "click" && operation != "hover+click") {
4140
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3872
4141
  throw new Error("operation is not supported");
3873
4142
  }
3874
4143
  const state = {
@@ -3937,6 +4206,17 @@ class StableBrowser {
3937
4206
  state.element = results[0];
3938
4207
  await performAction("hover+click", state.element, options, this, state, _params);
3939
4208
  break;
4209
+ case "hover":
4210
+ if (!options.css) {
4211
+ throw new Error("css is not defined");
4212
+ }
4213
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4214
+ if (result1.length === 0) {
4215
+ throw new Error(`Element not found in cell area`);
4216
+ }
4217
+ state.element = result1[0];
4218
+ await performAction("hover", state.element, options, this, state, _params);
4219
+ break;
3940
4220
  default:
3941
4221
  throw new Error("operation is not supported");
3942
4222
  }
@@ -4052,6 +4332,7 @@ class StableBrowser {
4052
4332
  if (world && world.attach) {
4053
4333
  world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4054
4334
  }
4335
+ this.context.loadedRoutes = null;
4055
4336
  this.beforeScenarioCalled = true;
4056
4337
  if (scenario && scenario.pickle && scenario.pickle.name) {
4057
4338
  this.scenarioName = scenario.pickle.name;
@@ -4081,8 +4362,10 @@ class StableBrowser {
4081
4362
  }
4082
4363
  async afterScenario(world, scenario) { }
4083
4364
  async beforeStep(world, step) {
4365
+ this.stepTags = [];
4084
4366
  if (!this.beforeScenarioCalled) {
4085
4367
  this.beforeScenario(world, step);
4368
+ this.context.loadedRoutes = null;
4086
4369
  }
4087
4370
  if (this.stepIndex === undefined) {
4088
4371
  this.stepIndex = 0;
@@ -4092,7 +4375,12 @@ class StableBrowser {
4092
4375
  }
4093
4376
  if (step && step.pickleStep && step.pickleStep.text) {
4094
4377
  this.stepName = step.pickleStep.text;
4095
- this.logger.info("step: " + this.stepName);
4378
+ let printableStepName = this.stepName;
4379
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4380
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4381
+ return `\x1b[33m"${p1}"\x1b[0m`;
4382
+ });
4383
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4096
4384
  }
4097
4385
  else if (step && step.text) {
4098
4386
  this.stepName = step.text;
@@ -4107,7 +4395,10 @@ class StableBrowser {
4107
4395
  }
4108
4396
  if (this.initSnapshotTaken === false) {
4109
4397
  this.initSnapshotTaken = true;
4110
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4398
+ if (world &&
4399
+ world.attach &&
4400
+ !process.env.DISABLE_SNAPSHOT &&
4401
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4111
4402
  const snapshot = await this.getAriaSnapshot();
4112
4403
  if (snapshot) {
4113
4404
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -4115,7 +4406,12 @@ class StableBrowser {
4115
4406
  }
4116
4407
  }
4117
4408
  this.context.routeResults = null;
4118
- await registerBeforeStepRoutes(this.context, this.stepName);
4409
+ this.context.loadedRoutes = null;
4410
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4411
+ networkBeforeStep(this.stepName, this.context);
4412
+ }
4413
+ setStepTags(tags) {
4414
+ this.stepTags = tags;
4119
4415
  }
4120
4416
  async getAriaSnapshot() {
4121
4417
  try {
@@ -4218,7 +4514,11 @@ class StableBrowser {
4218
4514
  if (this.context) {
4219
4515
  this.context.examplesRow = null;
4220
4516
  }
4221
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4517
+ if (world &&
4518
+ world.attach &&
4519
+ !process.env.DISABLE_SNAPSHOT &&
4520
+ !this.fastMode &&
4521
+ !this.stepTags.includes("fast-mode")) {
4222
4522
  const snapshot = await this.getAriaSnapshot();
4223
4523
  if (snapshot) {
4224
4524
  const obj = {};
@@ -4226,6 +4526,11 @@ class StableBrowser {
4226
4526
  }
4227
4527
  }
4228
4528
  this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4529
+ if (this.context.routeResults) {
4530
+ if (world && world.attach) {
4531
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4532
+ }
4533
+ }
4229
4534
  if (!process.env.TEMP_RUN) {
4230
4535
  const state = {
4231
4536
  world,
@@ -4249,6 +4554,13 @@ class StableBrowser {
4249
4554
  await _commandFinally(state, this);
4250
4555
  }
4251
4556
  }
4557
+ networkAfterStep(this.stepName, this.context);
4558
+ if (process.env.TEMP_RUN === "true") {
4559
+ // Put a sleep for some time to allow the browser to finish processing
4560
+ if (!this.stepTags.includes("fast-mode")) {
4561
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4562
+ }
4563
+ }
4252
4564
  }
4253
4565
  }
4254
4566
  function createTimedPromise(promise, label) {