automation_model 1.0.761-dev → 1.0.761-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]))) {
@@ -530,14 +542,13 @@ class StableBrowser {
530
542
  info.locatorLog = new LocatorLog(selectorHierarchy);
531
543
  }
532
544
  let locatorSearch = selectorHierarchy[index];
533
- let originalLocatorSearch = "";
534
545
  try {
535
- originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
536
- locatorSearch = JSON.parse(originalLocatorSearch);
546
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
537
547
  }
538
548
  catch (e) {
539
549
  console.error(e);
540
550
  }
551
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
541
552
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
542
553
  let locator = null;
543
554
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
@@ -679,40 +690,186 @@ class StableBrowser {
679
690
  }
680
691
  return { rerun: false };
681
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
+ }
682
757
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
683
758
  if (!timeout) {
684
759
  timeout = 30000;
685
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
+ }
686
768
  for (let i = 0; i < 3; i++) {
687
769
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
688
770
  for (let j = 0; j < selectors.locators.length; j++) {
689
771
  let selector = selectors.locators[j];
690
772
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
691
773
  }
692
- 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
+ }
693
817
  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
- // }
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
+ }
701
859
  const scope = element._frame ?? element.page();
702
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
703
860
  let prefixSelector = "";
704
861
  const frameControlSelector = " >> internal:control=enter-frame";
705
862
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
706
863
  if (frameSelectorIndex !== -1) {
707
864
  // remove everything after the >> internal:control=enter-frame
708
865
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
709
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
866
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
710
867
  }
711
868
  // if (element?._frame?._selector) {
712
869
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
713
870
  // }
714
871
  const newSelector = prefixSelector + newElementSelector;
715
- return scope.locator(newSelector);
872
+ return scope.locator(newSelector).first();
716
873
  }
717
874
  }
718
875
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -733,7 +890,7 @@ class StableBrowser {
733
890
  for (let i = 0; i < frame.selectors.length; i++) {
734
891
  let frameLocator = frame.selectors[i];
735
892
  if (frameLocator.css) {
736
- let testframescope = framescope.frameLocator(frameLocator.css);
893
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
737
894
  if (frameLocator.index) {
738
895
  testframescope = framescope.nth(frameLocator.index);
739
896
  }
@@ -745,7 +902,7 @@ class StableBrowser {
745
902
  break;
746
903
  }
747
904
  catch (error) {
748
- console.error("frame not found " + frameLocator.css);
905
+ // console.error("frame not found " + frameLocator.css);
749
906
  }
750
907
  }
751
908
  }
@@ -811,6 +968,14 @@ class StableBrowser {
811
968
  });
812
969
  }
