disco-reaper/tests/test_ui.py
2026-03-30 16:28:59 +05:30

117 lines
4.8 KiB
Python

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