automation_model 1.0.753-dev → 1.0.753-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,19 +21,20 @@ 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",
32
35
  NAVIGATE: "navigate",
33
36
  GO_BACK: "go_back",
34
- GO_FORWARD: " go_forward",
37
+ GO_FORWARD: "go_forward",
35
38
  FILL: "fill_element",
36
39
  EXECUTE: "execute_page_method", //
37
40
  OPEN: "open_environment", //
@@ -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,15 +66,15 @@ 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",
77
+ CONDITIONAL_WAIT: "conditional_wait",
74
78
  };
75
79
  export const apps = {};
76
80
  const formatElementName = (elementName) => {
@@ -83,6 +87,7 @@ class StableBrowser {
83
87
  context;
84
88
  world;
85
89
  fastMode;
90
+ stepTags;
86
91
  project_path = null;
87
92
  webLogFile = null;
88
93
  networkLogger = null;
@@ -91,13 +96,15 @@ class StableBrowser {
91
96
  tags = null;
92
97
  isRecording = false;
93
98
  initSnapshotTaken = false;
94
- 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 = []) {
95
101
  this.browser = browser;
96
102
  this.page = page;
97
103
  this.logger = logger;
98
104
  this.context = context;
99
105
  this.world = world;
100
106
  this.fastMode = fastMode;
107
+ this.stepTags = stepTags;
101
108
  if (!this.logger) {
102
109
  this.logger = console;
103
110
  }
@@ -130,6 +137,7 @@ class StableBrowser {
130
137
  this.fastMode = true;
131
138
  }
132
139
  if (process.env.FAST_MODE === "true") {
140
+ // console.log("Fast mode enabled from environment variable");
133
141
  this.fastMode = true;
134
142
  }
135
143
  if (process.env.FAST_MODE === "false") {
@@ -172,6 +180,7 @@ class StableBrowser {
172
180
  registerNetworkEvents(this.world, this, context, this.page);
173
181
  registerDownloadEvent(this.page, this.world, context);
174
182
  page.on("close", async () => {
183
+ // return if browser context is already closed
175
184
  if (this.context && this.context.pages && this.context.pages.length > 1) {
176
185
  this.context.pages.pop();
177
186
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -181,7 +190,12 @@ class StableBrowser {
181
190
  console.log("Switched to page " + title);
182
191
  }
183
192
  catch (error) {
184
- 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
+ }
185
199
  }
186
200
  }
187
201
  });
@@ -190,7 +204,12 @@ class StableBrowser {
190
204
  console.log("Switch page: " + (await page.title()));
191
205
  }
192
206
  catch (e) {
193
- 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
+ }
194
213
  }
195
214
  context.pageLoading.status = false;
196
215
  }.bind(this));
@@ -218,7 +237,7 @@ class StableBrowser {
218
237
  if (newContextCreated) {
219
238
  this.registerEventListeners(this.context);
220
239
  await this.goto(this.context.environment.baseUrl);
221
- if (!this.fastMode) {
240
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
222
241
  await this.waitForPageLoad();
223
242
  }
224
243
  }
@@ -502,12 +521,6 @@ class StableBrowser {
502
521
  if (!el.setAttribute) {
503
522
  el = el.parentElement;
504
523
  }
505
- // remove any attributes start with data-blinq-id
506
- // for (let i = 0; i < el.attributes.length; i++) {
507
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
508
- // el.removeAttribute(el.attributes[i].name);
509
- // }
510
- // }
511
524
  el.setAttribute("data-blinq-id-" + randomToken, "");
512
525
  return true;
513
526
  }, [tag1, randomToken]))) {
@@ -529,14 +542,13 @@ class StableBrowser {
529
542
  info.locatorLog = new LocatorLog(selectorHierarchy);
530
543
  }
531
544
  let locatorSearch = selectorHierarchy[index];
532
- let originalLocatorSearch = "";
533
545
  try {
534
- originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
535
- locatorSearch = JSON.parse(originalLocatorSearch);
546
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
536
547
  }
537
548
  catch (e) {
538
549
  console.error(e);
539
550
  }
551
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
540
552
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
541
553
  let locator = null;
542
554
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
@@ -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,14 @@ 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
+ if (locator.css && !locator.css.endsWith(">> visible=true")) {
975
+ locator.css = locator.css + " >> visible=true";
976
+ }
977
+ });
978
+ }
813
979
  if (!info) {
814
980
  info = {};
815
981
  info.failCause = {};
@@ -822,7 +988,6 @@ class StableBrowser {
822
988
  let locatorsCount = 0;
823
989
  let lazy_scroll = false;
824
990
  //let arrayMode = Array.isArray(selectors);
825
- let scope = await this._findFrameScope(selectors, timeout, info);
826
991
  let selectorsLocators = null;
827
992
  selectorsLocators = selectors.locators;
828
993
  // group selectors by priority
@@ -850,6 +1015,7 @@ class StableBrowser {
850
1015
  let highPriorityOnly = true;
851
1016
  let visibleOnly = true;
852
1017
  while (true) {
1018
+ let scope = await this._findFrameScope(selectors, timeout, info);
853
1019
  locatorsCount = 0;
854
1020
  let result = [];
855
1021
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -965,9 +1131,13 @@ class StableBrowser {
965
1131
  }
966
1132
  }
967
1133
  if (foundLocators.length === 1) {
1134
+ let box = null;
1135
+ if (!this.onlyFailuresScreenshot) {
1136
+ box = await foundLocators[0].boundingBox();
1137
+ }
968
1138
  result.foundElements.push({
969
1139
  locator: foundLocators[0],
970
- box: await foundLocators[0].boundingBox(),
1140
+ box: box,
971
1141
  unique: true,
972
1142
  });
973
1143
  result.locatorIndex = i;
@@ -1122,11 +1292,16 @@ class StableBrowser {
1122
1292
  operation: "click",
1123
1293
  log: "***** click on " + selectors.element_name + " *****\n",
1124
1294
  };
1295
+ check_performance("click_all ***", this.context, true);
1125
1296
  try {
1297
+ check_performance("click_preCommand", this.context, true);
1126
1298
  await _preCommand(state, this);
1299
+ check_performance("click_preCommand", this.context, false);
1127
1300
  await performAction("click", state.element, options, this, state, _params);
1128
- if (!this.fastMode) {
1129
- await this.waitForPageLoad();
1301
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1302
+ check_performance("click_waitForPageLoad", this.context, true);
1303
+ await this.waitForPageLoad({ noSleep: true });
1304
+ check_performance("click_waitForPageLoad", this.context, false);
1130
1305
  }
1131
1306
  return state.info;
1132
1307
  }
@@ -1134,7 +1309,13 @@ class StableBrowser {
1134
1309
  await _commandError(state, e, this);
1135
1310
  }
1136
1311
  finally {
1312
+ check_performance("click_commandFinally", this.context, true);
1137
1313
  await _commandFinally(state, this);
1314
+ check_performance("click_commandFinally", this.context, false);
1315
+ check_performance("click_all ***", this.context, false);
1316
+ if (this.context.profile) {
1317
+ console.log(JSON.stringify(this.context.profile, null, 2));
1318
+ }
1138
1319
  }
1139
1320
  }
1140
1321
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1226,7 +1407,7 @@ class StableBrowser {
1226
1407
  }
1227
1408
  }
1228
1409
  }
1229
- await this.waitForPageLoad();
1410
+ //await this.waitForPageLoad();
1230
1411
  return state.info;
1231
1412
  }
