Add DemoCraftClient for complete offline demo mode
- DemoCraftClient in demo.py: duck-typed CraftClient replacement with 8 canned satellites, synthetic pass predictions, and time-varying LEO arcs that drive real AOS/TCA/LOS pass events to the camera overlay - Fix sky map EL signal fidelity: snap _el to _target_el at sweep start so 2D scans compute correct per-row RSSI (was using stale elevation) - Branch on demo_mode in app.py _setup_craft_client() to inject DemoCraftClient instead of the HTTP CraftClient - Add test_demo_craft.py: 5 tests exercising search, passes, tracking, and WAITING state through the full TUI without mocks - Update take_screenshots.py to cover all 8 screens (dashboard, control, craft search, craft tracking, signal, system, console, camera)
This commit is contained in:
parent
7035d814a1
commit
3013eeee4c
4 changed files with 518 additions and 13 deletions
164
tui/tests/test_demo_craft.py
Normal file
164
tui/tests/test_demo_craft.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue