#!/usr/bin/env python3 """ Textual Testing Framework - Simplified testing for Textual apps """ import asyncio import sys from pathlib import Path from typing import Optional, Dict, Any, List, Callable from contextlib import asynccontextmanager class TextualTestRunner: """Test runner for Textual applications""" def __init__(self, app_class): self.app_class = app_class self.app = None @asynccontextmanager async def run_app(self, **app_kwargs): """Context manager to run app for testing""" self.app = self.app_class(**app_kwargs) try: # Start the app async with self.app.run_test() as pilot: yield pilot finally: self.app = None async def test_widget_exists(self, selector: str) -> bool: """Test if a widget exists""" if not self.app: return False try: widget = self.app.query_one(selector) return widget is not None except: return False async def test_widget_visible(self, selector: str) -> bool: """Test if a widget is visible""" if not self.app: return False try: widget = self.app.query_one(selector) return widget.visible if widget else False except: return False async def test_widget_text(self, selector: str, expected_text: str) -> bool: """Test if a widget contains expected text""" if not self.app: return False try: widget = self.app.query_one(selector) if hasattr(widget, 'label'): return expected_text in str(widget.label) elif hasattr(widget, 'text'): return expected_text in str(widget.text) return False except: return False async def test_button_count(self, container_selector: str, expected_count: int) -> bool: """Test if a container has expected number of buttons""" if not self.app: return False try: container = self.app.query_one(container_selector) buttons = container.query("Button") return len(buttons) == expected_count except: return False async def simulate_key_press(self, key: str): """Simulate a key press""" if self.app: self.app.action_key(key) await asyncio.sleep(0.1) # Allow time for processing async def simulate_button_click(self, button_selector: str): """Simulate clicking a button""" if not self.app: return False try: button = self.app.query_one(button_selector) if button: button.press() await asyncio.sleep(0.1) return True except: pass return False class TextualTestSuite: """Test suite for organizing Textual tests""" def __init__(self, name: str): self.name = name self.tests = [] self.setup_func = None self.teardown_func = None def setup(self, func): """Decorator for setup function""" self.setup_func = func return func def teardown(self, func): """Decorator for teardown function""" self.teardown_func = func return func def test(self, name: str): """Decorator for test functions""" def decorator(func): self.tests.append((name, func)) return func return decorator async def run(self, runner: TextualTestRunner) -> Dict[str, Any]: """Run all tests in the suite""" results = { 'suite': self.name, 'total': len(self.tests), 'passed': 0, 'failed': 0, 'errors': [], 'details': [] } print(f"๐Ÿงช Running test suite: {self.name}") print("=" * 50) for test_name, test_func in self.tests: try: # Setup if self.setup_func: await self.setup_func(runner) # Run test print(f" Running: {test_name}...", end="") success = await test_func(runner) if success: print(" โœ… PASS") results['passed'] += 1 else: print(" โŒ FAIL") results['failed'] += 1 results['details'].append({ 'name': test_name, 'passed': success, 'error': None }) # Teardown if self.teardown_func: await self.teardown_func(runner) except Exception as e: print(f" ๐Ÿ’ฅ ERROR: {e}") results['failed'] += 1 results['errors'].append(f"{test_name}: {e}") results['details'].append({ 'name': test_name, 'passed': False, 'error': str(e) }) return results def create_sample_test_suite(): """Create a sample test suite for the StreamLens app""" suite = TextualTestSuite("StreamLens Button Tests") @suite.test("Overview button exists") async def test_overview_button(runner): async with runner.run_app() as pilot: return await runner.test_widget_exists("#btn-overview") @suite.test("Overview button has correct text") async def test_overview_button_text(runner): async with runner.run_app() as pilot: return await runner.test_widget_text("#btn-overview", "Overview") @suite.test("Filter bar contains buttons") async def test_filter_bar_buttons(runner): async with runner.run_app() as pilot: # Allow time for buttons to be created await asyncio.sleep(1) return await runner.test_button_count("#filter-bar", 1) # At least overview button @suite.test("Key press navigation works") async def test_key_navigation(runner): async with runner.run_app() as pilot: await runner.simulate_key_press("1") await asyncio.sleep(0.5) # Check if overview is selected (would need app-specific logic) return True # Placeholder return suite async def main(): print("๐Ÿงช Textual Testing Framework") print("=" * 50) # Example usage with StreamLens try: from analyzer.tui.textual.app_v2 import StreamLensAppV2 runner = TextualTestRunner(StreamLensAppV2) suite = create_sample_test_suite() results = await suite.run(runner) print(f"\n๐Ÿ“Š Test Results for {results['suite']}:") print(f" Total: {results['total']}") print(f" Passed: {results['passed']}") print(f" Failed: {results['failed']}") if results['errors']: print(f"\nโŒ Errors:") for error in results['errors']: print(f" {error}") if results['passed'] == results['total']: print(f"\n๐ŸŽ‰ All tests passed!") else: print(f"\nโš ๏ธ {results['failed']} tests failed") except ImportError: print("StreamLens app not found. Here's how to use this framework:") print("\n1. Import your Textual app class") print("2. Create a TextualTestRunner with your app class") print("3. Create test suites with TextualTestSuite") print("4. Run tests with suite.run(runner)") print(f"\n๐Ÿ“ Example usage:") print(f""" from your_app import YourTextualApp from textual_test_framework import TextualTestRunner, TextualTestSuite async def run_tests(): runner = TextualTestRunner(YourTextualApp) suite = TextualTestSuite("My Tests") @suite.test("Widget exists") async def test_widget(runner): async with runner.run_app() as pilot: return await runner.test_widget_exists("#my-widget") results = await suite.run(runner) print(f"Passed: {{results['passed']}}/{{results['total']}}") asyncio.run(run_tests()) """) if __name__ == "__main__": asyncio.run(main())