"""Test DemoCraftClient — full Craft lifecycle in demo mode. Unlike test_craft_mode.py which uses MagicMock, these tests run with the real DemoCraftClient to verify the end-to-end demo experience: search, pass predictions, and tracking with time-varying satellite positions. No network calls are made. """ import asyncio import pytest from birdcage_tui.app import BirdcageApp from birdcage_tui.screens.control import ControlScreen from birdcage_tui.widgets.craft_panel import CraftPanel, CraftTrackingStatus async def _switch_to_craft(pilot, app) -> None: """Navigate to F2 Control > Craft sub-mode.""" await pilot.press("f2") await pilot.pause() control = app.query_one("#control", ControlScreen) control.switch_mode("craft") await pilot.pause() @pytest.mark.asyncio async def test_demo_search_returns_results(): """DemoCraftClient search should populate the DataTable.""" app = BirdcageApp() app.demo_mode = True async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await _switch_to_craft(pilot, app) panel = app.query_one("#ctrl-craft-panel", CraftPanel) panel.post_message(CraftPanel.SearchRequested("ISS")) await asyncio.sleep(0.5) from textual.widgets import DataTable table = app.query_one("#craft-results-table", DataTable) assert table.row_count >= 1 first_key = list(table.rows.keys())[0] row = table.get_row(first_key) assert "ISS" in row[0] @pytest.mark.asyncio async def test_demo_broad_search(): """Empty query should return the full demo catalog.""" app = BirdcageApp() app.demo_mode = True async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await _switch_to_craft(pilot, app) panel = app.query_one("#ctrl-craft-panel", CraftPanel) panel.post_message(CraftPanel.SearchRequested("")) await asyncio.sleep(0.5) from textual.widgets import DataTable table = app.query_one("#craft-results-table", DataTable) assert table.row_count == 8 # Full demo catalog @pytest.mark.asyncio async def test_demo_passes(): """DemoCraftClient should return synthetic pass predictions.""" app = BirdcageApp() app.demo_mode = True async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await _switch_to_craft(pilot, app) control = app.query_one("#control", ControlScreen) panel = app.query_one("#ctrl-craft-panel", CraftPanel) panel.post_message(CraftPanel.PassesRequested(norad_id=25544)) await asyncio.sleep(0.5) from birdcage_tui.widgets.craft_panel import CraftPassInfo info = app.query_one("#craft-pass-info", CraftPassInfo) # Should contain max elevation of 55.0 degrees (ISS arc) assert "55.0" in info.passes_text @pytest.mark.asyncio async def test_demo_tracking_drives_device(): """DemoCraftClient tracking should move the demo device.""" app = BirdcageApp() app.demo_mode = True async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await _switch_to_craft(pilot, app) panel = app.query_one("#ctrl-craft-panel", CraftPanel) # Track the Moon — always above horizon in demo panel.post_message( CraftPanel.TrackRequested( target_type="celestial", target_id="moon", name="Moon", min_el=5.0, ) ) await asyncio.sleep(2.5) status = app.query_one("#craft-tracking-status", CraftTrackingStatus) assert status.state == "TRACKING" assert status.moves >= 1 # Device should have been commanded toward the Moon's position assert app.device._target_az > 100.0 # Moon is around AZ 145 assert app.device._target_el > 20.0 # Moon is around EL 32 # Screenshot for visual verification app.save_screenshot("/tmp/birdcage_demo_craft_tracking.svg") # Stop tracking control = app.query_one("#control", ControlScreen) control._stop_craft_tracking() await pilot.pause() @pytest.mark.asyncio async def test_demo_tracking_waiting_below_horizon(): """Tracking a below-horizon LEO target shows WAITING.""" app = BirdcageApp() app.demo_mode = True async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await _switch_to_craft(pilot, app) panel = app.query_one("#ctrl-craft-panel", CraftPanel) # SO-50 may be below horizon depending on timing. # Use Jupiter at min_el=90 to guarantee WAITING. panel.post_message( CraftPanel.TrackRequested( target_type="celestial", target_id="jupiter", name="Jupiter", min_el=90.0, # Impossible — forces WAITING ) ) await asyncio.sleep(2.0) status = app.query_one("#craft-tracking-status", CraftTrackingStatus) assert status.state == "WAITING" # Cleanup control = app.query_one("#control", ControlScreen) control._stop_craft_tracking() await pilot.pause()