from pathlib import Path import pytest from app.navigator import Navigator, VisibleEntry from app.ui import ( RenderLine, build_render_lines, format_entry, move_selection, open_terminal_streams, ) def test_move_selection_stays_within_bounds() -> None: assert move_selection(current_index=0, step=-1, entry_count=3) == 0 assert move_selection(current_index=1, step=1, entry_count=3) == 2 assert move_selection(current_index=2, step=1, entry_count=3) == 2 def test_format_entry_supports_parent_and_tree_indicators(tmp_path: Path) -> None: parent_entry = VisibleEntry( name="..", path=tmp_path, depth=0, has_children=True, is_expanded=False, is_parent=True, ) collapsed_entry = VisibleEntry( name="scripts", path=tmp_path / "scripts", depth=0, has_children=True, is_expanded=False, ) expanded_child = VisibleEntry( name="build", path=tmp_path / "scripts" / "build", depth=1, has_children=False, is_expanded=False, ) assert format_entry(parent_entry) == ".." assert format_entry(collapsed_entry) == "▶ scripts" assert format_entry(expanded_child) == " build" def test_build_render_lines_marks_selected_entry(tmp_path: Path) -> None: workspace = tmp_path / "workspace" workspace.mkdir() alpha = workspace / "alpha" beta = workspace / "beta" alpha.mkdir() beta.mkdir() navigator = Navigator(workspace) navigator.set_selected_index(2) assert build_render_lines(navigator) == [ RenderLine(text="..", is_selected=False), RenderLine(text=" alpha", is_selected=False), RenderLine(text=" beta", is_selected=True), ] def test_open_terminal_streams_raises_without_terminal( monkeypatch: pytest.MonkeyPatch, ) -> None: def raise_os_error(*args: object, **kwargs: object) -> object: raise OSError monkeypatch.setattr("builtins.open", raise_os_error) monkeypatch.setattr("sys.stdin.isatty", lambda: False) monkeypatch.setattr("sys.stderr.isatty", lambda: False) with pytest.raises(RuntimeError, match="Interactive terminal is required"): with open_terminal_streams(): pass