813
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
+ if (locator.css && !locator.css.endsWith(">> visible=true")) {
975
+ locator.css = locator.css + " >> visible=true";
976
+ }
977
+ });
978
+ }
814
979
  if (!info) {
815
980
  info = {};
816
981
  info.failCause = {};
@@ -823,7 +988,6 @@ class StableBrowser {
823
988
  let locatorsCount = 0;
824
989
  let lazy_scroll = false;
825
990
  //let arrayMode = Array.isArray(selectors);
826
- let scope = await this._findFrameScope(selectors, timeout, info);
827
991
  let selectorsLocators = null;
828
992
  selectorsLocators = selectors.locators;
829
993
  // group selectors by priority
@@ -851,6 +1015,7 @@ class StableBrowser {
851
1015
  let highPriorityOnly = true;
852
1016
  let visibleOnly = true;
853
1017
  while (true) {
1018
+ let scope = await this._findFrameScope(selectors, timeout, info);
854
1019
  locatorsCount = 0;
855
1020
  let result = [];
856
1021
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -966,9 +1131,13 @@ class StableBrowser {
966
1131
  }
967
1132
  }
968
1133
  if (foundLocators.length === 1) {
1134
+ let box = null;
1135
+ if (!this.onlyFailuresScreenshot) {
1136
+ box = await foundLocators[0].boundingBox();
1137
+ }
969
1138
  result.foundElements.push({
970
1139
  locator: foundLocators[0],
971
- box: await foundLocators[0].boundingBox(),
1140
+ box: box,
972
1141
  unique: true,
973
1142
  });
974
1143
  result.locatorIndex = i;
@@ -1123,11 +1292,22 @@ class StableBrowser {
1123
1292
  operation: "click",
1124
1293
  log: "***** click on " + selectors.element_name + " *****\n",
1125
1294
  };
1295
+ check_performance("click_all ***", this.context, true);
1296
+ let stepFastMode = this.stepTags.includes("fast-mode");
1297
+ if (stepFastMode) {
1298
+ state.onlyFailuresScreenshot = true;
1299
+ state.scroll = false;
1300
+ state.highlight = false;
1301
+ }
1126
1302
  try {
1303
+ check_performance("click_preCommand", this.context, true);
1127
1304
  await _preCommand(state, this);
1305
+ check_performance("click_preCommand", this.context, false);
1128
1306
  await performAction("click", state.element, options, this, state, _params);
1129
- if (!this.fastMode) {
1130
- await this.waitForPageLoad();
1307
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1308
+ check_performance("click_waitForPageLoad", this.context, true);
1309
+ await this.waitForPageLoad({ noSleep: true });
1310
+ check_performance("click_waitForPageLoad", this.context, false);
1131
1311
  }
1132
1312
  return state.info;
1133
1313
  }
@@ -1135,7 +1315,13 @@ class StableBrowser {
1135
1315
  await _commandError(state, e, this);
1136
1316
  }
1137
1317
  finally {
1318
+ check_performance("click_commandFinally", this.context, true);
1138
1319
  await _commandFinally(state, this);
1320
+ check_performance("click_commandFinally", this.context, false);
1321
+ check_performance("click_all ***", this.context, false);
1322
+ if (this.context.profile) {
1323
+ console.log(JSON.stringify(this.context.profile, null, 2));
1324
+ }
1139
1325
  }
1140
1326
  }
1141
1327
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1227,7 +1413,7 @@ class StableBrowser {
1227
1413
  }
1228
1414
  }
1229
1415
  }
1230
- await this.waitForPageLoad();
1416
+ //await this.waitForPageLoad();
1231
1417
  return state.info;
1232
1418
  }
