Migrate TUI to design guide
This commit is contained in:
parent
3ce7ba8d5c
commit
563ec7c705
14 changed files with 741 additions and 442 deletions
|
|
@ -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")
|
||||
|
|
@ -89,6 +87,24 @@ async def test_filter_shortcut_does_not_stack_filter_modals():
|
|||
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."""
|
||||
|
|
@ -111,6 +127,8 @@ async def test_filter_modal_tabs_through_controls_in_visual_order():
|
|||
"search-comments",
|
||||
"search-ips",
|
||||
"search-case-sensitive",
|
||||
"cancel",
|
||||
"reset",
|
||||
"apply",
|
||||
):
|
||||
await pilot.press("tab")
|
||||
|
|
@ -148,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."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue