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