import pytest import asyncio from pathlib import Path from unittest.mock import MagicMock, AsyncMock, patch from textual.app import App from src.ui.main_app import ReaperApp, ConfigSelectionScreen, ConfigScreen from src.ui.mode_screen import ModeScreen from src.core.configuration import AppConfig import os from textual.widgets import ListItem, ListView, Input, Button @pytest.fixture def mock_configs(tmp_path, log): reaper_dir = tmp_path / "ReaperFiles-TestConfig" reaper_dir.mkdir() (reaper_dir / "reaper_config.yaml").write_text("discord_bot_token: 'fake'\ndiscord_server_id: '123'\ntool_mode: 'backup_only'") old_cwd = os.getcwd() os.chdir(tmp_path) log(f"CWD changed to: {tmp_path}") yield tmp_path os.chdir(old_cwd) async def wait_for_screen(app, screen_class, timeout=5.0): import time start = time.time() while time.time() - start < timeout: if isinstance(app.screen, screen_class): return True await asyncio.sleep(0.1) return False @pytest.mark.asyncio async def test_ui_minimal_launch(mock_configs, log): """Verify app launch and screen transition to ModeScreen.""" log("Running test_ui_minimal_launch") try: # Reverting to AsyncMock to avoid WorkerError. # RuntimeWarnings are now handled globally in conftest.py. with patch("src.ui.main_app.ConfigSelectionScreen.check_updates", AsyncMock()): app = ReaperApp() async with app.run_test() as pilot: await wait_for_screen(app, ConfigSelectionScreen) await pilot.click(ListItem) await wait_for_screen(app, ModeScreen) assert isinstance(app.screen, ModeScreen) log("test_ui_minimal_launch PASSED") except Exception as e: log(f"test_ui_minimal_launch FAILED: {e}") raise @pytest.mark.asyncio async def test_ui_config_wizard_save(mock_configs, log): """Verify configuration editing and saving.""" log("Running test_ui_config_wizard_save") try: with patch("src.ui.main_app.ConfigSelectionScreen.check_updates", AsyncMock()): app = ReaperApp() async with app.run_test() as pilot: await wait_for_screen(app, ConfigSelectionScreen) await pilot.click(ListItem) await wait_for_screen(app, ModeScreen) await pilot.click("#btn_config") await wait_for_screen(app, ConfigScreen) screen = app.screen inp = screen.query_one("#inp_discord_token", Input) inp.value = "new_fake_token" with patch("src.ui.main_app.save_config") as mock_save: await pilot.click("#btn_save") await pilot.pause(0.2) assert mock_save.called log("test_ui_config_wizard_save PASSED") except Exception as e: log(f"test_ui_config_wizard_save FAILED: {e}") raise @pytest.mark.asyncio async def test_ui_operation_trigger(mock_configs, log): """Verify that an operation can be triggered.""" log("Running test_ui_operation_trigger") from src.ui.shuttle_ops import OperationPane from src.ui.modals import ChannelPickerScreen, ProgressScreen try: with patch("src.ui.main_app.ConfigSelectionScreen.check_updates", AsyncMock()): # run_validate is decorated with @work in shuttle_ops.py, so it MUST be a coroutine (AsyncMock) with patch.object(OperationPane, "run_validate", AsyncMock()): app = ReaperApp() async with app.run_test() as pilot: await wait_for_screen(app, ConfigSelectionScreen) await pilot.click(ListItem) await wait_for_screen(app, ModeScreen) pane = app.screen.query_one(OperationPane) pane.tokens_valid = True pane.src_channels = [{"id": 1, "name": "t"}] pane.src_cat_map = {None: "D"} pane.tgt_channels = [{"id": 2, "name": "t"}] pane.tgt_cat_map = {None: "D"} pane.all_tgt_channels = pane.tgt_channels btn = pane.query_one("#op_backup_msgs", Button) btn.disabled = False await pilot.pause(0.2) btn.focus() await pilot.press("enter") await wait_for_screen(app, (ChannelPickerScreen, ProgressScreen)) assert isinstance(app.screen, (ChannelPickerScreen, ProgressScreen)) log("test_ui_operation_trigger PASSED") except Exception as e: log(f"test_ui_operation_trigger FAILED: {e}") raise