Initial release: cd-browser v0.1.0
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
43
tests/test_cli.py
Normal file
43
tests/test_cli.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.cli import run_cli
|
||||
from app.navigator import Navigator
|
||||
|
||||
|
||||
class StubUI:
|
||||
def __init__(self, result: Path) -> None:
|
||||
self.result = result
|
||||
self.navigator: Navigator | None = None
|
||||
|
||||
def run(self, navigator: Navigator) -> Path:
|
||||
self.navigator = navigator
|
||||
return self.result
|
||||
|
||||
|
||||
def test_run_cli_uses_current_working_directory(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
stub_ui = StubUI(tmp_path)
|
||||
monkeypatch.setattr(Path, "cwd", lambda: tmp_path)
|
||||
|
||||
result = run_cli(ui=stub_ui)
|
||||
|
||||
assert result == tmp_path.resolve()
|
||||
assert stub_ui.navigator is not None
|
||||
assert stub_ui.navigator.current_path == tmp_path.resolve()
|
||||
|
||||
|
||||
def test_run_cli_uses_provided_start_path(tmp_path: Path) -> None:
|
||||
start_path = tmp_path / "workspace"
|
||||
start_path.mkdir()
|
||||
final_path = tmp_path / "final"
|
||||
final_path.mkdir()
|
||||
stub_ui = StubUI(final_path)
|
||||
|
||||
result = run_cli(start_path=start_path, ui=stub_ui)
|
||||
|
||||
assert result == final_path.resolve()
|
||||
assert stub_ui.navigator is not None
|
||||
assert stub_ui.navigator.current_path == start_path.resolve()
|
||||
46
tests/test_filesystem.py
Normal file
46
tests/test_filesystem.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.filesystem import DirectoryEntry, has_subdirectories, list_directories
|
||||
|
||||
|
||||
def test_has_subdirectories_detects_nested_directory(tmp_path: Path) -> None:
|
||||
parent = tmp_path / "parent"
|
||||
child = parent / "child"
|
||||
child.mkdir(parents=True)
|
||||
|
||||
assert has_subdirectories(parent) is True
|
||||
|
||||
|
||||
def test_has_subdirectories_returns_false_without_nested_directories(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
directory = tmp_path / "empty"
|
||||
directory.mkdir()
|
||||
(directory / "file.txt").write_text("content", encoding="utf-8")
|
||||
|
||||
assert has_subdirectories(directory) is False
|
||||
|
||||
|
||||
def test_list_directories_returns_sorted_directory_metadata(tmp_path: Path) -> None:
|
||||
alpha = tmp_path / "alpha"
|
||||
beta = tmp_path / "Beta"
|
||||
file_path = tmp_path / "notes.txt"
|
||||
|
||||
alpha.mkdir()
|
||||
beta.mkdir()
|
||||
(beta / "nested").mkdir()
|
||||
file_path.write_text("ignore me", encoding="utf-8")
|
||||
|
||||
assert list_directories(tmp_path) == [
|
||||
DirectoryEntry(name="alpha", path=alpha, has_children=False),
|
||||
DirectoryEntry(name="Beta", path=beta, has_children=True),
|
||||
]
|
||||
|
||||
|
||||
def test_list_directories_raises_for_missing_directory(tmp_path: Path) -> None:
|
||||
missing_path = tmp_path / "missing"
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
list_directories(missing_path)
|
||||
84
tests/test_history.py
Normal file
84
tests/test_history.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.history import SessionHistory
|
||||
|
||||
|
||||
def test_visit_records_current_directory(tmp_path: Path) -> None:
|
||||
history = SessionHistory()
|
||||
first = tmp_path / "first"
|
||||
first.mkdir()
|
||||
|
||||
visited = history.visit(first)
|
||||
|
||||
assert visited == first.resolve()
|
||||
assert history.current == first.resolve()
|
||||
assert history.entries() == (first.resolve(),)
|
||||
|
||||
|
||||
def test_visit_drops_forward_history_after_new_branch(tmp_path: Path) -> None:
|
||||
history = SessionHistory()
|
||||
first = tmp_path / "first"
|
||||
second = tmp_path / "second"
|
||||
third = tmp_path / "third"
|
||||
for path in (first, second, third):
|
||||
path.mkdir()
|
||||
|
||||
history.visit(first)
|
||||
history.visit(second)
|
||||
assert history.back() == first.resolve()
|
||||
|
||||
history.visit(third)
|
||||
|
||||
assert history.entries() == (first.resolve(), third.resolve())
|
||||
assert history.current == third.resolve()
|
||||
assert history.forward() is None
|
||||
|
||||
|
||||
def test_visit_does_not_duplicate_current_entry(tmp_path: Path) -> None:
|
||||
history = SessionHistory()
|
||||
directory = tmp_path / "directory"
|
||||
directory.mkdir()
|
||||
|
||||
history.visit(directory)
|
||||
history.visit(directory)
|
||||
|
||||
assert history.entries() == (directory.resolve(),)
|
||||
|
||||
|
||||
def test_back_and_forward_move_through_history(tmp_path: Path) -> None:
|
||||
history = SessionHistory()
|
||||
first = tmp_path / "first"
|
||||
second = tmp_path / "second"
|
||||
for path in (first, second):
|
||||
path.mkdir()
|
||||
|
||||
history.visit(first)
|
||||
history.visit(second)
|
||||
|
||||
assert history.back() == first.resolve()
|
||||
assert history.back() is None
|
||||
assert history.forward() == second.resolve()
|
||||
assert history.forward() is None
|
||||
|
||||
|
||||
def test_select_moves_to_specific_history_entry(tmp_path: Path) -> None:
|
||||
history = SessionHistory()
|
||||
first = tmp_path / "first"
|
||||
second = tmp_path / "second"
|
||||
for path in (first, second):
|
||||
path.mkdir()
|
||||
|
||||
history.visit(first)
|
||||
history.visit(second)
|
||||
|
||||
assert history.select(0) == first.resolve()
|
||||
assert history.current == first.resolve()
|
||||
|
||||
|
||||
def test_select_raises_for_invalid_index() -> None:
|
||||
history = SessionHistory()
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
history.select(0)
|
||||
182
tests/test_navigator.py
Normal file
182
tests/test_navigator.py
Normal file
@@ -0,0 +1,182 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.history import SessionHistory
|
||||
from app.navigator import Navigator
|
||||
|
||||
|
||||
def test_navigator_initial_state_includes_parent_entry(tmp_path: Path) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
alpha = current / "alpha"
|
||||
beta = current / "beta"
|
||||
alpha.mkdir()
|
||||
beta.mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
|
||||
assert navigator.current_path == current.resolve()
|
||||
assert navigator.selected_index == 0
|
||||
assert [entry.name for entry in navigator.visible_entries] == [
|
||||
"..",
|
||||
"alpha",
|
||||
"beta",
|
||||
]
|
||||
|
||||
|
||||
def test_expand_selected_directory_reveals_nested_entries(tmp_path: Path) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
scripts = current / "scripts"
|
||||
scripts.mkdir()
|
||||
build = scripts / "build"
|
||||
build.mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
navigator.set_selected_index(1)
|
||||
|
||||
assert navigator.expand_selected_directory() is True
|
||||
assert [(entry.name, entry.depth) for entry in navigator.visible_entries] == [
|
||||
("..", 0),
|
||||
("scripts", 0),
|
||||
("build", 1),
|
||||
]
|
||||
|
||||
|
||||
def test_expand_selected_directory_returns_false_without_children(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
docs = current / "docs"
|
||||
docs.mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
navigator.set_selected_index(1)
|
||||
|
||||
assert navigator.expand_selected_directory() is False
|
||||
assert [entry.name for entry in navigator.visible_entries] == ["..", "docs"]
|
||||
|
||||
|
||||
def test_collapse_selected_directory_hides_nested_entries(tmp_path: Path) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
scripts = current / "scripts"
|
||||
scripts.mkdir()
|
||||
(scripts / "build").mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
navigator.set_selected_index(1)
|
||||
navigator.expand_selected_directory()
|
||||
|
||||
assert navigator.collapse_selected_directory() is True
|
||||
assert [entry.name for entry in navigator.visible_entries] == ["..", "scripts"]
|
||||
|
||||
|
||||
def test_collapse_selected_child_moves_to_parent_and_collapses(tmp_path: Path) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
scripts = current / "scripts"
|
||||
scripts.mkdir()
|
||||
(scripts / "build").mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
navigator.set_selected_index(1)
|
||||
navigator.expand_selected_directory()
|
||||
navigator.set_selected_index(2)
|
||||
|
||||
assert navigator.collapse_selected_directory() is True
|
||||
assert navigator.selected_entry.name == "scripts"
|
||||
assert [entry.name for entry in navigator.visible_entries] == ["..", "scripts"]
|
||||
|
||||
|
||||
def test_enter_selected_directory_changes_current_path_and_updates_history(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
child = current / "child"
|
||||
child.mkdir()
|
||||
|
||||
history = SessionHistory()
|
||||
navigator = Navigator(current, history=history)
|
||||
navigator.set_selected_index(1)
|
||||
|
||||
selected_path = navigator.enter_selected_directory()
|
||||
|
||||
assert selected_path == child.resolve()
|
||||
assert navigator.current_path == child.resolve()
|
||||
assert navigator.selected_index == 0
|
||||
assert navigator.history.entries() == (current.resolve(), child.resolve())
|
||||
|
||||
|
||||
def test_enter_parent_entry_moves_to_parent_directory(tmp_path: Path) -> None:
|
||||
parent = tmp_path / "parent"
|
||||
parent.mkdir()
|
||||
current = parent / "current"
|
||||
current.mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
|
||||
moved_path = navigator.enter_selected_directory()
|
||||
|
||||
assert moved_path == parent.resolve()
|
||||
assert navigator.current_path == parent.resolve()
|
||||
|
||||
|
||||
def test_go_to_parent_directory_moves_up_one_level(tmp_path: Path) -> None:
|
||||
parent = tmp_path / "parent"
|
||||
parent.mkdir()
|
||||
current = parent / "current"
|
||||
current.mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
|
||||
assert navigator.go_to_parent_directory() == parent.resolve()
|
||||
assert navigator.current_path == parent.resolve()
|
||||
|
||||
|
||||
def test_history_navigation_restores_previous_path(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
first = root / "first"
|
||||
second = root / "second"
|
||||
first.mkdir()
|
||||
second.mkdir()
|
||||
|
||||
navigator = Navigator(root)
|
||||
navigator.set_selected_index(1)
|
||||
navigator.enter_selected_directory()
|
||||
navigator.go_to_parent_directory()
|
||||
navigator.set_selected_index(2)
|
||||
navigator.enter_selected_directory()
|
||||
|
||||
assert navigator.go_back() == root.resolve()
|
||||
assert navigator.current_path == root.resolve()
|
||||
assert navigator.go_back() == first.resolve()
|
||||
assert navigator.go_forward() == root.resolve()
|
||||
assert navigator.go_forward() == second.resolve()
|
||||
|
||||
|
||||
def test_select_history_entry_changes_current_path(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
child = root / "child"
|
||||
child.mkdir()
|
||||
|
||||
navigator = Navigator(root)
|
||||
navigator.set_selected_index(1)
|
||||
navigator.enter_selected_directory()
|
||||
|
||||
assert navigator.select_history_entry(0) == root.resolve()
|
||||
assert navigator.current_path == root.resolve()
|
||||
|
||||
|
||||
def test_set_selected_index_raises_for_invalid_index(tmp_path: Path) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
navigator = Navigator(current)
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
navigator.set_selected_index(1)
|
||||
18
tests/test_smoke.py
Normal file
18
tests/test_smoke.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.main import main
|
||||
|
||||
|
||||
def test_main_runs(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
monkeypatch.setattr("app.main.run_cli", lambda: Path("/tmp/final"))
|
||||
|
||||
main()
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == "/tmp/final\n"
|
||||
assert captured.err == ""
|
||||
80
tests/test_ui.py
Normal file
80
tests/test_ui.py
Normal file
@@ -0,0 +1,80 @@
|
||||
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
|
||||
Reference in New Issue
Block a user