feat #14: Implement design guide (#15)

Reviewed-on: #15
Co-authored-by: phg <mail@philip-henning.com>
Co-committed-by: phg <mail@philip-henning.com>
This commit is contained in:
Philip Henning 2026-09-04 17:15:39 +00:00 committed by Philip Henning
parent 9f8e9c3415
commit 0b7b52521f
19 changed files with 1507 additions and 723 deletions

View file

@ -5,13 +5,14 @@ from unittest.mock import Mock, patch
import pytest
from textual.app import SuspendNotSupported
from textual.widgets import HelpPanel, RadioButton, Static
from textual.widgets import Input, RadioButton, Static
from src.hosts.core.filters import FilterOptions
from src.hosts.core.models import HostEntry, HostsFile
from src.hosts.tui.app import HostsManagerApp
from src.hosts.tui.custom_footer import CustomFooter
from src.hosts.tui.filter_modal import FilterModal
from src.hosts.tui.help_modal import HelpModal
@contextmanager
@ -45,16 +46,13 @@ async def test_filter_shortcut_opens_modal_and_applies_the_selected_filter():
async with app.run_test() as pilot:
footer = app.query_one("#custom-footer", CustomFooter)
footer_text = footer.query_one("#footer-right", Static).render()
assert "ctrl+f Filter entries" in str(footer_text)
assert "ctrl+f Filters" in str(footer_text)
app.action_help()
await pilot.pause()
assert isinstance(app.query_one(HelpPanel), HelpPanel)
assert any(
binding.action == "show_filters" and binding.description == "Filter entries"
for _, binding, _, _ in app.screen.active_bindings.values()
)
app.action_help()
assert isinstance(app.screen, HelpModal)
assert app.screen.query_one("#help-close") is not None
await pilot.press("escape")
await pilot.pause()
await pilot.press("ctrl+f")
@ -72,6 +70,71 @@ async def test_filter_shortcut_opens_modal_and_applies_the_selected_filter():
]
@pytest.mark.asyncio
async def test_filter_shortcut_does_not_stack_filter_modals():
"""Repeated filter shortcuts keep the existing filter modal in focus."""
app = app_with_filterable_entries()
async with app.run_test() as pilot:
await pilot.press("ctrl+f")
await pilot.pause()
await pilot.press("ctrl+f")
await pilot.press("ctrl+f")
await pilot.pause()
assert isinstance(app.screen, FilterModal)
assert sum(isinstance(screen, FilterModal) for screen in app.screen_stack) == 1
@pytest.mark.asyncio
async def test_help_overlay_closes_to_the_control_that_opened_it():
"""Help is an overlay and returns keyboard focus to its opener."""
app = app_with_filterable_entries()
async with app.run_test(size=(120, 40)) as pilot:
search = app.query_one("#search-input", Input)
search.focus()
app.action_help()
await pilot.pause()
assert isinstance(app.screen, HelpModal)
await pilot.press("escape")
await pilot.pause()
assert app.focused is search
@pytest.mark.asyncio
async def test_filter_modal_tabs_through_controls_in_visual_order():
"""The modal starts at Presets and tabs through the visible controls in order."""
app = app_with_filterable_entries()
async with app.run_test() as pilot:
await pilot.press("ctrl+f")
await pilot.pause()
assert app.focused is app.screen.query_one("#preset-select")
for expected_id in (
"load-preset",
"save-preset",
"delete-preset",
"status-filter-type",
"type-filter-type",
"resolution-filter-type",
"search-term",
"search-hostnames",
"search-comments",
"search-ips",
"search-case-sensitive",
"cancel",
"reset",
"apply",
):
await pilot.press("tab")
assert app.focused is app.screen.query_one(f"#{expected_id}")
@pytest.mark.asyncio
async def test_filter_reset_clears_filters_and_cancel_preserves_applied_filters():
"""Reset clears the form on Apply, while Cancel leaves the applied filter alone."""
@ -103,6 +166,79 @@ async def test_filter_reset_clears_filters_and_cancel_preserves_applied_filters(
]
@pytest.mark.asyncio
async def test_workspace_uses_read_only_detail_rows_and_hides_irrelevant_dns_fields():
"""An IP Host Entry has compact text details instead of disabled controls."""
app = app_with_filterable_entries()
async with app.run_test(size=(120, 40)) as pilot:
app.table_handler.populate_entries_table()
app.details_handler.update_entry_details()
await pilot.pause()
details = app.query_one("#entry-details-display")
assert not list(details.query(Input))
assert "192.0.2.1" in str(app.query_one("#details-ip-input", Static).render())
assert "Active" in str(
app.query_one("#details-active-checkbox", Static).render()
)
assert app.query_one("#details-dns-rows").has_class("hidden")
@pytest.mark.asyncio
async def test_workspace_exposes_dns_and_protection_details_and_durable_state():
"""DNS and Default Entry states remain explicit independently of colour."""
app = app_with_filterable_entries()
app.hosts_file.entries[0].dns_name = "active.example"
app.hosts_file.entries[0].dns_resolution_status = "resolved"
app.hosts_file.entries[0].resolved_ip = "192.0.2.1"
app.current_filter_options.active_only = True
app.current_filter_options.show_inactive = False
async with app.run_test(size=(100, 30)) as pilot:
app.table_handler.populate_entries_table()
app.details_handler.update_entry_details()
app._update_footer_status()
await pilot.pause()
assert not app.query_one("#details-dns-rows").has_class("hidden")
assert "Resolved" in str(
app.query_one("#details-dns-status-input", Static).render()
)
footer = app.query_one("#custom-footer", CustomFooter)
assert "Filters: 1" in footer._status_text
assert "READ-ONLY" in footer._status_text
assert app.has_class("compact-layout")
@pytest.mark.asyncio
async def test_small_viewport_replaces_workspace_with_an_explicit_safe_message():
"""A viewport below the contract cannot expose clipped mutating controls."""
app = app_with_filterable_entries()
async with app.run_test(size=(80, 24)) as pilot:
await pilot.pause()
assert not app.query_one("#workspace").display
minimum_size = app.query_one("#minimum-size", Static)
assert "requires at least 100 columns × 30 rows" in str(minimum_size.render())
@pytest.mark.asyncio
async def test_small_viewport_blocks_hidden_mutation_shortcuts():
"""A minimum-size presentation cannot open a hidden editor or privilege flow."""
app = app_with_filterable_entries()
app.push_screen = Mock()
app.manager = Mock()
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("n", "ctrl+e")
await pilot.pause()
app.push_screen.assert_not_called()
app.manager.enter_edit_mode.assert_not_called()
class TestPrivilegedModeAuthorization:
"""Test the user-visible privileged-mode authorization flow."""