395 lines
14 KiB
Python
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
|