Files
StreamLens/textual_test_framework.py

268 lines
8.5 KiB
Python

#!/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())