491 lines
16 KiB
Python
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 == '运维详情'
|