1232
1413
  catch (e) {
@@ -1252,7 +1433,7 @@ class StableBrowser {
1252
1433
  await _preCommand(state, this);
1253
1434
  await performAction("hover", state.element, options, this, state, _params);
1254
1435
  await _screenshot(state, this);
1255
- await this.waitForPageLoad();
1436
+ //await this.waitForPageLoad();
1256
1437
  return state.info;
1257
1438
  }
1258
1439
  catch (e) {
@@ -1288,7 +1469,7 @@ class StableBrowser {
1288
1469
  state.info.log += "selectOption failed, will try force" + "\n";
1289
1470
  await state.element.selectOption(values, { timeout: 10000, force: true });
1290
1471
  }
1291
- await this.waitForPageLoad();
1472
+ //await this.waitForPageLoad();
1292
1473
  return state.info;
1293
1474
  }
1294
1475
  catch (e) {
@@ -1474,6 +1655,14 @@ class StableBrowser {
1474
1655
  }
1475
1656
  try {
1476
1657
  await _preCommand(state, this);
1658
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1659
+ // tag the element
1660
+ let newElementSelector = await state.element.evaluate((el, token) => {
1661
+ // use attribute and not id
1662
+ const attrName = `data-blinq-id-${token}`;
1663
+ el.setAttribute(attrName, "");
1664
+ return `[${attrName}]`;
1665
+ }, randomToken);
1477
1666
  state.info.value = _value;
1478
1667
  if (!options.press) {
1479
1668
  try {
@@ -1499,6 +1688,25 @@ class StableBrowser {
1499
1688
  }
1500
1689
  }
1501
1690
  await new Promise((resolve) => setTimeout(resolve, 500));
1691
+ // check if the element exist after the click (no wait)
1692
+ const count = await state.element.count({ timeout: 0 });
1693
+ if (count === 0) {
1694
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1695
+ const scope = state.element._frame ?? element.page();
1696
+ let prefixSelector = "";
1697
+ const frameControlSelector = " >> internal:control=enter-frame";
1698
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1699
+ if (frameSelectorIndex !== -1) {
1700
+ // remove everything after the >> internal:control=enter-frame
1701
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1702
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1703
+ }
1704
+ // if (element?._frame?._selector) {
1705
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1706
+ // }
1707
+ const newSelector = prefixSelector + newElementSelector;
1708
+ state.element = scope.locator(newSelector).first();
1709
+ }
1502
1710
  const valueSegment = state.value.split("&&");
1503
1711
  for (let i = 0; i < valueSegment.length; i++) {
1504
1712
  if (i > 0) {
@@ -1570,8 +1778,8 @@ class StableBrowser {
1570
1778
  if (enter) {
1571
1779
  await new Promise((resolve) => setTimeout(resolve, 2000));
1572
1780
  await this.page.keyboard.press("Enter");
1781
+ await this.waitForPageLoad();
1573
1782
  }
1574
- await this.waitForPageLoad();
1575
1783
  return state.info;
1576
1784
  }
1577
1785
  catch (e) {
@@ -2485,47 +2693,54 @@ class StableBrowser {
2485
2693
  }
2486
2694
  state.info.value = val;
2487
2695
  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
- }
2696
+ state.info.value = val;
2697
+ const isRegex = expectedValue.startsWith("regex:");
2698
+ const isContains = expectedValue.startsWith("contains:");
2699
+ const isExact = expectedValue.startsWith("exact:");
2700
+ let matchPassed = false;
2701
+ if (isRegex) {
2702
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2703
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2704
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2705
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2706
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2707
+ const regex = new RegExp(patternBody, flags);
2708
+ state.info.regex = true;
2709
+ matchPassed = regex.test(val);
2506
2710
  }
2507
2711
  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
- }
2712
+ // Fallback: treat as literal
2713
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2714
+ const regex = new RegExp(escapedPattern, "g");
2715
+ matchPassed = regex.test(val);
2520
2716
  }
2521
2717
  }
2718
+ else if (isContains) {
2719
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2720
+ matchPassed = val.includes(containsValue);
2721
+ }
2722
+ else if (isExact) {
2723
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2724
+ matchPassed = val === exactValue;
2725
+ }
2726
+ else if (property === "innerText") {
2727
+ // Default innerText logic
2728
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2729
+ const valLines = val.split("\n");
2730
+ const expectedLines = normalizedExpectedValue.split("\n");
2731
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2732
+ }
2522
2733
  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
- }
2734
+ // Fallback exact or loose match
2735
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2736
+ const regex = new RegExp(escapedPattern, "g");
2737
+ matchPassed = regex.test(val);
2738
+ }
2739
+ if (!matchPassed) {
2740
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2741
+ state.info.failCause.assertionFailed = true;
2742
+ state.info.failCause.lastError = errorMessage;
2743
+ throw new Error(errorMessage);
2529
2744
  }
2530
2745
  return state.info;
2531
2746
  }
