# chuk-motion/tests/test_theme_manager.py
"""
Tests for ThemeManager class.
"""
import json
import pytest
from chuk_motion.themes.models import Theme
from chuk_motion.themes.theme_manager import ThemeManager
class TestTheme:
"""Test Theme class (Pydantic model)."""
def test_theme_creation(self, sample_theme):
"""Test creating a theme from Pydantic models."""
assert sample_theme.name == "Test Theme"
assert sample_theme.description == "A test theme"
assert len(sample_theme.colors.primary) == 3
assert len(sample_theme.use_cases) == 2
def test_theme_to_dict(self, sample_theme):
"""Test converting theme to dictionary using model_dump()."""
theme_dict = sample_theme.model_dump()
assert isinstance(theme_dict, dict)
assert "name" in theme_dict
assert "description" in theme_dict
assert "colors" in theme_dict
assert "typography" in theme_dict
assert "motion" in theme_dict
def test_theme_roundtrip(self, sample_theme):
"""Test that model_dump and Theme(**dict) are inverses."""
theme_dict = sample_theme.model_dump()
theme2 = Theme(**theme_dict)
assert sample_theme.name == theme2.name
assert sample_theme.description == theme2.description
assert sample_theme.colors.primary == theme2.colors.primary
class TestThemeManagerInit:
"""Test ThemeManager initialization."""
@pytest.mark.asyncio
async def test_manager_creation(self, vfs):
"""Test that ThemeManager can be created."""
manager = ThemeManager(vfs)
assert manager is not None
assert isinstance(manager.themes, dict)
@pytest.mark.asyncio
async def test_builtin_themes_loaded(self, theme_manager):
"""Test that built-in themes are loaded on init."""
themes = theme_manager.list_themes()
assert len(themes) > 0
assert "tech" in themes
assert "finance" in themes
assert "education" in themes
@pytest.mark.asyncio
async def test_all_seven_themes_present(self, theme_manager):
"""Test that all 7 YouTube themes are present."""
themes = theme_manager.list_themes()
expected = ["tech", "finance", "education", "lifestyle", "gaming", "minimal", "business"]
assert len(themes) == 7
for theme in expected:
assert theme in themes
@pytest.mark.asyncio
async def test_current_theme_initially_none(self, theme_manager):
"""Test that current theme is None on init."""
assert theme_manager.get_current_theme() is None
class TestThemeManagerRegistry:
"""Test theme registration and retrieval."""
def test_register_theme(self, theme_manager, sample_theme):
"""Test registering a custom theme."""
theme_manager.register_theme("custom", sample_theme)
themes = theme_manager.list_themes()
assert "custom" in themes
def test_get_existing_theme(self, theme_manager):
"""Test getting an existing theme."""
theme = theme_manager.get_theme("tech")
assert theme is not None
assert isinstance(theme, Theme)
assert theme.name == "Tech"
def test_get_nonexistent_theme(self, theme_manager):
"""Test getting a non-existent theme returns None."""
theme = theme_manager.get_theme("nonexistent")
assert theme is None
def test_list_themes_returns_keys(self, theme_manager):
"""Test that list_themes returns theme keys."""
themes = theme_manager.list_themes()
assert isinstance(themes, list)
assert all(isinstance(t, str) for t in themes)
class TestThemeManagerInfo:
"""Test get_theme_info method."""
def test_get_theme_info_structure(self, theme_manager):
"""Test theme info structure."""
info = theme_manager.get_theme_info("tech")
assert info is not None
assert "name" in info
assert "description" in info
assert "colors" in info
assert "typography" in info
assert "motion" in info
assert "use_cases" in info
def test_get_theme_info_colors(self, theme_manager):
"""Test that theme info includes all color categories."""
info = theme_manager.get_theme_info("tech")
colors = info["colors"]
assert "primary" in colors
assert "accent" in colors
assert "gradient" in colors
assert "background" in colors
assert "text" in colors
assert "semantic" in colors
def test_get_theme_info_nonexistent(self, theme_manager):
"""Test getting info for non-existent theme."""
info = theme_manager.get_theme_info("nonexistent")
assert info is None
class TestThemeManagerCurrentTheme:
"""Test current theme management."""
def test_set_current_theme_valid(self, theme_manager):
"""Test setting a valid current theme."""
success = theme_manager.set_current_theme("tech")
assert success is True
assert theme_manager.get_current_theme() == "tech"
def test_set_current_theme_invalid(self, theme_manager):
"""Test setting an invalid current theme."""
success = theme_manager.set_current_theme("nonexistent")
assert success is False
assert theme_manager.get_current_theme() is None
def test_change_current_theme(self, theme_manager):
"""Test changing the current theme."""
theme_manager.set_current_theme("tech")
assert theme_manager.get_current_theme() == "tech"
theme_manager.set_current_theme("gaming")
assert theme_manager.get_current_theme() == "gaming"
class TestThemeManagerComparison:
"""Test theme comparison."""
def test_compare_two_themes(self, theme_manager):
"""Test comparing two valid themes."""
comparison = theme_manager.compare_themes("tech", "gaming")
assert "themes" in comparison
assert "comparison" in comparison
assert comparison["themes"] == ["tech", "gaming"]
def test_comparison_structure(self, theme_manager):
"""Test comparison result structure."""
comparison = theme_manager.compare_themes("tech", "finance")
comp = comparison["comparison"]
assert "names" in comp
assert "descriptions" in comp
assert "primary_colors" in comp
assert "accent_colors" in comp
assert "motion_feel" in comp
assert "use_cases" in comp
# Each should be a 2-element list
assert len(comp["names"]) == 2
assert len(comp["primary_colors"]) == 2
def test_compare_nonexistent_theme(self, theme_manager):
"""Test comparing with non-existent theme."""
comparison = theme_manager.compare_themes("tech", "nonexistent")
assert "error" in comparison
class TestThemeManagerSearch:
"""Test theme search functionality."""
def test_search_by_name(self, theme_manager):
"""Test searching themes by name."""
results = theme_manager.search_themes("tech")
assert "tech" in results
def test_search_by_description(self, theme_manager):
"""Test searching themes by description."""
# "gaming" should be in gaming theme description
results = theme_manager.search_themes("gaming")
assert "gaming" in results
def test_search_by_use_case(self, theme_manager):
"""Test searching themes by use case."""
# Search for a use case
results = theme_manager.search_themes("professional")
# Should find business and/or minimal themes
assert len(results) > 0
def test_search_case_insensitive(self, theme_manager):
"""Test that search is case-insensitive."""
results_lower = theme_manager.search_themes("tech")
results_upper = theme_manager.search_themes("TECH")
assert results_lower == results_upper
def test_search_no_matches(self, theme_manager):
"""Test search with no matches."""
results = theme_manager.search_themes("xyznonexistent")
assert len(results) == 0
def test_get_themes_by_category(self, theme_manager):
"""Test getting themes by category."""
# This is an alias for search_themes
results = theme_manager.get_themes_by_category("education")
assert "education" in results
class TestThemeManagerValidation:
"""Test theme validation."""
def test_validate_valid_theme(self, theme_manager, sample_theme):
"""Test validating a valid theme."""
# Convert to dict for validation
theme_dict = sample_theme.model_dump()
validation = theme_manager.validate_theme(theme_dict)
assert validation["valid"] is True
assert len(validation["errors"]) == 0
def test_validate_missing_required_key(self, theme_manager):
"""Test validating theme missing required keys."""
invalid_theme = {
"name": "Test",
"description": "Test theme",
# Missing colors, typography, motion, spacing
}
validation = theme_manager.validate_theme(invalid_theme)
assert validation["valid"] is False
assert len(validation["errors"]) > 0
def test_validate_missing_color_tokens(self, theme_manager, sample_theme):
"""Test validating theme with incomplete color tokens."""
theme_dict = sample_theme.model_dump()
del theme_dict["colors"]["primary"]
validation = theme_manager.validate_theme(theme_dict)
assert validation["valid"] is False
assert any("primary" in error for error in validation["errors"])
def test_validate_missing_typography_tokens(self, theme_manager, sample_theme):
"""Test validating theme with incomplete typography tokens."""
theme_dict = sample_theme.model_dump()
del theme_dict["typography"]["primary_font"]
validation = theme_manager.validate_theme(theme_dict)
assert validation["valid"] is False
def test_validate_missing_motion_tokens(self, theme_manager, sample_theme):
"""Test validating theme with incomplete motion tokens."""
theme_dict = sample_theme.model_dump()
del theme_dict["motion"]["default_spring"]
validation = theme_manager.validate_theme(theme_dict)
assert validation["valid"] is False
class TestThemeManagerExportImport:
"""Test theme export and import."""
@pytest.mark.asyncio
async def test_export_theme(self, theme_manager):
"""Test exporting a theme to file."""
export_path = "exported_theme.json"
result = await theme_manager.export_theme("tech", export_path)
assert "Error" not in result
assert result == export_path
# Check file content
content = await theme_manager.vfs.read_text(export_path)
data = json.loads(content)
assert data["name"] == "Tech"
@pytest.mark.asyncio
async def test_export_nonexistent_theme(self, theme_manager):
"""Test exporting non-existent theme."""
export_path = "exported.json"
result = await theme_manager.export_theme("nonexistent", export_path)
assert "Error" in result
@pytest.mark.asyncio
async def test_import_theme(self, theme_manager, sample_theme):
"""Test importing a theme from file."""
# Create a file in vfs with properly formatted theme
theme_dict = sample_theme.model_dump()
await theme_manager.vfs.write_file("test_theme.json", json.dumps(theme_dict))
result = await theme_manager.import_theme("test_theme.json", "imported")
assert "Successfully imported" in result
assert "imported" in theme_manager.list_themes()
@pytest.mark.asyncio
async def test_import_invalid_theme(self, theme_manager, invalid_theme_data):
"""Test importing invalid theme."""
await theme_manager.vfs.write_file("invalid_theme.json", json.dumps(invalid_theme_data))
result = await theme_manager.import_theme("invalid_theme.json", "invalid")
assert "Error" in result
@pytest.mark.asyncio
async def test_import_nonexistent_file(self, theme_manager):
"""Test importing from non-existent file."""
result = await theme_manager.import_theme("/nonexistent/file.json")
assert "Error" in result
@pytest.mark.asyncio
async def test_export_import_roundtrip(self, theme_manager):
"""Test export then import preserves theme."""
export_path = "roundtrip.json"
# Export
await theme_manager.export_theme("tech", export_path)
# Import
await theme_manager.import_theme(export_path, "reimported_tech")
# Verify
original = theme_manager.get_theme_info("tech")
reimported = theme_manager.get_theme_info("reimported_tech")
assert original["name"] == reimported["name"]
assert original["description"] == reimported["description"]
class TestThemeManagerCustomThemes:
"""Test custom theme creation."""
def test_create_custom_theme_from_scratch(self, theme_manager):
"""Test creating custom theme requires base theme now."""
# With Pydantic models, we require a base_theme to ensure valid structure
result = theme_manager.create_custom_theme(
name="Custom",
description="A custom theme",
)
# Should error without base_theme
assert "Error" in result
def test_create_custom_theme_from_base(self, theme_manager):
"""Test creating custom theme based on existing theme."""
result = theme_manager.create_custom_theme(
name="Custom Tech",
description="Tech with custom colors",
base_theme="tech",
color_overrides={"primary": ["#FF0000", "#CC0000", "#990000"]},
)
assert "Error" not in result
theme_key = result
theme_info = theme_manager.get_theme_info(theme_key)
assert theme_info["colors"]["primary"][0] == "#FF0000"
def test_create_custom_theme_invalid(self, theme_manager):
"""Test creating invalid custom theme without base."""
result = theme_manager.create_custom_theme(
name="No Base", description="No base theme provided"
)
# Should fail without base theme
assert "Error" in result
def test_custom_theme_key_format(self, theme_manager):
"""Test that custom theme keys are formatted correctly."""
result = theme_manager.create_custom_theme(
name="My Custom Theme", description="Test", base_theme="tech"
)
assert result == "my_custom_theme" # Spaces should become underscores
class TestThemeManagerEdgeCases:
"""Test edge cases and error handling."""
def test_empty_theme_list_after_init(self, theme_manager):
"""Test that theme list is never empty after init."""
themes = theme_manager.list_themes()
assert len(themes) > 0
def test_register_duplicate_theme_key(self, theme_manager, sample_theme):
"""Test registering theme with duplicate key overwrites."""
theme_manager.register_theme("custom", sample_theme)
# Register again with same key
new_theme = Theme(
name="New Theme",
description="Different theme",
colors=sample_theme.colors,
typography=sample_theme.typography,
motion=sample_theme.motion,
spacing=sample_theme.spacing,
)
theme_manager.register_theme("custom", new_theme)
# Should be overwritten
theme = theme_manager.get_theme("custom")
assert theme.name == "New Theme"
def test_validate_empty_theme(self, theme_manager):
"""Test validating completely empty theme."""
validation = theme_manager.validate_theme({})
assert validation["valid"] is False
assert len(validation["errors"]) > 0
@pytest.mark.asyncio
async def test_export_with_default_filename(self, theme_manager):
"""Test export with default filename."""
result = await theme_manager.export_theme("tech")
# Should create file with default name
assert "Error" not in result
assert "tech_theme.json" in result