"""Test camera overlay — F6 toggle, manual capture, trigger controls. Uses the same testing pattern as test_craft_mode.py: - app.demo_mode = True for DemoDevice - async with app.run_test() for headless rendering - post_message() for button events (avoids coordinate issues) Note: ModalScreen widgets are on the screen stack, not the app's regular DOM. Use app.screen to get the active overlay, then query within it. """ import asyncio from pathlib import Path import pytest from birdcage_tui.app import BirdcageApp from birdcage_tui.screens.camera import CameraOverlay, CaptureStatusPanel @pytest.mark.asyncio async def test_f6_opens_camera_overlay(tmp_path: Path): """F6 should push the camera overlay.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() assert not app._camera_visible await pilot.press("f6") await pilot.pause() assert app._camera_visible # The top screen should be the camera overlay assert isinstance(app.screen, CameraOverlay) @pytest.mark.asyncio async def test_f6_closes_camera_overlay(tmp_path: Path): """F6 again should dismiss the camera overlay.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() assert app._camera_visible await pilot.press("f6") await pilot.pause() assert not app._camera_visible @pytest.mark.asyncio async def test_escape_closes_camera_overlay(tmp_path: Path): """Escape should dismiss the camera overlay.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() assert app._camera_visible await pilot.press("escape") await pilot.pause() assert not app._camera_visible @pytest.mark.asyncio async def test_manual_capture_creates_file(tmp_path: Path): """Pressing 'c' should create a capture file.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() # Trigger manual capture await pilot.press("c") await asyncio.sleep(0.5) # Check captures directory capture_dir = tmp_path / "captures" jpg_files = list(capture_dir.rglob("*.jpg")) json_files = list(capture_dir.rglob("*.json")) assert len(jpg_files) >= 1 assert len(json_files) >= 1 @pytest.mark.asyncio async def test_capture_button(tmp_path: Path): """Capture button should trigger a capture.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() # Get the overlay from the screen stack overlay = app.screen assert isinstance(overlay, CameraOverlay) from textual.widgets import Button btn = overlay.query_one("#btn-capture", Button) btn.post_message(Button.Pressed(btn)) await asyncio.sleep(0.5) capture_dir = tmp_path / "captures" jpg_files = list(capture_dir.rglob("*.jpg")) assert len(jpg_files) >= 1 @pytest.mark.asyncio async def test_capture_log_updates(tmp_path: Path): """Capture log should show entries after captures.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() # Two captures await pilot.press("c") await asyncio.sleep(0.3) await pilot.press("c") await asyncio.sleep(0.3) # Status panel should show count overlay = app.screen assert isinstance(overlay, CameraOverlay) panel = overlay.query_one("#capture-status", CaptureStatusPanel) assert panel._capture_count >= 2 @pytest.mark.asyncio async def test_trigger_toggles(tmp_path: Path): """AOS/TCA/LOS trigger buttons should toggle active state.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() overlay = app.screen assert isinstance(overlay, CameraOverlay) from textual.widgets import Button from birdcage_tui.capture_triggers import TriggerType # Toggle AOS on aos_btn = overlay.query_one("#btn-trigger-aos", Button) aos_btn.post_message(Button.Pressed(aos_btn)) await pilot.pause() assert overlay._pass_detector.is_enabled(TriggerType.AOS) # Toggle AOS off aos_btn.post_message(Button.Pressed(aos_btn)) await pilot.pause() assert not overlay._pass_detector.is_enabled(TriggerType.AOS) @pytest.mark.asyncio async def test_auto_interval_start_stop(tmp_path: Path): """Auto interval start/stop should control the timer.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() overlay = app.screen assert isinstance(overlay, CameraOverlay) from textual.widgets import Button # Start auto start_btn = overlay.query_one("#btn-auto-start", Button) start_btn.post_message(Button.Pressed(start_btn)) await pilot.pause() assert overlay._auto_running # Stop auto stop_btn = overlay.query_one("#btn-auto-stop", Button) stop_btn.post_message(Button.Pressed(stop_btn)) await pilot.pause() assert not overlay._auto_running @pytest.mark.asyncio async def test_camera_overlay_with_console(tmp_path: Path): """F5 and F6 should open independent overlays.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() # Open camera first await pilot.press("f6") await pilot.pause() assert app._camera_visible assert not app._console_visible # Close camera await pilot.press("f6") await pilot.pause() assert not app._camera_visible # Open console await pilot.press("f5") await pilot.pause() assert app._console_visible assert not app._camera_visible # Close console await pilot.press("f5") await pilot.pause() assert not app._console_visible @pytest.mark.asyncio async def test_status_panel_shows_formats(tmp_path: Path): """Status panel should list available output formats.""" app = BirdcageApp() app.demo_mode = True app.capture_dir = str(tmp_path / "captures") async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() await pilot.press("f6") await pilot.pause() overlay = app.screen assert isinstance(overlay, CameraOverlay) panel = overlay.query_one("#capture-status", CaptureStatusPanel) # Should always have JPEG + JSON at minimum assert "JPEG" in panel._formats assert "JSON" in panel._formats