1233
1419
  catch (e) {
@@ -1253,7 +1439,7 @@ class StableBrowser {
1253
1439
  await _preCommand(state, this);
1254
1440
  await performAction("hover", state.element, options, this, state, _params);
1255
1441
  await _screenshot(state, this);
1256
- await this.waitForPageLoad();
1442
+ //await this.waitForPageLoad();
1257
1443
  return state.info;
1258
1444
  }
1259
1445
  catch (e) {
@@ -1289,7 +1475,7 @@ class StableBrowser {
1289
1475
  state.info.log += "selectOption failed, will try force" + "\n";
1290
1476
  await state.element.selectOption(values, { timeout: 10000, force: true });
1291
1477
  }
1292
- await this.waitForPageLoad();
1478
+ //await this.waitForPageLoad();
1293
1479
  return state.info;
1294
1480
  }
1295
1481
  catch (e) {
@@ -1475,6 +1661,14 @@ class StableBrowser {
1475
1661
  }
1476
1662
  try {
1477
1663
  await _preCommand(state, this);
1664
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1665
+ // tag the element
1666
+ let newElementSelector = await state.element.evaluate((el, token) => {
1667
+ // use attribute and not id
1668
+ const attrName = `data-blinq-id-${token}`;
1669
+ el.setAttribute(attrName, "");
1670
+ return `[${attrName}]`;
1671
+ }, randomToken);
1478
1672
  state.info.value = _value;
1479
1673
  if (!options.press) {
1480
1674
  try {
@@ -1500,6 +1694,25 @@ class StableBrowser {
1500
1694
  }
1501
1695
  }
1502
1696
  await new Promise((resolve) => setTimeout(resolve, 500));
1697
+ // check if the element exist after the click (no wait)
1698
+ const count = await state.element.count({ timeout: 0 });
1699
+ if (count === 0) {
1700
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1701
+ const scope = state.element._frame ?? element.page();
1702
+ let prefixSelector = "";
1703
+ const frameControlSelector = " >> internal:control=enter-frame";
1704
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1705
+ if (frameSelectorIndex !== -1) {
1706
+ // remove everything after the >> internal:control=enter-frame
1707
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1708
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1709
+ }
1710
+ // if (element?._frame?._selector) {
1711
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1712
+ // }
1713
+ const newSelector = prefixSelector + newElementSelector;
1714
+ state.element = scope.locator(newSelector).first();
1715
+ }
1503
1716
  const valueSegment = state.value.split("&&");
1504
1717
  for (let i = 0; i < valueSegment.length; i++) {
1505
1718
  if (i > 0) {
@@ -1571,8 +1784,8 @@ class StableBrowser {
1571
1784
  if (enter) {
1572
1785
  await new Promise((resolve) => setTimeout(resolve, 2000));
1573
1786
  await this.page.keyboard.press("Enter");
1787
+ await this.waitForPageLoad();
1574
1788
  }
1575
- await this.waitForPageLoad();
1576
1789
  return state.info;
1577
1790
  }
1578
1791
  catch (e) {
@@ -2438,7 +2651,7 @@ class StableBrowser {
2438
2651
  let expectedValue;
2439
2652
  try {
2440
2653
  await _preCommand(state, this);
2441
- expectedValue = await replaceWithLocalTestData(state.value, world);
2654
+ expectedValue = await this._replaceWithLocalData(value, world);
2442
2655
  state.info.expectedValue = expectedValue;
2443
2656
  switch (property) {
2444
2657
  case "innerText":
@@ -2486,47 +2699,54 @@ class StableBrowser {
2486
2699
  }
2487
2700
  state.info.value = val;
2488
2701
  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
- }
2702
+ state.info.value = val;
2703
+ const isRegex = expectedValue.startsWith("regex:");
2704
+ const isContains = expectedValue.startsWith("contains:");
2705
+ const isExact = expectedValue.startsWith("exact:");
2706
+ let matchPassed = false;
2707
+ if (isRegex) {
2708
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2709
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2710
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2711
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2712
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2713
+ const regex = new RegExp(patternBody, flags);
2714
+ state.info.regex = true;
2715
+ matchPassed = regex.test(val);
2507
2716
  }
2508
2717
  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
- }
2718
+ // Fallback: treat as literal
2719
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2720
+ const regex = new RegExp(escapedPattern, "g");
2721
+ matchPassed = regex.test(val);
2521
2722
  }
2522
2723
  }
2724
+ else if (isContains) {
2725
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2726
+ matchPassed = val.includes(containsValue);
2727
+ }
2728
+ else if (isExact) {
2729
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2730
+ matchPassed = val === exactValue;
2731
+ }
2732
+ else if (property === "innerText") {
2733
+ // Default innerText logic
2734
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2735
+ const valLines = val.split("\n");
2736
+ const expectedLines = normalizedExpectedValue.split("\n");
2737
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2738
+ }
2523
2739
  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
- }
2740
+ // Fallback exact or loose match
2741
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2742
+ const regex = new RegExp(escapedPattern, "g");
2743
+ matchPassed = regex.test(val);
2744
+ }
2745
+ if (!matchPassed) {
2746
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2747
+ state.info.failCause.assertionFailed = true;
2748
+ state.info.failCause.lastError = errorMessage;
2749
+ throw new Error(errorMessage);
2530
2750
  }
2531
2751
  return state.info;
2532
2752
  }
@@ -2555,82 +2775,114 @@ class StableBrowser {
2555
2775
  operation: "conditionalWait",
2556
2776
  log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2557
2777
  allowDisabled: true,
2558
- info: {}
2778
+ info: {},
2559
2779
  };
2560
- try {
2561
- await _preCommand(state, this);
2562
- const startTime = Date.now();
2563
- let conditionMet = false;
2564
- let currentValue = null;
2565
- const checkCondition = async () => {
2566
- try {
2567
- switch (condition.toLowerCase()) {
2568
- case "checked":
2569
- currentValue = await state.element.isChecked();
2570
- return currentValue === true;
2571
- case "unchecked":
2572
- currentValue = await state.element.isChecked();
2573
- return currentValue === false;
2574
- case "visible":
2575
- currentValue = await state.element.isVisible();
2576
- return currentValue === true;
2577
- case "hidden":
2578
- currentValue = await state.element.isVisible();
2579
- return currentValue === false;
2580
- case "enabled":
2581
- currentValue = await state.element.isDisabled();
2582
- return currentValue === false;
2583
- case "disabled":
2584
- currentValue = await state.element.isDisabled();
2585
- return currentValue === true;
2586
- case "editable":
2587
- currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2588
- return currentValue === true;
2589
- default:
2590
- state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2591
- state.info.success = false;
2592
- return false;
2780
+ state.options ??= { timeout: timeoutMs };
2781
+ // Initialize startTime outside try block to ensure it's always accessible
2782
+ const startTime = Date.now();
2783
+ let conditionMet = false;
2784
+ let currentValue = null;
2785
+ let lastError = null;
2786
+ // Main retry loop - continues until timeout or condition is met
2787
+ while (Date.now() - startTime < timeoutMs) {
2788
+ const elapsedTime = Date.now() - startTime;
2789
+ const remainingTime = timeoutMs - elapsedTime;
2790
+ try {
2791
+ // Try to execute _preCommand (element location)
2792
+ await _preCommand(state, this);
2793
+ // If _preCommand succeeds, start condition checking
2794
+ const checkCondition = async () => {
2795
+ try {
2796
+ switch (condition.toLowerCase()) {
2797
+ case "checked":
2798
+ currentValue = await state.element.isChecked();
2799
+ return currentValue === true;
2800
+ case "unchecked":
2801
+ currentValue = await state.element.isChecked();
2802
+ return currentValue === false;
2803
+ case "visible":
2804
+ currentValue = await state.element.isVisible();
2805
+ return currentValue === true;
2806
+ case "hidden":
2807
+ currentValue = await state.element.isVisible();
2808
+ return currentValue === false;
2809
+ case "enabled":
2810
+ currentValue = await state.element.isDisabled();
2811
+ return currentValue === false;
2812
+ case "disabled":
2813
+ currentValue = await state.element.isDisabled();
2814
+ return currentValue === true;
2815
+ case "editable":
2816
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2817
+ currentValue = await state.element.isContentEditable();
2818
+ return currentValue === true;
2819
+ default:
2820
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2821
+ state.info.success = false;
2822
+ return false;
2823
+ }
2824
+ }
2825
+ catch (error) {
2826
+ // Don't throw here, just return false to continue retrying
2827
+ return false;
2828
+ }
2829
+ };
2830
+ // Inner loop for condition checking (once element is located)
2831
+ while (Date.now() - startTime < timeoutMs) {
2832
+ const currentElapsedTime = Date.now() - startTime;
2833
+ conditionMet = await checkCondition();
2834
+ if (conditionMet) {
2835
+ break;
2836
+ }
2837
+ // Check if we still have time for another attempt
2838
+ if (Date.now() - startTime + 50 < timeoutMs) {
2839
+ await new Promise((res) => setTimeout(res, 50));
2840
+ }
2841
+ else {
2842
+ break;
2593
2843
  }
2594
2844
  }
2595
- catch {
2596
- return false;
2845
+ // If we got here and condition is met, break out of main loop
2846
+ if (conditionMet) {
2847
+ break;
2597
2848
  }
2598
- };
2599
- while (Date.now() - startTime < timeoutMs) { // Use milliseconds for comparison
2600
- conditionMet = await checkCondition();
2601
- if (conditionMet)
2849
+ // If condition not met but no exception, we've timed out
2850
+ break;
2851
+ }
2852
+ catch (e) {
2853
+ lastError = e;
2854
+ const currentElapsedTime = Date.now() - startTime;
2855
+ const timeLeft = timeoutMs - currentElapsedTime;
2856
+ // Check if we have enough time left to retry
2857
+ if (timeLeft > 100) {
2858
+ await new Promise((resolve) => setTimeout(resolve, 50));
2859
+ }
2860
+ else {
2602
2861
  break;
2603
- await new Promise(res => setTimeout(res, 50));
2604
- }
2605
- const actualWaitTime = Date.now() - startTime;
2606
- state.info = {
2607
- success: conditionMet,
2608
- conditionMet,
2609
- actualWaitTime,
2610
- currentValue,
2611
- message: conditionMet
2612
- ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2613
- : `Condition '${condition}' not met within ${timeout}s timeout`, // Use original seconds value
2614
- };
2615
- state.log += state.info.message + "\n";
2616
- return state.info;
2862
+ }
2863
+ }
2617
2864
  }
2618
- catch (e) {
2619
- state.info = {
2620
- success: false,
2621
- conditionMet: false,
2622
- actualWaitTime: timeoutMs, // Store as milliseconds
2623
- currentValue: null,
2624
- error: e.message,
2625
- message: `Error during conditional wait: ${e.message}`,
2626
- };
2627
- state.log += `Error during conditional wait: ${e.message}\n`;
2628
- await new Promise(resolve => setTimeout(resolve, timeoutMs)); // Use milliseconds
2629
- return state.info;
2865
+ const actualWaitTime = Date.now() - startTime;
2866
+ state.info = {
2867
+ success: conditionMet,
2868
+ conditionMet,
2869
+ actualWaitTime,
2870
+ currentValue,
2871
+ lastError: lastError?.message || null,
2872
+ message: conditionMet
2873
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2874
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2875
+ };
2876
+ if (lastError) {
2877
+ state.log += `Last error: ${lastError.message}\n`;
2630
2878
  }
2631
- finally {
2879
+ try {
2632
2880
  await _commandFinally(state, this);
2633
2881
  }
2882
+ catch (finallyError) {
2883
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2884
+ }
2885
+ return state.info;
2634
2886
  }
2635
2887
  async extractEmailData(emailAddress, options, world) {
2636
2888
  if (!emailAddress) {
@@ -3228,6 +3480,8 @@ class StableBrowser {
3228
3480
  operation: "verify_text_with_relation",
3229
3481
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3230
3482
  };
3483
+ const cmdStartTime = Date.now();
3484
+ let cmdEndTime = null;
3231
3485
  const timeout = this._getFindElementTimeout(options);
3232
3486
  await new Promise((resolve) => setTimeout(resolve, 2000));
3233
3487
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3263,6 +3517,17 @@ class StableBrowser {
3263
3517
  await new Promise((resolve) => setTimeout(resolve, 1000));
3264
3518
  continue;
3265
3519
  }
3520
+ else {
3521
+ cmdEndTime = Date.now();
3522
+ if (cmdEndTime - cmdStartTime > 55000) {
3523
+ if (foundAncore) {
3524
+ throw new Error(`Text ${textToVerify} not found in page`);
3525
+ }
3526
+ else {
3527
+ throw new Error(`Text ${textAnchor} not found in page`);
3528
+ }
3529
+ }
3530
+ }
3266
3531
  try {
3267
3532
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3268
3533
  foundAncore = true;
@@ -3401,7 +3666,7 @@ class StableBrowser {
3401
3666
  Object.assign(e, { info: info });
3402
3667
  error = e;
3403
3668
  // throw e;
3404
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3669
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3405
3670
  }
3406
3671
  finally {
3407
3672
  const endTime = Date.now();
@@ -3750,6 +4015,22 @@ class StableBrowser {
3750
4015
  }
3751
4016
  }
3752
4017
  async waitForPageLoad(options = {}, world = null) {
4018
+ // try {
4019
+ // let currentPagePath = null;
4020
+ // currentPagePath = new URL(this.page.url()).pathname;
4021
+ // if (this.latestPagePath) {
4022
+ // // get the currect page path and compare with the latest page path
4023
+ // if (this.latestPagePath === currentPagePath) {
4024
+ // // if the page path is the same, do not wait for page load
4025
+ // console.log("No page change: " + currentPagePath);
4026
+ // return;
4027
+ // }
4028
+ // }
4029
+ // this.latestPagePath = currentPagePath;
4030
+ // } catch (e) {
4031
+ // console.debug("Error getting current page path: ", e);
4032
+ // }
4033
+ //console.log("Waiting for page load");
3753
4034
  let timeout = this._getLoadTimeout(options);
3754
4035
  const promiseArray = [];
3755
4036
  // let waitForNetworkIdle = true;
@@ -3784,7 +4065,10 @@ class StableBrowser {
3784
4065
  }
3785
4066
  }
3786
4067
  finally {
3787
- await new Promise((resolve) => setTimeout(resolve, 2000));
4068
+ await new Promise((resolve) => setTimeout(resolve, 500));
4069
+ if (options && !options.noSleep) {
4070
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4071
+ }
3788
4072
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3789
4073
  const endTime = Date.now();
3790
4074
  _reportToWorld(world, {
@@ -3838,7 +4122,7 @@ class StableBrowser {
3838
4122
  }
3839
4123
  operation = options.operation;
3840
4124
  // validate operation is one of the supported operations
3841
- if (operation != "click" && operation != "hover+click") {
4125
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3842
4126
  throw new Error("operation is not supported");
3843
4127
  }
3844
4128
  const state = {
@@ -3907,6 +4191,17 @@ class StableBrowser {
3907
4191
  state.element = results[0];
3908
4192
  await performAction("hover+click", state.element, options, this, state, _params);
3909
4193
  break;
4194
+ case "hover":
4195
+ if (!options.css) {
4196
+ throw new Error("css is not defined");
4197
+ }
4198
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4199
+ if (result1.length === 0) {
4200
+ throw new Error(`Element not found in cell area`);
4201
+ }
4202
+ state.element = result1[0];
4203
+ await performAction("hover", state.element, options, this, state, _params);
4204
+ break;
3910
4205
  default:
3911
4206
  throw new Error("operation is not supported");
3912
4207
  }
@@ -4019,6 +4314,10 @@ class StableBrowser {
4019
4314
  }
4020
4315
  }
4021
4316
  async beforeScenario(world, scenario) {
4317
+ if (world && world.attach) {
4318
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4319
+ }
4320
+ this.context.loadedRoutes = null;
4022
4321
  this.beforeScenarioCalled = true;
4023
4322
  if (scenario && scenario.pickle && scenario.pickle.name) {
4024
4323
  this.scenarioName = scenario.pickle.name;
@@ -4048,8 +4347,10 @@ class StableBrowser {
4048
4347
  }
4049
4348
  async afterScenario(world, scenario) { }
4050
4349
  async beforeStep(world, step) {
4350
+ this.stepTags = [];
4051
4351
  if (!this.beforeScenarioCalled) {
4052
4352
  this.beforeScenario(world, step);
4353
+ this.context.loadedRoutes = null;
4053
4354
  }
4054
4355
  if (this.stepIndex === undefined) {
4055
4356
  this.stepIndex = 0;
@@ -4059,7 +4360,12 @@ class StableBrowser {
4059
4360
  }
4060
4361
  if (step && step.pickleStep && step.pickleStep.text) {
4061
4362
  this.stepName = step.pickleStep.text;
4062
- this.logger.info("step: " + this.stepName);
4363
+ let printableStepName = this.stepName;
4364
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4365
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4366
+ return `\x1b[33m"${p1}"\x1b[0m`;
4367
+ });
4368
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4063
4369
  }
4064
4370
  else if (step && step.text) {
4065
4371
  this.stepName = step.text;
@@ -4074,7 +4380,10 @@ class StableBrowser {
4074
4380
  }
4075
4381
  if (this.initSnapshotTaken === false) {
4076
4382
  this.initSnapshotTaken = true;
4077
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4383
+ if (world &&
4384
+ world.attach &&
4385
+ !process.env.DISABLE_SNAPSHOT &&
4386
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4078
4387
  const snapshot = await this.getAriaSnapshot();
4079
4388
  if (snapshot) {
4080
4389
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -4082,7 +4391,12 @@ class StableBrowser {
4082
4391
  }
4083
4392
  }
4084
4393
  this.context.routeResults = null;
4085
- await registerBeforeStepRoutes(this.context, this.stepName);
4394
+ this.context.loadedRoutes = null;
4395
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4396
+ networkBeforeStep(this.stepName, this.context);
4397
+ }
4398
+ setStepTags(tags) {
4399
+ this.stepTags = tags;
4086
4400
  }
4087
4401
  async getAriaSnapshot() {
4088
4402
  try {
@@ -4102,12 +4416,18 @@ class StableBrowser {
4102
4416
  try {
4103
4417
  // Ensure frame is attached and has body
4104
4418
  const body = frame.locator("body");
4105
- await body.waitFor({ timeout: 200 }); // wait explicitly
4419
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4106
4420
  const snapshot = await body.ariaSnapshot({ timeout });
4421
+ if (!snapshot) {
4422
+ continue;
4423
+ }
4107
4424
  content.push(`- frame: ${i}`);
4108
4425
  content.push(snapshot);
4109
4426
  }
4110
- catch (innerErr) { }
4427
+ catch (innerErr) {
4428
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4429
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4430
+ }
4111
4431
  }
4112
4432
  return content.join("\n");
4113
4433
  }
@@ -4179,7 +4499,11 @@ class StableBrowser {
4179
4499
  if (this.context) {
4180
4500
  this.context.examplesRow = null;
4181
4501
  }
4182
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4502
+ if (world &&
4503
+ world.attach &&
4504
+ !process.env.DISABLE_SNAPSHOT &&
4505
+ !this.fastMode &&
4506
+ !this.stepTags.includes("fast-mode")) {
4183
4507
  const snapshot = await this.getAriaSnapshot();
4184
4508
  if (snapshot) {
4185
4509
  const obj = {};
@@ -4187,6 +4511,11 @@ class StableBrowser {
4187
4511
  }
4188
4512
  }
4189
4513
  this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4514
+ if (this.context.routeResults) {
4515
+ if (world && world.attach) {
4516
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4517
+ }
4518
+ }
4190
4519
  if (!process.env.TEMP_RUN) {
4191
4520
  const state = {
4192
4521
  world,
@@ -4210,6 +4539,13 @@ class StableBrowser {
4210
4539
  await _commandFinally(state, this);
4211
4540
  }
4212
4541
  }
4542
+ networkAfterStep(this.stepName, this.context);
4543
+ if (process.env.TEMP_RUN === "true") {
4544
+ // Put a sleep for some time to allow the browser to finish processing
4545
+ if (!this.stepTags.includes("fast-mode")) {
4546
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4547
+ }
4548
+ }
4213
4549
  }
4214
4550
  }
4215
4551
  function createTimedPromise(promise, label) {