@@ -2536,6 +2751,133 @@ class StableBrowser {
2536
2751
  await _commandFinally(state, this);
2537
2752
  }
2538
2753
  }
2754
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2755
+ // Convert timeout from seconds to milliseconds
2756
+ const timeoutMs = timeout * 1000;
2757
+ const state = {
2758
+ selectors,
2759
+ _params,
2760
+ condition,
2761
+ timeout: timeoutMs, // Store as milliseconds for internal use
2762
+ options,
2763
+ world,
2764
+ type: Types.CONDITIONAL_WAIT,
2765
+ highlight: true,
2766
+ screenshot: true,
2767
+ text: `Conditional wait for element`,
2768
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2769
+ operation: "conditionalWait",
2770
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2771
+ allowDisabled: true,
2772
+ info: {},
2773
+ };
2774
+ state.options ??= { timeout: timeoutMs };
2775
+ // Initialize startTime outside try block to ensure it's always accessible
2776
+ const startTime = Date.now();
2777
+ let conditionMet = false;
2778
+ let currentValue = null;
2779
+ let lastError = null;
2780
+ // Main retry loop - continues until timeout or condition is met
2781
+ while (Date.now() - startTime < timeoutMs) {
2782
+ const elapsedTime = Date.now() - startTime;
2783
+ const remainingTime = timeoutMs - elapsedTime;
2784
+ try {
2785
+ // Try to execute _preCommand (element location)
2786
+ await _preCommand(state, this);
2787
+ // If _preCommand succeeds, start condition checking
2788
+ const checkCondition = async () => {
2789
+ try {
2790
+ switch (condition.toLowerCase()) {
2791
+ case "checked":
2792
+ currentValue = await state.element.isChecked();
2793
+ return currentValue === true;
2794
+ case "unchecked":
2795
+ currentValue = await state.element.isChecked();
2796
+ return currentValue === false;
2797
+ case "visible":
2798
+ currentValue = await state.element.isVisible();
2799
+ return currentValue === true;
2800
+ case "hidden":
2801
+ currentValue = await state.element.isVisible();
2802
+ return currentValue === false;
2803
+ case "enabled":
2804
+ currentValue = await state.element.isDisabled();
2805
+ return currentValue === false;
2806
+ case "disabled":
2807
+ currentValue = await state.element.isDisabled();
2808
+ return currentValue === true;
2809
+ case "editable":
2810
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2811
+ currentValue = await state.element.isContentEditable();
2812
+ return currentValue === true;
2813
+ default:
2814
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2815
+ state.info.success = false;
2816
+ return false;
2817
+ }
2818
+ }
2819
+ catch (error) {
2820
+ // Don't throw here, just return false to continue retrying
2821
+ return false;
2822
+ }
2823
+ };
2824
+ // Inner loop for condition checking (once element is located)
2825
+ while (Date.now() - startTime < timeoutMs) {
2826
+ const currentElapsedTime = Date.now() - startTime;
2827
+ conditionMet = await checkCondition();
2828
+ if (conditionMet) {
2829
+ break;
2830
+ }
2831
+ // Check if we still have time for another attempt
2832
+ if (Date.now() - startTime + 50 < timeoutMs) {
2833
+ await new Promise((res) => setTimeout(res, 50));
2834
+ }
2835
+ else {
2836
+ break;
2837
+ }
2838
+ }
2839
+ // If we got here and condition is met, break out of main loop
2840
+ if (conditionMet) {
2841
+ break;
2842
+ }
2843
+ // If condition not met but no exception, we've timed out
2844
+ break;
2845
+ }
2846
+ catch (e) {
2847
+ lastError = e;
2848
+ const currentElapsedTime = Date.now() - startTime;
2849
+ const timeLeft = timeoutMs - currentElapsedTime;
2850
+ // Check if we have enough time left to retry
2851
+ if (timeLeft > 100) {
2852
+ await new Promise((resolve) => setTimeout(resolve, 50));
2853
+ }
2854
+ else {
2855
+ break;
2856
+ }
2857
+ }
2858
+ }
2859
+ const actualWaitTime = Date.now() - startTime;
2860
+ state.info = {
2861
+ success: conditionMet,
2862
+ conditionMet,
2863
+ actualWaitTime,
2864
+ currentValue,
2865
+ lastError: lastError?.message || null,
2866
+ message: conditionMet
2867
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2868
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2869
+ };
2870
+ if (lastError) {
2871
+ state.log += `Last error: ${lastError.message}\n`;
2872
+ }
2873
+ try {
2874
+ await _commandFinally(state, this);
2875
+ }
2876
+ catch (finallyError) {
2877
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2878
+ }
2879
+ return state.info;
2880
+ }
2539
2881
  async extractEmailData(emailAddress, options, world) {
2540
2882
  if (!emailAddress) {
2541
2883
  throw new Error("email address is null");
@@ -3132,6 +3474,8 @@ class StableBrowser {
3132
3474
  operation: "verify_text_with_relation",
3133
3475
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3134
3476
  };
3477
+ const cmdStartTime = Date.now();
3478
+ let cmdEndTime = null;
3135
3479
  const timeout = this._getFindElementTimeout(options);
3136
3480
  await new Promise((resolve) => setTimeout(resolve, 2000));
3137
3481
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3167,6 +3511,17 @@ class StableBrowser {
3167
3511
  await new Promise((resolve) => setTimeout(resolve, 1000));
3168
3512
  continue;
3169
3513
  }
3514
+ else {
3515
+ cmdEndTime = Date.now();
3516
+ if (cmdEndTime - cmdStartTime > 55000) {
3517
+ if (foundAncore) {
3518
+ throw new Error(`Text ${textToVerify} not found in page`);
3519
+ }
3520
+ else {
3521
+ throw new Error(`Text ${textAnchor} not found in page`);
3522
+ }
3523
+ }
3524
+ }
3170
3525
  try {
3171
3526
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3172
3527
  foundAncore = true;
@@ -3305,7 +3660,7 @@ class StableBrowser {
3305
3660
  Object.assign(e, { info: info });
3306
3661
  error = e;
3307
3662
  // throw e;
3308
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3663
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3309
3664
  }
3310
3665
  finally {
3311
3666
  const endTime = Date.now();
@@ -3654,6 +4009,22 @@ class StableBrowser {
3654
4009
  }
3655
4010
  }
3656
4011
  async waitForPageLoad(options = {}, world = null) {
4012
+ // try {
4013
+ // let currentPagePath = null;
4014
+ // currentPagePath = new URL(this.page.url()).pathname;
4015
+ // if (this.latestPagePath) {
4016
+ // // get the currect page path and compare with the latest page path
4017
+ // if (this.latestPagePath === currentPagePath) {
4018
+ // // if the page path is the same, do not wait for page load
4019
+ // console.log("No page change: " + currentPagePath);
4020
+ // return;
4021
+ // }
4022
+ // }
4023
+ // this.latestPagePath = currentPagePath;
4024
+ // } catch (e) {
4025
+ // console.debug("Error getting current page path: ", e);
4026
+ // }
4027
+ //console.log("Waiting for page load");
3657
4028
  let timeout = this._getLoadTimeout(options);
3658
4029
  const promiseArray = [];
3659
4030
  // let waitForNetworkIdle = true;
@@ -3686,10 +4057,12 @@ class StableBrowser {
3686
4057
  else if (e.label === "domcontentloaded") {
3687
4058
  console.log("waited for the domcontent loaded timeout");
3688
4059
  }
3689
- console.log(".");
3690
4060
  }
3691
4061
  finally {
3692
- await new Promise((resolve) => setTimeout(resolve, 2000));
4062
+ await new Promise((resolve) => setTimeout(resolve, 500));
4063
+ if (options && !options.noSleep) {
4064
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4065
+ }
3693
4066
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3694
4067
  const endTime = Date.now();
3695
4068
  _reportToWorld(world, {
@@ -3730,7 +4103,6 @@ class StableBrowser {
3730
4103
  await this.page.close();
3731
4104
  }
3732
4105
  catch (e) {
3733
- console.log(".");
3734
4106
  await _commandError(state, e, this);
3735
4107
  }
3736
4108
  finally {
@@ -3744,7 +4116,7 @@ class StableBrowser {
3744
4116
  }
3745
4117
  operation = options.operation;
3746
4118
  // validate operation is one of the supported operations
3747
- if (operation != "click" && operation != "hover+click") {
4119
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3748
4120
  throw new Error("operation is not supported");
3749
4121
  }
3750
4122
  const state = {
@@ -3813,6 +4185,17 @@ class StableBrowser {
3813
4185
  state.element = results[0];
3814
4186
  await performAction("hover+click", state.element, options, this, state, _params);
3815
4187
  break;
4188
+ case "hover":
4189
+ if (!options.css) {
4190
+ throw new Error("css is not defined");
4191
+ }
4192
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4193
+ if (result1.length === 0) {
4194
+ throw new Error(`Element not found in cell area`);
4195
+ }
4196
+ state.element = result1[0];
4197
+ await performAction("hover", state.element, options, this, state, _params);
4198
+ break;
3816
4199
  default:
3817
4200
  throw new Error("operation is not supported");
3818
4201
  }
@@ -3845,7 +4228,6 @@ class StableBrowser {
3845
4228
  await this.page.setViewportSize({ width: width, height: hight });
3846
4229
  }
3847
4230
  catch (e) {
3848
- console.log(".");
3849
4231
  await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
3850
4232
  }
3851
4233
  finally {
@@ -3883,7 +4265,6 @@ class StableBrowser {
3883
4265
  await this.page.reload();
3884
4266
  }
3885
4267
  catch (e) {
3886
- console.log(".");
3887
4268
  await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
3888
4269
  }
3889
4270
  finally {
@@ -3927,6 +4308,10 @@ class StableBrowser {
3927
4308
  }
3928
4309
  }
3929
4310
  async beforeScenario(world, scenario) {
4311
+ if (world && world.attach) {
4312
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4313
+ }
4314
+ this.context.loadedRoutes = null;
3930
4315
  this.beforeScenarioCalled = true;
3931
4316
  if (scenario && scenario.pickle && scenario.pickle.name) {
3932
4317
  this.scenarioName = scenario.pickle.name;
@@ -3956,8 +4341,10 @@ class StableBrowser {
3956
4341
  }
3957
4342
  async afterScenario(world, scenario) { }
3958
4343
  async beforeStep(world, step) {
4344
+ this.stepTags = [];
3959
4345
  if (!this.beforeScenarioCalled) {
3960
4346
  this.beforeScenario(world, step);
4347
+ this.context.loadedRoutes = null;
3961
4348
  }
3962
4349
  if (this.stepIndex === undefined) {
3963
4350
  this.stepIndex = 0;
@@ -3967,7 +4354,12 @@ class StableBrowser {
3967
4354
  }
3968
4355
  if (step && step.pickleStep && step.pickleStep.text) {
3969
4356
  this.stepName = step.pickleStep.text;
3970
- this.logger.info("step: " + this.stepName);
4357
+ let printableStepName = this.stepName;
4358
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4359
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4360
+ return `\x1b[33m"${p1}"\x1b[0m`;
4361
+ });
4362
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
3971
4363
  }
3972
4364
  else if (step && step.text) {
3973
4365
  this.stepName = step.text;
@@ -3982,7 +4374,10 @@ class StableBrowser {
3982
4374
  }
3983
4375
  if (this.initSnapshotTaken === false) {
3984
4376
  this.initSnapshotTaken = true;
3985
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4377
+ if (world &&
4378
+ world.attach &&
4379
+ !process.env.DISABLE_SNAPSHOT &&
4380
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
3986
4381
  const snapshot = await this.getAriaSnapshot();
3987
4382
  if (snapshot) {
3988
4383
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -3990,7 +4385,12 @@ class StableBrowser {
3990
4385
  }
3991
4386
  }
3992
4387
  this.context.routeResults = null;
3993
- await registerBeforeStepRoutes(this.context, this.stepName);
4388
+ this.context.loadedRoutes = null;
4389
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4390
+ networkBeforeStep(this.stepName, this.context);
4391
+ }
4392
+ setStepTags(tags) {
4393
+ this.stepTags = tags;
3994
4394
  }
3995
4395
  async getAriaSnapshot() {
3996
4396
  try {
@@ -4010,12 +4410,18 @@ class StableBrowser {
4010
4410
  try {
4011
4411
  // Ensure frame is attached and has body
4012
4412
  const body = frame.locator("body");
4013
- await body.waitFor({ timeout: 200 }); // wait explicitly
4413
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
4014
4414
  const snapshot = await body.ariaSnapshot({ timeout });
4415
+ if (!snapshot) {
4416
+ continue;
4417
+ }
4015
4418
  content.push(`- frame: ${i}`);
4016
4419
  content.push(snapshot);
4017
4420
  }
4018
- catch (innerErr) { }
4421
+ catch (innerErr) {
4422
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4423
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4424
+ }
4019
4425
  }
4020
4426
  return content.join("\n");
4021
4427
  }
@@ -4087,7 +4493,11 @@ class StableBrowser {
4087
4493
  if (this.context) {
4088
4494
  this.context.examplesRow = null;
4089
4495
  }
4090
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4496
+ if (world &&
4497
+ world.attach &&
4498
+ !process.env.DISABLE_SNAPSHOT &&
4499
+ !this.fastMode &&
4500
+ !this.stepTags.includes("fast-mode")) {
4091
4501
  const snapshot = await this.getAriaSnapshot();
4092
4502
  if (snapshot) {
4093
4503
  const obj = {};
@@ -4095,6 +4505,11 @@ class StableBrowser {
4095
4505
  }
4096
4506
  }
4097
4507
  this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4508
+ if (this.context.routeResults) {
4509
+ if (world && world.attach) {
4510
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4511
+ }
4512
+ }
4098
4513
  if (!process.env.TEMP_RUN) {
4099
4514
  const state = {
4100
4515
  world,
@@ -4118,6 +4533,13 @@ class StableBrowser {
4118
4533
  await _commandFinally(state, this);
4119
4534
  }
4120
4535
  }
4536
+ networkAfterStep(this.stepName, this.context);
4537
+ if (process.env.TEMP_RUN === "true") {
4538
+ // Put a sleep for some time to allow the browser to finish processing
4539
+ if (!this.stepTags.includes("fast-mode")) {
4540
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4541
+ }
4542
+ }
4121
4543
  }
4122
4544
  }
4123
4545
  function createTimedPromise(promise, label) {