Sprint 1 - viz-platform Plugin completed (13/13 issues): - Commands: 7 files (initial-setup, chart, dashboard, theme, theme-new, theme-css, component) - Agents: 3 files (theme-setup, layout-builder, component-check) - Documentation: README.md, claude-md-integration.md - Tests: 94 tests passing (68-99% coverage) - CHANGELOG updated with completion status Closes: #178, #179, #180, #181, #182 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
305 lines
8.3 KiB
Python
305 lines
8.3 KiB
Python
"""
|
|
Unit tests for theme management tools.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
@pytest.fixture
|
|
def theme_store():
|
|
"""Create a fresh ThemeStore instance"""
|
|
from mcp_server.theme_store import ThemeStore
|
|
store = ThemeStore()
|
|
store._themes = {} # Clear any existing themes
|
|
return store
|
|
|
|
|
|
@pytest.fixture
|
|
def theme_tools(theme_store):
|
|
"""Create ThemeTools instance with fresh store"""
|
|
from mcp_server.theme_tools import ThemeTools
|
|
return ThemeTools(store=theme_store)
|
|
|
|
|
|
def test_theme_store_init():
|
|
"""Test theme store initialization"""
|
|
from mcp_server.theme_store import ThemeStore
|
|
|
|
store = ThemeStore()
|
|
|
|
# Should have default theme
|
|
assert store.get_theme("default") is not None
|
|
|
|
|
|
def test_default_theme_structure():
|
|
"""Test default theme has required structure"""
|
|
from mcp_server.theme_store import DEFAULT_THEME
|
|
|
|
assert "name" in DEFAULT_THEME
|
|
assert "tokens" in DEFAULT_THEME
|
|
assert "colors" in DEFAULT_THEME["tokens"]
|
|
assert "spacing" in DEFAULT_THEME["tokens"]
|
|
assert "typography" in DEFAULT_THEME["tokens"]
|
|
assert "radii" in DEFAULT_THEME["tokens"]
|
|
|
|
|
|
def test_default_theme_colors():
|
|
"""Test default theme has required color tokens"""
|
|
from mcp_server.theme_store import DEFAULT_THEME
|
|
|
|
colors = DEFAULT_THEME["tokens"]["colors"]
|
|
|
|
assert "primary" in colors
|
|
assert "secondary" in colors
|
|
assert "success" in colors
|
|
assert "warning" in colors
|
|
assert "error" in colors
|
|
assert "background" in colors
|
|
assert "text" in colors
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_create(theme_tools):
|
|
"""Test creating a new theme"""
|
|
tokens = {
|
|
"colors": {
|
|
"primary": "#ff0000"
|
|
}
|
|
}
|
|
|
|
result = await theme_tools.theme_create("my-theme", tokens)
|
|
|
|
assert result["name"] == "my-theme"
|
|
assert "tokens" in result
|
|
assert result["tokens"]["colors"]["primary"] == "#ff0000"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_create_merges_with_defaults(theme_tools):
|
|
"""Test that new theme merges with default tokens"""
|
|
tokens = {
|
|
"colors": {
|
|
"primary": "#ff0000"
|
|
}
|
|
}
|
|
|
|
result = await theme_tools.theme_create("partial-theme", tokens)
|
|
|
|
# Should have primary from our tokens
|
|
assert result["tokens"]["colors"]["primary"] == "#ff0000"
|
|
# Should inherit secondary from defaults
|
|
assert "secondary" in result["tokens"]["colors"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_create_duplicate_name(theme_tools, theme_store):
|
|
"""Test creating theme with existing name fails"""
|
|
# Create first theme
|
|
await theme_tools.theme_create("existing", {"colors": {}})
|
|
|
|
# Try to create with same name
|
|
result = await theme_tools.theme_create("existing", {"colors": {}})
|
|
|
|
assert "error" in result
|
|
assert "already exists" in result["error"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_extend(theme_tools, theme_store):
|
|
"""Test extending an existing theme"""
|
|
# Create base theme
|
|
await theme_tools.theme_create("base", {
|
|
"colors": {"primary": "#0000ff"}
|
|
})
|
|
|
|
# Extend it
|
|
result = await theme_tools.theme_extend(
|
|
base_theme="base",
|
|
overrides={"colors": {"secondary": "#00ff00"}},
|
|
new_name="extended"
|
|
)
|
|
|
|
assert result["name"] == "extended"
|
|
# Should have base primary
|
|
assert result["tokens"]["colors"]["primary"] == "#0000ff"
|
|
# Should have override secondary
|
|
assert result["tokens"]["colors"]["secondary"] == "#00ff00"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_extend_nonexistent_base(theme_tools):
|
|
"""Test extending nonexistent theme fails"""
|
|
result = await theme_tools.theme_extend(
|
|
base_theme="nonexistent",
|
|
overrides={},
|
|
new_name="new"
|
|
)
|
|
|
|
assert "error" in result
|
|
assert "not found" in result["error"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_extend_default_name(theme_tools, theme_store):
|
|
"""Test extending creates default name if not provided"""
|
|
await theme_tools.theme_create("base", {"colors": {}})
|
|
|
|
result = await theme_tools.theme_extend(
|
|
base_theme="base",
|
|
overrides={}
|
|
# No new_name provided
|
|
)
|
|
|
|
assert result["name"] == "base_extended"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_validate(theme_tools, theme_store):
|
|
"""Test theme validation"""
|
|
await theme_tools.theme_create("test-theme", {
|
|
"colors": {"primary": "#ff0000"},
|
|
"spacing": {"md": "16px"}
|
|
})
|
|
|
|
result = await theme_tools.theme_validate("test-theme")
|
|
|
|
assert "complete" in result or "validation" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_validate_nonexistent(theme_tools):
|
|
"""Test validating nonexistent theme"""
|
|
result = await theme_tools.theme_validate("nonexistent")
|
|
|
|
assert "error" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_export_css(theme_tools, theme_store):
|
|
"""Test exporting theme as CSS"""
|
|
await theme_tools.theme_create("css-theme", {
|
|
"colors": {"primary": "#ff0000"},
|
|
"spacing": {"md": "16px"}
|
|
})
|
|
|
|
result = await theme_tools.theme_export_css("css-theme")
|
|
|
|
assert "css" in result
|
|
# CSS should contain custom properties
|
|
assert "--" in result["css"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_export_css_nonexistent(theme_tools):
|
|
"""Test exporting nonexistent theme"""
|
|
result = await theme_tools.theme_export_css("nonexistent")
|
|
|
|
assert "error" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_list(theme_tools, theme_store):
|
|
"""Test listing themes"""
|
|
await theme_tools.theme_create("theme1", {"colors": {}})
|
|
await theme_tools.theme_create("theme2", {"colors": {}})
|
|
|
|
result = await theme_tools.theme_list()
|
|
|
|
assert "themes" in result
|
|
assert "theme1" in result["themes"]
|
|
assert "theme2" in result["themes"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_activate(theme_tools, theme_store):
|
|
"""Test activating a theme"""
|
|
await theme_tools.theme_create("active-theme", {"colors": {}})
|
|
|
|
result = await theme_tools.theme_activate("active-theme")
|
|
|
|
assert result.get("active_theme") == "active-theme" or result.get("success") is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_theme_activate_nonexistent(theme_tools):
|
|
"""Test activating nonexistent theme"""
|
|
result = await theme_tools.theme_activate("nonexistent")
|
|
|
|
assert "error" in result
|
|
|
|
|
|
def test_theme_store_get_theme(theme_store):
|
|
"""Test getting theme from store"""
|
|
from mcp_server.theme_store import DEFAULT_THEME
|
|
|
|
# Add a theme first, then retrieve it
|
|
theme_store._themes["test-theme"] = {"name": "test-theme", "tokens": {}}
|
|
result = theme_store.get_theme("test-theme")
|
|
|
|
assert result is not None
|
|
assert result["name"] == "test-theme"
|
|
|
|
|
|
def test_theme_store_list_themes(theme_store):
|
|
"""Test listing themes from store"""
|
|
result = theme_store.list_themes()
|
|
|
|
assert isinstance(result, list)
|
|
|
|
|
|
def test_deep_merge(theme_tools):
|
|
"""Test deep merging of token dicts"""
|
|
base = {
|
|
"colors": {
|
|
"primary": "#000",
|
|
"secondary": "#111"
|
|
},
|
|
"spacing": {"sm": "8px"}
|
|
}
|
|
|
|
override = {
|
|
"colors": {
|
|
"primary": "#fff"
|
|
}
|
|
}
|
|
|
|
result = theme_tools._deep_merge(base, override)
|
|
|
|
# primary should be overridden
|
|
assert result["colors"]["primary"] == "#fff"
|
|
# secondary should remain
|
|
assert result["colors"]["secondary"] == "#111"
|
|
# spacing should remain
|
|
assert result["spacing"]["sm"] == "8px"
|
|
|
|
|
|
def test_validate_tokens(theme_tools):
|
|
"""Test token validation"""
|
|
from mcp_server.theme_store import REQUIRED_TOKEN_CATEGORIES
|
|
|
|
tokens = {
|
|
"colors": {"primary": "#000"},
|
|
"spacing": {"md": "16px"},
|
|
"typography": {"fontFamily": "Inter"},
|
|
"radii": {"md": "8px"}
|
|
}
|
|
|
|
result = theme_tools._validate_tokens(tokens)
|
|
|
|
assert "complete" in result
|
|
# Check for either "missing" or "missing_required" key
|
|
assert "missing" in result or "missing_required" in result or "missing_optional" in result
|
|
|
|
|
|
def test_validate_tokens_incomplete(theme_tools):
|
|
"""Test validation of incomplete tokens"""
|
|
tokens = {
|
|
"colors": {"primary": "#000"}
|
|
# Missing spacing, typography, radii
|
|
}
|
|
|
|
result = theme_tools._validate_tokens(tokens)
|
|
|
|
# Should flag missing categories
|
|
assert result["complete"] is False or len(result.get("missing", [])) > 0
|