- Move src/clide/ to clide/ (flatten directory structure) - Add SettingsService for persistent user preferences - Settings stored in ~/.clide/settings.json - Persist theme, panel visibility, and compact mode state - Fix terminal rendering glitch with throttled refresh (~60fps) - Update pyproject.toml, Makefile, and tests for new structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
644 lines
21 KiB
Python
644 lines
21 KiB
Python
"""Tests for widget components."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
from clide.models.diff import DiffContent
|
|
from clide.models.editor import FileBuffer
|
|
from clide.models.git import ChangeStatus, GitChange, GitCommit
|
|
from clide.models.problems import Problem, Severity
|
|
from clide.models.todos import TodoItem, TodoType
|
|
|
|
|
|
class TestFilesView:
|
|
"""Tests for FilesView component."""
|
|
|
|
def test_filter_paths_excludes_hidden(self, tmp_path: Path):
|
|
"""Test that hidden files are filtered out."""
|
|
from clide.widgets.components.files_view import FilesView
|
|
|
|
view = FilesView(path=tmp_path)
|
|
paths = [
|
|
tmp_path / "visible.py",
|
|
tmp_path / ".hidden",
|
|
tmp_path / ".git",
|
|
tmp_path / "__pycache__",
|
|
tmp_path / "node_modules",
|
|
tmp_path / ".venv",
|
|
tmp_path / "src",
|
|
]
|
|
filtered = view.filter_paths(paths)
|
|
|
|
assert tmp_path / "visible.py" in filtered
|
|
assert tmp_path / "src" in filtered
|
|
assert tmp_path / ".hidden" not in filtered
|
|
assert tmp_path / ".git" not in filtered
|
|
assert tmp_path / "__pycache__" not in filtered
|
|
assert tmp_path / "node_modules" not in filtered
|
|
assert tmp_path / ".venv" not in filtered
|
|
|
|
def test_file_selected_message(self):
|
|
"""Test FileSelected message."""
|
|
from clide.widgets.components.files_view import FilesView
|
|
|
|
mock_node = MagicMock()
|
|
msg = FilesView.FileSelected(mock_node, Path("/test/file.py"))
|
|
assert msg.path == Path("/test/file.py")
|
|
assert msg.node == mock_node
|
|
|
|
def test_directory_selected_message(self):
|
|
"""Test DirectorySelected message."""
|
|
from clide.widgets.components.files_view import FilesView
|
|
|
|
mock_node = MagicMock()
|
|
msg = FilesView.DirectorySelected(mock_node, Path("/test/dir"))
|
|
assert msg.path == Path("/test/dir")
|
|
assert msg.node == mock_node
|
|
|
|
|
|
class TestGitChangesView:
|
|
"""Tests for GitChangesView component."""
|
|
|
|
def test_initial_state(self):
|
|
"""Test initial state with no changes."""
|
|
from clide.widgets.components.git_changes import GitChangesView
|
|
|
|
view = GitChangesView()
|
|
assert view._staged == []
|
|
assert view._unstaged == []
|
|
|
|
def test_initial_state_with_changes(self):
|
|
"""Test initial state with provided changes."""
|
|
from clide.widgets.components.git_changes import GitChangesView
|
|
|
|
staged = [GitChange(path="a.py", status=ChangeStatus.ADDED, staged=True)]
|
|
unstaged = [GitChange(path="b.py", status=ChangeStatus.MODIFIED, staged=False)]
|
|
|
|
view = GitChangesView(staged=staged, unstaged=unstaged)
|
|
assert len(view._staged) == 1
|
|
assert len(view._unstaged) == 1
|
|
|
|
def test_file_clicked_message(self):
|
|
"""Test FileClicked message."""
|
|
from clide.widgets.components.git_changes import GitChangesView
|
|
|
|
change = GitChange(path="test.py", status=ChangeStatus.MODIFIED, staged=False)
|
|
msg = GitChangesView.FileClicked(change)
|
|
assert msg.change == change
|
|
|
|
def test_stage_requested_message(self):
|
|
"""Test StageRequested message."""
|
|
from clide.widgets.components.git_changes import GitChangesView
|
|
|
|
msg = GitChangesView.StageRequested("test.py")
|
|
assert msg.path == "test.py"
|
|
|
|
def test_unstage_requested_message(self):
|
|
"""Test UnstageRequested message."""
|
|
from clide.widgets.components.git_changes import GitChangesView
|
|
|
|
msg = GitChangesView.UnstageRequested("test.py")
|
|
assert msg.path == "test.py"
|
|
|
|
|
|
class TestGitChangeItem:
|
|
"""Tests for GitChangeItem component."""
|
|
|
|
def test_status_icons(self):
|
|
"""Test status icon mapping."""
|
|
from clide.widgets.components.git_changes import GitChangeItem
|
|
|
|
assert GitChangeItem.STATUS_ICONS[ChangeStatus.ADDED] == "+"
|
|
assert GitChangeItem.STATUS_ICONS[ChangeStatus.MODIFIED] == "~"
|
|
assert GitChangeItem.STATUS_ICONS[ChangeStatus.DELETED] == "-"
|
|
assert GitChangeItem.STATUS_ICONS[ChangeStatus.RENAMED] == "→"
|
|
assert GitChangeItem.STATUS_ICONS[ChangeStatus.UNTRACKED] == "?"
|
|
|
|
def test_create_item(self):
|
|
"""Test creating a GitChangeItem."""
|
|
from clide.widgets.components.git_changes import GitChangeItem
|
|
|
|
change = GitChange(path="test.py", status=ChangeStatus.ADDED, staged=True)
|
|
item = GitChangeItem(change)
|
|
assert item.change == change
|
|
|
|
|
|
class TestGitGraphView:
|
|
"""Tests for GitGraphView component."""
|
|
|
|
def test_initial_state(self):
|
|
"""Test initial state with no commits."""
|
|
from clide.widgets.components.git_graph import GitGraphView
|
|
|
|
view = GitGraphView()
|
|
assert view._commits == []
|
|
|
|
def test_initial_state_with_commits(self):
|
|
"""Test initial state with provided commits."""
|
|
from clide.widgets.components.git_graph import GitGraphView
|
|
|
|
commits = [
|
|
GitCommit(
|
|
hash="abc123def456789",
|
|
short_hash="abc123",
|
|
message="Test commit",
|
|
author="Author",
|
|
date="2024-01-01",
|
|
)
|
|
]
|
|
view = GitGraphView(commits=commits)
|
|
assert len(view._commits) == 1
|
|
|
|
def test_graph_symbols(self):
|
|
"""Test graph drawing symbols."""
|
|
from clide.widgets.components.git_graph import GitGraphView
|
|
|
|
assert GitGraphView.COMMIT == "●"
|
|
assert GitGraphView.MERGE == "◆"
|
|
assert GitGraphView.LINE == "│"
|
|
assert GitGraphView.BRANCH == "├"
|
|
assert GitGraphView.JOIN == "┴"
|
|
|
|
def test_commit_selected_message(self):
|
|
"""Test CommitSelected message."""
|
|
from clide.widgets.components.git_graph import GitGraphView
|
|
|
|
commit = GitCommit(
|
|
hash="abc123def456",
|
|
short_hash="abc",
|
|
message="Test",
|
|
author="A",
|
|
date="2024-01-01",
|
|
)
|
|
msg = GitGraphView.CommitSelected(commit)
|
|
assert msg.commit == commit
|
|
|
|
def test_format_commit_line(self):
|
|
"""Test commit line formatting."""
|
|
from clide.widgets.components.git_graph import GitGraphView
|
|
|
|
view = GitGraphView()
|
|
commit = GitCommit(
|
|
hash="abc123def456789",
|
|
short_hash="abc123",
|
|
message="Test commit message",
|
|
author="Author",
|
|
date="2024-01-01",
|
|
is_merge=False,
|
|
refs=(),
|
|
)
|
|
line = view._format_commit_line(commit)
|
|
assert "abc123" in line
|
|
assert "Test commit message" in line
|
|
assert "Author" in line
|
|
|
|
def test_format_merge_commit_line(self):
|
|
"""Test merge commit line formatting."""
|
|
from clide.widgets.components.git_graph import GitGraphView
|
|
|
|
view = GitGraphView()
|
|
commit = GitCommit(
|
|
hash="abc123def456789",
|
|
short_hash="abc123",
|
|
message="Merge branch",
|
|
author="Author",
|
|
date="2024-01-01",
|
|
is_merge=True,
|
|
refs=("main", "HEAD"),
|
|
)
|
|
line = view._format_commit_line(commit)
|
|
assert "◆" in line # Merge symbol
|
|
assert "main" in line
|
|
assert "HEAD" in line
|
|
|
|
|
|
class TestBranchStatus:
|
|
"""Tests for BranchStatus component."""
|
|
|
|
def test_initial_state(self):
|
|
"""Test initial state."""
|
|
from clide.widgets.components.branch_status import BranchStatus
|
|
|
|
status = BranchStatus()
|
|
assert status._current == "main"
|
|
assert status._branches == []
|
|
assert status._popout_visible is False
|
|
|
|
def test_initial_state_with_branch(self):
|
|
"""Test initial state with custom branch."""
|
|
from clide.widgets.components.branch_status import BranchStatus
|
|
|
|
status = BranchStatus(current_branch="develop")
|
|
assert status._current == "develop"
|
|
|
|
def test_branch_property(self):
|
|
"""Test branch property getter."""
|
|
from clide.widgets.components.branch_status import BranchStatus
|
|
|
|
status = BranchStatus(current_branch="feature")
|
|
assert status.branch == "feature"
|
|
|
|
def test_branch_changed_message(self):
|
|
"""Test BranchChanged message."""
|
|
from clide.widgets.components.branch_status import BranchStatus
|
|
|
|
msg = BranchStatus.BranchChanged("develop")
|
|
assert msg.branch == "develop"
|
|
|
|
def test_branch_change_requested_alias(self):
|
|
"""Test BranchChangeRequested is alias for BranchChanged."""
|
|
from clide.widgets.components.branch_status import BranchStatus
|
|
|
|
assert BranchStatus.BranchChangeRequested is BranchStatus.BranchChanged
|
|
|
|
|
|
class TestEditorPane:
|
|
"""Tests for EditorPane component."""
|
|
|
|
def test_initial_state_no_buffer(self):
|
|
"""Test initial state without buffer."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
pane = EditorPane()
|
|
assert pane._buffer is None
|
|
assert pane.current_file is None
|
|
assert pane.modified is False
|
|
|
|
def test_initial_state_with_buffer(self):
|
|
"""Test initial state with buffer."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
buffer = FileBuffer(path=Path("/test.py"), content="print('hello')")
|
|
pane = EditorPane(buffer=buffer)
|
|
assert pane._buffer == buffer
|
|
assert pane.current_file == Path("/test.py")
|
|
|
|
def test_status_text_no_buffer(self):
|
|
"""Test status text with no buffer."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
pane = EditorPane()
|
|
assert pane._get_status_text() == ""
|
|
|
|
def test_status_text_with_buffer(self):
|
|
"""Test status text with buffer."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
buffer = FileBuffer(
|
|
path=Path("/test.py"),
|
|
content="print('hello')",
|
|
language="python",
|
|
)
|
|
pane = EditorPane(buffer=buffer)
|
|
status = pane._get_status_text()
|
|
assert "Ln" in status
|
|
assert "Col" in status
|
|
assert "python" in status
|
|
|
|
def test_content_changed_message(self):
|
|
"""Test ContentChanged message."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
msg = EditorPane.ContentChanged(Path("/test.py"), "new content")
|
|
assert msg.path == Path("/test.py")
|
|
assert msg.content == "new content"
|
|
|
|
def test_cursor_moved_message(self):
|
|
"""Test CursorMoved message."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
msg = EditorPane.CursorMoved(Path("/test.py"), 10, 5)
|
|
assert msg.path == Path("/test.py")
|
|
assert msg.line == 10
|
|
assert msg.column == 5
|
|
|
|
def test_save_requested_message(self):
|
|
"""Test SaveRequested message."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
msg = EditorPane.SaveRequested(Path("/test.py"))
|
|
assert msg.path == Path("/test.py")
|
|
|
|
def test_file_saved_message(self):
|
|
"""Test FileSaved message."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
msg = EditorPane.FileSaved(Path("/test.py"))
|
|
assert msg.path == Path("/test.py")
|
|
|
|
def test_modified_property(self):
|
|
"""Test modified property."""
|
|
from clide.widgets.components.editor_pane import EditorPane
|
|
|
|
buffer = FileBuffer(path=Path("/test.py"), content="", is_modified=True)
|
|
pane = EditorPane(buffer=buffer)
|
|
assert pane.modified is True
|
|
|
|
buffer2 = FileBuffer(path=Path("/test2.py"), content="", is_modified=False)
|
|
pane2 = EditorPane(buffer=buffer2)
|
|
assert pane2.modified is False
|
|
|
|
|
|
class TestDiffPane:
|
|
"""Tests for DiffPane component."""
|
|
|
|
def test_initial_state_no_diff(self):
|
|
"""Test initial state without diff."""
|
|
from clide.widgets.components.diff_pane import DiffPane
|
|
|
|
pane = DiffPane()
|
|
assert pane._diff is None
|
|
assert pane._is_proposal is False
|
|
|
|
def test_initial_state_with_diff(self):
|
|
"""Test initial state with diff."""
|
|
from clide.widgets.components.diff_pane import DiffPane
|
|
|
|
diff = DiffContent(file_path="test.py", hunks=())
|
|
pane = DiffPane(diff=diff, is_proposal=True)
|
|
assert pane._diff == diff
|
|
assert pane._is_proposal is True
|
|
|
|
def test_accept_clicked_message(self):
|
|
"""Test AcceptClicked message."""
|
|
from clide.widgets.components.diff_pane import DiffPane
|
|
|
|
msg = DiffPane.AcceptClicked("test.py")
|
|
assert msg.file_path == "test.py"
|
|
|
|
def test_reject_clicked_message(self):
|
|
"""Test RejectClicked message."""
|
|
from clide.widgets.components.diff_pane import DiffPane
|
|
|
|
msg = DiffPane.RejectClicked("test.py")
|
|
assert msg.file_path == "test.py"
|
|
|
|
|
|
class TestTerminalPane:
|
|
"""Tests for TerminalPane component."""
|
|
|
|
def test_initial_state(self, tmp_path: Path):
|
|
"""Test initial state."""
|
|
from clide.widgets.components.terminal_pane import TerminalPane
|
|
|
|
pane = TerminalPane(cwd=tmp_path)
|
|
assert pane.cwd == tmp_path
|
|
assert pane._history == []
|
|
assert pane._history_index == 0
|
|
|
|
def test_default_cwd(self):
|
|
"""Test default cwd is current directory."""
|
|
from clide.widgets.components.terminal_pane import TerminalPane
|
|
|
|
pane = TerminalPane()
|
|
assert pane.cwd == Path.cwd()
|
|
|
|
def test_cwd_property(self, tmp_path: Path):
|
|
"""Test cwd property."""
|
|
from clide.widgets.components.terminal_pane import TerminalPane
|
|
|
|
pane = TerminalPane()
|
|
pane._cwd = tmp_path
|
|
assert pane.cwd == tmp_path
|
|
|
|
def test_command_submitted_message(self):
|
|
"""Test CommandSubmitted message."""
|
|
from clide.widgets.components.terminal_pane import TerminalPane
|
|
|
|
msg = TerminalPane.CommandSubmitted("ls -la")
|
|
assert msg.command == "ls -la"
|
|
|
|
|
|
class TestProblemsView:
|
|
"""Tests for ProblemsView component."""
|
|
|
|
def test_initial_state_no_problems(self):
|
|
"""Test initial state without problems."""
|
|
from clide.widgets.components.problems_view import ProblemsView
|
|
|
|
view = ProblemsView()
|
|
assert view._problems == []
|
|
|
|
def test_initial_state_with_problems(self):
|
|
"""Test initial state with problems."""
|
|
from clide.widgets.components.problems_view import ProblemsView
|
|
|
|
problems = [
|
|
Problem(
|
|
file_path=Path("/test.py"),
|
|
line=10,
|
|
column=5,
|
|
severity=Severity.ERROR,
|
|
message="Error",
|
|
source="ruff",
|
|
)
|
|
]
|
|
view = ProblemsView(problems=problems)
|
|
assert len(view._problems) == 1
|
|
|
|
def test_filter_by_file(self):
|
|
"""Test filtering problems by file."""
|
|
from clide.widgets.components.problems_view import ProblemsView
|
|
|
|
problems = [
|
|
Problem(
|
|
file_path=Path("/a.py"),
|
|
line=1,
|
|
column=1,
|
|
severity=Severity.ERROR,
|
|
message="Error 1",
|
|
source="ruff",
|
|
),
|
|
Problem(
|
|
file_path=Path("/b.py"),
|
|
line=1,
|
|
column=1,
|
|
severity=Severity.ERROR,
|
|
message="Error 2",
|
|
source="ruff",
|
|
),
|
|
Problem(
|
|
file_path=Path("/a.py"),
|
|
line=5,
|
|
column=1,
|
|
severity=Severity.WARNING,
|
|
message="Warning 1",
|
|
source="ruff",
|
|
),
|
|
]
|
|
view = ProblemsView(problems=problems)
|
|
filtered = view.filter_by_file(Path("/a.py"))
|
|
assert len(filtered) == 2
|
|
|
|
def test_problem_clicked_message(self):
|
|
"""Test ProblemClicked message."""
|
|
from clide.widgets.components.problems_view import ProblemsView
|
|
|
|
problem = Problem(
|
|
file_path=Path("/test.py"),
|
|
line=10,
|
|
column=5,
|
|
severity=Severity.ERROR,
|
|
message="Test error",
|
|
source="ruff",
|
|
)
|
|
msg = ProblemsView.ProblemClicked(problem)
|
|
assert msg.problem == problem
|
|
|
|
|
|
class TestProblemItem:
|
|
"""Tests for ProblemItem component."""
|
|
|
|
def test_create_item(self):
|
|
"""Test creating a ProblemItem."""
|
|
from clide.widgets.components.problems_view import ProblemItem
|
|
|
|
problem = Problem(
|
|
file_path=Path("/test.py"),
|
|
line=10,
|
|
column=5,
|
|
severity=Severity.ERROR,
|
|
message="Test error",
|
|
source="ruff",
|
|
)
|
|
item = ProblemItem(problem)
|
|
assert item.problem == problem
|
|
|
|
|
|
class TestTodosView:
|
|
"""Tests for TodosView component."""
|
|
|
|
def test_initial_state_no_items(self):
|
|
"""Test initial state without items."""
|
|
from clide.widgets.components.todos_view import TodosView
|
|
|
|
view = TodosView()
|
|
assert view._items == []
|
|
|
|
def test_initial_state_with_items(self):
|
|
"""Test initial state with items."""
|
|
from clide.widgets.components.todos_view import TodosView
|
|
|
|
items = [
|
|
TodoItem(
|
|
file_path=Path("/test.py"),
|
|
line=10,
|
|
column=1,
|
|
todo_type=TodoType.TODO,
|
|
text="Fix this",
|
|
context_line="# TODO: Fix this",
|
|
)
|
|
]
|
|
view = TodosView(items=items)
|
|
assert len(view._items) == 1
|
|
|
|
def test_filter_by_type(self):
|
|
"""Test filtering items by type."""
|
|
from clide.widgets.components.todos_view import TodosView
|
|
|
|
items = [
|
|
TodoItem(
|
|
file_path=Path("/a.py"),
|
|
line=1,
|
|
column=1,
|
|
todo_type=TodoType.TODO,
|
|
text="TODO 1",
|
|
context_line="# TODO: TODO 1",
|
|
),
|
|
TodoItem(
|
|
file_path=Path("/b.py"),
|
|
line=1,
|
|
column=1,
|
|
todo_type=TodoType.FIXME,
|
|
text="FIXME 1",
|
|
context_line="# FIXME: FIXME 1",
|
|
),
|
|
TodoItem(
|
|
file_path=Path("/a.py"),
|
|
line=5,
|
|
column=1,
|
|
todo_type=TodoType.TODO,
|
|
text="TODO 2",
|
|
context_line="# TODO: TODO 2",
|
|
),
|
|
]
|
|
view = TodosView(items=items)
|
|
todos = view.filter_by_type(TodoType.TODO)
|
|
assert len(todos) == 2
|
|
fixmes = view.filter_by_type(TodoType.FIXME)
|
|
assert len(fixmes) == 1
|
|
|
|
def test_todo_clicked_message(self):
|
|
"""Test TodoClicked message."""
|
|
from clide.widgets.components.todos_view import TodosView
|
|
|
|
item = TodoItem(
|
|
file_path=Path("/test.py"),
|
|
line=10,
|
|
column=1,
|
|
todo_type=TodoType.TODO,
|
|
text="Test todo",
|
|
context_line="# TODO: Test todo",
|
|
)
|
|
msg = TodosView.TodoClicked(item)
|
|
assert msg.item == item
|
|
|
|
|
|
class TestTodoListItem:
|
|
"""Tests for TodoListItem component."""
|
|
|
|
def test_create_item(self):
|
|
"""Test creating a TodoListItem."""
|
|
from clide.widgets.components.todos_view import TodoListItem
|
|
|
|
item = TodoItem(
|
|
file_path=Path("/test.py"),
|
|
line=10,
|
|
column=1,
|
|
todo_type=TodoType.FIXME,
|
|
text="Fix this bug",
|
|
context_line="# FIXME: Fix this bug",
|
|
)
|
|
list_item = TodoListItem(item)
|
|
assert list_item.item == item
|
|
|
|
|
|
class TestJiraView:
|
|
"""Tests for JiraView component."""
|
|
|
|
def test_initial_state_enabled(self):
|
|
"""Test initial state when enabled."""
|
|
from clide.widgets.components.jira_view import JiraView
|
|
|
|
view = JiraView(enabled=True)
|
|
assert view._enabled is True
|
|
assert view._content == ""
|
|
|
|
def test_initial_state_disabled(self):
|
|
"""Test initial state when disabled."""
|
|
from clide.widgets.components.jira_view import JiraView
|
|
|
|
view = JiraView(enabled=False)
|
|
assert view._enabled is False
|
|
|
|
def test_initial_state_with_content(self):
|
|
"""Test initial state with content."""
|
|
from clide.widgets.components.jira_view import JiraView
|
|
|
|
view = JiraView(content="# Issues\n- PROJ-123")
|
|
assert view._content == "# Issues\n- PROJ-123"
|
|
|
|
def test_refresh_requested_message(self):
|
|
"""Test RefreshRequested message."""
|
|
from clide.widgets.components.jira_view import JiraView
|
|
|
|
msg = JiraView.RefreshRequested()
|
|
assert msg is not None
|
|
|
|
def test_issue_clicked_message(self):
|
|
"""Test IssueClicked message."""
|
|
from clide.widgets.components.jira_view import JiraView
|
|
|
|
msg = JiraView.IssueClicked("PROJ-123")
|
|
assert msg.issue_key == "PROJ-123"
|