automation_model 1.0.741-dev → 1.0.741-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 (48) hide show
  1. package/README.md +4 -1
  2. package/lib/api.js +4 -3
  3. package/lib/api.js.map +1 -1
  4. package/lib/auto_page.d.ts +3 -1
  5. package/lib/auto_page.js +60 -9
  6. package/lib/auto_page.js.map +1 -1
  7. package/lib/browser_manager.js +19 -25
  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.js +17 -1
  14. package/lib/command_common.js.map +1 -1
  15. package/lib/file_checker.js +129 -25
  16. package/lib/file_checker.js.map +1 -1
  17. package/lib/index.js +1 -0
  18. package/lib/index.js.map +1 -1
  19. package/lib/init_browser.d.ts +1 -2
  20. package/lib/init_browser.js +121 -125
  21. package/lib/init_browser.js.map +1 -1
  22. package/lib/locator.d.ts +1 -0
  23. package/lib/locator.js +9 -2
  24. package/lib/locator.js.map +1 -1
  25. package/lib/locator_log.js.map +1 -1
  26. package/lib/network.d.ts +2 -0
  27. package/lib/network.js +398 -87
  28. package/lib/network.js.map +1 -1
  29. package/lib/route.d.ts +83 -0
  30. package/lib/route.js +682 -0
  31. package/lib/route.js.map +1 -0
  32. package/lib/scripts/axe.mini.js +23994 -1
  33. package/lib/snapshot_validation.js.map +1 -1
  34. package/lib/stable_browser.d.ts +18 -3
  35. package/lib/stable_browser.js +725 -54
  36. package/lib/stable_browser.js.map +1 -1
  37. package/lib/table.d.ts +9 -7
  38. package/lib/table.js +82 -12
  39. package/lib/table.js.map +1 -1
  40. package/lib/table_helper.js +14 -0
  41. package/lib/table_helper.js.map +1 -1
  42. package/lib/test_context.d.ts +1 -0
  43. package/lib/test_context.js +1 -0
  44. package/lib/test_context.js.map +1 -1
  45. package/lib/utils.d.ts +5 -1
  46. package/lib/utils.js +36 -9
  47. package/lib/utils.js.map +1 -1
  48. package/package.json +15 -10
@@ -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,7 +11,8 @@ 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";
13
- import { _convertToRegexQuery, _copyContext, _fixLocatorUsingParams, _fixUsingParams, _getServerUrl, extractStepExampleParameters, KEYBOARD_EVENTS, maskValue, replaceWithLocalTestData, scrollPageToLoadLazyElements, unEscapeString, _getDataFile, testForRegex, performAction, } from "./utils.js";
14
+ import errorStackParser from "error-stack-parser";
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";
16
18
  import readline from "readline";
