move python clide to legacy/
Clide is being rebuilt as a Flutter desktop app. The Python Textual implementation moves wholesale into legacy/ rather than being deleted: its pane model, panel set, git skills, and panel communication design are real thought that should remain readable next to the new code while the rebuild finds its shape. Git's rename tracking preserves history, so `git log -- legacy/` still works. The Flutter rebuild lives at the repo root alongside a Go sidecar (the architecture claudian was heading toward, which folds into clide as a core component rather than a separate plugin project). Bootstrap of the new shape lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Clide test suite."""
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Shared pytest fixtures for Clide tests."""
|
||||
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from pathlib import Path
|
||||
|
||||
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()
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Full application test harness for Clide."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
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: ClideSettings | None = None,
|
||||
extension_manager: ExtensionManager | None = None,
|
||||
) -> None:
|
||||
self.workdir = workdir
|
||||
self.settings = settings or ClideSettings()
|
||||
self.extension_manager = extension_manager or ExtensionManager()
|
||||
self._app: ClideApp | None = None
|
||||
self._pilot: Pilot | None = 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()
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Controller isolation test harness for Clide."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
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: type | None = 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: type | None = 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
|
||||
@@ -0,0 +1 @@
|
||||
"""Integration tests for Clide."""
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Integration tests for FilesView widget."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from textual.app import App, ComposeResult
|
||||
|
||||
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():
|
||||
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():
|
||||
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_noisy_dirs(test_directory: Path):
|
||||
"""Test that noisy directories are filtered but hidden files are shown."""
|
||||
# Create hidden files/dirs and noisy 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()
|
||||
|
||||
# Get visible items in the tree
|
||||
visible_names = {node.data.path.name for node in files_view.root.children if node.data}
|
||||
|
||||
# Noisy directories should be filtered out
|
||||
assert ".git" not in visible_names
|
||||
assert "__pycache__" not in visible_names
|
||||
|
||||
# Hidden files are shown (they'll be dimmed in the UI)
|
||||
assert ".hidden_file" in visible_names
|
||||
|
||||
# Regular files/dirs are shown
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
"""Snapshot (visual regression) tests for Clide."""
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
+226
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
+226
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for Clide."""
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Tests for ClideApp."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
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 (alt-based per CLAUDE.md design)
|
||||
assert "alt+q" in bindings # Quit
|
||||
assert "alt+b" in bindings # Toggle sidebar
|
||||
assert "alt+p" in bindings # Command palette
|
||||
assert "alt+`" in bindings # Toggle terminal
|
||||
assert "alt+1" in bindings # Focus Claude
|
||||
assert "f11" in bindings # Fullscreen
|
||||
assert "escape" in bindings # Exit fullscreen
|
||||
|
||||
|
||||
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
|
||||
@@ -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"
|
||||
@@ -0,0 +1,417 @@
|
||||
"""Tests for controller classes."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import 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 DiffContent, DiffHunk
|
||||
from clide.models.editor import FileBuffer
|
||||
from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitStatus
|
||||
from clide.models.problems import Problem, Severity
|
||||
from clide.models.todos import TodoItem, TodoType
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
from clide.models.editor import CursorPosition, EditorState, FileBuffer
|
||||
from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitStatus
|
||||
from clide.models.problems import Problem, ProblemsSummary, Severity
|
||||
from clide.models.theme import ThemeColors, ThemeDefinition, ThemeMetadata
|
||||
from clide.models.todos import TodoItem, TodosSummary, 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+`"
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Tests for panel widgets."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
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.get_active_tab_type() is None
|
||||
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_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
|
||||
@@ -0,0 +1,286 @@
|
||||
"""Tests for service classes."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from clide.models.todos import 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_async(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_async(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_async(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")) == "typescript"
|
||||
|
||||
@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, project_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, project_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
|
||||
@@ -0,0 +1,635 @@
|
||||
"""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_noisy_dirs(self, tmp_path: Path):
|
||||
"""Test that noisy directories are filtered out but hidden files are shown."""
|
||||
from clide.widgets.components.files_view import FilesView
|
||||
|
||||
view = FilesView(path=tmp_path)
|
||||
paths = [
|
||||
tmp_path / "visible.py",
|
||||
tmp_path / ".hidden", # Hidden file - shown (dimmed)
|
||||
tmp_path / ".git", # Noisy dir - filtered
|
||||
tmp_path / "__pycache__", # Noisy dir - filtered
|
||||
tmp_path / "node_modules", # Noisy dir - filtered
|
||||
tmp_path / ".venv", # Dimmed path - shown (dimmed)
|
||||
tmp_path / "src",
|
||||
]
|
||||
filtered = view.filter_paths(paths)
|
||||
|
||||
# Regular files/dirs are shown
|
||||
assert tmp_path / "visible.py" in filtered
|
||||
assert tmp_path / "src" in filtered
|
||||
|
||||
# Hidden files are shown (will be dimmed in render)
|
||||
assert tmp_path / ".hidden" in filtered
|
||||
assert tmp_path / ".venv" in filtered
|
||||
|
||||
# Noisy directories are filtered out
|
||||
assert tmp_path / ".git" not in filtered
|
||||
assert tmp_path / "__pycache__" not in filtered
|
||||
assert tmp_path / "node_modules" 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 in CommitItem."""
|
||||
from clide.widgets.components.git_graph import CommitItem
|
||||
|
||||
assert CommitItem.COMMIT == "●"
|
||||
assert CommitItem.MERGE == "◆"
|
||||
|
||||
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_commit_item_stores_commit(self):
|
||||
"""Test CommitItem stores commit correctly."""
|
||||
from clide.widgets.components.git_graph import CommitItem
|
||||
|
||||
commit = GitCommit(
|
||||
hash="abc123def456789",
|
||||
short_hash="abc123",
|
||||
message="Test commit message",
|
||||
author="Author",
|
||||
date="2024-01-01",
|
||||
is_merge=False,
|
||||
refs=(),
|
||||
)
|
||||
item = CommitItem(commit)
|
||||
assert item.commit == commit
|
||||
assert item.commit.short_hash == "abc123"
|
||||
assert item.commit.message == "Test commit message"
|
||||
|
||||
def test_commit_item_merge_commit(self):
|
||||
"""Test CommitItem handles merge commit."""
|
||||
from clide.widgets.components.git_graph import CommitItem
|
||||
|
||||
commit = GitCommit(
|
||||
hash="abc123def456789",
|
||||
short_hash="abc123",
|
||||
message="Merge branch",
|
||||
author="Author",
|
||||
date="2024-01-01",
|
||||
is_merge=True,
|
||||
refs=("main", "HEAD"),
|
||||
)
|
||||
item = CommitItem(commit)
|
||||
assert item.commit.is_merge is True
|
||||
assert "main" in item.commit.refs
|
||||
assert "HEAD" in item.commit.refs
|
||||
|
||||
|
||||
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._terminal is None
|
||||
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user