automation_model 1.0.785-dev → 1.0.785-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.
Files changed (44) hide show
  1. package/lib/api.js +28 -11
  2. package/lib/api.js.map +1 -1
  3. package/lib/auto_page.d.ts +1 -1
  4. package/lib/auto_page.js +58 -17
  5. package/lib/auto_page.js.map +1 -1
  6. package/lib/browser_manager.d.ts +2 -2
  7. package/lib/browser_manager.js +102 -52
  8. package/lib/browser_manager.js.map +1 -1
  9. package/lib/bruno.js.map +1 -1
  10. package/lib/check_performance.d.ts +1 -0
  11. package/lib/check_performance.js +57 -0
  12. package/lib/check_performance.js.map +1 -0
  13. package/lib/command_common.d.ts +1 -1
  14. package/lib/command_common.js +26 -16
  15. package/lib/command_common.js.map +1 -1
  16. package/lib/file_checker.js +7 -0
  17. package/lib/file_checker.js.map +1 -1
  18. package/lib/index.js +1 -0
  19. package/lib/index.js.map +1 -1
  20. package/lib/init_browser.d.ts +1 -2
  21. package/lib/init_browser.js +124 -128
  22. package/lib/init_browser.js.map +1 -1
  23. package/lib/locator_log.js.map +1 -1
  24. package/lib/network.d.ts +2 -2
  25. package/lib/network.js +341 -178
  26. package/lib/network.js.map +1 -1
  27. package/lib/route.d.ts +64 -2
  28. package/lib/route.js +493 -192
  29. package/lib/route.js.map +1 -1
  30. package/lib/scripts/axe.mini.js +23978 -1
  31. package/lib/snapshot_validation.js +3 -0
  32. package/lib/snapshot_validation.js.map +1 -1
  33. package/lib/stable_browser.d.ts +13 -7
  34. package/lib/stable_browser.js +474 -114
  35. package/lib/stable_browser.js.map +1 -1
  36. package/lib/table_helper.js +14 -0
  37. package/lib/table_helper.js.map +1 -1
  38. package/lib/test_context.d.ts +1 -0
  39. package/lib/test_context.js +1 -0
  40. package/lib/test_context.js.map +1 -1
  41. package/lib/utils.d.ts +6 -2
  42. package/lib/utils.js +121 -14
  43. package/lib/utils.js.map +1 -1
  44. package/package.json +18 -11
@@ -1,4 +1,5 @@
1
1
  // @ts-nocheck
2
+ import { check_performance } from "./check_performance.js";
2
3
  import { expect } from "@playwright/test";
3
4
  import dayjs from "dayjs";
4
5
  import fs from "fs";
@@ -10,6 +11,7 @@ import { getDateTimeValue } from "./date_time.js";
10
11
  import drawRectangle from "./drawRect.js";
11
12
  //import { closeUnexpectedPopups } from "./popups.js";
12
13
  import { getTableCells, getTableData } from "./table_analyze.js";
14
+ import errorStackParser from "error-stack-parser";
13
15
  import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, _getTestData, } from "./utils.js";
14
16
  import csv from "csv-parser";
15
17
  import { Readable } from "node:stream";
@@ -26,6 +28,7 @@ import { _findCellArea, findElementsInArea } from "./table_helper.js";
26
28
  import { highlightSnapshot, snapshotValidation } from "./snapshot_validation.js";
27
29
  import { loadBrunoParams } from "./bruno.js";
28
30
  import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
