ShiErFeng/shierfeng-fastapi-backend/tests/cli/tui/test_tui_command_app.py
2026-07-24 09:43:10 +08:00

491 lines
16 KiB
Python

import importlib
from types import SimpleNamespace
from typing import Any
import pytest
import typer
from pytest import MonkeyPatch
from typer.testing import CliRunner
def test_tui_command_returns_dependency_error_when_textual_missing(
monkeypatch: MonkeyPatch,
tui_base_modules: SimpleNamespace,
) -> None:
runner = CliRunner()
monkeypatch.chdir(tui_base_modules.BACKEND_DIR)
def raise_missing_textual(module_name: str) -> Any:
if module_name == 'cli.tui.app':
raise ModuleNotFoundError("No module named 'textual'", name='textual')
return importlib.import_module(module_name)
monkeypatch.setattr(tui_base_modules.cli_tui_commands, 'import_module', raise_missing_textual)
isolated_cli = typer.Typer()
tui_base_modules.cli_tui_commands.TUI_COMMAND_REGISTRATION.register(isolated_cli)
result = runner.invoke(isolated_cli, ['tui', '--env=dev'])
assert result.exit_code == tui_base_modules.DEPENDENCY_ERROR
assert '当前环境未安装 TUI 可选依赖' in result.stdout
assert 'pip install -r requirements.txt' in result.stdout
def test_tui_app_mount_pushes_first_workspace_screen(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
recorded_calls: list[tuple[str, object]] = []
class FakeDashboardScreen:
def __init__(
self,
snapshot: object,
env: str,
active_view: str,
navigation_items: list[object],
refreshed_at: str,
) -> None:
self.snapshot = snapshot
self.env = env
self.active_view = active_view
self.navigation_items = navigation_items
self.refreshed_at = refreshed_at
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'dashboard',
lambda env: tui_modules.cli_tui_app.DashboardSnapshot(env=env, panels=[]),
)
monkeypatch.setattr(tui_modules.cli_tui_app, 'DashboardScreen', FakeDashboardScreen)
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
monkeypatch.setattr(app, 'push_screen', lambda screen: recorded_calls.append(('push', screen)))
monkeypatch.setattr(app, 'switch_screen', lambda screen: recorded_calls.append(('switch', screen)))
app.on_mount()
assert recorded_calls
assert recorded_calls[0][0] == 'push'
assert isinstance(recorded_calls[0][1], FakeDashboardScreen)
assert recorded_calls[0][1].env == 'dev'
assert recorded_calls[0][1].active_view == 'dashboard'
assert recorded_calls[0][1].navigation_items
assert recorded_calls[0][1].refreshed_at
assert app.current_view == 'dashboard'
assert app.screen_navigator.initialized is True
def test_open_view_switches_to_expected_action(tui_modules: SimpleNamespace) -> None:
calls: list[str] = []
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
def fake_show_view(view_key: str) -> None:
calls.append(tui_modules.cli_tui_app.TUI_VIEW_REGISTRY.resolve_view_key(view_key))
app.show_view = fake_show_view # type: ignore[method-assign]
app.open_view('jobs')
app.open_view('app')
app.open_view('ops')
app.open_view('crypto')
app.open_view('unknown')
assert calls == ['jobs', 'app', 'ops', 'crypto', 'dashboard']
def test_workspace_header_render_includes_particle_line(tui_modules: SimpleNamespace) -> None:
header = tui_modules.cli_tui_widgets.WorkspaceHeader('dev', 'dashboard')
rendered = header.render()
rendered_text = rendered.plain
assert rendered.__class__.__name__ == 'Text'
assert 'ShiErFeng 控制台' in rendered_text
assert '环境 DEV · 页面 总览' in rendered_text
assert any(symbol in rendered_text for symbol in ('', '', '', '', '·', ''))
def test_workspace_header_centers_title_using_runtime_width(tui_modules: SimpleNamespace) -> None:
header = tui_modules.cli_tui_widgets.WorkspaceHeader('dev', 'dashboard')
header.styles.width = 140
first_line = header.render().plain.splitlines()[0]
assert first_line.startswith(' ')
assert 'ShiErFeng 控制台' in first_line
def test_workspace_header_keeps_title_alignment_stable_across_frames(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
header = tui_modules.cli_tui_widgets.WorkspaceHeader('dev', 'dashboard')
monotonic_values = iter([10.0, 10.0, 11.0, 11.0])
workspace_module = importlib.import_module('cli.tui.widgets.workspace')
monkeypatch.setattr(workspace_module, 'monotonic', lambda: next(monotonic_values))
first_line_a = header.render().plain.splitlines()[0]
first_line_b = header.render().plain.splitlines()[0]
assert first_line_a.index('ShiErFeng 控制台') == first_line_b.index('ShiErFeng 控制台')
def test_workspace_hero_pulses_border_with_expected_palette(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
hero = tui_modules.cli_tui_widgets.WorkspaceHero(
title='标题',
subtitle='副标题',
env='dev',
active_view='dashboard',
summary='摘要',
refreshed_at='2026-05-12 12:00:00',
)
workspace_module = importlib.import_module('cli.tui.widgets.workspace')
monkeypatch.setattr(workspace_module, 'monotonic', lambda: 10.0)
original_border_text = str(hero.styles.border)
hero._pulse_border()
border_text = str(hero.styles.border)
assert 'double' in border_text
assert border_text != original_border_text
def test_workspace_hero_render_uses_rich_text_for_title_glow(tui_modules: SimpleNamespace) -> None:
hero = tui_modules.cli_tui_widgets.WorkspaceHero(
title='标题',
subtitle='副标题',
env='dev',
active_view='dashboard',
summary='摘要',
refreshed_at='2026-05-12 12:00:00',
)
rendered = hero._build_render_text()
assert rendered.__class__.__name__ == 'Text'
assert '标题' in rendered.plain
assert '副标题' in rendered.plain
assert '运行摘要 · 摘要' in rendered.plain
def test_tui_app_runner_runs_created_application(tui_modules: SimpleNamespace) -> None:
recorded_envs: list[str] = []
recorded_runs: list[str] = []
class FakeApp:
def __init__(self, env: str) -> None:
recorded_envs.append(env)
def run(self) -> None:
recorded_runs.append('run')
runner = tui_modules.cli_tui_app.TuiAppRunner(FakeApp)
runner.run('dev')
assert recorded_envs == ['dev']
assert recorded_runs == ['run']
def test_tui_app_show_jobs_uses_remembered_filter(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
recorded_filter_keys: list[tuple[str, str]] = []
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
app.remember_browser_filter('jobs', 'failed')
app.remember_browser_query('jobs', 'sync')
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'jobs',
lambda env, filter_key='all', query='': (
recorded_filter_keys.append((filter_key, query))
or tui_modules.cli_tui_app.BrowserPageSnapshot(
title='任务',
subtitle='subtitle',
records=[],
shared_sections=[],
filters=[],
active_filter_key=filter_key,
search=None,
)
),
)
monkeypatch.setattr(app.screen_navigator, 'show', lambda screen: None)
app.action_show_jobs()
assert recorded_filter_keys == [('failed', 'sync')]
def test_tui_app_show_configs_uses_remembered_filter(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
recorded_filter_keys: list[tuple[str, str]] = []
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
app.remember_browser_filter('configs', 'cache-drift')
app.remember_browser_query('configs', 'site')
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'configs',
lambda env, filter_key='all', query='': (
recorded_filter_keys.append((filter_key, query))
or tui_modules.cli_tui_app.BrowserPageSnapshot(
title='参数配置',
subtitle='subtitle',
records=[],
shared_sections=[],
filters=[],
active_filter_key=filter_key,
search=None,
)
),
)
monkeypatch.setattr(app.screen_navigator, 'show', lambda screen: None)
app.action_show_configs()
assert recorded_filter_keys == [('cache-drift', 'site')]
def test_tui_app_show_cache_and_gen_use_remembered_query(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
cache_queries: list[str] = []
gen_queries: list[str] = []
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
app.remember_browser_query('cache', 'sys')
app.remember_browser_query('gen', 'user')
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'cache',
lambda env, query='': (
cache_queries.append(query)
or tui_modules.cli_tui_app.BrowserPageSnapshot(
title='缓存',
subtitle='subtitle',
records=[],
shared_sections=[],
filters=[],
active_filter_key=None,
search=None,
)
),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'gen',
lambda env, query='': (
gen_queries.append(query)
or tui_modules.cli_tui_app.BrowserPageSnapshot(
title='代码生成',
subtitle='subtitle',
records=[],
shared_sections=[],
filters=[],
active_filter_key=None,
search=None,
)
),
)
monkeypatch.setattr(app.screen_navigator, 'show', lambda screen: None)
app.action_show_cache()
app.action_show_gen()
assert cache_queries == ['sys']
assert gen_queries == ['user']
def test_tui_app_detail_views_use_remembered_query(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
database_queries: list[str] = []
app_queries: list[str] = []
ops_queries: list[str] = []
crypto_queries: list[str] = []
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
app.remember_browser_query('database', 'head')
app.remember_browser_query('app', 'route')
app.remember_browser_query('ops', 'disk')
app.remember_browser_query('crypto', 'kid')
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'database',
lambda env, query='': (
database_queries.append(query)
or tui_modules.cli_tui_app.DetailPageSnapshot(title='数据库', subtitle='subtitle', sections=[])
),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'app',
lambda env, query='': (
app_queries.append(query)
or tui_modules.cli_tui_app.DetailPageSnapshot(title='应用', subtitle='subtitle', sections=[])
),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'ops',
lambda env, query='': (
ops_queries.append(query)
or tui_modules.cli_tui_app.DetailPageSnapshot(title='运维', subtitle='subtitle', sections=[])
),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'crypto',
lambda env, query='': (
crypto_queries.append(query)
or tui_modules.cli_tui_app.DetailPageSnapshot(title='传输加密', subtitle='subtitle', sections=[])
),
)
monkeypatch.setattr(app.screen_navigator, 'show', lambda screen: None)
app.action_show_database()
app.action_show_app()
app.action_show_ops()
app.action_show_crypto()
assert database_queries == ['head']
assert app_queries == ['route']
assert ops_queries == ['disk']
assert crypto_queries == ['kid']
@pytest.mark.asyncio
async def test_tui_app_open_view_updates_screen_across_multiple_switches(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'dashboard',
lambda env: tui_modules.cli_tui_app.DashboardSnapshot(env=env, panels=[]),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'app',
lambda env, query='': tui_modules.cli_tui_app.DetailPageSnapshot(
title='应用详情',
subtitle=query or 'app-subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(
title='应用分区',
status='ok',
lines=['app-line'],
)
],
),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'ops',
lambda env, query='': tui_modules.cli_tui_app.DetailPageSnapshot(
title='运维详情',
subtitle=query or 'ops-subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(
title='运维分区',
status='warn',
lines=['ops-line'],
)
],
),
)
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
async with app.run_test(size=(120, 40)) as pilot:
await pilot.pause()
await pilot.pause()
assert type(app.screen).__name__ == 'DashboardScreen'
assert app.current_view == 'dashboard'
app.open_view('app')
await pilot.pause()
await pilot.pause()
assert type(app.screen).__name__ == 'DetailScreen'
assert app.current_view == 'app'
assert app.screen.snapshot.title == '应用详情'
app.open_view('ops')
await pilot.pause()
await pilot.pause()
assert type(app.screen).__name__ == 'DetailScreen'
assert app.current_view == 'ops'
assert app.screen.snapshot.title == '运维详情'
@pytest.mark.asyncio
async def test_tui_sidebar_highlight_updates_screen_across_multiple_switches(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'dashboard',
lambda env: tui_modules.cli_tui_app.DashboardSnapshot(env=env, panels=[]),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'app',
lambda env, query='': tui_modules.cli_tui_app.DetailPageSnapshot(
title='应用详情',
subtitle='app-subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(
title='应用分区',
status='ok',
lines=['app-line'],
)
],
),
)
monkeypatch.setitem(
tui_modules.cli_tui_app.TUI_SNAPSHOT_COLLECTOR_REGISTRY.collectors,
'ops',
lambda env, query='': tui_modules.cli_tui_app.DetailPageSnapshot(
title='运维详情',
subtitle='ops-subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(
title='运维分区',
status='warn',
lines=['ops-line'],
)
],
),
)
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
async with app.run_test(size=(120, 40)) as pilot:
await pilot.pause()
await pilot.pause()
assert type(app.screen).__name__ == 'DashboardScreen'
assert app.current_view == 'dashboard'
await pilot.press('down')
await pilot.pause()
await pilot.pause()
assert type(app.screen).__name__ == 'DetailScreen'
assert app.current_view == 'app'
assert app.screen.snapshot.title == '应用详情'
await pilot.press('down')
await pilot.pause()
await pilot.pause()
assert type(app.screen).__name__ == 'DetailScreen'
assert app.current_view == 'ops'
assert app.screen.snapshot.title == '运维详情'