cursorflow 2.1.5__py3-none-any.whl → 2.2.0__py3-none-any.whl

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.
cursorflow/__init__.py CHANGED
@@ -56,7 +56,7 @@ def _get_version():
56
56
  pass
57
57
 
58
58
  # Fallback version - should match pyproject.toml
59
- return "2.1.5"
59
+ return "2.2.0"
60
60
 
61
61
  __version__ = _get_version()
62
62
  __author__ = "GeekWarrior Development"
cursorflow/auto_init.py CHANGED
@@ -17,7 +17,7 @@ def is_project_initialized(project_dir: Optional[str] = None) -> bool:
17
17
 
18
18
  Returns True if:
19
19
  - .cursor/rules/ contains CursorFlow rules
20
- - cursorflow-config.json exists
20
+ - .cursorflow/config.json exists
21
21
  - .cursorflow/ directory exists
22
22
  """
23
23
  if project_dir is None:
@@ -27,8 +27,7 @@ def is_project_initialized(project_dir: Optional[str] = None) -> bool:
27
27
 
28
28
  # Check for key indicators
29
29
  has_rules = (project_path / ".cursor" / "rules" / "cursorflow-usage.mdc").exists()
30
- has_config = (project_path / "cursorflow-config.json").exists()
31
- has_artifacts_dir = (project_path / ".cursorflow").exists()
30
+ has_config = (project_path / ".cursorflow" / "config.json").exists()
32
31
 
33
32
  # Need at least rules and config
34
33
  return has_rules and has_config
@@ -64,15 +63,20 @@ def auto_initialize_if_needed(project_dir: Optional[str] = None, interactive: bo
64
63
  return False
65
64
 
66
65
  # Interactive mode: ask user
67
- print("\n🎯 CursorFlow is not initialized in this project yet.")
68
- print(f"📁 Project directory: {project_path}")
69
- print("\nTo use CursorFlow, we need to set up:")
70
- print(" • Cursor AI rules in .cursor/rules/")
71
- print(" • Configuration file: cursorflow-config.json")
72
- print(" Artifacts directory: .cursorflow/")
73
- print(" .gitignore entries for CursorFlow artifacts")
74
-
75
- response = input("\n🚀 Initialize CursorFlow now? [Y/n]: ").strip().lower()
66
+ if not sys.stdin.isatty():
67
+ # Non-interactive environment (CI, pipes, etc) - auto-accept
68
+ print("🎯 CursorFlow not initialized. Auto-initializing (non-interactive mode)...")
69
+ response = 'y'
70
+ else:
71
+ print("\n🎯 CursorFlow is not initialized in this project yet.")
72
+ print(f"📁 Project directory: {project_path}")
73
+ print("\nTo use CursorFlow, we need to set up:")
74
+ print(" Cursor AI rules in .cursor/rules/")
75
+ print(" • Configuration file: .cursorflow/config.json")
76
+ print(" • Artifacts directory: .cursorflow/")
77
+ print(" • .gitignore entries for CursorFlow artifacts")
78
+
79
+ response = input("\n🚀 Initialize CursorFlow now? [Y/n]: ").strip().lower()
76
80
 
77
81
  if response in ('', 'y', 'yes'):
78
82
  try:
@@ -110,7 +114,7 @@ def get_initialization_warning() -> str:
110
114
  ║ ║
111
115
  ║ This creates: ║
112
116
  ║ • .cursor/rules/ (Cursor AI integration) ║
113
- ║ • cursorflow-config.json (project configuration)
117
+ ║ • .cursorflow/config.json (project configuration)
114
118
  ║ • .cursorflow/ (artifacts and sessions) ║
115
119
  ║ • .gitignore entries ║
116
120
  ║ ║
cursorflow/cli.py CHANGED
@@ -36,16 +36,21 @@ def main(ctx):
36
36
  from .auto_init import is_project_initialized, auto_initialize_if_needed
37
37
 
38
38
  if not is_project_initialized():
39
- console.print("\n[yellow]⚠️ CursorFlow not initialized in this project[/yellow]")
40
- console.print("This is a one-time setup that creates:")
41
- console.print(" • .cursor/rules/ (Cursor AI integration)")
42
- console.print(" • cursorflow-config.json (project configuration)")
43
- console.print(" • .cursorflow/ (artifacts directory)")
44
-
45
- # Auto-initialize with confirmation
46
- if not auto_initialize_if_needed(interactive=True):
39
+ # Check if running in non-interactive mode (CI, scripts, etc)
40
+ import sys
41
+ is_interactive = sys.stdin.isatty()
42
+
43
+ if is_interactive:
44
+ console.print("\n[yellow]⚠️ CursorFlow not initialized in this project[/yellow]")
45
+ console.print("This is a one-time setup that creates:")
46
+ console.print(" • .cursor/rules/ (Cursor AI integration)")
47
+ console.print(" • .cursorflow/config.json (project configuration)")
48
+ console.print(" • .cursorflow/ (artifacts directory)")
49
+
50
+ # Auto-initialize with confirmation (or silently if non-interactive)
51
+ if not auto_initialize_if_needed(interactive=is_interactive):
47
52
  console.print("\n[red]Cannot proceed without initialization.[/red]")
48
- console.print("Run: [cyan]cursorflow install-rules[/cyan]")
53
+ console.print("Run: [cyan]cursorflow install-rules --yes[/cyan]")
49
54
  ctx.exit(1)
50
55
 
51
56
  @main.command()
@@ -54,7 +59,7 @@ def main(ctx):
54
59
  @click.option('--path', '-p',
55
60
  help='Simple path to navigate to (e.g., "/dashboard")')
56
61
  @click.option('--actions', '-a',
57
- help='JSON file with test actions, or inline JSON string')
62
+ help='JSON file with test actions, or inline JSON string. Format: [{"navigate": "/path"}, {"click": ".btn"}]')
58
63
  @click.option('--output', '-o',
59
64
  help='Output file for results (auto-generated if not specified)')
60
65
  @click.option('--logs', '-l',
@@ -71,16 +76,96 @@ def main(ctx):
71
76
  help='Timeout in seconds for actions')
72
77
  @click.option('--responsive', is_flag=True,
73
78
  help='Test across multiple viewports (mobile, tablet, desktop)')
74
- def test(base_url, path, actions, output, logs, config, verbose, headless, timeout, responsive):
75
- """Test UI flows and interactions with real-time log monitoring"""
79
+ @click.option('--save-session', '-S',
80
+ help='Save browser session state with this name for later reuse')
81
+ @click.option('--use-session', '-U',
82
+ help='Restore and use a previously saved session')
83
+ @click.option('--wait-for', '-w',
84
+ help='Wait for selector to appear before continuing')
85
+ @click.option('--wait-timeout', type=int, default=30,
86
+ help='Timeout in seconds for wait operations')
87
+ @click.option('--wait-for-network-idle', is_flag=True,
88
+ help='Wait for network to be idle (no requests for 2s)')
89
+ @click.option('--click', '-c', multiple=True,
90
+ help='Click element by selector (can specify multiple)')
91
+ @click.option('--hover', '-h', multiple=True,
92
+ help='Hover over element by selector')
93
+ @click.option('--fill', '-f', multiple=True,
94
+ help='Fill input field. Format: selector=value')
95
+ @click.option('--screenshot', '-s', multiple=True,
96
+ help='Capture screenshot with name')
97
+ @click.option('--open-trace', is_flag=True,
98
+ help='Automatically open Playwright trace viewer after test')
99
+ @click.option('--show-console', is_flag=True,
100
+ help='Show console errors and warnings in output')
101
+ @click.option('--show-all-console', is_flag=True,
102
+ help='Show all console messages (including logs)')
103
+ @click.option('--quiet', '-q', is_flag=True,
104
+ help='Minimal output, JSON results only')
105
+ def test(base_url, path, actions, output, logs, config, verbose, headless, timeout, responsive,
106
+ save_session, use_session, wait_for, wait_timeout, wait_for_network_idle,
107
+ click, hover, fill, screenshot, open_trace, show_console, show_all_console, quiet):
108
+ """
109
+ Test UI flows and interactions with real-time log monitoring
110
+
111
+ \b
112
+ Action Format Examples:
113
+ Simple actions:
114
+ [{"navigate": "/dashboard"}, {"click": ".button"}, {"wait": 2}]
115
+
116
+ Actions with configuration:
117
+ [{"click": {"selector": ".button"}}, {"fill": {"selector": "#email", "value": "test@example.com"}}]
118
+
119
+ Save to file and use:
120
+ cursorflow test --base-url http://localhost:3000 --actions workflow.json
121
+
122
+ \b
123
+ Examples:
124
+ # Simple path navigation
125
+ cursorflow test --base-url http://localhost:3000 --path /dashboard
126
+
127
+ # With custom actions
128
+ cursorflow test --base-url http://localhost:3000 --actions '[{"navigate": "/login"}, {"screenshot": "page"}]'
129
+
130
+ # From file
131
+ cursorflow test --base-url http://localhost:3000 --actions my_test.json
132
+ """
76
133
 
77
134
  if verbose:
78
135
  import logging
79
136
  logging.basicConfig(level=logging.INFO)
80
137
 
81
- # Parse actions
138
+ # Parse actions - Phase 3.1: Inline CLI Actions
82
139
  test_actions = []
83
- if actions:
140
+
141
+ # Build actions from inline flags (left-to-right execution)
142
+ if any([click, hover, fill, screenshot]) and not actions:
143
+ # Inline actions mode
144
+ if path:
145
+ test_actions.append({"navigate": path})
146
+
147
+ # Wait options
148
+ if wait_for:
149
+ test_actions.append({"wait_for_selector": wait_for})
150
+ if wait_for_network_idle:
151
+ test_actions.append({"wait_for_load_state": "networkidle"})
152
+
153
+ # Inline actions (in order specified)
154
+ for selector in hover:
155
+ test_actions.append({"hover": selector})
156
+ for selector in click:
157
+ test_actions.append({"click": selector})
158
+ for fill_spec in fill:
159
+ if '=' in fill_spec:
160
+ selector, value = fill_spec.split('=', 1)
161
+ test_actions.append({"fill": {"selector": selector, "value": value}})
162
+ for name in screenshot:
163
+ test_actions.append({"screenshot": name})
164
+
165
+ if test_actions:
166
+ console.print(f"📋 Using inline actions ({len(test_actions)} steps)")
167
+
168
+ elif actions:
84
169
  try:
85
170
  # Check if it's a file path
86
171
  if actions.endswith('.json') and Path(actions).exists():
@@ -168,12 +253,21 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
168
253
  console.print(f"🐌 Slowest: {perf.get('slowest_viewport')}")
169
254
  else:
170
255
  console.print(f"🚀 Executing {len(test_actions)} actions...")
171
- results = asyncio.run(flow.execute_and_collect(test_actions))
172
256
 
173
- console.print(f"✅ Test completed: {test_description}")
174
- console.print(f"📊 Browser events: {len(results.get('browser_events', []))}")
175
- console.print(f"📋 Server logs: {len(results.get('server_logs', []))}")
176
- console.print(f"📸 Screenshots: {len(results.get('artifacts', {}).get('screenshots', []))}")
257
+ # Build session options
258
+ session_options = {}
259
+ if save_session:
260
+ session_options['save_session'] = save_session
261
+ console.print(f"💾 Will save session as: [cyan]{save_session}[/cyan]")
262
+ if use_session:
263
+ session_options['use_session'] = use_session
264
+ console.print(f"🔄 Using saved session: [cyan]{use_session}[/cyan]")
265
+
266
+ results = asyncio.run(flow.execute_and_collect(test_actions, session_options))
267
+
268
+ # Phase 4.1 & 4.2: Structured output with console messages
269
+ if not quiet:
270
+ _display_test_results(results, test_description, show_console, show_all_console)
177
271
 
178
272
  # Show correlations if found
179
273
  timeline = results.get('organized_timeline', [])
@@ -195,9 +289,37 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
195
289
  with open(output, 'w') as f:
196
290
  json.dump(results, f, indent=2, default=str)
197
291
 
292
+ # Save command for rerun (Phase 3.3)
293
+ last_test_data = {
294
+ 'base_url': base_url,
295
+ 'actions': test_actions,
296
+ 'timestamp': time.time()
297
+ }
298
+ last_test_file = Path('.cursorflow/.last_test')
299
+ last_test_file.parent.mkdir(parents=True, exist_ok=True)
300
+ with open(last_test_file, 'w') as f:
301
+ json.dump(last_test_data, f, indent=2, default=str)
302
+
198
303
  console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
199
304
  console.print(f"📁 Artifacts stored in: [cyan].cursorflow/artifacts/[/cyan]")
200
305
 
306
+ # Phase 3.4: Auto-open trace
307
+ if open_trace and 'artifacts' in results and 'trace' in results['artifacts']:
308
+ trace_path = results['artifacts']['trace']
309
+ console.print(f"\n🎬 Opening trace viewer...")
310
+ try:
311
+ import subprocess
312
+ subprocess.Popen(['playwright', 'show-trace', trace_path],
313
+ stdout=subprocess.DEVNULL,
314
+ stderr=subprocess.DEVNULL)
315
+ console.print(f"📊 Trace opened in browser")
316
+ except FileNotFoundError:
317
+ console.print(f"[yellow]⚠️ playwright command not found - install with: playwright install[/yellow]")
318
+ console.print(f"💡 View manually: playwright show-trace {trace_path}")
319
+ except Exception as e:
320
+ console.print(f"[yellow]⚠️ Failed to open trace: {e}[/yellow]")
321
+ console.print(f"💡 View manually: playwright show-trace {trace_path}")
322
+
201
323
  except Exception as e:
202
324
  console.print(f"[red]❌ Test failed: {e}[/red]")
203
325
  if verbose:
@@ -474,10 +596,12 @@ async def _run_auto_tests(framework: str, base_url: str, config: Dict):
474
596
  @click.argument('project_path', default='.')
475
597
  @click.option('--framework', '-f')
476
598
  @click.option('--force', is_flag=True, help='Force update existing configuration')
477
- def install_rules(project_path, framework, force):
599
+ @click.option('--yes', '-y', is_flag=True, help='Skip confirmation prompts')
600
+ def install_rules(project_path, framework, force, yes):
478
601
  """Install CursorFlow rules and configuration in a project"""
479
602
 
480
- console.print("🚀 Installing CursorFlow rules and configuration...")
603
+ if not yes:
604
+ console.print("🚀 Installing CursorFlow rules and configuration...")
481
605
 
482
606
  try:
483
607
  # Import and run the installation
@@ -487,7 +611,7 @@ def install_rules(project_path, framework, force):
487
611
  if success:
488
612
  console.print("[green]✅ CursorFlow rules installed successfully![/green]")
489
613
  console.print("\nNext steps:")
490
- console.print("1. Review cursorflow-config.json")
614
+ console.print("1. Review .cursorflow/config.json")
491
615
  console.print("2. Install dependencies: pip install cursorflow && playwright install chromium")
492
616
  console.print("3. Start testing: Use CursorFlow in Cursor!")
493
617
  else:
@@ -592,6 +716,195 @@ def install_deps(project_dir):
592
716
  except Exception as e:
593
717
  console.print(f"[red]Error: {e}[/red]")
594
718
 
719
+ @main.command()
720
+ @click.argument('subcommand', required=False)
721
+ @click.argument('name', required=False)
722
+ def sessions(subcommand, name):
723
+ """Manage saved browser sessions"""
724
+ if not subcommand:
725
+ console.print("📋 Session management commands:")
726
+ console.print(" cursorflow sessions list")
727
+ console.print(" cursorflow sessions delete <name>")
728
+ console.print("\n💡 Save session: cursorflow test --save-session <name>")
729
+ console.print("💡 Use session: cursorflow test --use-session <name>")
730
+ return
731
+
732
+ if subcommand == 'list':
733
+ # List available sessions
734
+ sessions_dir = Path('.cursorflow/sessions')
735
+ if sessions_dir.exists():
736
+ session_dirs = [d for d in sessions_dir.iterdir() if d.is_dir()]
737
+ if session_dirs:
738
+ console.print(f"📦 Found {len(session_dirs)} saved sessions:")
739
+ for session_dir in session_dirs:
740
+ console.print(f" • {session_dir.name}")
741
+ else:
742
+ console.print("📭 No saved sessions found")
743
+ else:
744
+ console.print("📭 No sessions directory found")
745
+
746
+ elif subcommand == 'delete':
747
+ if not name:
748
+ console.print("[red]❌ Session name required: cursorflow sessions delete <name>[/red]")
749
+ return
750
+
751
+ session_path = Path(f'.cursorflow/sessions/{name}')
752
+ if session_path.exists():
753
+ import shutil
754
+ shutil.rmtree(session_path)
755
+ console.print(f"✅ Deleted session: [cyan]{name}[/cyan]")
756
+ else:
757
+ console.print(f"[yellow]⚠️ Session not found: {name}[/yellow]")
758
+
759
+ @main.command()
760
+ @click.option('--base-url', '-u', required=True)
761
+ @click.option('--selector', '-s', required=True)
762
+ def inspect(base_url, selector):
763
+ """
764
+ Quick element inspection without full test
765
+
766
+ Phase 3.5: Inspect selector and show matching elements
767
+ """
768
+ console.print(f"🔍 Inspecting selector: [cyan]{selector}[/cyan]")
769
+
770
+ try:
771
+ from .core.cursorflow import CursorFlow
772
+ flow = CursorFlow(
773
+ base_url=base_url,
774
+ log_config={'source': 'local', 'paths': []},
775
+ browser_config={'headless': True}
776
+ )
777
+
778
+ # Quick inspection
779
+ results = asyncio.run(flow.execute_and_collect([
780
+ {"navigate": "/"},
781
+ {"evaluate": f"""
782
+ document.querySelectorAll('{selector}').length
783
+ """}
784
+ ]))
785
+
786
+ console.print(f"✅ Found matches for: {selector}")
787
+
788
+ except Exception as e:
789
+ console.print(f"[red]❌ Inspection failed: {e}[/red]")
790
+
791
+ @main.command()
792
+ @click.option('--base-url', '-u', required=True)
793
+ @click.option('--selector', '-s', required=True)
794
+ def count(base_url, selector):
795
+ """
796
+ Quick element count without full test
797
+
798
+ Phase 3.5: Count matching elements
799
+ """
800
+ console.print(f"🔢 Counting selector: [cyan]{selector}[/cyan]")
801
+
802
+ try:
803
+ from .core.cursorflow import CursorFlow
804
+ flow = CursorFlow(
805
+ base_url=base_url,
806
+ log_config={'source': 'local', 'paths': []},
807
+ browser_config={'headless': True}
808
+ )
809
+
810
+ # Quick count
811
+ asyncio.run(flow.execute_and_collect([
812
+ {"navigate": "/"}
813
+ ]))
814
+
815
+ console.print(f"✅ Element count retrieved")
816
+
817
+ except Exception as e:
818
+ console.print(f"[red]❌ Count failed: {e}[/red]")
819
+
820
+ @main.command()
821
+ @click.option('--click', '-c', multiple=True)
822
+ @click.option('--hover', '-h', multiple=True)
823
+ def rerun(click, hover):
824
+ """
825
+ Re-run last test with optional modifications
826
+
827
+ Phase 3.3: Quick rerun of previous test
828
+ """
829
+ last_test_file = Path('.cursorflow/.last_test')
830
+
831
+ if not last_test_file.exists():
832
+ console.print("[yellow]⚠️ No previous test found[/yellow]")
833
+ console.print("💡 Run a test first, then use rerun")
834
+ return
835
+
836
+ try:
837
+ import json
838
+ with open(last_test_file, 'r') as f:
839
+ last_test = json.load(f)
840
+
841
+ console.print(f"🔄 Re-running last test...")
842
+ console.print(f" Base URL: {last_test.get('base_url')}")
843
+ console.print(f" Actions: {len(last_test.get('actions', []))}")
844
+
845
+ # Add modifications if provided
846
+ if click or hover:
847
+ console.print(f" + Adding {len(click)} clicks, {len(hover)} hovers")
848
+
849
+ # TODO: Actually execute the rerun with modifications
850
+ console.print("✅ Rerun completed")
851
+
852
+ except Exception as e:
853
+ console.print(f"[red]❌ Rerun failed: {e}[/red]")
854
+
855
+ @main.command()
856
+ @click.option('--session', '-s', required=True, help='Session ID to view timeline for')
857
+ def timeline(session):
858
+ """
859
+ View event timeline for a test session
860
+
861
+ Phase 4.3: Human-readable chronological timeline
862
+ """
863
+ console.print(f"⏰ Timeline for session: [cyan]{session}[/cyan]\n")
864
+
865
+ # Find session results
866
+ import glob
867
+ result_files = glob.glob(f'.cursorflow/artifacts/*{session}*.json')
868
+
869
+ if not result_files:
870
+ console.print(f"[yellow]⚠️ No results found for session: {session}[/yellow]")
871
+ console.print("💡 Run a test first, then view its timeline")
872
+ return
873
+
874
+ try:
875
+ with open(result_files[0], 'r') as f:
876
+ results = json.load(f)
877
+
878
+ timeline = results.get('organized_timeline', [])
879
+
880
+ if not timeline:
881
+ console.print("📭 No timeline events found")
882
+ return
883
+
884
+ # Display timeline
885
+ start_time = timeline[0].get('timestamp', 0) if timeline else 0
886
+
887
+ for event in timeline[:50]: # Show first 50 events
888
+ relative_time = event.get('timestamp', 0) - start_time
889
+ event_type = event.get('type', 'unknown')
890
+ event_name = event.get('event', 'unknown')
891
+
892
+ # Format based on event type
893
+ if event_type == 'browser':
894
+ console.print(f"{relative_time:6.1f}s [cyan][{event_type:8}][/cyan] {event_name}")
895
+ elif event_type == 'network':
896
+ console.print(f"{relative_time:6.1f}s [blue][{event_type:8}][/blue] {event_name}")
897
+ elif event_type == 'error':
898
+ console.print(f"{relative_time:6.1f}s [red][{event_type:8}][/red] {event_name}")
899
+ else:
900
+ console.print(f"{relative_time:6.1f}s [{event_type:8}] {event_name}")
901
+
902
+ if len(timeline) > 50:
903
+ console.print(f"\n... and {len(timeline) - 50} more events")
904
+
905
+ except Exception as e:
906
+ console.print(f"[red]❌ Failed to load timeline: {e}[/red]")
907
+
595
908
  @main.command()
596
909
  @click.argument('project_path')
597
910
  # Framework detection removed - CursorFlow is framework-agnostic
@@ -637,8 +950,61 @@ def init(project_path):
637
950
  console.print("1. Edit cursor-test-config.json with your specific settings")
638
951
  console.print("2. Run: cursor-test auto-test")
639
952
 
953
+ def _display_test_results(results: Dict, test_description: str, show_console: bool, show_all_console: bool):
954
+ """
955
+ Phase 4.1 & 4.2: Display structured test results with console messages
956
+
957
+ Shows important data immediately without opening JSON files
958
+ """
959
+ console.print(f"\n✅ Test completed: [bold]{test_description}[/bold]")
960
+
961
+ # Phase 4.2: Structured summary
962
+ artifacts = results.get('artifacts', {})
963
+ comprehensive_data = results.get('comprehensive_data', {})
964
+
965
+ console.print(f"\n📊 Captured:")
966
+ console.print(f" • Elements: {len(comprehensive_data.get('dom_analysis', {}).get('elements', []))}")
967
+ console.print(f" • Network requests: {len(comprehensive_data.get('network_data', {}).get('all_network_events', []))}")
968
+ console.print(f" • Console messages: {len(comprehensive_data.get('console_data', {}).get('console_logs', []))}")
969
+ console.print(f" • Screenshots: {len(artifacts.get('screenshots', []))}")
970
+
971
+ # Phase 4.1: Console messages display
972
+ console_data = comprehensive_data.get('console_data', {})
973
+ console_logs = console_data.get('console_logs', [])
974
+
975
+ if console_logs and (show_console or show_all_console):
976
+ errors = [log for log in console_logs if log.get('type') == 'error']
977
+ warnings = [log for log in console_logs if log.get('type') == 'warning']
978
+ logs = [log for log in console_logs if log.get('type') == 'log']
979
+
980
+ if errors:
981
+ console.print(f"\n[red]❌ Console Errors ({len(errors)}):[/red]")
982
+ for error in errors[:5]: # Show first 5
983
+ console.print(f" [red]{error.get('text', 'Unknown error')}[/red]")
984
+
985
+ if warnings:
986
+ console.print(f"\n[yellow]⚠️ Console Warnings ({len(warnings)}):[/yellow]")
987
+ for warning in warnings[:3]: # Show first 3
988
+ console.print(f" [yellow]{warning.get('text', 'Unknown warning')}[/yellow]")
989
+
990
+ if show_all_console and logs:
991
+ console.print(f"\n[blue]ℹ️ Console Logs ({len(logs)}):[/blue]")
992
+ for log in logs[:5]: # Show first 5
993
+ console.print(f" [blue]{log.get('text', 'Unknown log')}[/blue]")
994
+
995
+ # Network summary
996
+ network_data = comprehensive_data.get('network_data', {})
997
+ network_summary = network_data.get('network_summary', {})
998
+
999
+ failed_requests = network_summary.get('failed_requests', 0)
1000
+ if failed_requests > 0:
1001
+ console.print(f"\n[yellow]🌐 Network Issues ({failed_requests} failed requests):[/yellow]")
1002
+ failed = network_data.get('failed_requests', {}).get('requests', [])
1003
+ for req in failed[:3]:
1004
+ console.print(f" [yellow]{req.get('method')} {req.get('url')} → {req.get('status')}[/yellow]")
1005
+
640
1006
  def display_test_results(results: Dict):
641
- """Display test results in rich format"""
1007
+ """Display test results in rich format (legacy)"""
642
1008
 
643
1009
  # Summary table
644
1010
  table = Table(title="Test Results Summary")