@@ -19,16 +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";
30
+ import { registerAfterStepRoutes, registerBeforeStepRoutes } from "./route.js";
31
+ import { existsSync } from "node:fs";
28
32
  export const Types = {
29
33
  CLICK: "click_element",
30
34
  WAIT_ELEMENT: "wait_element",
31
- NAVIGATE: "navigate", ///
35
+ NAVIGATE: "navigate",
36
+ GO_BACK: "go_back",
37
+ GO_FORWARD: "go_forward",
32
38
  FILL: "fill_element",
33
39
  EXECUTE: "execute_page_method", //
34
40
  OPEN: "open_environment", //
@@ -41,6 +47,7 @@ export const Types = {
41
47
  VERIFY_PAGE_CONTAINS_NO_TEXT: "verify_page_contains_no_text",
42
48
  ANALYZE_TABLE: "analyze_table",
43
49
  SELECT: "select_combobox", //
50
+ VERIFY_PROPERTY: "verify_element_property",
44
51
  VERIFY_PAGE_PATH: "verify_page_path",
45
52
  VERIFY_PAGE_TITLE: "verify_page_title",
46
53
  TYPE_PRESS: "type_press",
@@ -49,6 +56,7 @@ export const Types = {
49
56
  CHECK: "check_element",
50
57
  UNCHECK: "uncheck_element",
51
58
  EXTRACT: "extract_attribute",
59
+ EXTRACT_PROPERTY: "extract_property",
52
60
  CLOSE_PAGE: "close_page",
53
61
  TABLE_OPERATION: "table_operation",
54
62
  SET_DATE_TIME: "set_date_time",
@@ -60,12 +68,13 @@ export const Types = {
60
68
  VERIFY_ATTRIBUTE: "verify_element_attribute",
61
69
  VERIFY_TEXT_WITH_RELATION: "verify_text_with_relation",
62
70
  BRUNO: "bruno",
63
- SNAPSHOT_VALIDATION: "snapshot_validation",
64
71
  VERIFY_FILE_EXISTS: "verify_file_exists",
65
72
  SET_INPUT_FILES: "set_input_files",
73
+ SNAPSHOT_VALIDATION: "snapshot_validation",
66
74
  REPORT_COMMAND: "report_command",
67
75
  STEP_COMPLETE: "step_complete",
68
76
  SLEEP: "sleep",
77
+ CONDITIONAL_WAIT: "conditional_wait",
69
78
  };
70
79
  export const apps = {};
71
80
  const formatElementName = (elementName) => {
@@ -78,6 +87,7 @@ class StableBrowser {
78
87
  context;
79
88
  world;
80
89
  fastMode;
90
+ stepTags;
81
91
  project_path = null;
82
92
  webLogFile = null;
83
93
  networkLogger = null;
@@ -86,13 +96,15 @@ class StableBrowser {
86
96
  tags = null;
87
97
  isRecording = false;
88
98
  initSnapshotTaken = false;
89
- 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 = []) {
90
101
  this.browser = browser;
91
102
  this.page = page;
92
103
  this.logger = logger;
93
104
  this.context = context;
94
105
  this.world = world;
95
106
  this.fastMode = fastMode;
107
+ this.stepTags = stepTags;
96
108
  if (!this.logger) {
97
109
  this.logger = console;
98
110
  }
@@ -121,9 +133,16 @@ class StableBrowser {
121
133
  context.pages = [this.page];
122
134
  const logFolder = path.join(this.project_path, "logs", "web");
123
135
  this.world = world;
136
+ if (this.configuration && this.configuration.fastMode === true) {
137
+ this.fastMode = true;
138
+ }
124
139
  if (process.env.FAST_MODE === "true") {
140
+ // console.log("Fast mode enabled from environment variable");
125
141
  this.fastMode = true;
126
142
  }
143
+ if (process.env.FAST_MODE === "false") {
144
+ this.fastMode = false;
145
+ }
127
146
  if (this.context) {
128
147
  this.context.fastMode = this.fastMode;
129
148
  }
@@ -161,6 +180,7 @@ class StableBrowser {
161
180
  registerNetworkEvents(this.world, this, context, this.page);
162
181
  registerDownloadEvent(this.page, this.world, context);
163
182
  page.on("close", async () => {
183
+ // return if browser context is already closed
164
184
  if (this.context && this.context.pages && this.context.pages.length > 1) {
165
185
  this.context.pages.pop();
166
186
  this.page = this.context.pages[this.context.pages.length - 1];
@@ -170,7 +190,12 @@ class StableBrowser {
170
190
  console.log("Switched to page " + title);
171
191
  }
172
192
  catch (error) {
173
- 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
+ }
174
199
  }
175
200
  }
176
201
  });
@@ -179,7 +204,12 @@ class StableBrowser {
179
204
  console.log("Switch page: " + (await page.title()));
180
205
  }
181
206
  catch (e) {
182
- 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
+ }
183
213
  }
184
214
  context.pageLoading.status = false;
185
215
  }.bind(this));
@@ -207,7 +237,7 @@ class StableBrowser {
207
237
  if (newContextCreated) {
208
238
  this.registerEventListeners(this.context);
209
239
  await this.goto(this.context.environment.baseUrl);
210
- if (!this.fastMode) {
240
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
211
241
  await this.waitForPageLoad();
212
242
  }
213
243
  }
@@ -335,6 +365,64 @@ class StableBrowser {
335
365
  await _commandFinally(state, this);
336
366
  }
337
367
  }
368
+ async goBack(options, world = null) {
369
+ const state = {
370
+ value: "",
371
+ world: world,
372
+ type: Types.GO_BACK,
373
+ text: `Browser navigate back`,
374
+ operation: "goBack",
375
+ log: "***** navigate back *****\n",
376
+ info: {},
377
+ locate: false,
378
+ scroll: false,
379
+ screenshot: false,
380
+ highlight: false,
381
+ };
382
+ try {
383
+ await _preCommand(state, this);
384
+ await this.page.goBack({
385
+ waitUntil: "load",
386
+ });
387
+ await _screenshot(state, this);
388
+ }
389
+ catch (error) {
390
+ console.error("Error on goBack", error);
391
+ _commandError(state, error, this);
392
+ }
393
+ finally {
394
+ await _commandFinally(state, this);
395
+ }
396
+ }
397
+ async goForward(options, world = null) {
398
+ const state = {
399
+ value: "",
400
+ world: world,
401
+ type: Types.GO_FORWARD,
402
+ text: `Browser navigate forward`,
403
+ operation: "goForward",
404
+ log: "***** navigate forward *****\n",
405
+ info: {},
406
+ locate: false,
407
+ scroll: false,
408
+ screenshot: false,
409
+ highlight: false,
410
+ };
411
+ try {
412
+ await _preCommand(state, this);
413
+ await this.page.goForward({
414
+ waitUntil: "load",
415
+ });
416
+ await _screenshot(state, this);
417
+ }
418
+ catch (error) {
419
+ console.error("Error on goForward", error);
420
+ _commandError(state, error, this);
421
+ }
422
+ finally {
423
+ await _commandFinally(state, this);
424
+ }
425
+ }
338
426
  async _getLocator(locator, scope, _params) {
339
427
  locator = _fixLocatorUsingParams(locator, _params);
340
428
  // locator = await this._replaceWithLocalData(locator);
@@ -433,12 +521,6 @@ class StableBrowser {
433
521
  if (!el.setAttribute) {
434
522
  el = el.parentElement;
435
523
  }
436
- // remove any attributes start with data-blinq-id
437
- // for (let i = 0; i < el.attributes.length; i++) {
438
- // if (el.attributes[i].name.startsWith("data-blinq-id")) {
439
- // el.removeAttribute(el.attributes[i].name);
440
- // }
441
- // }
442
524
  el.setAttribute("data-blinq-id-" + randomToken, "");
443
525
  return true;
444
526
  }, [tag1, randomToken]))) {
@@ -460,14 +542,13 @@ class StableBrowser {
460
542
  info.locatorLog = new LocatorLog(selectorHierarchy);
461
543
  }
462
544
  let locatorSearch = selectorHierarchy[index];
463
- let originalLocatorSearch = "";
464
545
  try {
465
- originalLocatorSearch = _fixUsingParams(JSON.stringify(locatorSearch), _params);
466
- locatorSearch = JSON.parse(originalLocatorSearch);
546
+ locatorSearch = _fixLocatorUsingParams(locatorSearch, _params);
467
547
  }
468
548
  catch (e) {
469
549
  console.error(e);
470
550
  }
551
+ let originalLocatorSearch = JSON.stringify(locatorSearch);
471
552
  //info.log += "searching for locator " + JSON.stringify(locatorSearch) + "\n";
472
553
  let locator = null;
473
554
  if (locatorSearch.climb && locatorSearch.climb >= 0) {
@@ -609,34 +690,205 @@ class StableBrowser {
609
690
  }
610
691
  return { rerun: false };
611
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
+ }
612
757
  async _locate(selectors, info, _params, timeout, allowDisabled = false) {
613
758
  if (!timeout) {
614
759
  timeout = 30000;
615
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
+ }
616
768
  for (let i = 0; i < 3; i++) {
617
769
  info.log += "attempt " + i + ": total locators " + selectors.locators.length + "\n";
618
770
  for (let j = 0; j < selectors.locators.length; j++) {
619
771
  let selector = selectors.locators[j];
620
772
  info.log += "searching for locator " + j + ":" + JSON.stringify(selector) + "\n";
621
773
  }
622
- 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
+ }
623
817
  if (!element.rerun) {
624
- const randomToken = Math.random().toString(36).substring(7);
625
- await element.evaluate((el, randomToken) => {
626
- el.setAttribute("data-blinq-id-" + randomToken, "");
627
- }, randomToken);
628
- // if (element._frame) {
629
- // return element;
630
- // }
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
+ newElementSelector = await element.evaluate((el, token) => {
855
+ const id = el.id || "";
856
+ if (id) {
857
+ try {
858
+ // count elements with this id
859
+ const count = document.querySelectorAll(`#${CSS.escape(id)}`).length;
860
+ if (count === 1) {
861
+ // unique id → use it
862
+ return `#${CSS.escape(id)}`;
863
+ }
864
+ else {
865
+ // duplicate id → fallback to custom attribute
866
+ const attrName = `data-blinq-id-${token}`;
867
+ el.setAttribute(attrName, "");
868
+ return `[${attrName}]`;
869
+ }
870
+ }
871
+ catch {
872
+ // invalid CSS chars in id → fallback to XPath
873
+ return `//*[@id="${id.replace(/"/g, '\\"')}"]`;
874
+ }
875
+ }
876
+ else {
877
+ // no id → assign the random token as the element's id
878
+ el.setAttribute("id", token);
879
+ return `#${token}`;
880
+ }
881
+ }, randomToken);
882
+ }
883
+ }
631
884
  const scope = element._frame ?? element.page();
632
- let newElementSelector = "[data-blinq-id-" + randomToken + "]";
633
885
  let prefixSelector = "";
634
886
  const frameControlSelector = " >> internal:control=enter-frame";
635
887
  const frameSelectorIndex = element._selector.lastIndexOf(frameControlSelector);
636
888
  if (frameSelectorIndex !== -1) {
637
889
  // remove everything after the >> internal:control=enter-frame
638
890
  const frameSelector = element._selector.substring(0, frameSelectorIndex);
639
- prefixSelector = frameSelector + " >> internal:control=enter-frame >>";
891
+ prefixSelector = frameSelector + " >> internal:control=enter-frame >> ";
640
892
  }
641
893
  // if (element?._frame?._selector) {
642
894
  // prefixSelector = element._frame._selector + " >> " + prefixSelector;
@@ -675,7 +927,7 @@ class StableBrowser {
675
927
  break;
676
928
  }
677
929
  catch (error) {
678
- console.error("frame not found " + frameLocator.css);
930
+ // console.error("frame not found " + frameLocator.css);
679
931
  }
680
932
  }
681
933
  }
@@ -753,7 +1005,6 @@ class StableBrowser {
753
1005
  let locatorsCount = 0;
754
1006
  let lazy_scroll = false;
755
1007
  //let arrayMode = Array.isArray(selectors);
756
- let scope = await this._findFrameScope(selectors, timeout, info);
757
1008
  let selectorsLocators = null;
758
1009
  selectorsLocators = selectors.locators;
759
1010
  // group selectors by priority
@@ -781,6 +1032,7 @@ class StableBrowser {
781
1032
  let highPriorityOnly = true;
782
1033
  let visibleOnly = true;
783
1034
  while (true) {
1035
+ let scope = await this._findFrameScope(selectors, timeout, info);
784
1036
  locatorsCount = 0;
785
1037
  let result = [];
786
1038
  let popupResult = await this.closeUnexpectedPopups(info, _params);
@@ -896,9 +1148,13 @@ class StableBrowser {
896
1148
  }
897
1149
  }
898
1150
  if (foundLocators.length === 1) {
1151
+ let box = null;
1152
+ if (!this.onlyFailuresScreenshot) {
1153
+ box = await foundLocators[0].boundingBox();
1154
+ }
899
1155
  result.foundElements.push({
900
1156
  locator: foundLocators[0],
901
- box: await foundLocators[0].boundingBox(),
1157
+ box: box,
902
1158
  unique: true,
903
1159
  });
904
1160
  result.locatorIndex = i;
@@ -1053,11 +1309,16 @@ class StableBrowser {
1053
1309
  operation: "click",
1054
1310
  log: "***** click on " + selectors.element_name + " *****\n",
1055
1311
  };
1312
+ check_performance("click_all ***", this.context, true);
1056
1313
  try {
1314
+ check_performance("click_preCommand", this.context, true);
1057
1315
  await _preCommand(state, this);
1316
+ check_performance("click_preCommand", this.context, false);
1058
1317
  await performAction("click", state.element, options, this, state, _params);
1059
- if (!this.fastMode) {
1060
- await this.waitForPageLoad();
1318
+ if (!this.fastMode && !this.stepTags.includes("fast-mode")) {
1319
+ check_performance("click_waitForPageLoad", this.context, true);
1320
+ await this.waitForPageLoad({ noSleep: true });
1321
+ check_performance("click_waitForPageLoad", this.context, false);
1061
1322
  }
1062
1323
  return state.info;
1063
1324
  }
@@ -1065,7 +1326,13 @@ class StableBrowser {
1065
1326
  await _commandError(state, e, this);
1066
1327
  }
1067
1328
  finally {
1329
+ check_performance("click_commandFinally", this.context, true);
1068
1330
  await _commandFinally(state, this);
1331
+ check_performance("click_commandFinally", this.context, false);
1332
+ check_performance("click_all ***", this.context, false);
1333
+ if (this.context.profile) {
1334
+ console.log(JSON.stringify(this.context.profile, null, 2));
1335
+ }
1069
1336
  }
1070
1337
  }
1071
1338
  async waitForElement(selectors, _params, options = {}, world = null) {
@@ -1157,7 +1424,7 @@ class StableBrowser {
1157
1424
  }
1158
1425
  }
1159
1426
  }
1160
- await this.waitForPageLoad();
1427
+ //await this.waitForPageLoad();
1161
1428
  return state.info;
1162
1429
  }
1163
1430
  catch (e) {
@@ -1183,7 +1450,7 @@ class StableBrowser {
1183
1450
  await _preCommand(state, this);
1184
1451
  await performAction("hover", state.element, options, this, state, _params);
1185
1452
  await _screenshot(state, this);
1186
- await this.waitForPageLoad();
1453
+ //await this.waitForPageLoad();
1187
1454
  return state.info;
1188
1455
  }
1189
1456
  catch (e) {
@@ -1219,7 +1486,7 @@ class StableBrowser {
1219
1486
  state.info.log += "selectOption failed, will try force" + "\n";
1220
1487
  await state.element.selectOption(values, { timeout: 10000, force: true });
1221
1488
  }
1222
- await this.waitForPageLoad();
1489
+ //await this.waitForPageLoad();
1223
1490
  return state.info;
1224
1491
  }
1225
1492
  catch (e) {
@@ -1501,8 +1768,8 @@ class StableBrowser {
1501
1768
  if (enter) {
1502
1769
  await new Promise((resolve) => setTimeout(resolve, 2000));
1503
1770
  await this.page.keyboard.press("Enter");
1771
+ await this.waitForPageLoad();
1504
1772
  }
1505
- await this.waitForPageLoad();
1506
1773
  return state.info;
1507
1774
  }
1508
1775
  catch (e) {
@@ -1957,12 +2224,7 @@ class StableBrowser {
1957
2224
  }
1958
2225
  }
1959
2226
  getTestData(world = null) {
1960
- const dataFile = _getDataFile(world, this.context, this);
1961
- let data = {};
1962
- if (fs.existsSync(dataFile)) {
1963
- data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
1964
- }
1965
- return data;
2227
+ return _getTestData(world, this.context, this);
1966
2228
  }
1967
2229
  async _screenShot(options = {}, world = null, info = null) {
1968
2230
  // collect url/path/title
@@ -2182,6 +2444,77 @@ class StableBrowser {
2182
2444
  await _commandFinally(state, this);
2183
2445
  }
2184
2446
  }
2447
+ async extractProperty(selectors, property, variable, _params = null, options = {}, world = null) {
2448
+ const state = {
2449
+ selectors,
2450
+ _params,
2451
+ property,
2452
+ variable,
2453
+ options,
2454
+ world,
2455
+ type: Types.EXTRACT_PROPERTY,
2456
+ text: `Extract property from element`,
2457
+ _text: `Extract property ${property} from ${selectors.element_name}`,
2458
+ operation: "extractProperty",
2459
+ log: "***** extract property " + property + " from " + selectors.element_name + " *****\n",
2460
+ allowDisabled: true,
2461
+ };
2462
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2463
+ try {
2464
+ await _preCommand(state, this);
2465
+ switch (property) {
2466
+ case "inner_text":
2467
+ state.value = await state.element.innerText();
2468
+ break;
2469
+ case "href":
2470
+ state.value = await state.element.getAttribute("href");
2471
+ break;
2472
+ case "value":
2473
+ state.value = await state.element.inputValue();
2474
+ break;
2475
+ case "text":
2476
+ state.value = await state.element.textContent();
2477
+ break;
2478
+ default:
2479
+ if (property.startsWith("dataset.")) {
2480
+ const dataAttribute = property.substring(8);
2481
+ state.value = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2482
+ }
2483
+ else {
2484
+ state.value = String(await state.element.evaluate((element, prop) => element[prop], property));
2485
+ }
2486
+ }
2487
+ if (options !== null) {
2488
+ if (options.regex && options.regex !== "") {
2489
+ // Construct a regex pattern from the provided string
2490
+ const regex = options.regex.slice(1, -1);
2491
+ const regexPattern = new RegExp(regex, "g");
2492
+ const matches = state.value.match(regexPattern);
2493
+ if (matches) {
2494
+ let newValue = "";
2495
+ for (const match of matches) {
2496
+ newValue += match;
2497
+ }
2498
+ state.value = newValue;
2499
+ }
2500
+ }
2501
+ if (options.trimSpaces && options.trimSpaces === true) {
2502
+ state.value = state.value.trim();
2503
+ }
2504
+ }
2505
+ state.info.value = state.value;
2506
+ this.setTestData({ [variable]: state.value }, world);
2507
+ this.logger.info("set test data: " + variable + "=" + state.value);
2508
+ // await new Promise((resolve) => setTimeout(resolve, 500));
2509
+ return state.info;
2510
+ }
2511
+ catch (e) {
2512
+ await _commandError(state, e, this);
2513
+ }
2514
+ finally {
2515
+ await _commandFinally(state, this);
2516
+ }
2517
+ }
2185
2518
  async verifyAttribute(selectors, attribute, value, _params = null, options = {}, world = null) {
2186
2519
  const state = {
2187
2520
  selectors,
@@ -2280,6 +2613,261 @@ class StableBrowser {
2280
2613
  await _commandFinally(state, this);
2281
2614
  }
2282
2615
  }
2616
+ async verifyProperty(selectors, property, value, _params = null, options = {}, world = null) {
2617
+ const state = {
2618
+ selectors,
2619
+ _params,
2620
+ property,
2621
+ value,
2622
+ options,
2623
+ world,
2624
+ type: Types.VERIFY_PROPERTY,
2625
+ highlight: true,
2626
+ screenshot: true,
2627
+ text: `Verify element property`,
2628
+ _text: `Verify property ${property} from ${selectors.element_name} is ${value}`,
2629
+ operation: "verifyProperty",
2630
+ log: "***** verify property " + property + " from " + selectors.element_name + " *****\n",
2631
+ allowDisabled: true,
2632
+ };
2633
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2634
+ let val;
2635
+ let expectedValue;
2636
+ try {
2637
+ await _preCommand(state, this);
2638
+ expectedValue = await replaceWithLocalTestData(state.value, world);
2639
+ state.info.expectedValue = expectedValue;
2640
+ switch (property) {
2641
+ case "innerText":
2642
+ val = String(await state.element.innerText());
2643
+ break;
2644
+ case "text":
2645
+ val = String(await state.element.textContent());
2646
+ break;
2647
+ case "value":
2648
+ val = String(await state.element.inputValue());
2649
+ break;
2650
+ case "checked":
2651
+ val = String(await state.element.isChecked());
2652
+ break;
2653
+ case "disabled":
2654
+ val = String(await state.element.isDisabled());
2655
+ break;
2656
+ case "readOnly":
2657
+ const isEditable = await state.element.isEditable();
2658
+ val = String(!isEditable);
2659
+ break;
2660
+ case "innerHTML":
2661
+ val = String(await state.element.innerHTML());
2662
+ break;
2663
+ case "outerHTML":
2664
+ val = String(await state.element.evaluate((element) => element.outerHTML));
2665
+ break;
2666
+ default:
2667
+ if (property.startsWith("dataset.")) {
2668
+ const dataAttribute = property.substring(8);
2669
+ val = String(await state.element.getAttribute(`data-${dataAttribute}`)) || "";
2670
+ }
2671
+ else {
2672
+ val = String(await state.element.evaluate((element, prop) => element[prop], property));
2673
+ }
2674
+ }
2675
+ // Helper function to remove all style="" attributes
2676
+ const removeStyleAttributes = (htmlString) => {
2677
+ return htmlString.replace(/\s*style\s*=\s*"[^"]*"/gi, "");
2678
+ };
2679
+ // Remove style attributes for innerHTML and outerHTML properties
2680
+ if (property === "innerHTML" || property === "outerHTML") {
2681
+ val = removeStyleAttributes(val);
2682
+ expectedValue = removeStyleAttributes(expectedValue);
2683
+ }
2684
+ state.info.value = val;
2685
+ let regex;
2686
+ state.info.value = val;
2687
+ const isRegex = expectedValue.startsWith("regex:");
2688
+ const isContains = expectedValue.startsWith("contains:");
2689
+ const isExact = expectedValue.startsWith("exact:");
2690
+ let matchPassed = false;
2691
+ if (isRegex) {
2692
+ const rawPattern = expectedValue.slice(6); // remove "regex:"
2693
+ const lastSlashIndex = rawPattern.lastIndexOf("/");
2694
+ if (rawPattern.startsWith("/") && lastSlashIndex > 0) {
2695
+ const patternBody = rawPattern.slice(1, lastSlashIndex).replace(/\n/g, ".*");
2696
+ const flags = rawPattern.slice(lastSlashIndex + 1) || "gs";
2697
+ const regex = new RegExp(patternBody, flags);
2698
+ state.info.regex = true;
2699
+ matchPassed = regex.test(val);
2700
+ }
2701
+ else {
2702
+ // Fallback: treat as literal
2703
+ const escapedPattern = rawPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2704
+ const regex = new RegExp(escapedPattern, "g");
2705
+ matchPassed = regex.test(val);
2706
+ }
2707
+ }
2708
+ else if (isContains) {
2709
+ const containsValue = expectedValue.slice(9); // remove "contains:"
2710
+ matchPassed = val.includes(containsValue);
2711
+ }
2712
+ else if (isExact) {
2713
+ const exactValue = expectedValue.slice(6); // remove "exact:"
2714
+ matchPassed = val === exactValue;
2715
+ }
2716
+ else if (property === "innerText") {
2717
+ // Default innerText logic
2718
+ const normalizedExpectedValue = expectedValue.replace(/\\n/g, "\n");
2719
+ const valLines = val.split("\n");
2720
+ const expectedLines = normalizedExpectedValue.split("\n");
2721
+ matchPassed = expectedLines.every((expectedLine) => valLines.some((valLine) => valLine.trim() === expectedLine.trim()));
2722
+ }
2723
+ else {
2724
+ // Fallback exact or loose match
2725
+ const escapedPattern = expectedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2726
+ const regex = new RegExp(escapedPattern, "g");
2727
+ matchPassed = regex.test(val);
2728
+ }
2729
+ if (!matchPassed) {
2730
+ let errorMessage = `The ${property} property has a value of "${val}", but the expected value is "${expectedValue}"`;
2731
+ state.info.failCause.assertionFailed = true;
2732
+ state.info.failCause.lastError = errorMessage;
2733
+ throw new Error(errorMessage);
2734
+ }
2735
+ return state.info;
2736
+ }
2737
+ catch (e) {
2738
+ await _commandError(state, e, this);
2739
+ }
2740
+ finally {
2741
+ await _commandFinally(state, this);
2742
+ }
2743
+ }
2744
+ async conditionalWait(selectors, condition, timeout = 1000, _params = null, options = {}, world = null) {
2745
+ // Convert timeout from seconds to milliseconds
2746
+ const timeoutMs = timeout * 1000;
2747
+ const state = {
2748
+ selectors,
2749
+ _params,
2750
+ condition,
2751
+ timeout: timeoutMs, // Store as milliseconds for internal use
2752
+ options,
2753
+ world,
2754
+ type: Types.CONDITIONAL_WAIT,
2755
+ highlight: true,
2756
+ screenshot: true,
2757
+ text: `Conditional wait for element`,
2758
+ _text: `Wait for ${selectors.element_name} to be ${condition} (timeout: ${timeout}s)`, // Display original seconds
2759
+ operation: "conditionalWait",
2760
+ log: `***** conditional wait for ${condition} on ${selectors.element_name} *****\n`,
2761
+ allowDisabled: true,
2762
+ info: {},
2763
+ };
2764
+ state.options ??= { timeout: timeoutMs };
2765
+ // Initialize startTime outside try block to ensure it's always accessible
2766
+ const startTime = Date.now();
2767
+ let conditionMet = false;
2768
+ let currentValue = null;
2769
+ let lastError = null;
2770
+ // Main retry loop - continues until timeout or condition is met
2771
+ while (Date.now() - startTime < timeoutMs) {
2772
+ const elapsedTime = Date.now() - startTime;
2773
+ const remainingTime = timeoutMs - elapsedTime;
2774
+ try {
2775
+ // Try to execute _preCommand (element location)
2776
+ await _preCommand(state, this);
2777
+ // If _preCommand succeeds, start condition checking
2778
+ const checkCondition = async () => {
2779
+ try {
2780
+ switch (condition.toLowerCase()) {
2781
+ case "checked":
2782
+ currentValue = await state.element.isChecked();
2783
+ return currentValue === true;
2784
+ case "unchecked":
2785
+ currentValue = await state.element.isChecked();
2786
+ return currentValue === false;
2787
+ case "visible":
2788
+ currentValue = await state.element.isVisible();
2789
+ return currentValue === true;
2790
+ case "hidden":
2791
+ currentValue = await state.element.isVisible();
2792
+ return currentValue === false;
2793
+ case "enabled":
2794
+ currentValue = await state.element.isDisabled();
2795
+ return currentValue === false;
2796
+ case "disabled":
2797
+ currentValue = await state.element.isDisabled();
2798
+ return currentValue === true;
2799
+ case "editable":
2800
+ // currentValue = await String(await state.element.evaluate((element, prop) => element[prop], "isContentEditable"));
2801
+ currentValue = await state.element.isContentEditable();
2802
+ return currentValue === true;
2803
+ default:
2804
+ state.info.message = `Unsupported condition: '${condition}'. Supported conditions are: checked, unchecked, visible, hidden, enabled, disabled, editable.`;
2805
+ state.info.success = false;
2806
+ return false;
2807
+ }
2808
+ }
2809
+ catch (error) {
2810
+ // Don't throw here, just return false to continue retrying
2811
+ return false;
2812
+ }
2813
+ };
2814
+ // Inner loop for condition checking (once element is located)
2815
+ while (Date.now() - startTime < timeoutMs) {
2816
+ const currentElapsedTime = Date.now() - startTime;
2817
+ conditionMet = await checkCondition();
2818
+ if (conditionMet) {
2819
+ break;
2820
+ }
2821
+ // Check if we still have time for another attempt
2822
+ if (Date.now() - startTime + 50 < timeoutMs) {
2823
+ await new Promise((res) => setTimeout(res, 50));
2824
+ }
2825
+ else {
2826
+ break;
2827
+ }
2828
+ }
2829
+ // If we got here and condition is met, break out of main loop
2830
+ if (conditionMet) {
2831
+ break;
2832
+ }
2833
+ // If condition not met but no exception, we've timed out
2834
+ break;
2835
+ }
2836
+ catch (e) {
2837
+ lastError = e;
2838
+ const currentElapsedTime = Date.now() - startTime;
2839
+ const timeLeft = timeoutMs - currentElapsedTime;
2840
+ // Check if we have enough time left to retry
2841
+ if (timeLeft > 100) {
2842
+ await new Promise((resolve) => setTimeout(resolve, 50));
2843
+ }
2844
+ else {
2845
+ break;
2846
+ }
2847
+ }
2848
+ }
2849
+ const actualWaitTime = Date.now() - startTime;
2850
+ state.info = {
2851
+ success: conditionMet,
2852
+ conditionMet,
2853
+ actualWaitTime,
2854
+ currentValue,
2855
+ lastError: lastError?.message || null,
2856
+ message: conditionMet
2857
+ ? `Condition '${condition}' met after ${(actualWaitTime / 1000).toFixed(2)}s`
2858
+ : `Condition '${condition}' not met within ${timeout}s timeout`,
2859
+ };
2860
+ if (lastError) {
2861
+ state.log += `Last error: ${lastError.message}\n`;
2862
+ }
2863
+ try {
2864
+ await _commandFinally(state, this);
2865
+ }
2866
+ catch (finallyError) {
2867
+ state.log += `Error in _commandFinally: ${finallyError.message}\n`;
2868
+ }
2869
+ return state.info;
2870
+ }
2283
2871
  async extractEmailData(emailAddress, options, world) {
2284
2872
  if (!emailAddress) {
2285
2873
  throw new Error("email address is null");
@@ -2876,6 +3464,8 @@ class StableBrowser {
2876
3464
  operation: "verify_text_with_relation",
2877
3465
  log: "***** search for " + textAnchor + " climb " + climb + " and verify " + textToVerify + " found *****\n",
2878
3466
  };
3467
+ const cmdStartTime = Date.now();
3468
+ let cmdEndTime = null;
2879
3469
  const timeout = this._getFindElementTimeout(options);
2880
3470
  await new Promise((resolve) => setTimeout(resolve, 2000));
2881
3471
  let newValue = await this._replaceWithLocalData(textAnchor, world);
@@ -2911,6 +3501,17 @@ class StableBrowser {
2911
3501
  await new Promise((resolve) => setTimeout(resolve, 1000));
2912
3502
  continue;
2913
3503
  }
3504
+ else {
3505
+ cmdEndTime = Date.now();
3506
+ if (cmdEndTime - cmdStartTime > 55000) {
3507
+ if (foundAncore) {
3508
+ throw new Error(`Text ${textToVerify} not found in page`);
3509
+ }
3510
+ else {
3511
+ throw new Error(`Text ${textAnchor} not found in page`);
3512
+ }
3513
+ }
3514
+ }
2914
3515
  try {
2915
3516
  for (let i = 0; i < resultWithElementsFound.length; i++) {
2916
3517
  foundAncore = true;
@@ -3049,7 +3650,7 @@ class StableBrowser {
3049
3650
  Object.assign(e, { info: info });
3050
3651
  error = e;
3051
3652
  // throw e;
3052
- await _commandError({ text: "visualVerification", operation: "visualVerification", text, info }, e, this);
3653
+ await _commandError({ text: "visualVerification", operation: "visualVerification", info }, e, this);
3053
3654
  }
3054
3655
  finally {
3055
3656
  const endTime = Date.now();
@@ -3398,6 +3999,22 @@ class StableBrowser {
3398
3999
  }
3399
4000
  }
3400
4001
  async waitForPageLoad(options = {}, world = null) {
4002
+ // try {
4003
+ // let currentPagePath = null;
4004
+ // currentPagePath = new URL(this.page.url()).pathname;
4005
+ // if (this.latestPagePath) {
4006
+ // // get the currect page path and compare with the latest page path
4007
+ // if (this.latestPagePath === currentPagePath) {
4008
+ // // if the page path is the same, do not wait for page load
4009
+ // console.log("No page change: " + currentPagePath);
4010
+ // return;
4011
+ // }
4012
+ // }
4013
+ // this.latestPagePath = currentPagePath;
4014
+ // } catch (e) {
4015
+ // console.debug("Error getting current page path: ", e);
4016
+ // }
4017
+ //console.log("Waiting for page load");
3401
4018
  let timeout = this._getLoadTimeout(options);
3402
4019
  const promiseArray = [];
3403
4020
  // let waitForNetworkIdle = true;
@@ -3430,10 +4047,12 @@ class StableBrowser {
3430
4047
  else if (e.label === "domcontentloaded") {
3431
4048
  console.log("waited for the domcontent loaded timeout");
3432
4049
  }
3433
- console.log(".");
3434
4050
  }
3435
4051
  finally {
3436
- await new Promise((resolve) => setTimeout(resolve, 2000));
4052
+ await new Promise((resolve) => setTimeout(resolve, 500));
4053
+ if (options && !options.noSleep) {
4054
+ await new Promise((resolve) => setTimeout(resolve, 1500));
4055
+ }
3437
4056
  ({ screenshotId, screenshotPath } = await this._screenShot(options, world));
3438
4057
  const endTime = Date.now();
3439
4058
  _reportToWorld(world, {
@@ -3474,7 +4093,6 @@ class StableBrowser {
3474
4093
  await this.page.close();
3475
4094
  }
3476
4095
  catch (e) {
3477
- console.log(".");
3478
4096
  await _commandError(state, e, this);
3479
4097
  }
3480
4098
  finally {
@@ -3488,7 +4106,7 @@ class StableBrowser {
3488
4106
  }
3489
4107
  operation = options.operation;
3490
4108
  // validate operation is one of the supported operations
3491
- if (operation != "click" && operation != "hover+click") {
4109
+ if (operation != "click" && operation != "hover+click" && operation != "hover") {
3492
4110
  throw new Error("operation is not supported");
3493
4111
  }
3494
4112
  const state = {
@@ -3557,6 +4175,17 @@ class StableBrowser {
3557
4175
  state.element = results[0];
3558
4176
  await performAction("hover+click", state.element, options, this, state, _params);
3559
4177
  break;
4178
+ case "hover":
4179
+ if (!options.css) {
4180
+ throw new Error("css is not defined");
4181
+ }
4182
+ const result1 = await findElementsInArea(options.css, cellArea, this, options);
4183
+ if (result1.length === 0) {
4184
+ throw new Error(`Element not found in cell area`);
4185
+ }
4186
+ state.element = result1[0];
4187
+ await performAction("hover", state.element, options, this, state, _params);
4188
+ break;
3560
4189
  default:
3561
4190
  throw new Error("operation is not supported");
3562
4191
  }
@@ -3589,7 +4218,6 @@ class StableBrowser {
3589
4218
  await this.page.setViewportSize({ width: width, height: hight });
3590
4219
  }
3591
4220
  catch (e) {
3592
- console.log(".");
3593
4221
  await _commandError({ text: "setViewportSize", operation: "setViewportSize", width, hight, info }, e, this);
3594
4222
  }
3595
4223
  finally {
@@ -3627,7 +4255,6 @@ class StableBrowser {
3627
4255
  await this.page.reload();
3628
4256
  }
3629
4257
  catch (e) {
3630
- console.log(".");
3631
4258
  await _commandError({ text: "reloadPage", operation: "reloadPage", info }, e, this);
3632
4259
  }
3633
4260
  finally {
@@ -3671,6 +4298,10 @@ class StableBrowser {
3671
4298
  }
3672
4299
  }
3673
4300
  async beforeScenario(world, scenario) {
4301
+ if (world && world.attach) {
4302
+ world.attach(this.context.reportFolder, { mediaType: "text/plain" });
4303
+ }
4304
+ this.context.loadedRoutes = null;
3674
4305
  this.beforeScenarioCalled = true;
3675
4306
  if (scenario && scenario.pickle && scenario.pickle.name) {
3676
4307
  this.scenarioName = scenario.pickle.name;
@@ -3700,8 +4331,10 @@ class StableBrowser {
3700
4331
  }
3701
4332
  async afterScenario(world, scenario) { }
3702
4333
  async beforeStep(world, step) {
4334
+ this.stepTags = [];
3703
4335
  if (!this.beforeScenarioCalled) {
3704
4336
  this.beforeScenario(world, step);
4337
+ this.context.loadedRoutes = null;
3705
4338
  }
3706
4339
  if (this.stepIndex === undefined) {
3707
4340
  this.stepIndex = 0;
@@ -3711,7 +4344,12 @@ class StableBrowser {
3711
4344
  }
3712
4345
  if (step && step.pickleStep && step.pickleStep.text) {
3713
4346
  this.stepName = step.pickleStep.text;
3714
- this.logger.info("step: " + this.stepName);
4347
+ let printableStepName = this.stepName;
4348
+ // take the printableStepName and replace quated value with \x1b[33m and \x1b[0m
4349
+ printableStepName = printableStepName.replace(/"([^"]*)"/g, (match, p1) => {
4350
+ return `\x1b[33m"${p1}"\x1b[0m`;
4351
+ });
4352
+ this.logger.info("\x1b[38;5;208mstep:\x1b[0m " + printableStepName);
3715
4353
  }
3716
4354
  else if (step && step.text) {
3717
4355
  this.stepName = step.text;
@@ -3726,13 +4364,23 @@ class StableBrowser {
3726
4364
  }
3727
4365
  if (this.initSnapshotTaken === false) {
3728
4366
  this.initSnapshotTaken = true;
3729
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT && !this.fastMode) {
4367
+ if (world &&
4368
+ world.attach &&
4369
+ !process.env.DISABLE_SNAPSHOT &&
4370
+ (!this.fastMode || this.stepTags.includes("fast-mode"))) {
3730
4371
  const snapshot = await this.getAriaSnapshot();
3731
4372
  if (snapshot) {
3732
4373
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-before");
3733
4374
  }
3734
4375
  }
3735
4376
  }
4377
+ this.context.routeResults = null;
4378
+ this.context.loadedRoutes = null;
4379
+ await registerBeforeStepRoutes(this.context, this.stepName, world);
4380
+ networkBeforeStep(this.stepName, this.context);
4381
+ }
4382
+ setStepTags(tags) {
4383
+ this.stepTags = tags;
3736
4384
  }
3737
4385
  async getAriaSnapshot() {
3738
4386
  try {
@@ -3752,12 +4400,18 @@ class StableBrowser {
3752
4400
  try {
3753
4401
  // Ensure frame is attached and has body
3754
4402
  const body = frame.locator("body");
3755
- await body.waitFor({ timeout: 200 }); // wait explicitly
4403
+ //await body.waitFor({ timeout: 2000 }); // wait explicitly
3756
4404
  const snapshot = await body.ariaSnapshot({ timeout });
4405
+ if (!snapshot) {
4406
+ continue;
4407
+ }
3757
4408
  content.push(`- frame: ${i}`);
3758
4409
  content.push(snapshot);
3759
4410
  }
3760
- catch (innerErr) { }
4411
+ catch (innerErr) {
4412
+ console.warn(`Frame ${i} snapshot failed:`, innerErr);
4413
+ content.push(`- frame: ${i} - error: ${innerErr.message}`);
4414
+ }
3761
4415
  }
3762
4416
  return content.join("\n");
3763
4417
  }
@@ -3829,13 +4483,23 @@ class StableBrowser {
3829
4483
  if (this.context) {
3830
4484
  this.context.examplesRow = null;
3831
4485
  }
3832
- if (world && world.attach && !process.env.DISABLE_SNAPSHOT) {
4486
+ if (world &&
4487
+ world.attach &&
4488
+ !process.env.DISABLE_SNAPSHOT &&
4489
+ !this.fastMode &&
4490
+ !this.stepTags.includes("fast-mode")) {
3833
4491
  const snapshot = await this.getAriaSnapshot();
3834
4492
  if (snapshot) {
3835
4493
  const obj = {};
3836
4494
  await world.attach(JSON.stringify(snapshot), "application/json+snapshot-after");
3837
4495
  }
3838
4496
  }
4497
+ this.context.routeResults = await registerAfterStepRoutes(this.context, world);
4498
+ if (this.context.routeResults) {
4499
+ if (world && world.attach) {
4500
+ await world.attach(JSON.stringify(this.context.routeResults), "application/json+intercept-results");
4501
+ }
4502
+ }
3839
4503
  if (!process.env.TEMP_RUN) {
3840
4504
  const state = {
3841
4505
  world,
@@ -3859,6 +4523,13 @@ class StableBrowser {
3859
4523
  await _commandFinally(state, this);
3860
4524
  }
3861
4525
  }
4526
+ networkAfterStep(this.stepName, this.context);
4527
+ if (process.env.TEMP_RUN === "true") {
4528
+ // Put a sleep for some time to allow the browser to finish processing
4529
+ if (!this.stepTags.includes("fast-mode")) {
4530
+ await new Promise((resolve) => setTimeout(resolve, 3000));
4531
+ }
4532
+ }
3862
4533
  }
3863
4534
  }
3864
4535
  function createTimedPromise(promise, label) {