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

395 lines
14 KiB
Python

from types import SimpleNamespace
from typing import Any
import pytest
from pytest import MonkeyPatch
def test_detail_screen_updates_section_detail_view(tui_modules: SimpleNamespace) -> None:
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='Jobs',
subtitle='subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='Section A', status='ok', lines=['line a']),
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='Section B', status='warn', lines=['line b']),
],
)
screen = tui_modules.cli_tui_detail.DetailScreen(
snapshot,
env='dev',
active_view='jobs',
navigation_items=tui_modules.cli_tui_app.NAVIGATION_ITEMS,
refreshed_at='2026-04-30 10:00:00',
)
recorded_sections: list[object] = []
class FakeSectionDetailView:
def show_section(self, section: object, query: str = '') -> None:
del query
recorded_sections.append(section)
screen.query_one = lambda widget_type: FakeSectionDetailView() # type: ignore[method-assign]
screen._update_selected_section(1)
assert screen.selected_section_index == 1
assert recorded_sections
assert recorded_sections[0].title == 'Section B'
def test_detail_screen_search_submit_remembers_and_refreshes(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='数据库',
subtitle='subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='历史版本', status='ok', lines=['line a']),
],
search=tui_modules.cli_tui_search.PageSearchContext(
placeholder='按 revision 搜索',
query='',
suggestions=['head', 'base'],
),
)
screen = tui_modules.cli_tui_detail.DetailScreen(
snapshot,
env='dev',
active_view='database',
navigation_items=tui_modules.cli_tui_app.NAVIGATION_ITEMS,
refreshed_at='2026-04-30 10:00:00',
)
recorded_queries: list[tuple[str, str]] = []
refresh_calls: list[str] = []
fake_app = SimpleNamespace(
remember_browser_query=lambda view_key, query: recorded_queries.append((view_key, query)),
action_refresh_current_view=lambda: refresh_calls.append('refresh'),
)
monkeypatch.setattr(tui_modules.cli_tui_detail.DetailScreen, 'app', property(lambda self: fake_app))
screen._handle_search_submitted('head')
assert recorded_queries == [('database', 'head')]
assert refresh_calls == ['refresh']
def test_detail_screen_clear_search_remembers_empty_and_refreshes(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='数据库',
subtitle='subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='历史版本', status='ok', lines=['line a']),
],
search=tui_modules.cli_tui_search.PageSearchContext(
placeholder='按 revision 搜索',
query='head',
suggestions=['head', 'base'],
),
)
screen = tui_modules.cli_tui_detail.DetailScreen(
snapshot,
env='dev',
active_view='database',
navigation_items=tui_modules.cli_tui_app.NAVIGATION_ITEMS,
refreshed_at='2026-04-30 10:00:00',
)
recorded_queries: list[tuple[str, str]] = []
refresh_calls: list[str] = []
fake_app = SimpleNamespace(
remember_browser_query=lambda view_key, query: recorded_queries.append((view_key, query)),
action_refresh_current_view=lambda: refresh_calls.append('refresh'),
)
monkeypatch.setattr(tui_modules.cli_tui_detail.DetailScreen, 'app', property(lambda self: fake_app))
screen.action_clear_search()
assert recorded_queries == [('database', '')]
assert refresh_calls == ['refresh']
@pytest.mark.asyncio
async def test_detail_screen_execute_external_action_suspends_app(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='数据库',
subtitle='subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='迁移版本', status='ok', lines=['line a']),
],
)
screen = tui_modules.cli_tui_detail.DetailScreen(
snapshot,
env='dev',
active_view='database',
navigation_items=tui_modules.cli_tui_app.NAVIGATION_ITEMS,
refreshed_at='2026-04-30 10:00:00',
)
action = tui_modules.cli_tui_detail.TUI_ACTION_REGISTRY.resolve_detail_action(
view_key='database',
slot='global',
env='dev',
)
assert action is not None
recorded_suspend: list[str] = []
class DummySuspend:
def __enter__(self) -> None:
recorded_suspend.append('enter')
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: Any,
) -> None:
del exc_type, exc, tb
recorded_suspend.append('exit')
def build_suspend() -> DummySuspend:
return DummySuspend()
def refresh_current_view() -> None:
pass
fake_app = SimpleNamespace(
suspend=build_suspend,
action_refresh_current_view=refresh_current_view,
)
monkeypatch.setattr(tui_modules.cli_tui_detail.DetailScreen, 'app', property(lambda self: fake_app))
monkeypatch.setattr(
tui_modules.cli_tui_interactions,
'TUI_ACTION_EXECUTION_SERVICE',
SimpleNamespace(
execute_external=lambda spec: tui_modules.cli_tui_detail.TuiActionResult(
spec=spec,
external_exit_code=0,
external_message='外部交互命令已执行完成',
),
build_result_lines=lambda result: [],
),
)
monkeypatch.setattr(screen, 'notify', lambda *args, **kwargs: None)
await screen._execute_action(action)
assert recorded_suspend == ['enter', 'exit']
@pytest.mark.asyncio
async def test_detail_screen_execute_nested_json_action_without_suspend(
monkeypatch: MonkeyPatch,
tui_modules: SimpleNamespace,
) -> None:
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='传输加密',
subtitle='subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='运行校验', status='ok', lines=['line a']),
],
)
screen = tui_modules.cli_tui_detail.DetailScreen(
snapshot,
env='dev',
active_view='crypto',
navigation_items=tui_modules.cli_tui_app.NAVIGATION_ITEMS,
refreshed_at='2026-04-30 10:00:00',
)
action = tui_modules.cli_tui_detail.TUI_ACTION_REGISTRY.resolve_detail_action(
view_key='crypto',
slot='global',
env='dev',
)
assert action is not None
assert action.execution_mode == 'nested_json'
suspend_calls: list[str] = []
execute_calls: list[tuple[object, str]] = []
class DummySuspend:
def __enter__(self) -> None:
suspend_calls.append('enter')
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: Any,
) -> None:
del exc_type, exc, tb
suspend_calls.append('exit')
def build_suspend() -> DummySuspend:
return DummySuspend()
def refresh_current_view() -> None:
pass
fake_app = SimpleNamespace(
suspend=build_suspend,
action_refresh_current_view=refresh_current_view,
)
monkeypatch.setattr(tui_modules.cli_tui_detail.DetailScreen, 'app', property(lambda self: fake_app))
monkeypatch.setattr(
tui_modules.cli_tui_interactions,
'TUI_ACTION_EXECUTION_SERVICE',
SimpleNamespace(
execute=lambda spec, env: (
execute_calls.append((spec, env))
or tui_modules.cli_tui_detail.TuiActionResult(
spec=spec,
payload={'ok': True, 'message': '轮换预演完成'},
)
),
build_result_lines=lambda result: [],
),
)
monkeypatch.setattr(screen, 'notify', lambda *args, **kwargs: None)
await screen._execute_action(action)
assert suspend_calls == []
assert execute_calls == [(action, 'dev')]
def test_detail_screen_support_builds_summary_actions_and_fallbacks(
tui_modules: SimpleNamespace,
) -> None:
support = tui_modules.cli_tui_detail.DetailScreenSupport()
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='数据库',
subtitle='subtitle',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='迁移版本', status='ok', lines=['line a']),
tui_modules.cli_tui_adapters.DetailSectionSnapshot(title='Heads 状态', status='warn', lines=['line b']),
],
search=tui_modules.cli_tui_search.PageSearchContext(
placeholder='按 revision 搜索',
query='head',
suggestions=['head'],
),
)
summary = support.build_summary_text(snapshot, 'database')
query = support.current_search_query(snapshot)
action = support.resolve_action(active_view='database', slot='global', env='dev')
empty_section = support.build_empty_section()
assert '2 个分区' in summary
assert query == 'head'
assert action is not None
assert action.command_args[:2] == ('wizard', 'db-upgrade')
assert empty_section.title == tui_modules.cli_tui_copy.TUI_COPY.build_detail_empty_section_copy('title')
def test_detail_screen_focus_service_moves_and_scrolls_between_focus_targets(
tui_modules: SimpleNamespace,
) -> None:
focus_service = tui_modules.cli_tui_detail.DetailScreenFocusService()
focused: list[str] = []
sidebar = SimpleNamespace(focus=lambda: focused.append('sidebar'))
sections = SimpleNamespace(focus=lambda: focused.append('sections'))
detail = SimpleNamespace(focus=lambda: focused.append('detail'))
workspace_main = object()
fake_app = SimpleNamespace(focused=None)
fake_screen = SimpleNamespace(
app=fake_app,
query_one=lambda selector, widget_type=None: {
tui_modules.cli_tui_widgets.WorkspaceSidebar: sidebar,
tui_modules.cli_tui_widgets.SectionNavigator: sections,
tui_modules.cli_tui_widgets.SectionDetailView: detail,
'#workspace-main': workspace_main,
}[selector],
)
focus_service.move_focus(fake_screen, 1)
fake_app.focused = sections
focus_service.move_focus(fake_screen, 1)
fake_app.focused = object()
scroll_target = focus_service.get_scroll_target(fake_screen)
assert focused == ['sidebar', 'detail']
assert scroll_target is workspace_main
def test_detail_screen_unmount_cancels_action_task(tui_modules: SimpleNamespace) -> None:
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='数据库',
subtitle='subtitle',
sections=[],
)
screen = tui_modules.cli_tui_detail.DetailScreen(
snapshot,
env='dev',
active_view='database',
navigation_items=tui_modules.cli_tui_app.NAVIGATION_ITEMS,
refreshed_at='2026-04-30 10:00:00',
)
cancelled: list[str] = []
class DummyTask:
def done(self) -> bool:
return False
def cancel(self) -> None:
cancelled.append('action')
screen._action_task = DummyTask() # type: ignore[assignment]
screen.on_unmount()
assert cancelled == ['action']
@pytest.mark.asyncio
async def test_detail_screen_scrolls_in_small_viewport(tui_modules: SimpleNamespace) -> None:
thin_scrollbar_size = 1
app = tui_modules.cli_tui_app.ShierfengTuiApp('dev')
app.action_show_dashboard = lambda: None # type: ignore[method-assign]
app.current_view = 'app'
snapshot = tui_modules.cli_tui_app.DetailPageSnapshot(
title='应用',
subtitle='超长详情',
sections=[
tui_modules.cli_tui_adapters.DetailSectionSnapshot(
title='配置总览',
status='warn',
lines=[f'{i:02d}行 内容很长 内容很长 内容很长 内容很长' for i in range(1, 40)],
)
],
)
screen = app.screen_factory.build(
snapshot=snapshot,
env=app.env,
active_view=app.current_view,
navigation_items=tui_modules.cli_tui_app.NAVIGATION_ITEMS,
action_feedback_lines=app.get_action_feedback_lines(app.current_view),
)
async with app.run_test(size=(80, 24)) as pilot:
app.screen_navigator.show(screen)
await pilot.pause()
await pilot.pause()
main = app.screen.query_one('#workspace-main')
detail_body = app.screen.query_one('#detail-body')
detail_view = app.screen.query_one(tui_modules.cli_tui_widgets.SectionDetailView)
assert main.styles.scrollbar_size_vertical == thin_scrollbar_size
assert main.styles.scrollbar_size_horizontal == thin_scrollbar_size
assert main.virtual_size.height > main.size.height
assert detail_body.styles.scrollbar_size_vertical == thin_scrollbar_size
assert detail_body.styles.scrollbar_size_horizontal == thin_scrollbar_size
assert detail_view.styles.scrollbar_size_vertical == thin_scrollbar_size
assert detail_view.styles.scrollbar_size_horizontal == thin_scrollbar_size