Add Clide project structure and initial implementation

Set up the TUI IDE wrapper for Claude Code CLI with:
- Core app structure using Textual framework
- Panel architecture (sidebar, workspace, claude, context)
- Theme system with 22 built-in themes (Summer Night default)
- Pydantic models for configuration and data
- Makefile for development commands
- Project documentation and specs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 21:04:44 +01:00
co-authored by Claude Opus 4.5
parent 12825e15e2
commit c5f8b2e615
103 changed files with 11660 additions and 11 deletions
+1
View File
@@ -0,0 +1 @@
"""Clide test suite."""
+81
View File
@@ -0,0 +1,81 @@
"""Shared pytest fixtures for Clide tests."""
from pathlib import Path
from typing import AsyncGenerator, Generator
from unittest.mock import MagicMock
import pytest
from textual.pilot import Pilot
from clide.app import ClideApp
from clide.extensions.manager import ExtensionManager
from clide.models.config import ClideSettings
from tests.harnesses.app_harness import AppHarness
from tests.harnesses.controller_harness import ControllerHarness
@pytest.fixture
def temp_workdir(tmp_path: Path) -> Path:
"""Create a temporary working directory with sample files."""
# Create sample directory structure
(tmp_path / "src").mkdir()
(tmp_path / "tests").mkdir()
(tmp_path / "src" / "main.py").write_text("# Main file")
(tmp_path / "README.md").write_text("# Test Project")
# Initialize git repo
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def mock_settings() -> ClideSettings:
"""Create test settings with defaults."""
return ClideSettings(
theme="dark",
claude_path="/usr/bin/echo", # Safe mock
auto_save=False,
confirm_exit=False,
)
@pytest.fixture
def mock_extension_manager() -> ExtensionManager:
"""Create an extension manager without loading external extensions."""
manager = ExtensionManager()
# Don't load entry points in tests
return manager
@pytest.fixture
def app_harness(
temp_workdir: Path,
mock_settings: ClideSettings,
mock_extension_manager: ExtensionManager,
) -> Generator[AppHarness, None, None]:
"""Create a full application test harness."""
harness = AppHarness(
workdir=temp_workdir,
settings=mock_settings,
extension_manager=mock_extension_manager,
)
yield harness
@pytest.fixture
async def running_app(
app_harness: AppHarness,
) -> AsyncGenerator[tuple[ClideApp, Pilot], None]:
"""Start the app and yield (app, pilot) for interaction."""
app, pilot = await app_harness.start()
try:
yield app, pilot
finally:
await app_harness.stop()
@pytest.fixture
def controller_harness() -> ControllerHarness:
"""Create an isolated controller test harness."""
return ControllerHarness()
+6
View File
@@ -0,0 +1,6 @@
"""Test harnesses for Clide testing."""
from tests.harnesses.app_harness import AppHarness
from tests.harnesses.controller_harness import ControllerHarness
__all__ = ["AppHarness", "ControllerHarness"]
+84
View File
@@ -0,0 +1,84 @@
"""Full application test harness for Clide."""
from pathlib import Path
from typing import Optional
from textual.pilot import Pilot
from clide.app import ClideApp
from clide.extensions.manager import ExtensionManager
from clide.models.config import ClideSettings
class AppHarness:
"""Test harness for running the full Clide application.
Provides a controlled environment for integration testing with
mocked services and isolated file systems.
Usage:
harness = AppHarness(workdir=tmp_path)
app, pilot = await harness.start()
await pilot.press("ctrl+q")
await harness.stop()
"""
def __init__(
self,
workdir: Path,
settings: Optional[ClideSettings] = None,
extension_manager: Optional[ExtensionManager] = None,
) -> None:
self.workdir = workdir
self.settings = settings or ClideSettings()
self.extension_manager = extension_manager or ExtensionManager()
self._app: Optional[ClideApp] = None
self._pilot: Optional[Pilot] = None
async def start(self) -> tuple[ClideApp, Pilot]:
"""Start the application and return app and pilot for testing.
Returns:
Tuple of (ClideApp instance, Pilot for simulating input)
"""
self._app = ClideApp(workdir=self.workdir)
# Inject test dependencies
self._app.extension_manager = self.extension_manager
# Start app in test mode
async with self._app.run_test() as pilot:
self._pilot = pilot
return self._app, pilot
async def stop(self) -> None:
"""Clean shutdown of the application."""
if self._app:
await self._app.action_quit()
self._app = None
self._pilot = None
@property
def app(self) -> ClideApp:
"""Get the running app instance."""
if self._app is None:
raise RuntimeError("App not started. Call start() first.")
return self._app
@property
def pilot(self) -> Pilot:
"""Get the pilot for simulating user input."""
if self._pilot is None:
raise RuntimeError("App not started. Call start() first.")
return self._pilot
async def press_keys(self, *keys: str) -> None:
"""Simulate pressing a sequence of keys."""
await self.pilot.press(*keys)
async def click(self, selector: str) -> None:
"""Click on a widget by CSS selector."""
await self.pilot.click(selector)
async def wait_for_animation(self) -> None:
"""Wait for any running animations to complete."""
await self.pilot.pause()
+97
View File
@@ -0,0 +1,97 @@
"""Controller isolation test harness for Clide."""
from typing import TYPE_CHECKING, Any, Optional
from unittest.mock import AsyncMock, MagicMock
from textual.message import Message
if TYPE_CHECKING:
from clide.controllers.base import BaseController
class MockApp:
"""Minimal mock of a Textual App for controller testing."""
def __init__(self) -> None:
self.messages: list[Message] = []
self.post_message = MagicMock(side_effect=self._capture_message)
def _capture_message(self, message: Message) -> None:
self.messages.append(message)
def get_messages(self, message_type: Optional[type] = None) -> list[Message]:
"""Get captured messages, optionally filtered by type."""
if message_type is None:
return self.messages.copy()
return [m for m in self.messages if isinstance(m, message_type)]
def clear_messages(self) -> None:
"""Clear captured messages."""
self.messages.clear()
class ControllerHarness:
"""Test harness for isolated controller testing.
Provides a mock app environment for testing controllers without
the full Textual application overhead.
Usage:
harness = ControllerHarness()
controller = GitController(harness.mock_app)
await controller.initialize()
await controller.refresh_status()
messages = harness.get_messages()
"""
def __init__(self) -> None:
self._mock_app = MockApp()
self._controllers: list["BaseController"] = []
self._mocks: dict[str, Any] = {}
@property
def mock_app(self) -> MockApp:
"""Get the mock app for controller injection."""
return self._mock_app
def register_controller(self, controller: "BaseController") -> None:
"""Register a controller for lifecycle management."""
self._controllers.append(controller)
async def initialize_all(self) -> None:
"""Initialize all registered controllers."""
for controller in self._controllers:
await controller.initialize()
async def shutdown_all(self) -> None:
"""Shutdown all registered controllers."""
for controller in self._controllers:
await controller.shutdown()
def get_messages(self, message_type: Optional[type] = None) -> list[Message]:
"""Get messages posted to the mock app."""
return self._mock_app.get_messages(message_type)
def clear_messages(self) -> None:
"""Clear all captured messages."""
self._mock_app.clear_messages()
def add_mock(self, name: str, mock: Any) -> None:
"""Add a named mock for dependency injection.
Args:
name: Identifier for the mock
mock: Mock object or AsyncMock
"""
self._mocks[name] = mock
def get_mock(self, name: str) -> Any:
"""Retrieve a named mock."""
return self._mocks.get(name)
def create_async_mock(self, return_value: Any = None) -> AsyncMock:
"""Create an AsyncMock with optional return value."""
mock = AsyncMock()
if return_value is not None:
mock.return_value = return_value
return mock
+1
View File
@@ -0,0 +1 @@
"""Integration tests for Clide."""
+150
View File
@@ -0,0 +1,150 @@
"""Integration tests for FilesView widget."""
from pathlib import Path
import pytest
from textual.app import App, ComposeResult
from textual.widgets import Static
from clide.widgets.components.files_view import FilesView
class FilesViewTestApp(App):
"""Test app for FilesView."""
def __init__(self, path: Path) -> None:
super().__init__()
self.test_path = path
self.selected_files: list[Path] = []
self.selected_dirs: list[Path] = []
def compose(self) -> ComposeResult:
yield FilesView(self.test_path, id="files")
def on_files_view_file_selected(self, event: FilesView.FileSelected) -> None:
self.selected_files.append(event.path)
def on_files_view_directory_selected(self, event: FilesView.DirectorySelected) -> None:
self.selected_dirs.append(event.path)
@pytest.fixture
def test_directory(tmp_path: Path) -> Path:
"""Create a test directory structure."""
# Create directories
(tmp_path / "src").mkdir()
(tmp_path / "src" / "components").mkdir()
(tmp_path / "tests").mkdir()
# Create files
(tmp_path / "README.md").write_text("# Test")
(tmp_path / "src" / "main.py").write_text("print('hello')")
(tmp_path / "src" / "components" / "button.py").write_text("class Button: pass")
(tmp_path / "tests" / "test_main.py").write_text("def test_main(): pass")
return tmp_path
async def test_files_view_renders(test_directory: Path):
"""Test that FilesView renders without errors."""
app = FilesViewTestApp(test_directory)
async with app.run_test() as pilot:
files_view = app.query_one("#files", FilesView)
assert files_view is not None
assert files_view.path == test_directory
async def test_files_view_shows_files(test_directory: Path):
"""Test that FilesView shows files in the directory."""
app = FilesViewTestApp(test_directory)
async with app.run_test() as pilot:
files_view = app.query_one("#files", FilesView)
# The root should be loaded
assert files_view.root is not None
async def test_directory_click_expands(test_directory: Path):
"""Test that clicking a directory expands it and emits event."""
app = FilesViewTestApp(test_directory)
async with app.run_test() as pilot:
files_view = app.query_one("#files", FilesView)
# Wait for initial load
await pilot.pause()
# Find the src directory node and click it
for node in files_view.root.children:
if node.data and node.data.path.name == "src":
files_view.select_node(node)
await pilot.pause()
break
# Check that directory was selected
assert len(app.selected_dirs) >= 1
assert any(p.name == "src" for p in app.selected_dirs)
async def test_file_click_emits_event(test_directory: Path):
"""Test that clicking a file emits FileSelected event."""
app = FilesViewTestApp(test_directory)
async with app.run_test() as pilot:
files_view = app.query_one("#files", FilesView)
# Wait for initial load
await pilot.pause()
# Find and click README.md
for node in files_view.root.children:
if node.data and node.data.path.name == "README.md":
files_view.select_node(node)
await pilot.pause()
break
# Check that file was selected
assert len(app.selected_files) >= 1
assert any(p.name == "README.md" for p in app.selected_files)
async def test_filter_paths_hides_hidden_files(test_directory: Path):
"""Test that hidden files are filtered out."""
# Create hidden files/dirs
(test_directory / ".git").mkdir()
(test_directory / ".hidden_file").write_text("hidden")
(test_directory / "__pycache__").mkdir()
app = FilesViewTestApp(test_directory)
async with app.run_test() as pilot:
files_view = app.query_one("#files", FilesView)
# Wait for initial load
await pilot.pause()
# Check that hidden items are not in the tree
visible_names = {
node.data.path.name
for node in files_view.root.children
if node.data
}
assert ".git" not in visible_names
assert ".hidden_file" not in visible_names
assert "__pycache__" not in visible_names
assert "src" in visible_names
assert "README.md" in visible_names
async def test_render_label_shows_icons(test_directory: Path):
"""Test that render_label produces proper icons."""
app = FilesViewTestApp(test_directory)
async with app.run_test() as pilot:
files_view = app.query_one("#files", FilesView)
# Wait for initial load
await pilot.pause()
# Check that nodes have labels rendered (icons are part of label)
for node in files_view.root.children:
if node.data:
# The label should contain the filename
label_text = str(node.label)
assert node.data.path.name in label_text
+1
View File
@@ -0,0 +1 @@
"""Snapshot (visual regression) tests for Clide."""
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