31
+ import { existsSync } from "node:fs";
29
32
  export const Types = {
30
33
  CLICK: "click_element",
31
34
  WAIT_ELEMENT: "wait_element",
@@ -44,6 +47,7 @@ export const Types = {
44
47
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
45
48
  ANALYZE_TABLE: "analyze_table",
46
49
  SELECT: "select_combobox", //
50
+ VERIFY_PROPERTY: "verify_element_property",
47
51
  VERIFY_PAGE_PATH: "verify_page_path",
48
52
  VERIFY_PAGE_TITLE: "verify_page_title",
49
53
  TYPE_PRESS: "type_press",
@@ -62,12 +66,11 @@ export const Types = {
62
66
  SET_INPUT: "set_input",
63
67
  WAIT_FOR_TEXT_TO_DISAPPEAR: "wait_for_text_to_disappear",
64
68
  VERIFY_ATTRIBUTE: "verify_element_attribute",
65
- VERIFY_PROPERTY: "verify_element_property",
66
69
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
67
70
  BRUNO: "bruno",
68
- SNAPSHOT_VALIDATION: "snapshot_validation",
69
71
  VERIFY_FILE_EXISTS: "verify_file_exists",
70
72
  SET_INPUT_FILES: "set_input_files",
73
+ SNAPSHOT_VALIDATION: "snapshot_validation",
71
74
  REPORT_COMMAND: "report_command",
72
75
  STEP_COMPLETE: "step_complete",
73
76
  SLEEP: "sleep",
@@ -84,6 +87,7 @@ class StableBrowser {
84
87
  context;
85
88
  world;
86
89
  fastMode;
90
+ stepTags;
87
91
  project_path = null;
88
92
  webLogFile = null;
89
93
  networkLogger = null;
@@ -92,13 +96,17 @@ class StableBrowser {
92
96
  tags = null;
93
97
  isRecording = false;
94
98
  initSnapshotTaken = false;
95
- constructor(browser, page, logger = null, context = null, world = null, fastMode = false) {
99
+ onlyFailuresScreenshot = process.env.SCREENSHOT_ON_FAILURE_ONLY === "true";
100
+ // set to true if the step issue a report
101
+ inStepReport = false;
102
+ constructor(browser, page, logger = null, context = null, world = null, fastMode = false, stepTags = []) {
96
103
  this.browser = browser;
97
104
  this.page = page;
98
105
  this.logger = logger;
99
106
  this.context = context;
100
107
  this.world = world;
101
108
  this.fastMode = fastMode;
109
+ this.stepTags = stepTags;
102
110
  if (!this.logger) {
103
111
  this.logger = console;
104
112
  }
@@ -131,7 +139,7 @@ class StableBrowser {
131
139
  this.fastMode = true;
132
140
  }
133
141
  if (process.env.FAST_MODE === "true") {
134
- console.log("Fast mode enabled from environment variable");
142
+ // console.log("Fast mode enabled from environment variable");
135
143
  this.fastMode = true;
136
144
  }
137
145
  if (process.env.FAST_MODE === "false") {
@@ -174,6 +182,7 @@ class StableBrowser {
174
182
  registerNetworkEvents(this.world, this, context, this.page);
175
183
  registerDownloadEvent(this.page, this.world, context);
176
184
  page.on("close", async () => {
185
+ // return if browser context is already closed
177
186
  if (this.context && this.context.pages && this.context.pages.length > 1) {
178
187
  this.context.pages.pop();
179
188
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -183,7 +192,12 @@ class StableBrowser {
183
192
  console.log("Switched to page " + title);
184
193
  }
185
194
  catch (error) {
186
- 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
+ }
187
201
  }
188
202
  }
189
203
  });
@@ -192,7 +206,12 @@ class StableBrowser {
192
206
  console.log("Switch page: " + (await page.title()));
193
207
  }
194
208
  catch (e) {
195
- 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
+ }
196
215
  }
197
216
  context.pageLoading.status = false;
198
217
  }.bind(this));
@@ -204,7 +223,7 @@ class StableBrowser {
204
223
  }
205
224
  let newContextCreated = false;
206
225
  if (!apps[appName]) {
207
- let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder);
226
+ let newContext = await getContext(null, this.context.headless ? this.context.headless : false, this, this.logger, appName, false, this, -1, this.context.reportFolder, null, null, this.tags);
208
227
  newContextCreated = true;
209
228
  apps[appName] = {
210
229
  context: newContext,
@@ -220,7 +239,7 @@ class StableBrowser {
220
239
  if (newContextCreated) {
221
240
  this.registerEventListeners(this.context);
222
241
  await this.goto(this.context.environment.baseUrl);
223
- if (!this.fastMode) {
242
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
224
243
  await this.waitForPageLoad();
225
244
  }
226
245
  }
@@ -312,7 +331,7 @@ class StableBrowser {
312
331
  // async closeUnexpectedPopups() {
313
332
  // await closeUnexpectedPopups(this.page);
314
333
  // }
315
- async goto(url, world = null) {
334
+ async goto(url, world = null, options = {}) {
316
335
  if (!url) {
317
336
  throw new Error("url is null, verify that the environment file is correct");
318
337
  }
@@ -333,10 +352,17 @@ class StableBrowser {
333
352
  screenshot: false,
334
353
  highlight: false,
335
354
  };
355
+ let timeout = 60000;
356
+ if (this.configuration && this.configuration.page_timeout) {
357
+ timeout = this.configuration.page_timeout;
358
+ }
359
+ if (options && options["timeout"]) {
360
+ timeout = options["timeout"];
361
+ }
336
362
  try {
337
363
  await _preCommand(state, this);
338
364
  await this.page.goto(url, {
339
- timeout: 60000,
365
+ timeout: timeout,
340
366
  });
341
367
  await _screenshot(state, this);
342
368
  }
@@ -504,12 +530,6 @@ class StableBrowser {
504
530
  if (!el.setAttribute) {
505
531
  el = el.parentElement;
506
532
  }
507
- // remove any attributes start with data-blinq-id
508
- // for (let i = 0; i < el.attributes.length; i++) {
509
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
510
- // el.removeAttribute(el.attributes[i].name);
511
- // }
512
- // }
513
533
  el.setAttribute("data-blinq-id-" + randomToken, "");
514
534
  return true;
515
535
  }, [tag1, randomToken]))) {
@@ -679,40 +699,186 @@ class StableBrowser {
679
699
  }
680
700
  return { rerun: false };
681
701
  }
702
+ getFilePath() {
703
+ const stackFrames = errorStackParser.parse(new Error());
704
+ const mjsFrames = stackFrames.filter((frame) => frame.fileName && frame.fileName.endsWith(".mjs"));
705
+ const stackFrame = mjsFrames[mjsFrames.length - 2];
706
+ const filepath = stackFrame?.fileName;
707
+ if (filepath) {
708
+ let jsonFilePath = filepath.replace(".mjs", ".json");
709
+ if (existsSync(jsonFilePath)) {
710
+ return jsonFilePath;
711
+ }
712
+ const config = this.configuration ?? {};
713
+ if (!config?.locatorsMetadataDir) {
714
+ config.locatorsMetadataDir = "features/step_definitions/locators";
715
+ }
716
+ if (config && config.locatorsMetadataDir) {
717
+ jsonFilePath = path.join(config.locatorsMetadataDir, path.basename(jsonFilePath));
718
+ }
719
+ if (existsSync(jsonFilePath)) {
720
+ return jsonFilePath;
721
+ }
722
+ return null;
723
+ }
724
+ return null;
725
+ }
726
+ getFullElementLocators(selectors, filePath) {
727
+ if (!filePath || !existsSync(filePath)) {
728
+ return null;
729
+ }
730
+ const content = fs.readFileSync(filePath, "utf8");
731
+ try {
732
+ const allElements = JSON.parse(content);
733
+ const element_key = selectors?.element_key;
734
+ if (element_key && allElements[element_key]) {
735
+ return allElements[element_key];
736
+ }
737
+ for (const elementKey in allElements) {
738
+ const element = allElements[elementKey];
739
+ let foundStrategy = null;
740
+ for (const key in element) {
741
+ if (key === "strategy") {
742
+ continue;
743
+ }
744
+ const locators = element[key];
745
+ if (!locators || !locators.length) {
746
+ continue;
747
+ }
748
+ for (const locator of locators) {
749
+ delete locator.score;
750
+ }
751
+ if (JSON.stringify(locators) === JSON.stringify(selectors.locators)) {
752
+ foundStrategy = key;
753
+ break;
754
+ }
755
+ }
756
+ if (foundStrategy) {
757
+ return element;
758
+ }
759
+ }
760
+ }
761
+ catch (error) {
762
+ console.error("Error parsing locators from file: " + filePath, error);
763
+ }
764
+ return null;
765
+ }
682
766
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
683
767
  if (!timeout) {
684
768
  timeout = 30000;
685
769
  }
770
+ let element = null;
771
+ let allStrategyLocators = null;
772
+ let selectedStrategy = null;
773
+ if (this.tryAllStrategies) {
774
+ allStrategyLocators = this.getFullElementLocators(selectors, this.getFilePath());
775
+ selectedStrategy = allStrategyLocators?.strategy;
776
+ }
686
777
  for (let i = 0; i < 3; i++) {
687
778
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
688
779
  for (let j = 0; j < selectors.locators.length; j++) {
689
780
  let selector = selectors.locators[j];
690
781
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
691
782
  }
692
- let element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
783
+ if (this.tryAllStrategies && selectedStrategy) {
784
+ const strategyLocators = allStrategyLocators[selectedStrategy];
785
+ let err;
786
+ if (strategyLocators && strategyLocators.length) {
787
+ try {
788
+ selectors.locators = strategyLocators;
789
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
790
+ info.selectedStrategy = selectedStrategy;
791
+ info.log += "element found using strategy " + selectedStrategy + "\n";
792
+ }
793
+ catch (error) {
794
+ err = error;
795
+ }
796
+ }
797
+ if (!element) {
798
+ for (const key in allStrategyLocators) {
799
+ if (key === "strategy" || key === selectedStrategy) {
800
+ continue;
801
+ }
802
+ const strategyLocators = allStrategyLocators[key];
803
+ if (strategyLocators && strategyLocators.length) {
804
+ try {
805
+ info.log += "using strategy " + key + " with locators " + JSON.stringify(strategyLocators) + "\n";
806
+ selectors.locators = strategyLocators;
807
+ element = await this._locate_internal(selectors, info, _params, 10_000, allowDisabled);
808
+ err = null;
809
+ info.selectedStrategy = key;
810
+ info.log += "element found using strategy " + key + "\n";
811
+ break;
812
+ }
813
+ catch (error) {
814
+ err = error;
815
+ }
816
+ }
817
+ }
818
+ }
819
+ if (err) {
820
+ throw err;
821
+ }
822
+ }
823
+ else {
824
+ element = await this._locate_internal(selectors, info, _params, timeout, allowDisabled);
825
+ }
693
826
  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
- // }
827
+ let newElementSelector = "";
828
+ if (this.configuration && this.configuration.stableLocatorStrategy === "csschain") {
829
+ const cssSelector = await element.evaluate((el) => {
830
+ function getCssSelector(el) {
831
+ if (!el || el.nodeType !== 1 || el === document.body)
832
+ return el.tagName.toLowerCase();
833
+ const parent = el.parentElement;
834
+ const tag = el.tagName.toLowerCase();
835
+ // Find the index of the element among its siblings of the same tag
836
+ let index = 1;
837
+ for (let sibling = el.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
838
+ if (sibling.tagName === el.tagName) {
839
+ index++;
840
+ }
841
+ }
842
+ // Use nth-child if necessary (i.e., if there's more than one of the same tag)
843
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
844
+ const needsNthChild = siblings.length > 1;
845
+ const selector = needsNthChild ? `${tag}:nth-child(${[...parent.children].indexOf(el) + 1})` : tag;
846
+ return getCssSelector(parent) + " > " + selector;
847
+ }
848
+ const cssSelector = getCssSelector(el);
849
+ return cssSelector;
850
+ });
851
+ newElementSelector = cssSelector;
852
+ }
853
+ else {
854
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
855
+ if (this.configuration && this.configuration.stableLocatorStrategy === "data-attribute") {
856
+ const dataAttribute = "data-blinq-id";
857
+ await element.evaluate((el, [dataAttribute, randomToken]) => {
858
+ el.setAttribute(dataAttribute, randomToken);
859
+ }, [dataAttribute, randomToken]);
860
+ newElementSelector = `[${dataAttribute}="${randomToken}"]`;
861
+ }
862
+ else {
863
+ // the default case just return the located element
864
+ // will not work for click and type if the locator is placeholder and the placeholder change due to the click event
865
+ return element;
866
+ }
867
+ }
701
868
  const scope = element._frame ?? element.page();
702
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
703
869
  let prefixSelector = "";
704
870
  const frameControlSelector = " >> internal:control=enter-frame";
705
871
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
706
872
  if (frameSelectorIndex !== -1) {
707
873
  // remove everything after the >> internal:control=enter-frame
708
874
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
709
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
875
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
710
876
  }
711
877
  // if (element?._frame?._selector) {
712
878
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
713
879
  // }
714
880
  const newSelector = prefixSelector + newElementSelector;
715
- return scope.locator(newSelector);
881
+ return scope.locator(newSelector).first();
716
882
  }
717
883
  }
718
884
  throw new Error("unable to locate element " + JSON.stringify(selectors));
@@ -733,7 +899,7 @@ class StableBrowser {
733
899
  for (let i = 0; i < frame.selectors.length; i++) {
734
900
  let frameLocator = frame.selectors[i];
735
901
  if (frameLocator.css) {
736
- let testframescope = framescope.frameLocator(frameLocator.css);
902
+ let testframescope = framescope.frameLocator(`${frameLocator.css} >> visible=true`);
737
903
  if (frameLocator.index) {
738
904
  testframescope = framescope.nth(frameLocator.index);
739
905
  }
@@ -811,6 +977,15 @@ class StableBrowser {
811
977
  });
812
978
  }
813
979
  async _locate_internal(selectors, info, _params, timeout = 30000, allowDisabled = false) {
980
+ if (selectors.locators && Array.isArray(selectors.locators)) {
981
+ selectors.locators.forEach((locator) => {
982
+ locator.index = locator.index ?? 0;
983
+ locator.visible = locator.visible ?? true;
984
+ if (locator.visible && locator.css && !locator.css.endsWith(">> visible=true")) {
985
+ locator.css = locator.css + " >> visible=true";
986
+ }
987
+ });
988
+ }
814
989
  if (!info) {
815
990
  info = {};
816
991
  info.failCause = {};
@@ -823,7 +998,6 @@ class StableBrowser {
823
998
  let locatorsCount = 0;
824
999
  let lazy_scroll = false;
825
1000
  //let arrayMode = Array.isArray(selectors);
826
- let scope = await this._findFrameScope(selectors, timeout, info);
827
1001
  let selectorsLocators = null;
828
1002
  selectorsLocators = selectors.locators;
829
1003
  // group selectors by priority
@@ -851,6 +1025,7 @@ class StableBrowser {
851
1025
  let highPriorityOnly = true;
852
1026
  let visibleOnly = true;
853
1027
  while (true) {
1028
+ let scope = await this._findFrameScope(selectors, timeout, info);
854
1029
  locatorsCount = 0;
855
1030
  let result = [];
856
1031
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -966,9 +1141,13 @@ class StableBrowser {
966
1141
  }
967
1142
  }
968
1143
  if (foundLocators.length === 1) {
1144
+ let box = null;
1145
+ if (!this.onlyFailuresScreenshot) {
1146
+ box = await foundLocators[0].boundingBox();
1147
+ }
969
1148
  result.foundElements.push({
970
1149
  locator: foundLocators[0],
971
- box: await foundLocators[0].boundingBox(),
1150
+ box: box,
972
1151
  unique: true,
973
1152
  });
974
1153
  result.locatorIndex = i;
@@ -1123,11 +1302,22 @@ class StableBrowser {
1123
1302
  operation: "click",
1124
1303
  log: "***** click on " + selectors.element_name + " *****\n",
1125
1304
  };
1305
+ check_performance("click_all ***", this.context, true);
1306
+ let stepFastMode = this.stepTags.includes("fast-mode");
1307
+ if (stepFastMode) {
1308
+ state.onlyFailuresScreenshot = true;
1309
+ state.scroll = false;
1310
+ state.highlight = false;
1311
+ }
1126
1312
  try {
1313
+ check_performance("click_preCommand", this.context, true);
1127
1314
  await _preCommand(state, this);
1315
+ check_performance("click_preCommand", this.context, false);
1128
1316
  await performAction("click", state.element, options, this, state, _params);
1129
- if (!this.fastMode) {
1130
- await this.waitForPageLoad();
1317
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1318
+ check_performance("click_waitForPageLoad", this.context, true);
1319
+ await this.waitForPageLoad({ noSleep: true });
1320
+ check_performance("click_waitForPageLoad", this.context, false);
1131
1321
  }
1132
1322
  return state.info;
1133
1323
  }
@@ -1135,7 +1325,13 @@ class StableBrowser {
1135
1325
  await _commandError(state, e, this);
1136
1326
  }
1137
1327
  finally {
1328
+ check_performance("click_commandFinally", this.context, true);
1138
1329
  await _commandFinally(state, this);
1330
+ check_performance("click_commandFinally", this.context, false);
1331
+ check_performance("click_all ***", this.context, false);
1332
+ if (this.context.profile) {
1333
+ console.log(JSON.stringify(this.context.profile, null, 2));
1334
+ }
1139
1335
  }
1140
1336
  }
1141
1337
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1227,7 +1423,7 @@ class StableBrowser {
1227
1423
  }
1228
1424
  }
1229
1425
  }
1230
- await this.waitForPageLoad();
1426
+ //await this.waitForPageLoad();
1231
1427
  return state.info;
1232
1428
  }
1233
1429
  catch (e) {
@@ -1253,7 +1449,7 @@ class StableBrowser {
1253
1449
  await _preCommand(state, this);
1254
1450
  await performAction("hover", state.element, options, this, state, _params);
1255
1451
  await _screenshot(state, this);
1256
- await this.waitForPageLoad();
1452
+ //await this.waitForPageLoad();
1257
1453
  return state.info;
1258
1454
  }
1259
1455
  catch (e) {
@@ -1289,7 +1485,7 @@ class StableBrowser {
1289
1485
  state.info.log += "selectOption failed, will try force" + "\n";
1290
1486
  await state.element.selectOption(values, { timeout: 10000, force: true });
1291
1487
  }
1292
- await this.waitForPageLoad();
1488
+ //await this.waitForPageLoad();
1293
1489
  return state.info;
1294
1490
  }
1295
1491
  catch (e) {
@@ -1475,6 +1671,14 @@ class StableBrowser {
1475
1671
  }
1476
1672
  try {
1477
1673
  await _preCommand(state, this);
1674
+ const randomToken = "blinq_" + Math.random().toString(36).substring(7);
1675
+ // tag the element
1676
+ let newElementSelector = await state.element.evaluate((el, token) => {
1677
+ // use attribute and not id
1678
+ const attrName = `data-blinq-id-${token}`;
1679
+ el.setAttribute(attrName, "");
1680
+ return `[${attrName}]`;
1681
+ }, randomToken);
1478
1682
  state.info.value = _value;
1479
1683
  if (!options.press) {
1480
1684
  try {
@@ -1500,6 +1704,25 @@ class StableBrowser {
1500
1704
  }
1501
1705
  }
1502
1706
  await new Promise((resolve) => setTimeout(resolve, 500));
1707
+ // check if the element exist after the click (no wait)
1708
+ const count = await state.element.count({ timeout: 0 });
1709
+ if (count === 0) {
1710
+ // the locator changed after the click (placeholder) we need to locate the element using the data-blinq-id
1711
+ const scope = state.element._frame ?? element.page();
1712
+ let prefixSelector = "";
1713
+ const frameControlSelector = " >> internal:control=enter-frame";
1714
+ const frameSelectorIndex = state.element._selector.lastIndexOf(frameControlSelector);
1715
+ if (frameSelectorIndex !== -1) {
1716
+ // remove everything after the >> internal:control=enter-frame
1717
+ const frameSelector = state.element._selector.substring(0, frameSelectorIndex);
1718
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
1719
+ }
1720
+ // if (element?._frame?._selector) {
1721
+ // prefixSelector = element._frame._selector + " >> " + prefixSelector;
1722
+ // }
1723
+ const newSelector = prefixSelector + newElementSelector;
1724
+ state.element = scope.locator(newSelector).first();
1725
+ }
1503
1726
  const valueSegment = state.value.split("&&");
1504
1727
  for (let i = 0; i < valueSegment.length; i++) {
1505
1728
  if (i > 0) {
@@ -1571,8 +1794,8 @@ class StableBrowser {
1571
1794
  if (enter) {
1572
1795
  await new Promise((resolve) => setTimeout(resolve, 2000));
1573
1796
  await this.page.keyboard.press("Enter");
1797
+ await this.waitForPageLoad();
1574
1798
  }
1575
- await this.waitForPageLoad();
1576
1799
  return state.info;
1577
1800
  }
1578
1801
  catch (e) {
@@ -1831,11 +2054,12 @@ class StableBrowser {
1831
2054
  throw new Error("referanceSnapshot is null");
1832
2055
  }
1833
2056
  let text = null;
1834
- if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"))) {
1835
- text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yml"), "utf8");
2057
+ const snapshotsFolder = process.env.BVT_TEMP_SNAPSHOTS_FOLDER ?? this.context.snapshotFolder; //path .join(this.project_path, "data", "snapshots");
2058
+ if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"))) {
2059
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yml"), "utf8");
1836
2060
  }
1837
- else if (fs.existsSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"))) {
1838
- text = fs.readFileSync(path.join(this.project_path, "data", "snapshots", this.context.environment.name, referanceSnapshot + ".yaml"), "utf8");
2061
+ else if (fs.existsSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"))) {
2062
+ text = fs.readFileSync(path.join(snapshotsFolder, referanceSnapshot + ".yaml"), "utf8");
1839
2063
  }
1840
2064
  else if (referanceSnapshot.startsWith("yaml:")) {
1841
2065
  text = referanceSnapshot.substring(5);
@@ -1859,7 +2083,13 @@ class StableBrowser {
1859
2083
  scope = await this._findFrameScope(frameSelectors, timeout, state.info);
1860
2084
  }
1861
2085
  const snapshot = await scope.locator("body").ariaSnapshot({ timeout });
2086
+ if (snapshot && snapshot.length <= 10) {
2087
+ console.log("Page snapshot length is suspiciously small:", snapshot);
2088
+ }
1862
2089
  matchResult = snapshotValidation(snapshot, newValue, referanceSnapshot);
2090
+ if (matchResult === undefined) {
2091
+ console.log("snapshotValidation returned undefined");
2092
+ }
1863
2093
  if (matchResult.errorLine !== -1) {
1864
2094
  throw new Error("Snapshot validation failed at line " + matchResult.errorLineText);
1865
2095
  }
@@ -2030,6 +2260,9 @@ class StableBrowser {
2030
2260
  return _getTestData(world, this.context, this);
2031
2261
  }
2032
2262
  async _screenShot(options = {}, world = null, info = null) {
2263
+ if (!options) {
2264
+ options = {};
2265
+ }
2033
2266
  // collect url/path/title
2034
2267
  if (info) {
2035
2268
  if (!info.title) {
@@ -2058,7 +2291,7 @@ class StableBrowser {
2058
2291
  const uuidStr = "id_" + randomUUID();
2059
2292
  const screenshotPath = path.join(world.screenshotPath, uuidStr + ".png");
2060
2293
  try {
2061
- await this.takeScreenshot(screenshotPath);
2294
+ await this.takeScreenshot(screenshotPath, options.fullPage === true);
2062
2295
  // let buffer = await this.page.screenshot({ timeout: 4000 });
2063
2296
  // // save the buffer to the screenshot path asynchrously
2064
2297
  // fs.writeFile(screenshotPath, buffer, (err) => {
@@ -2079,7 +2312,7 @@ class StableBrowser {
2079
2312
  else if (options && options.screenshot) {
2080
2313
  result.screenshotPath = options.screenshotPath;
2081
2314
  try {
2082
- await this.takeScreenshot(options.screenshotPath);
2315
+ await this.takeScreenshot(options.screenshotPath, options.fullPage === true);
2083
2316
  // let buffer = await this.page.screenshot({ timeout: 4000 });
2084
2317
  // // save the buffer to the screenshot path asynchrously
2085
2318
  // fs.writeFile(options.screenshotPath, buffer, (err) => {
@@ -2097,7 +2330,7 @@ class StableBrowser {
2097
2330
  }
2098
2331
  return result;
2099
2332
  }
2100
- async takeScreenshot(screenshotPath) {
2333
+ async takeScreenshot(screenshotPath, fullPage = false) {
2101
2334
  const playContext = this.context.playContext;
2102
2335
  // Using CDP to capture the screenshot
2103
2336
  const viewportWidth = Math.max(...(await this.page.evaluate(() => [
@@ -2122,13 +2355,7 @@ class StableBrowser {
2122
2355
  const client = await playContext.newCDPSession(this.page);
2123
2356
  const { data } = await client.send("Page.captureScreenshot", {
2124
2357
  format: "png",
2125
- // clip: {
2126
- // x: 0,
2127
- // y: 0,
2128
- // width: viewportWidth,
2129
- // height: viewportHeight,
2130
- // scale: 1,
2131
- // },
2358
+ captureBeyondViewport: fullPage,
2132
2359
  });
2133
2360
  await client.detach();
2134
2361
  if (!screenshotPath) {
@@ -2137,7 +2364,7 @@ class StableBrowser {
2137
2364
  screenshotBuffer = Buffer.from(data, "base64");
2138
2365
  }
2139
2366
  else {
2140
- screenshotBuffer = await this.page.screenshot();
2367
+ screenshotBuffer = await this.page.screenshot({ fullPage: fullPage });
2141
2368
  }
2142
2369
  // if (focusedElement) {
2143
2370
  // // console.log(`Focused element ${JSON.stringify(focusedElement._selector)}`)
@@ -2237,6 +2464,12 @@ class StableBrowser {
2237
2464
  state.info.value = state.value;
2238
2465
  this.setTestData({ [variable]: state.value }, world);
2239
2466
  this.logger.info("set test data: " + variable + "=" + state.value);
2467
+ if (process.env.MODE === "executions") {
2468
+ const globalDataFile = "global_test_data.json";
2469
+ if (existsSync(globalDataFile)) {
2470
+ this.saveTestDataAsGlobal({}, world);
2471
+ }
2472
+ }
2240
2473
  // await new Promise((resolve) => setTimeout(resolve, 500));
2241
2474
  return state.info;
2242
2475
  }
@@ -2308,6 +2541,12 @@ class StableBrowser {
2308
2541
  state.info.value = state.value;
2309
2542
  this.setTestData({ [variable]: state.value }, world);
2310
2543
  this.logger.info("set test data: " + variable + "=" + state.value);
2544
+ if (process.env.MODE === "executions") {
2545
+ const globalDataFile = "global_test_data.json";
2546
+ if (existsSync(globalDataFile)) {
2547
+ this.saveTestDataAsGlobal({}, world);
2548
+ }
2549
+ }
2311
2550
  // await new Promise((resolve) => setTimeout(resolve, 500));
2312
2551
  return state.info;
2313
2552
  }
@@ -2438,7 +2677,7 @@ class StableBrowser {
2438
2677
  let expectedValue;
2439
2678
  try {
2440
2679
  await _preCommand(state, this);
2441
- expectedValue = await replaceWithLocalTestData(state.value, world);
2680
+ expectedValue = await this._replaceWithLocalData(value, world);
2442
2681
  state.info.expectedValue = expectedValue;
2443
2682
  switch (property) {
2444
2683
  case "innerText":
@@ -2486,47 +2725,54 @@ class StableBrowser {
2486
2725
  }
2487
2726
  state.info.value = val;
2488
2727
  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
- }
2728
+ state.info.value = val;
2729
+ const isRegex = expectedValue.startsWith("regex:");
2730
+ const isContains = expectedValue.startsWith("contains:");
2731
+ const isExact = expectedValue.startsWith("exact:");
2732
+ let matchPassed = false;
2733
+ if (isRegex) {
2734
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2735
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2736
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2737
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2738
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2739
+ const regex = new RegExp(patternBody, flags);
2740
+ state.info.regex = true;
2741
+ matchPassed = regex.test(val);
2507
2742
  }
2508
2743
  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
- }
2744
+ // Fallback: treat as literal
2745
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2746
+ const regex = new RegExp(escapedPattern, "g");
2747
+ matchPassed = regex.test(val);
2521
2748
  }
2522
2749
  }
2750
+ else if (isContains) {
2751
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2752
+ matchPassed = val.includes(containsValue);
2753
+ }
2754
+ else if (isExact) {
2755
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2756
+ matchPassed = val === exactValue;
2757
+ }
2758
+ else if (property === "innerText") {
2759
+ // Default innerText logic
2760
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2761
+ const valLines = val.split("\n");
2762
+ const expectedLines = normalizedExpectedValue.split("\n");
2763
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2764
+ }
2523
2765
  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
- }
2766
+ // Fallback exact or loose match
2767
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2768
+ const regex = new RegExp(escapedPattern, "g");
2769
+ matchPassed = regex.test(val);
2770
+ }
2771
+ if (!matchPassed) {
2772
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2773
+ state.info.failCause.assertionFailed = true;
2774
+ state.info.failCause.lastError = errorMessage;
2775
+ throw new Error(errorMessage);
2530
2776
  }
2531
2777
  return state.info;
2532
2778
  }
@@ -2557,6 +2803,7 @@ class StableBrowser {
2557
2803
  allowDisabled: true,
2558
2804
  info: {},
2559
2805
  };
2806
+ state.options ??= { timeout: timeoutMs };
2560
2807
  // Initialize startTime outside try block to ensure it's always accessible
2561
2808
  const startTime = Date.now();
2562
2809
  let conditionMet = false;
@@ -2726,6 +2973,12 @@ class StableBrowser {
2726
2973
  emailUrl = url;
2727
2974
  codeOrUrlFound = true;
2728
2975
  }
2976
+ if (process.env.MODE === "executions") {
2977
+ const globalDataFile = "global_test_data.json";
2978
+ if (existsSync(globalDataFile)) {
2979
+ this.saveTestDataAsGlobal({}, world);
2980
+ }
2981
+ }
2729
2982
  if (codeOrUrlFound) {
2730
2983
  return { emailUrl, emailCode };
2731
2984
  }
@@ -3132,7 +3385,16 @@ class StableBrowser {
3132
3385
  text = text.replace(/\\"/g, '"');
3133
3386
  }
3134
3387
  const timeout = this._getFindElementTimeout(options);
3135
- await new Promise((resolve) => setTimeout(resolve, 2000));
3388
+ //if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
3389
+ let stepFastMode = this.stepTags.includes("fast-mode");
3390
+ if (!stepFastMode) {
3391
+ if (!this.fastMode) {
3392
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3393
+ }
3394
+ else {
3395
+ await new Promise((resolve) => setTimeout(resolve, 500));
3396
+ }
3397
+ }
3136
3398
  const newValue = await this._replaceWithLocalData(text, world);
3137
3399
  if (newValue !== text) {
3138
3400
  this.logger.info(text + "=" + newValue);
@@ -3140,6 +3402,11 @@ class StableBrowser {
3140
3402
  }
3141
3403
  let dateAlternatives = findDateAlternatives(text);
3142
3404
  let numberAlternatives = findNumberAlternatives(text);
3405
+ if (stepFastMode) {
3406
+ state.onlyFailuresScreenshot = true;
3407
+ state.scroll = false;
3408
+ state.highlight = false;
3409
+ }
3143
3410
  try {
3144
3411
  await _preCommand(state, this);
3145
3412
  state.info.text = text;
@@ -3259,6 +3526,8 @@ class StableBrowser {
3259
3526
  operation: "verify_text_with_relation",
3260
3527
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
3261
3528
  };
3529
+ const cmdStartTime = Date.now();
3530
+ let cmdEndTime = null;
3262
3531
  const timeout = this._getFindElementTimeout(options);
3263
3532
  await new Promise((resolve) => setTimeout(resolve, 2000));
3264
3533
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -3294,6 +3563,17 @@ class StableBrowser {
3294
3563
  await new Promise((resolve) => setTimeout(resolve, 1000));
3295
3564
  continue;
3296
3565
  }
3566
+ else {
3567
+ cmdEndTime = Date.now();
3568
+ if (cmdEndTime - cmdStartTime > 55000) {
3569
+ if (foundAncore) {
3570
+ throw new Error(`Text ${textToVerify} not found in page`);
3571
+ }
3572
+ else {
3573
+ throw new Error(`Text ${textAnchor} not found in page`);
3574
+ }
3575
+ }
3576
+ }
3297
3577
  try {
3298
3578
  for (let i = 0; i < resultWithElementsFound.length; i++) {
3299
3579
  foundAncore = true;
@@ -3432,7 +3712,7 @@ class StableBrowser {
3432
3712
  Object.assign(e, { info: info });
3433
3713
  error = e;
3434
3714
  // throw e;
3435
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3715
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3436
3716
  }
3437
3717
  finally {
3438
3718
  const endTime = Date.now();
@@ -3781,6 +4061,22 @@ class StableBrowser {
3781
4061
  }
3782
4062
  }
3783
4063
  async waitForPageLoad(options = {}, world = null) {
4064
+ // try {
4065
+ // let currentPagePath = null;
4066
+ // currentPagePath = new URL(this.page.url()).pathname;
4067
+ // if (this.latestPagePath) {
4068
+ // // get the currect page path and compare with the latest page path
4069
+ // if (this.latestPagePath === currentPagePath) {
4070
+ // // if the page path is the same, do not wait for page load
4071
+ // console.log("No page change: " + currentPagePath);
4072
+ // return;
4073
+ // }
4074
+ // }
4075
+ // this.latestPagePath = currentPagePath;
4076
+ // } catch (e) {
4077
+ // console.debug("Error getting current page path: ", e);
4078
+ // }
4079
+ //console.log("Waiting for page load");
3784
4080
  let timeout = this._getLoadTimeout(options);
3785
4081
  const promiseArray = [];
3786
4082
  // let waitForNetworkIdle = true;
@@ -3815,7 +4111,10 @@ class StableBrowser {
3815
4111
  }
3816
4112
  }
3817
4113
  finally {
3818
- await new Promise((resolve) => setTimeout(resolve, 2000));
4114
+ await new Promise((resolve) => setTimeout(resolve, 500));
4115
+ if (options && !options.noSleep) {
4116
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4117
+ }
3819
4118
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3820
4119
  const endTime = Date.now();
3821
4120
  _reportToWorld(world, {
@@ -3869,7 +4168,7 @@ class StableBrowser {
3869
4168
  }
3870
4169
  operation = options.operation;
3871
4170
  // validate operation is one of the supported operations
3872
- if (operation != "click" && operation != "hover+click") {
4171
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3873
4172
  throw new Error("operation is not supported");
3874
4173
  }
3875
4174
  const state = {
@@ -3938,6 +4237,17 @@ class StableBrowser {
3938
4237
  state.element = results[0];
3939
4238
  await performAction("hover+click", state.element, options, this, state, _params);
3940
4239
  break;
4240
+ case "hover":
4241
+ if (!options.css) {
4242
+ throw new Error("css is not defined");
4243
+ }
4244
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4245
+ if (result1.length === 0) {
4246
+ throw new Error(`Element not found in cell area`);
4247
+ }
4248
+ state.element = result1[0];
4249
+ await performAction("hover", state.element, options, this, state, _params);
4250
+ break;
3941
4251
  default:
3942
4252
  throw new Error("operation is not supported");
3943
4253
  }
@@ -3951,6 +4261,12 @@ class StableBrowser {
3951
4261
  }
3952
4262
  saveTestDataAsGlobal(options, world) {
3953
4263
  const dataFile = _getDataFile(world, this.context, this);
4264
+ if (process.env.MODE === "executions") {
4265
+ const globalDataFile = path.join(this.project_path, "global_test_data.json");
4266
+ fs.copyFileSync(dataFile, globalDataFile);
4267
+ this.logger.info("Save the scenario test data to " + globalDataFile + " as global for the following scenarios.");
4268
+ return;
4269
+ }
3954
4270
  process.env.GLOBAL_TEST_DATA_FILE = dataFile;
3955
4271
  this.logger.info("Save the scenario test data as global for the following scenarios.");
3956
4272
  }
@@ -4053,6 +4369,7 @@ class StableBrowser {
4053
4369
  if (world && world.attach) {
4054
4370
  world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4055
4371
  }
4372
+ this.context.loadedRoutes = null;
4056
4373
  this.beforeScenarioCalled = true;
4057
4374
  if (scenario && scenario.pickle && scenario.pickle.name) {
4058
4375
  this.scenarioName = scenario.pickle.name;
@@ -4079,11 +4396,32 @@ class StableBrowser {
4079
4396
  await getTestData(envName, world, undefined, this.featureName, this.scenarioName, this.context);
4080
4397
  }
4081
4398
  await loadBrunoParams(this.context, this.context.environment.name);
4399
+ if ((process.env.TRACE === "true" || this.configuration.trace === true) && this.context) {
4400
+ this.trace = true;
4401
+ const traceFolder = path.join(this.context.reportFolder, "trace");
4402
+ if (!fs.existsSync(traceFolder)) {
4403
+ fs.mkdirSync(traceFolder, { recursive: true });
4404
+ }
4405
+ this.traceFolder = traceFolder;
4406
+ await this.context.playContext.tracing.start({ screenshots: true, snapshots: true });
4407
+ }
4408
+ }
4409
+ async afterScenario(world, scenario) {
4410
+ const id = scenario.testCaseStartedId;
4411
+ if (this.trace) {
4412
+ await this.context.playContext.tracing.stop({
4413
+ path: path.join(this.traceFolder, `trace-${id}.zip`),
4414
+ });
4415
+ }
4082
4416
  }
4083
- async afterScenario(world, scenario) { }
4084
4417
  async beforeStep(world, step) {
4418
+ if (step?.pickleStep && this.trace) {
4419
+ await this.context.playContext.tracing.group(`Step: ${step.pickleStep.text}`);
4420
+ }
4421
+ this.stepTags = [];
4085
4422
  if (!this.beforeScenarioCalled) {
4086
4423
  this.beforeScenario(world, step);
4424
+ this.context.loadedRoutes = null;
4087
4425
  }
4088
4426
  if (this.stepIndex === undefined) {
4089
4427
  this.stepIndex = 0;
@@ -4093,7 +4431,12 @@ class StableBrowser {
4093
4431
  }
4094
4432
  if (step && step.pickleStep && step.pickleStep.text) {
4095
4433
  this.stepName = step.pickleStep.text;
4096
- this.logger.info("step: " + this.stepName);
4434
+ let printableStepName = this.stepName;
4435
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4436
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4437
+ return `\x1b[33m"${p1}"\x1b[0m`;
4438
+ });
4439
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
4097
4440
  }
4098
4441
  else if (step && step.text) {
4099
4442
  this.stepName = step.text;
@@ -4108,7 +4451,10 @@ class StableBrowser {
4108
4451
  }
4109
4452
  if (this.initSnapshotTaken === false) {
4110
4453
  this.initSnapshotTaken = true;
4111
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4454
+ if (world &&
4455
+ world.attach &&
4456
+ !process.env.DISABLE_SNAPSHOT &&
4457
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
4112
4458
  const snapshot = await this.getAriaSnapshot();
4113
4459
  if (snapshot) {
4114
4460
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
@@ -4118,7 +4464,11 @@ class StableBrowser {
4118
4464
  this.context.routeResults = null;
4119
4465
  this.context.loadedRoutes = null;
4120
4466
  await registerBeforeStepRoutes(this.context, this.stepName, world);
4121
- networkBeforeStep(this.stepName);
4467
+ networkBeforeStep(this.stepName, this.context);
4468
+ this.inStepReport = false;
4469
+ }
4470
+ setStepTags(tags) {
4471
+ this.stepTags = tags;
4122
4472
  }
4123
4473
  async getAriaSnapshot() {
4124
4474
  try {
@@ -4192,7 +4542,7 @@ class StableBrowser {
4192
4542
  state.payload = payload;
4193
4543
  if (commandStatus === "FAILED") {
4194
4544
  state.throwError = true;
4195
- throw new Error("Command failed");
4545
+ throw new Error(commandText);
4196
4546
  }
4197
4547
  }
4198
4548
  catch (e) {
@@ -4202,26 +4552,22 @@ class StableBrowser {
4202
4552
  await _commandFinally(state, this);
4203
4553
  }
4204
4554
  }
4205
- async afterStep(world, step) {
4555
+ async afterStep(world, step, result) {
4206
4556
  this.stepName = null;
4207
- if (this.context && this.context.browserObject && this.context.browserObject.trace === true) {
4208
- if (this.context.browserObject.context) {
4209
- await this.context.browserObject.context.tracing.stopChunk({
4210
- path: path.join(this.context.browserObject.traceFolder, `trace-${this.stepIndex}.zip`),
4211
- });
4212
- if (world && world.attach) {
4213
- await world.attach(JSON.stringify({
4214
- type: "trace",
4215
- traceFilePath: `trace-${this.stepIndex}.zip`,
4216
- }), "application/json+trace");
4217
- }
4218
- // console.log("trace file created", `trace-${this.stepIndex}.zip`);
4219
- }
4220
- }
4221
4557
  if (this.context) {
4222
4558
  this.context.examplesRow = null;
4223
4559
  }
4224
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4560
+ if (!this.inStepReport) {
4561
+ // check the step result
4562
+ if (result && result.status === "FAILED" && world && world.attach) {
4563
+ await this.addCommandToReport(result.message ? result.message : "Step failed", "FAILED", `${result.message}`, { type: "text", screenshot: true }, world);
4564
+ }
4565
+ }
4566
+ if (world &&
4567
+ world.attach &&
4568
+ !process.env.DISABLE_SNAPSHOT &&
4569
+ !this.fastMode &&
4570
+ !this.stepTags.includes("fast-mode")) {
4225
4571
  const snapshot = await this.getAriaSnapshot();
4226
4572
  if (snapshot) {
4227
4573
  const obj = {};
@@ -4229,6 +4575,11 @@ class StableBrowser {
4229
4575
  }
4230
4576
  }
4231
4577
  this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4578
+ if (this.context.routeResults) {
4579
+ if (world && world.attach) {
4580
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4581
+ }
4582
+ }
4232
4583
  if (!process.env.TEMP_RUN) {
4233
4584
  const state = {
4234
4585
  world,
@@ -4252,7 +4603,16 @@ class StableBrowser {
4252
4603
  await _commandFinally(state, this);
4253
4604
  }
4254
4605
  }
4255
- networkAfterStep(this.stepName);
4606
+ networkAfterStep(this.stepName, this.context);
4607
+ if (process.env.TEMP_RUN === "true") {
4608
+ // Put a sleep for some time to allow the browser to finish processing
4609
+ if (!this.stepTags.includes("fast-mode")) {
4610
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4611
+ }
4612
+ }
4613
+ if (this.trace) {
4614
+ await this.context.playContext.tracing.groupEnd();
4615
+ }
4256
4616
  }
4257
4617
  }
4258
4618
  function createTimedPromise(promise, label) {