"""Tests for the TodoList side-panel widget."""
from __future__ import annotations
from pathlib import Path
import pytest
from rich.text import Text
from clients.terminal.tui.widgets.todo_list import TodoList
def _plain(content: str) -> str:
"""Strip Rich markup so assertions see the visible text, not colour tags."""
return Text.from_markup(content).plain
@pytest.fixture(autouse=True)
def tmp_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Override the state dir so tests never touch ~/.navi_code."""
from clients.terminal import config
original = config.settings.state_dir
config.settings.state_dir = tmp_path
import clients.terminal.tui.settings as settings_module
settings_module._tui_settings = None
yield tmp_path
config.settings.state_dir = original
settings_module._tui_settings = None
@pytest.mark.anyio
async def test_todo_list_empty_state() -> None:
"""A fresh TodoList shows the 'No plan yet' placeholder."""
from clients.terminal.tui.tui_app import NaviCodeTui
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
todo = pilot.app.query_one(TodoList)
# _build_content is the source of truth (never name this _render —
# it shadows Widget._render and crashes Textual's layout pass).
assert "No plan yet" in todo._build_content()
@pytest.mark.anyio
async def test_todo_list_renders_tasks_with_header() -> None:
from clients.terminal.tui.tui_app import NaviCodeTui
tasks = [
{"index": 1, "text": "step one", "status": "done", "validation": "ran tests"},
{"index": 2, "text": "step two", "status": "in_progress", "validation": ""},
{"index": 3, "text": "step three", "status": "failed", "validation": "boom"},
]
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
todo = pilot.app.query_one(TodoList)
todo.set_tasks(tasks)
plain = _plain(todo._build_content())
assert "1/3 done" in plain
assert "step one" in plain
assert "step two" in plain
assert "(in_progress)" in plain
assert "(failed)" in plain
assert "✓ ran tests" in plain # validation note on the done step
assert "✓ boom" in plain # validation note on the failed step
@pytest.mark.anyio
async def test_todo_list_clear_resets_to_placeholder() -> None:
from clients.terminal.tui.tui_app import NaviCodeTui
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
todo = pilot.app.query_one(TodoList)
todo.set_tasks([{"index": 1, "text": "x", "status": "pending", "validation": ""}])
assert "x" in todo._build_content()
todo.clear()
assert "No plan yet" in todo._build_content()
def test_build_content_never_named_render():
"""Guard against the Textual _render shadow pitfall: a method called _render
on a Widget subclass crashes the layout pass (AttributeError get_height)."""
assert "_render" not in TodoList.__dict__, (
"TodoList must not override _render — it shadows Widget._render "
"(Textual calls it expecting a Strip, gets a str → AttributeError)."
)
assert "_build_content" in TodoList.__dict__
@pytest.mark.anyio
async def test_status_markers_are_coloured_and_emphasised() -> None:
"""Each status gets its own marker colour, and the active step is bolded
while completed/skipped steps are dimmed — so progress is legible at a glance."""
from clients.terminal.tui.tui_app import NaviCodeTui
from clients.terminal.tui.themes import get_active_theme
tasks = [
{"index": 1, "text": "done step", "status": "done", "validation": ""},
{"index": 2, "text": "active step", "status": "in_progress", "validation": ""},
{"index": 3, "text": "failed step", "status": "failed", "validation": ""},
{"index": 4, "text": "queued step", "status": "pending", "validation": ""},
]
theme = get_active_theme()
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
todo = pilot.app.query_one(TodoList)
todo.set_tasks(tasks)
content = todo._build_content()
# Marker icons are wrapped in their status colour.
assert f"[{theme.success.hex}]✓" in content # done
assert f"[{theme.accent.hex}]◎" in content # in_progress
assert f"[{theme.error.hex}]✗" in content # failed
assert f"[{theme.text_dim.hex}]○" in content # pending
# The active step is bold; completed steps are dimmed.
assert "bold" in content
assert "dim" in content
# Plain text still carries every step.
plain = _plain(content)
assert "active step" in plain
assert "queued step" in plain
@pytest.mark.anyio
async def test_todo_panel_is_scrollable_and_wraps_list() -> None:
"""The todo lives in its own scrollable panel below the info block, not
inside StatusPanel."""
from textual.containers import ScrollableContainer
from clients.terminal.tui.tui_app import NaviCodeTui
from clients.terminal.tui.widgets.todo_list import TodoPanel
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
panel = pilot.app.query_one(TodoPanel)
assert isinstance(panel, ScrollableContainer)
# The TodoList is a child of the panel.
assert panel.query_one(TodoList) is not None
# The panel fills the remaining column height; the info block shrinks.
assert str(panel.styles.height) == "1fr"