+37
View File
@@ -0,0 +1,37 @@
"""Snapshot tests for Clide UI."""
from clide.app import ClideApp
def test_initial_layout(snap_compare) -> None:
"""Test the initial application layout renders correctly."""
app = ClideApp(test_mode=True)
assert snap_compare(app, terminal_size=(120, 40))
def test_layout_without_right_panel(snap_compare) -> None:
"""Test layout with right panel hidden."""
async def hide_right_panel(pilot):
await pilot.press("f2")
app = ClideApp(test_mode=True)
assert snap_compare(
app,
terminal_size=(120, 40),
run_before=hide_right_panel,
)
def test_layout_without_left_panel(snap_compare) -> None:
"""Test layout with left panel hidden."""
async def hide_left_panel(pilot):
await pilot.press("f1")
app = ClideApp(test_mode=True)
assert snap_compare(
app,
terminal_size=(120, 40),
run_before=hide_left_panel,
)
+1
View File
@@ -0,0 +1 @@
"""Unit tests for Clide."""
+187
View File
@@ -0,0 +1,187 @@
"""Tests for ClideApp."""
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from clide.models.config import ClideSettings
class TestClideAppInit:
"""Tests for ClideApp initialization."""
def test_default_initialization(self):
"""Test default initialization."""
from clide.app import ClideApp
app = ClideApp()
assert app.workdir == Path.cwd()
assert isinstance(app.settings, ClideSettings)
# Note: reactive properties can't be tested directly without running the app
# because watchers try to query the DOM
def test_initialization_with_workdir(self, tmp_path: Path):
"""Test initialization with custom workdir."""
from clide.app import ClideApp
app = ClideApp(workdir=tmp_path)
assert app.workdir == tmp_path
def test_initialization_with_settings(self):
"""Test initialization with custom settings."""
from clide.app import ClideApp
settings = ClideSettings(theme="dracula", jira_enabled=True)
app = ClideApp(settings=settings)
assert app.settings.theme == "dracula"
assert app.settings.jira_enabled is True
def test_controllers_initialized(self, tmp_path: Path):
"""Test that all controllers are initialized."""
from clide.app import ClideApp
from clide.controllers.diff import DiffController
from clide.controllers.editor import EditorController
from clide.controllers.git import GitController
from clide.controllers.jira import JiraController
from clide.controllers.problems import ProblemsController
from clide.controllers.todos import TodosController
app = ClideApp(workdir=tmp_path)
assert isinstance(app.git_controller, GitController)
assert isinstance(app.editor_controller, EditorController)
assert isinstance(app.diff_controller, DiffController)
assert isinstance(app.problems_controller, ProblemsController)
assert isinstance(app.todos_controller, TodosController)
assert isinstance(app.jira_controller, JiraController)
def test_jira_controller_enabled_from_settings(self):
"""Test Jira controller uses settings."""
from clide.app import ClideApp
settings = ClideSettings(jira_enabled=True)
app = ClideApp(settings=settings)
assert app.jira_controller.enabled is True
settings_disabled = ClideSettings(jira_enabled=False)
app_disabled = ClideApp(settings=settings_disabled)
assert app_disabled.jira_controller.enabled is False
class TestClideAppBindings:
"""Tests for ClideApp keybindings."""
def test_bindings_defined(self):
"""Test that keybindings are defined."""
from clide.app import ClideApp
app = ClideApp()
bindings = {b.key for b in app.BINDINGS}
# Check key bindings exist
assert "ctrl+q" in bindings
assert "ctrl+b" in bindings
assert "ctrl+shift+p" in bindings
assert "ctrl+`" in bindings
assert "ctrl+1" in bindings
assert "f11" in bindings
assert "escape" in bindings
class TestClideAppMeta:
"""Tests for ClideApp metadata."""
def test_title(self):
"""Test app title."""
from clide.app import ClideApp
app = ClideApp()
assert app.TITLE == "Clide"
assert app.SUB_TITLE == "Claude Code IDE"
def test_css_defined(self):
"""Test CSS is defined."""
from clide.app import ClideApp
assert ClideApp.CSS
assert "#main-container" in ClideApp.CSS
assert "SidebarPanel" in ClideApp.CSS
assert "ContextPanel" in ClideApp.CSS
assert "WorkspacePanel" in ClideApp.CSS
assert "ClaudePanel" in ClideApp.CSS
class TestClideAppReactive:
"""Tests for ClideApp reactive properties.
Note: Most reactive property tests require running the app
because accessing them triggers watchers that query the DOM.
These tests verify the property definitions exist.
"""
def test_reactive_properties_defined(self):
"""Test that reactive properties are defined."""
from clide.app import ClideApp
# Check the reactive descriptors exist on the class
assert hasattr(ClideApp, "workspace_visible")
assert hasattr(ClideApp, "compact_mode")
assert hasattr(ClideApp, "fullscreen_panel")
assert hasattr(ClideApp, "current_file")
class TestClideAppThemes:
"""Tests for ClideApp theme registration."""
def test_themes_registered(self):
"""Test that themes are registered."""
from clide.app import ClideApp
app = ClideApp()
# Default theme should be set
assert app.theme == "summer-night"
def test_custom_theme_from_settings(self):
"""Test that custom theme from settings is applied."""
from clide.app import ClideApp
settings = ClideSettings(theme="dracula")
app = ClideApp(settings=settings)
assert app.theme == "dracula"
class TestClideAppActions:
"""Tests for ClideApp action methods.
Note: Action methods that modify reactive properties or
query the DOM cannot be fully tested without running the app.
"""
def test_action_methods_exist(self):
"""Test that action methods are defined."""
from clide.app import ClideApp
app = ClideApp()
# Verify action methods exist
assert hasattr(app, "action_toggle_compact")
assert hasattr(app, "action_toggle_sidebar")
assert hasattr(app, "action_toggle_context")
assert hasattr(app, "action_toggle_terminal")
assert hasattr(app, "action_focus_claude")
assert callable(app.action_toggle_compact)
class TestClideSettings:
"""Tests for ClideSettings."""
def test_default_settings(self):
"""Test default settings values."""
settings = ClideSettings()
assert settings.theme == "summer-night"
assert settings.jira_enabled is False
def test_custom_settings(self):
"""Test custom settings values."""
settings = ClideSettings(theme="nord", jira_enabled=True)
assert settings.theme == "nord"
assert settings.jira_enabled is True
+75
View File
@@ -0,0 +1,75 @@
"""Tests for configuration models."""
import pytest
from pydantic import ValidationError
from clide.models.config import ClideSettings, KeybindingsConfig, PanelConfig
class TestClideSettings:
"""Tests for ClideSettings model."""
def test_default_settings(self) -> None:
"""Default settings should be valid."""
settings = ClideSettings()
assert settings.theme == "summer-night"
assert settings.auto_save is True
assert settings.confirm_exit is True
assert settings.jira_enabled is False
def test_custom_settings(self) -> None:
"""Custom settings should be applied."""
settings = ClideSettings(
theme="dracula",
claude_path="/custom/path/claude",
auto_save=False,
jira_enabled=True,
)
assert settings.theme == "dracula"
assert settings.claude_path == "/custom/path/claude"
assert settings.auto_save is False
assert settings.jira_enabled is True
class TestPanelConfig:
"""Tests for PanelConfig model."""
def test_default_values(self) -> None:
"""Default values should be set correctly."""
config = PanelConfig()
assert config.sidebar_visible is True
assert config.context_visible is True
assert config.workspace_visible is False
assert config.sidebar_width_percent == 20
assert config.context_width_percent == 25
def test_frozen_model(self) -> None:
"""PanelConfig should be immutable."""
config = PanelConfig()
with pytest.raises(ValidationError):
config.sidebar_visible = False # type: ignore
def test_strict_mode(self) -> None:
"""PanelConfig should enforce strict types."""
with pytest.raises(ValidationError):
PanelConfig(sidebar_width_percent="30") # type: ignore
class TestKeybindingsConfig:
"""Tests for KeybindingsConfig model."""
def test_default_keybindings(self) -> None:
"""Default keybindings should be valid."""
config = KeybindingsConfig()
assert config.toggle_sidebar == "ctrl+b"
assert config.toggle_terminal == "ctrl+`"
assert config.focus_claude == "ctrl+1"
def test_custom_keybindings(self) -> None:
"""Custom keybindings should be applied."""
config = KeybindingsConfig(
toggle_sidebar="ctrl+shift+s",
save="cmd+s",
)
assert config.toggle_sidebar == "ctrl+shift+s"
assert config.save == "cmd+s"
+419
View File
@@ -0,0 +1,419 @@
"""Tests for controller classes."""
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from clide.controllers.diff import DiffController
from clide.controllers.editor import EditorController
from clide.controllers.git import GitController
from clide.controllers.jira import JiraController
from clide.controllers.problems import ProblemsController
from clide.controllers.todos import TodosController
from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine
from clide.models.editor import CursorPosition, FileBuffer
from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitStatus
from clide.models.problems import Problem, ProblemsSummary, Severity
from clide.models.todos import TodoItem, TodosSummary, TodoType
from clide.services.git_service import GitService
from clide.services.process_service import CommandResult
class TestGitController:
"""Tests for GitController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> GitController:
return GitController(tmp_path)
@pytest.mark.asyncio
async def test_get_status(self, controller: GitController):
mock_status = GitStatus(
branch="main",
staged=(GitChange(path="a.py", status=ChangeStatus.ADDED, staged=True),),
unstaged=(),
)
with patch.object(controller._service, "get_status", return_value=mock_status):
status = await controller.get_status()
assert status.branch == "main"
assert len(status.staged) == 1
@pytest.mark.asyncio
async def test_get_branches(self, controller: GitController):
mock_branches = [
GitBranch(name="main", is_current=True, is_remote=False),
GitBranch(name="develop", is_current=False, is_remote=False),
]
with patch.object(controller._service, "get_branches", return_value=mock_branches):
branches = await controller.get_branches()
assert len(branches) == 2
assert branches[0].is_current is True
@pytest.mark.asyncio
async def test_get_log(self, controller: GitController):
mock_commits = [
GitCommit(
hash="abc123def456",
short_hash="abc123",
message="Initial commit",
author="Test",
date="2024-01-01",
)
]
with patch.object(controller._service, "get_log", return_value=mock_commits):
commits = await controller.get_log(limit=10)
assert len(commits) == 1
assert commits[0].message == "Initial commit"
@pytest.mark.asyncio
async def test_stage_file(self, controller: GitController):
mock_status = GitStatus(branch="main", staged=(), unstaged=())
with patch.object(controller._service, "stage_file", return_value=True):
with patch.object(controller._service, "get_status", return_value=mock_status):
result = await controller.stage_file("test.py")
assert result is True
@pytest.mark.asyncio
async def test_unstage_file(self, controller: GitController):
mock_status = GitStatus(branch="main", staged=(), unstaged=())
with patch.object(controller._service, "unstage_file", return_value=True):
with patch.object(controller._service, "get_status", return_value=mock_status):
result = await controller.unstage_file("test.py")
assert result is True
@pytest.mark.asyncio
async def test_checkout_branch(self, controller: GitController):
mock_status = GitStatus(branch="develop", staged=(), unstaged=())
with patch.object(controller._service, "checkout_branch", return_value=True):
with patch.object(controller._service, "get_status", return_value=mock_status):
with patch.object(controller._service, "get_branches", return_value=[]):
result = await controller.checkout_branch("develop")
assert result is True
@pytest.mark.asyncio
async def test_current_branch_property(self, controller: GitController):
mock_status = GitStatus(branch="feature", staged=(), unstaged=())
with patch.object(controller._service, "get_status", return_value=mock_status):
await controller.get_status() # Populate _status
assert controller.current_branch == "feature"
def test_current_branch_unknown(self, controller: GitController):
assert controller.current_branch == "unknown"
class TestEditorController:
"""Tests for EditorController."""
@pytest.fixture
def controller(self) -> EditorController:
return EditorController()
def test_initial_state(self, controller: EditorController):
assert controller.active_buffer is None
assert controller.open_files == []
assert controller.has_unsaved_changes is False
def test_update_content(self, controller: EditorController):
buffer = FileBuffer(path=Path("/test.py"), content="original")
controller._state.buffers.append(buffer)
controller.update_content(Path("/test.py"), "modified")
assert buffer.content == "modified"
assert buffer.is_modified is True
def test_update_cursor(self, controller: EditorController):
buffer = FileBuffer(path=Path("/test.py"), content="test")
controller._state.buffers.append(buffer)
controller.update_cursor(Path("/test.py"), 5, 10)
assert buffer.cursor.line == 5
assert buffer.cursor.column == 10
def test_set_active_by_index(self, controller: EditorController):
buffer1 = FileBuffer(path=Path("/a.py"), content="a")
buffer2 = FileBuffer(path=Path("/b.py"), content="b")
controller._state.buffers = [buffer1, buffer2]
controller.set_active_by_index(1)
assert controller._state.active_buffer_index == 1
def test_set_active_invalid_index(self, controller: EditorController):
controller.set_active_by_index(10) # Should not crash
assert controller._state.active_buffer_index is None
class TestDiffController:
"""Tests for DiffController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> DiffController:
return DiffController(tmp_path)
def test_initial_state(self, controller: DiffController):
assert controller.diff is None
assert controller.is_proposal is False
def test_load_proposal(self, controller: DiffController):
old_content = "line1\nline2\n"
new_content = "line1\nmodified\n"
diff = controller.load_proposal("test.py", old_content, new_content)
assert diff.file_path == "test.py"
assert controller.is_proposal is True
def test_accept_hunk(self, controller: DiffController):
controller._state.diff = DiffContent(file_path="t.py", hunks=())
controller.accept_hunk(0)
assert 0 in controller._state.accepted_hunks
assert 0 not in controller._state.rejected_hunks
def test_reject_hunk(self, controller: DiffController):
controller._state.diff = DiffContent(file_path="t.py", hunks=())
controller.reject_hunk(0)
assert 0 in controller._state.rejected_hunks
assert 0 not in controller._state.accepted_hunks
def test_accept_all(self, controller: DiffController):
hunk = DiffHunk(
header="@@",
old_start=1,
old_count=1,
new_start=1,
new_count=1,
lines=(),
)
controller._state.diff = DiffContent(file_path="t.py", hunks=(hunk, hunk))
controller.accept_all()
assert len(controller._state.accepted_hunks) == 2
def test_reject_all(self, controller: DiffController):
hunk = DiffHunk(
header="@@",
old_start=1,
old_count=1,
new_start=1,
new_count=1,
lines=(),
)
controller._state.diff = DiffContent(file_path="t.py", hunks=(hunk,))
controller.reject_all()
assert len(controller._state.rejected_hunks) == 1
def test_clear(self, controller: DiffController):
controller._state.diff = DiffContent(file_path="t.py", hunks=())
controller._state.is_proposal = True
controller.clear()
assert controller.diff is None
assert controller.is_proposal is False
def test_toggle_side_by_side(self, controller: DiffController):
assert controller._state.side_by_side is True # Default is True
result = controller.toggle_side_by_side()
assert result is False # Toggled to False
assert controller._state.side_by_side is False
class TestProblemsController:
"""Tests for ProblemsController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> ProblemsController:
return ProblemsController(tmp_path)
def test_initial_state(self, controller: ProblemsController):
assert controller.problems == []
assert controller.error_count == 0
assert controller.warning_count == 0
def test_filter_by_severity(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="test",
),
Problem(
file_path=Path("/b.py"),
line=1,
column=1,
severity=Severity.WARNING,
message="warn",
source="test",
),
]
controller._state.problems = problems
errors = controller.filter_by_severity(Severity.ERROR)
assert len(errors) == 1
assert errors[0].severity == Severity.ERROR
def test_filter_by_source(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="ruff",
),
Problem(
file_path=Path("/b.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="mypy",
),
]
controller._state.problems = problems
ruff_problems = controller.filter_by_source("ruff")
assert len(ruff_problems) == 1
def test_next_problem(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="1",
source="test",
),
Problem(
file_path=Path("/b.py"),
line=2,
column=1,
severity=Severity.ERROR,
message="2",
source="test",
),
]
controller._state.problems = problems
p1 = controller.next_problem()
assert p1.message == "1"
p2 = controller.next_problem()
assert p2.message == "2"
# Should wrap around
p3 = controller.next_problem()
assert p3.message == "1"
def test_prev_problem(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="1",
source="test",
),
Problem(
file_path=Path("/b.py"),
line=2,
column=1,
severity=Severity.ERROR,
message="2",
source="test",
),
]
controller._state.problems = problems
p = controller.prev_problem()
assert p.message == "2"
def test_clear(self, controller: ProblemsController):
controller._state.problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="test",
)
]
controller.clear()
assert controller.problems == []
class TestTodosController:
"""Tests for TodosController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> TodosController:
return TodosController(tmp_path)
def test_initial_state(self, controller: TodosController):
assert controller.items == []
assert controller.total_count == 0
def test_filter_by_type(self, controller: TodosController):
items = [
TodoItem(file_path=Path("/a.py"), line=1, column=1, todo_type=TodoType.TODO, text="1", context_line="# TODO: 1"),
TodoItem(file_path=Path("/b.py"), line=2, column=1, todo_type=TodoType.FIXME, text="2", context_line="# FIXME: 2"),
]
controller._state.items = items
todos = controller.filter_by_type(TodoType.TODO)
assert len(todos) == 1
assert todos[0].todo_type == TodoType.TODO
def test_get_grouped_items(self, controller: TodosController):
items = [
TodoItem(file_path=Path("/a.py"), line=1, column=1, todo_type=TodoType.TODO, text="1", context_line="# TODO: 1"),
TodoItem(file_path=Path("/a.py"), line=5, column=1, todo_type=TodoType.FIXME, text="2", context_line="# FIXME: 2"),
TodoItem(file_path=Path("/b.py"), line=1, column=1, todo_type=TodoType.TODO, text="3", context_line="# TODO: 3"),
]
controller._state.items = items
grouped = controller.get_grouped_items()
assert len(grouped) == 2
assert len(grouped[Path("/a.py")]) == 2
assert len(grouped[Path("/b.py")]) == 1
def test_toggle_group_by_file(self, controller: TodosController):
assert controller._state.group_by_file is True # Default is True per TodosState model
result = controller.toggle_group_by_file()
assert result is False
assert controller._state.group_by_file is False
class TestJiraController:
"""Tests for JiraController."""
@pytest.fixture
def controller(self) -> JiraController:
return JiraController(enabled=True)
@pytest.fixture
def disabled_controller(self) -> JiraController:
return JiraController(enabled=False)
def test_initial_enabled_state(self, controller: JiraController):
assert controller.enabled is True
def test_initial_disabled_state(self, disabled_controller: JiraController):
assert disabled_controller.enabled is False
def test_enable_disable(self, disabled_controller: JiraController):
disabled_controller.enable()
assert disabled_controller.enabled is True
disabled_controller.disable()
assert disabled_controller.enabled is False
@pytest.mark.asyncio
async def test_run_command_when_disabled(self, disabled_controller: JiraController):
result = await disabled_controller.run_command("issue", "list")
assert "disabled" in result.lower()
@pytest.mark.asyncio
async def test_get_content_when_disabled(self, disabled_controller: JiraController):
result = await disabled_controller.get_content()
assert result is None
+52
View File
@@ -0,0 +1,52 @@
"""Tests for extension system."""
import pytest
from clide.extensions import hookimpl
from clide.extensions.manager import ExtensionManager
class SampleExtension:
"""Sample extension for testing."""
@hookimpl
def clide_on_app_startup(self, app: object) -> None:
"""Track that startup was called."""
self.startup_called = True
self.received_app = app
class TestExtensionManager:
"""Tests for ExtensionManager."""
def test_register_plugin(self) -> None:
"""Plugins can be registered manually."""
manager = ExtensionManager()
extension = SampleExtension()
manager.register_plugin(extension, "sample")
assert "sample" in manager.list_extensions()
def test_unregister_plugin(self) -> None:
"""Plugins can be unregistered."""
manager = ExtensionManager()
extension = SampleExtension()
manager.register_plugin(extension, "sample")
manager.unregister_plugin("sample")
assert "sample" not in manager.list_extensions()
@pytest.mark.asyncio
async def test_trigger_startup_hook(self) -> None:
"""Startup hooks are triggered for all extensions."""
manager = ExtensionManager()
extension = SampleExtension()
manager.register_plugin(extension, "sample")
mock_app = object()
await manager.trigger_app_startup(mock_app)
assert extension.startup_called is True
assert extension.received_app is mock_app
+437
View File
@@ -0,0 +1,437 @@
"""Tests for Pydantic models."""
from pathlib import Path
import pytest
from pydantic import ValidationError
from clide.models.config import ClideSettings, KeybindingsConfig, PanelConfig
from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine, DiffViewState
from clide.models.editor import CursorPosition, EditorState, FileBuffer, Selection
from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitGraph, GitStatus
from clide.models.problems import Problem, ProblemsSummary, ProblemsState, Severity
from clide.models.theme import ThemeColors, ThemeDefinition, ThemeMetadata
from clide.models.todos import TodoItem, TodosSummary, TodosState, TodoType
class TestCursorPosition:
"""Tests for CursorPosition model."""
def test_create_cursor(self):
cursor = CursorPosition(line=10, column=5)
assert cursor.line == 10
assert cursor.column == 5
def test_cursor_is_frozen(self):
cursor = CursorPosition(line=0, column=0)
with pytest.raises(ValidationError):
cursor.line = 1
class TestFileBuffer:
"""Tests for FileBuffer model."""
def test_create_buffer(self):
buffer = FileBuffer(
path=Path("/test/file.py"),
content="print('hello')",
language="python",
)
assert buffer.path == Path("/test/file.py")
assert buffer.content == "print('hello')"
assert buffer.language == "python"
assert buffer.is_modified is False
def test_buffer_display_name(self):
buffer = FileBuffer(path=Path("/test/file.py"), content="")
assert buffer.display_name == "file.py"
def test_buffer_modified_display_name(self):
buffer = FileBuffer(path=Path("/test/file.py"), content="", is_modified=True)
assert buffer.display_name == "● file.py"
def test_buffer_filename(self):
buffer = FileBuffer(path=Path("/some/deep/path/script.js"), content="")
assert buffer.filename == "script.js"
class TestEditorState:
"""Tests for EditorState model."""
def test_empty_state(self):
state = EditorState()
assert state.buffers == []
assert state.active_buffer_index is None
assert state.active_buffer is None
def test_active_buffer(self):
buffer1 = FileBuffer(path=Path("/a.py"), content="a")
buffer2 = FileBuffer(path=Path("/b.py"), content="b")
state = EditorState(buffers=[buffer1, buffer2], active_buffer_index=1)
assert state.active_buffer == buffer2
def test_get_buffer_by_path(self):
buffer = FileBuffer(path=Path("/test.py"), content="test")
state = EditorState(buffers=[buffer])
assert state.get_buffer_by_path(Path("/test.py")) == buffer
assert state.get_buffer_by_path(Path("/other.py")) is None
class TestGitChange:
"""Tests for GitChange model."""
def test_create_change(self):
from clide.models.git import ChangeStatus
change = GitChange(path="src/main.py", status=ChangeStatus.MODIFIED, staged=True)
assert change.path == "src/main.py"
assert change.status == ChangeStatus.MODIFIED
assert change.staged is True
def test_change_is_frozen(self):
from clide.models.git import ChangeStatus
change = GitChange(path="f", status=ChangeStatus.ADDED, staged=False)
with pytest.raises(ValidationError):
change.path = "new.py"
class TestGitStatus:
"""Tests for GitStatus model."""
def test_create_status(self):
staged = (GitChange(path="a.py", status=ChangeStatus.ADDED, staged=True),)
unstaged = (GitChange(path="b.py", status=ChangeStatus.MODIFIED, staged=False),)
status = GitStatus(branch="main", staged=staged, unstaged=unstaged)
assert status.branch == "main"
assert len(status.staged) == 1
assert len(status.unstaged) == 1
def test_empty_status(self):
status = GitStatus(branch="main", staged=(), unstaged=())
assert status.branch == "main"
assert len(status.staged) == 0
assert len(status.unstaged) == 0
class TestGitBranch:
"""Tests for GitBranch model."""
def test_create_branch(self):
branch = GitBranch(name="feature/test", is_current=True, is_remote=False)
assert branch.name == "feature/test"
assert branch.is_current is True
assert branch.is_remote is False
class TestGitCommit:
"""Tests for GitCommit model."""
def test_create_commit(self):
commit = GitCommit(
hash="abc123def456789",
short_hash="abc123",
message="Test commit",
author="Test Author",
date="2024-01-01",
)
assert commit.hash == "abc123def456789"
assert commit.short_hash == "abc123"
assert commit.message == "Test commit"
class TestDiffLine:
"""Tests for DiffLine model."""
def test_added_line(self):
line = DiffLine(change_type=ChangeType.ADDED, content="new line", new_line_num=10)
assert line.change_type == ChangeType.ADDED
assert line.content == "new line"
def test_removed_line(self):
line = DiffLine(change_type=ChangeType.REMOVED, content="old line", old_line_num=5)
assert line.change_type == ChangeType.REMOVED
def test_context_line(self):
line = DiffLine(
change_type=ChangeType.CONTEXT,
content="unchanged",
old_line_num=5,
new_line_num=5,
)
assert line.change_type == ChangeType.CONTEXT
class TestDiffHunk:
"""Tests for DiffHunk model."""
def test_create_hunk(self):
lines = (
DiffLine(change_type=ChangeType.REMOVED, content="old", old_line_num=1),
DiffLine(change_type=ChangeType.ADDED, content="new", new_line_num=1),
)
hunk = DiffHunk(
header="@@ -1,1 +1,1 @@",
old_start=1,
old_count=1,
new_start=1,
new_count=1,
lines=lines,
)
assert len(hunk.lines) == 2
class TestDiffContent:
"""Tests for DiffContent model."""
def test_create_diff(self):
hunk = DiffHunk(
header="@@ -1 +1 @@",
old_start=1,
old_count=1,
new_start=1,
new_count=1,
lines=(),
)
diff = DiffContent(file_path="test.py", hunks=(hunk,))
assert diff.file_path == "test.py"
assert len(diff.hunks) == 1
class TestProblem:
"""Tests for Problem model."""
def test_create_problem(self):
problem = Problem(
file_path=Path("/test.py"),
line=10,
column=5,
severity=Severity.ERROR,
message="Syntax error",
source="ruff",
code="E999",
)
assert problem.line == 10
assert problem.severity == Severity.ERROR
def test_severity_icon(self):
error = Problem(
file_path=Path("/t.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="test",
)
assert error.severity_icon == "✖"
warning = Problem(
file_path=Path("/t.py"),
line=1,
column=1,
severity=Severity.WARNING,
message="warn",
source="test",
)
assert warning.severity_icon == "⚠"
class TestProblemsSummary:
"""Tests for ProblemsSummary model."""
def test_create_summary(self):
summary = ProblemsSummary(errors=5, warnings=3, infos=1, hints=0)
assert summary.total == 9
class TestTodoItem:
"""Tests for TodoItem model."""
def test_create_todo(self):
todo = TodoItem(
file_path=Path("/src/main.py"),
line=42,
column=5,
todo_type=TodoType.TODO,
text="Implement this feature",
context_line="# TODO: Implement this feature",
)
assert todo.line == 42
assert todo.todo_type == TodoType.TODO
def test_type_icon(self):
todo = TodoItem(
file_path=Path("/t.py"),
line=1,
column=1,
todo_type=TodoType.TODO,
text="todo",
context_line="# TODO: todo",
)
assert todo.type_icon == "☐"
fixme = TodoItem(
file_path=Path("/t.py"),
line=1,
column=1,
todo_type=TodoType.FIXME,
text="fixme",
context_line="# FIXME: fixme",
)
assert fixme.type_icon == "🔧"
class TestTodosSummary:
"""Tests for TodosSummary model."""
def test_create_summary(self):
summary = TodosSummary(
todo_count=5,
fixme_count=3,
hack_count=2,
other_count=0,
)
assert summary.total == 10
assert summary.todo_count == 5
class TestThemeColors:
"""Tests for ThemeColors model."""
def test_valid_colors(self):
colors = ThemeColors(
primary="#00a3d2",
secondary="#00a9b9",
accent="#fa5f8b",
background="#21262f",
surface="#393e48",
panel="#292e38",
foreground="#e2e8f5",
success="#00ab9a",
warning="#d08447",
error="#f06c6f",
)
assert colors.primary == "#00a3d2"
def test_invalid_color_rejected(self):
with pytest.raises(ValidationError):
ThemeColors(
primary="invalid",
secondary="#00a9b9",
accent="#fa5f8b",
background="#21262f",
surface="#393e48",
panel="#292e38",
foreground="#e2e8f5",
success="#00ab9a",
warning="#d08447",
error="#f06c6f",
)
def test_color_normalized_to_lowercase(self):
colors = ThemeColors(
primary="#00A3D2",
secondary="#00A9B9",
accent="#FA5F8B",
background="#21262F",
surface="#393E48",
panel="#292E38",
foreground="#E2E8F5",
success="#00AB9A",
warning="#D08447",
error="#F06C6F",
)
assert colors.primary == "#00a3d2"
class TestThemeDefinition:
"""Tests for ThemeDefinition model."""
def test_create_theme(self):
colors = ThemeColors(
primary="#00a3d2",
secondary="#00a9b9",
accent="#fa5f8b",
background="#21262f",
surface="#393e48",
panel="#292e38",
foreground="#e2e8f5",
success="#00ab9a",
warning="#d08447",
error="#f06c6f",
)
theme = ThemeDefinition(
name="test-theme",
display_name="Test Theme",
dark=True,
colors=colors,
)
assert theme.name == "test-theme"
assert theme.dark is True
def test_to_textual_theme(self):
colors = ThemeColors(
primary="#00a3d2",
secondary="#00a9b9",
accent="#fa5f8b",
background="#21262f",
surface="#393e48",
panel="#292e38",
foreground="#e2e8f5",
success="#00ab9a",
warning="#d08447",
error="#f06c6f",
)
theme_def = ThemeDefinition(
name="test",
display_name="Test",
dark=True,
colors=colors,
)
textual_theme = theme_def.to_textual_theme()
assert textual_theme.name == "test"
class TestThemeMetadata:
"""Tests for ThemeMetadata model."""
def test_create_metadata(self):
meta = ThemeMetadata(
name="summer-night",
display_name="Summer Night",
dark=True,
category="core",
)
assert meta.name == "summer-night"
assert meta.category == "core"
class TestClideSettings:
"""Tests for ClideSettings model."""
def test_default_settings(self):
settings = ClideSettings()
assert settings.theme == "summer-night"
assert settings.jira_enabled is False
def test_custom_settings(self):
settings = ClideSettings(theme="dracula", jira_enabled=True)
assert settings.theme == "dracula"
assert settings.jira_enabled is True
class TestPanelConfig:
"""Tests for PanelConfig model."""
def test_default_panel_config(self):
config = PanelConfig()
assert config.sidebar_visible is True
assert config.context_visible is True
assert config.workspace_visible is False
class TestKeybindingsConfig:
"""Tests for KeybindingsConfig model."""
def test_default_keybindings(self):
config = KeybindingsConfig()
assert config.toggle_sidebar == "ctrl+b"
assert config.toggle_terminal == "ctrl+`"
+242
View File
@@ -0,0 +1,242 @@
"""Tests for panel widgets."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from clide.models.diff import DiffContent
from clide.models.git import GitBranch, GitChange
from clide.models.problems import Problem, Severity
from clide.models.todos import TodoItem, TodoType
class TestSidebarPanel:
"""Tests for SidebarPanel."""
def test_initial_state(self, tmp_path: Path):
"""Test initial state."""
from clide.widgets.panels.sidebar import SidebarPanel
panel = SidebarPanel(workdir=tmp_path)
assert panel._workdir == tmp_path
assert panel.current_branch == "main"
assert panel.visible is True
assert panel.id == "panel-sidebar"
def test_default_workdir(self):
"""Test default workdir is cwd."""
from clide.widgets.panels.sidebar import SidebarPanel
panel = SidebarPanel()
assert panel._workdir == Path.cwd()
def test_file_selected_message(self):
"""Test FileSelected message."""
from clide.widgets.panels.sidebar import SidebarPanel
msg = SidebarPanel.FileSelected(Path("/test/file.py"))
assert msg.path == Path("/test/file.py")
def test_git_file_selected_message(self):
"""Test GitFileSelected message."""
from clide.widgets.panels.sidebar import SidebarPanel
msg = SidebarPanel.GitFileSelected(Path("/test/file.py"), staged=True)
assert msg.path == Path("/test/file.py")
assert msg.staged is True
def test_branch_changed_message(self):
"""Test BranchChanged message."""
from clide.widgets.panels.sidebar import SidebarPanel
msg = SidebarPanel.BranchChanged("develop")
assert msg.branch == "develop"
class TestWorkspacePanel:
"""Tests for WorkspacePanel."""
def test_initial_state(self, tmp_path: Path):
"""Test initial state."""
from clide.widgets.panels.workspace import WorkspacePanel
panel = WorkspacePanel(workdir=tmp_path)
assert panel._workdir == tmp_path
assert panel.visible is False
assert panel.active_tab == "editor"
assert panel.id == "panel-workspace"
def test_default_workdir(self):
"""Test default workdir is cwd."""
from clide.widgets.panels.workspace import WorkspacePanel
panel = WorkspacePanel()
assert panel._workdir == Path.cwd()
def test_show_hide_toggle(self):
"""Test show/hide/toggle methods."""
from clide.widgets.panels.workspace import WorkspacePanel
panel = WorkspacePanel()
assert panel.visible is False
panel.show()
assert panel.visible is True
panel.hide()
assert panel.visible is False
panel.toggle()
assert panel.visible is True
panel.toggle()
assert panel.visible is False
def test_file_saved_message(self):
"""Test FileSaved message."""
from clide.widgets.panels.workspace import WorkspacePanel
msg = WorkspacePanel.FileSaved(Path("/test/file.py"))
assert msg.path == Path("/test/file.py")
def test_diff_accepted_message(self):
"""Test DiffAccepted message."""
from clide.widgets.panels.workspace import WorkspacePanel
msg = WorkspacePanel.DiffAccepted("test.py")
assert msg.file_path == "test.py"
def test_diff_rejected_message(self):
"""Test DiffRejected message."""
from clide.widgets.panels.workspace import WorkspacePanel
msg = WorkspacePanel.DiffRejected("test.py")
assert msg.file_path == "test.py"
def test_command_submitted_message(self):
"""Test CommandSubmitted message."""
from clide.widgets.panels.workspace import WorkspacePanel
msg = WorkspacePanel.CommandSubmitted("ls -la")
assert msg.command == "ls -la"
def test_close_requested_message(self):
"""Test CloseRequested message."""
from clide.widgets.panels.workspace import WorkspacePanel
msg = WorkspacePanel.CloseRequested()
assert msg is not None
class TestClaudePanel:
"""Tests for ClaudePanel."""
def test_initial_state(self, tmp_path: Path):
"""Test initial state."""
from clide.widgets.panels.claude import ClaudePanel
panel = ClaudePanel(workdir=tmp_path, auto_start=False)
assert panel.id == "panel-claude"
assert panel.workspace_visible is False
assert panel._workdir == tmp_path
assert panel._auto_start is False
assert panel._restart_on_exit is True
def test_default_workdir(self):
"""Test default workdir is cwd."""
from clide.widgets.panels.claude import ClaudePanel
panel = ClaudePanel(auto_start=False)
assert panel._workdir == Path.cwd()
def test_claude_exited_message(self):
"""Test ClaudeExited message."""
from clide.widgets.panels.claude import ClaudePanel
msg = ClaudePanel.ClaudeExited(0)
assert msg.return_code == 0
def test_claude_started_message(self):
"""Test ClaudeStarted message."""
from clide.widgets.panels.claude import ClaudePanel
msg = ClaudePanel.ClaudeStarted()
assert msg is not None
def test_workdir_property(self, tmp_path: Path):
"""Test workdir property."""
from clide.widgets.panels.claude import ClaudePanel
panel = ClaudePanel(auto_start=False)
panel.workdir = tmp_path
assert panel.workdir == tmp_path
def test_set_restart_on_exit(self):
"""Test set_restart_on_exit method."""
from clide.widgets.panels.claude import ClaudePanel
panel = ClaudePanel(auto_start=False)
assert panel._restart_on_exit is True
panel.set_restart_on_exit(False)
assert panel._restart_on_exit is False
class TestContextPanel:
"""Tests for ContextPanel."""
def test_initial_state_enabled(self):
"""Test initial state with Jira enabled."""
from clide.widgets.panels.context import ContextPanel
panel = ContextPanel(jira_enabled=True)
assert panel._jira_enabled is True
assert panel.problem_count == 0
assert panel.todo_count == 0
assert panel.visible is True
assert panel.id == "panel-context"
def test_initial_state_disabled(self):
"""Test initial state with Jira disabled."""
from clide.widgets.panels.context import ContextPanel
panel = ContextPanel(jira_enabled=False)
assert panel._jira_enabled is False
def test_problem_clicked_message(self):
"""Test ProblemClicked message."""
from clide.widgets.panels.context import ContextPanel
problem = Problem(
file_path=Path("/test.py"),
line=10,
column=5,
severity=Severity.ERROR,
message="Test error",
source="ruff",
)
msg = ContextPanel.ProblemClicked(problem)
assert msg.problem == problem
def test_todo_clicked_message(self):
"""Test TodoClicked message."""
from clide.widgets.panels.context import ContextPanel
item = TodoItem(
file_path=Path("/test.py"),
line=10,
column=1,
todo_type=TodoType.TODO,
text="Test todo",
context_line="# TODO: Test todo",
)
msg = ContextPanel.TodoClicked(item)
assert msg.item == item
def test_jira_refresh_requested_message(self):
"""Test JiraRefreshRequested message."""
from clide.widgets.panels.context import ContextPanel
msg = ContextPanel.JiraRefreshRequested()
assert msg is not None
+287
View File
@@ -0,0 +1,287 @@
"""Tests for service classes."""
import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from clide.models.git import GitBranch, GitChange, GitCommit, GitStatus
from clide.models.problems import Problem, Severity
from clide.models.todos import TodoItem, TodoType
from clide.services.file_service import FileService
from clide.services.git_service import GitService
from clide.services.linter_service import LinterService
from clide.services.process_service import CommandResult, ProcessService
from clide.services.todo_scanner import TodoScanner
class TestProcessService:
"""Tests for ProcessService."""
@pytest.fixture
def service(self, tmp_path: Path) -> ProcessService:
return ProcessService(cwd=tmp_path)
def test_run_sync_success(self, service: ProcessService):
result = service.run_sync("echo", "hello")
assert result.success is True
assert "hello" in result.stdout
def test_run_sync_failure(self, service: ProcessService):
result = service.run_sync("false") # Unix command that always fails
assert result.success is False
@pytest.mark.asyncio
async def test_run_async_success(self, service: ProcessService):
result = await service.run("echo", "world")
assert result.success is True
assert "world" in result.stdout
@pytest.mark.asyncio
async def test_run_async_with_timeout(self, service: ProcessService):
# This should complete quickly
result = await service.run("echo", "fast", timeout=5.0)
assert result.success is True
def test_command_result(self):
result = CommandResult(
returncode=0,
stdout="output",
stderr="",
)
assert result.success is True
assert result.stdout == "output"
def test_command_result_failure(self):
result = CommandResult(
returncode=1,
stdout="",
stderr="error",
)
assert result.success is False
class TestFileService:
"""Tests for FileService."""
@pytest.mark.asyncio
async def test_read_file(self, tmp_path: Path):
test_file = tmp_path / "test.txt"
test_file.write_text("Hello, World!")
service = FileService(tmp_path)
content = await service.read_file(test_file)
assert content == "Hello, World!"
@pytest.mark.asyncio
async def test_read_file_not_found(self, tmp_path: Path):
service = FileService(tmp_path)
with pytest.raises(FileNotFoundError):
await service.read_file(tmp_path / "nonexistent.txt")
@pytest.mark.asyncio
async def test_write_file(self, tmp_path: Path):
test_file = tmp_path / "output.txt"
service = FileService(tmp_path)
await service.write_file(test_file, "Test content")
assert test_file.read_text() == "Test content"
@pytest.mark.asyncio
async def test_get_language_python(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("test.py")) == "python"
@pytest.mark.asyncio
async def test_get_language_javascript(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("app.js")) == "javascript"
@pytest.mark.asyncio
async def test_get_language_typescript(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("component.tsx")) == "tsx"
@pytest.mark.asyncio
async def test_get_language_unknown(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("file.xyz")) is None
@pytest.mark.asyncio
async def test_get_language_markdown(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("README.md")) == "markdown"
class TestGitService:
"""Tests for GitService."""
@pytest.fixture
def git_repo(self, tmp_path: Path) -> Path:
"""Create a minimal git repo for testing."""
git_dir = tmp_path / ".git"
git_dir.mkdir()
(git_dir / "HEAD").write_text("ref: refs/heads/main")
(git_dir / "config").write_text("")
return tmp_path
@pytest.fixture
def service(self, git_repo: Path) -> GitService:
return GitService(git_repo)
@pytest.mark.asyncio
async def test_get_status(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
async def mock_status(*args, **kwargs):
if "status" in args:
return CommandResult(returncode=0, stdout="", stderr="")
elif "branch" in args:
return CommandResult(returncode=0, stdout="main\n", stderr="")
return CommandResult(returncode=0, stdout="0\t0", stderr="")
mock_run.side_effect = mock_status
status = await service.get_status()
assert status is not None
assert status.branch == "main"
@pytest.mark.asyncio
async def test_get_branches(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="*main|origin/main|abc1234|Test\n feature|origin/feature|def5678|Test2\n",
stderr="",
)
branches = await service.get_branches()
assert len(branches) == 2
assert branches[0].name == "main"
assert branches[0].is_current is True
@pytest.mark.asyncio
async def test_get_log(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="abc1234567890|abc1234|Test commit|Author|2024-01-01||HEAD -> main\n",
stderr="",
)
commits = await service.get_log(max_count=10)
assert len(commits) == 1
assert commits[0].message == "Test commit"
@pytest.mark.asyncio
async def test_stage_file(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="",
stderr="",
)
result = await service.stage_file("test.py")
assert result is True
@pytest.mark.asyncio
async def test_unstage_file(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="",
stderr="",
)
result = await service.unstage_file("test.py")
assert result is True
class TestLinterService:
"""Tests for LinterService."""
@pytest.fixture
def service(self, tmp_path: Path) -> LinterService:
return LinterService(tmp_path)
@pytest.mark.asyncio
async def test_run_ruff_no_issues(self, service: LinterService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="[]",
stderr="",
)
problems = await service.run_ruff()
assert problems == []
@pytest.mark.asyncio
async def test_run_ruff_with_issues(self, service: LinterService):
ruff_output = """[
{
"filename": "/test/file.py",
"location": {"row": 10, "column": 5},
"code": "E501",
"message": "Line too long"
}
]"""
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=1,
stdout=ruff_output,
stderr="",
)
problems = await service.run_ruff()
assert len(problems) == 1
assert problems[0].code == "E501"
class TestTodoScanner:
"""Tests for TodoScanner."""
@pytest.fixture
def scanner(self, tmp_path: Path) -> TodoScanner:
return TodoScanner(tmp_path)
@pytest.fixture
def project_with_todos(self, tmp_path: Path) -> Path:
"""Create a project with TODO comments."""
src = tmp_path / "src"
src.mkdir()
(src / "main.py").write_text("""
# TODO: Implement feature
def main():
pass # FIXME: Handle errors
# HACK: Temporary workaround
""")
return tmp_path
@pytest.mark.asyncio
async def test_scan_finds_todos(self, project_with_todos: Path):
scanner = TodoScanner(project_with_todos)
items, summary = await scanner.scan()
# Should find TODO, FIXME, and HACK
assert len(items) >= 1
assert summary.total >= 1
@pytest.mark.asyncio
async def test_scan_empty_project(self, tmp_path: Path):
scanner = TodoScanner(tmp_path)
items, summary = await scanner.scan()
assert items == []
assert summary.total == 0
def test_parse_ripgrep_output_todo(self, scanner: TodoScanner):
output = "test.py:10:# TODO: Fix this"
items = scanner._parse_ripgrep_output(output)
assert len(items) == 1
assert items[0].todo_type == TodoType.TODO
assert "Fix this" in items[0].text
def test_parse_ripgrep_output_fixme(self, scanner: TodoScanner):
output = "test.py:20:// FIXME: Broken code"
items = scanner._parse_ripgrep_output(output)
assert len(items) == 1
assert items[0].todo_type == TodoType.FIXME
def test_parse_ripgrep_output_hack(self, scanner: TodoScanner):
output = "test.py:30:/* HACK: Workaround */"
items = scanner._parse_ripgrep_output(output)
assert len(items) == 1
assert items[0].todo_type == TodoType.HACK
+645
View File
@@ -0,0 +1,645 @@
"""Tests for widget components."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine
from clide.models.editor import CursorPosition, FileBuffer
from clide.models.git import ChangeStatus, GitBranch, 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"