268 lines
8.5 KiB
Python
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()) |