Implements HTML rendering system for Information Spaces: - SpaceRenderer: Abstract base class for renderers - RenderConfig: Configuration for format, theme, TOC, etc. - RenderResult: Immutable result with content hash and metadata - ThemeConfig: Layered theme system with customization - CompositeRenderer: Multi-format renderer delegation - MarkdownToHTMLRenderer: Full markdown-to-HTML conversion - Theme support (github, dark, minimal, academic) - Code block handling - Link target="_blank" for external links - Table of contents generation - Heading ID generation for navigation - HTMLRendererFactory: Factory for common renderer configurations - SpaceRenderingService: Orchestration layer - Transclusion variable substitution - Render caching with automatic invalidation - Event emission (RENDER_STARTED, RENDER_COMPLETED, RENDER_FAILED) - Batch rendering support - Statistics tracking - SpaceRenderingServiceBuilder: Fluent builder pattern 60 unit tests covering all components. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
761 lines
23 KiB
Python
761 lines
23 KiB
Python
"""
|
|
Unit tests for Phase 4: HTML Rendering Mode components.
|
|
|
|
Tests cover:
|
|
- SpaceRenderer base class
|
|
- RenderConfig and ThemeConfig
|
|
- MarkdownToHTMLRenderer
|
|
- SpaceRenderingService
|
|
- Cache integration
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, MagicMock, patch
|
|
from pathlib import Path
|
|
|
|
from markitect.spaces.rendering import (
|
|
SpaceRenderer,
|
|
RenderConfig,
|
|
RenderResult,
|
|
RenderFormat,
|
|
ThemeConfig,
|
|
CompositeRenderer,
|
|
MarkdownToHTMLRenderer,
|
|
HTMLRendererFactory,
|
|
THEME_PROPERTIES,
|
|
combine_theme_properties,
|
|
SpaceRenderingService,
|
|
SpaceRenderingServiceBuilder,
|
|
)
|
|
from markitect.spaces.transclusion import (
|
|
RenderCache,
|
|
ReferenceGraph,
|
|
)
|
|
from markitect.spaces.events import EventBus, SpaceEventType
|
|
|
|
|
|
class TestRenderFormat:
|
|
"""Tests for RenderFormat enum."""
|
|
|
|
def test_html_format(self):
|
|
"""Test HTML format value."""
|
|
assert RenderFormat.HTML.value == "html"
|
|
|
|
def test_format_members(self):
|
|
"""Test all format members exist."""
|
|
assert RenderFormat.HTML
|
|
assert RenderFormat.PDF
|
|
assert RenderFormat.DOCX
|
|
assert RenderFormat.LATEX
|
|
|
|
|
|
class TestThemeConfig:
|
|
"""Tests for ThemeConfig dataclass."""
|
|
|
|
def test_default_values(self):
|
|
"""Test default configuration."""
|
|
config = ThemeConfig()
|
|
assert config.name == "default"
|
|
assert config.layers == ["basic"]
|
|
assert config.custom_css is None
|
|
assert config.custom_properties == {}
|
|
|
|
def test_custom_config(self):
|
|
"""Test custom configuration."""
|
|
config = ThemeConfig(
|
|
name="custom",
|
|
layers=["github", "dark"],
|
|
custom_css="body { color: red; }",
|
|
custom_properties={"max_width": "1000px"},
|
|
)
|
|
assert config.name == "custom"
|
|
assert config.layers == ["github", "dark"]
|
|
assert "color: red" in config.custom_css
|
|
assert config.custom_properties["max_width"] == "1000px"
|
|
|
|
|
|
class TestRenderConfig:
|
|
"""Tests for RenderConfig dataclass."""
|
|
|
|
def test_default_values(self):
|
|
"""Test default configuration."""
|
|
config = RenderConfig()
|
|
assert config.format == RenderFormat.HTML
|
|
assert config.include_toc is False
|
|
assert config.highlight_code is True
|
|
assert config.sanitize_html is True
|
|
assert config.link_target_blank is True
|
|
|
|
def test_custom_config(self):
|
|
"""Test custom configuration."""
|
|
theme = ThemeConfig(name="github")
|
|
config = RenderConfig(
|
|
format=RenderFormat.HTML,
|
|
theme=theme,
|
|
include_toc=True,
|
|
image_max_width="800px",
|
|
)
|
|
assert config.theme.name == "github"
|
|
assert config.include_toc is True
|
|
assert config.image_max_width == "800px"
|
|
|
|
|
|
class TestRenderResult:
|
|
"""Tests for RenderResult dataclass."""
|
|
|
|
def test_creation(self):
|
|
"""Test result creation."""
|
|
result = RenderResult(
|
|
content="<h1>Test</h1>",
|
|
format=RenderFormat.HTML,
|
|
content_hash="abc123",
|
|
source_hash="def456",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
assert result.content == "<h1>Test</h1>"
|
|
assert result.format == RenderFormat.HTML
|
|
assert result.document_id == "doc-1"
|
|
assert result.space_id == "space-1"
|
|
assert result.dependencies == set()
|
|
|
|
def test_with_dependencies(self):
|
|
"""Test result with dependencies."""
|
|
result = RenderResult(
|
|
content="<p>Test</p>",
|
|
format=RenderFormat.HTML,
|
|
content_hash="abc",
|
|
source_hash="def",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
dependencies={"doc-2", "doc-3"},
|
|
)
|
|
assert len(result.dependencies) == 2
|
|
assert "doc-2" in result.dependencies
|
|
|
|
def test_compute_hash(self):
|
|
"""Test hash computation."""
|
|
hash1 = RenderResult.compute_hash("test content")
|
|
hash2 = RenderResult.compute_hash("test content")
|
|
hash3 = RenderResult.compute_hash("different content")
|
|
|
|
assert hash1 == hash2
|
|
assert hash1 != hash3
|
|
assert len(hash1) == 16 # SHA256 truncated to 16 chars
|
|
|
|
|
|
class TestCombineThemeProperties:
|
|
"""Tests for theme property combination."""
|
|
|
|
def test_single_layer(self):
|
|
"""Test single theme layer."""
|
|
props = combine_theme_properties(["default"])
|
|
assert "font_family" in props
|
|
assert "body_color" in props
|
|
|
|
def test_multiple_layers(self):
|
|
"""Test multiple theme layers."""
|
|
props = combine_theme_properties(["default", "github"])
|
|
# GitHub layer should override default
|
|
assert props["max_width"] == "980px"
|
|
|
|
def test_unknown_layer(self):
|
|
"""Test with unknown layer."""
|
|
props = combine_theme_properties(["nonexistent"])
|
|
assert props == {}
|
|
|
|
def test_empty_layers(self):
|
|
"""Test with no layers."""
|
|
props = combine_theme_properties([])
|
|
assert props == {}
|
|
|
|
|
|
class TestMarkdownToHTMLRenderer:
|
|
"""Tests for MarkdownToHTMLRenderer."""
|
|
|
|
def test_supported_formats(self):
|
|
"""Test supported formats."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
assert RenderFormat.HTML in renderer.supported_formats
|
|
assert len(renderer.supported_formats) == 1
|
|
|
|
def test_simple_render(self):
|
|
"""Test simple markdown rendering."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
result = renderer.render(
|
|
content="# Hello World\n\nThis is a paragraph.",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert result.format == RenderFormat.HTML
|
|
assert "<h1" in result.content
|
|
assert "Hello World" in result.content
|
|
assert "<p>" in result.content
|
|
assert result.document_id == "doc-1"
|
|
assert result.space_id == "space-1"
|
|
|
|
def test_render_with_code(self):
|
|
"""Test rendering code blocks."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
markdown = """
|
|
# Code Example
|
|
|
|
```python
|
|
def hello():
|
|
print("Hello")
|
|
```
|
|
"""
|
|
result = renderer.render(
|
|
content=markdown,
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert "<pre>" in result.content or "<code>" in result.content
|
|
|
|
def test_render_with_links(self):
|
|
"""Test link rendering with target blank."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
result = renderer.render(
|
|
content="Visit [Google](https://google.com)",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert "https://google.com" in result.content
|
|
assert 'target="_blank"' in result.content
|
|
|
|
def test_render_with_toc(self):
|
|
"""Test table of contents generation."""
|
|
config = RenderConfig(include_toc=True)
|
|
renderer = MarkdownToHTMLRenderer(config)
|
|
|
|
markdown = """
|
|
# Main Title
|
|
|
|
## Section 1
|
|
|
|
Content here.
|
|
|
|
## Section 2
|
|
|
|
More content.
|
|
"""
|
|
result = renderer.render(
|
|
content=markdown,
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert 'class="toc"' in result.content
|
|
assert "Contents" in result.content
|
|
|
|
def test_render_with_dependencies(self):
|
|
"""Test dependencies are tracked."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
result = renderer.render(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
dependencies={"dep-1", "dep-2"},
|
|
)
|
|
|
|
assert len(result.dependencies) == 2
|
|
assert "dep-1" in result.dependencies
|
|
|
|
def test_content_hash_computation(self):
|
|
"""Test that hashes are computed."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
result = renderer.render(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert result.content_hash
|
|
assert result.source_hash
|
|
assert len(result.content_hash) == 16
|
|
|
|
def test_different_themes(self):
|
|
"""Test different theme configurations."""
|
|
github_config = RenderConfig(theme=ThemeConfig(name="github", layers=["github"]))
|
|
dark_config = RenderConfig(theme=ThemeConfig(name="dark", layers=["dark"]))
|
|
|
|
github_renderer = MarkdownToHTMLRenderer(github_config)
|
|
dark_renderer = MarkdownToHTMLRenderer(dark_config)
|
|
|
|
github_result = github_renderer.render("# Test", "doc", "space")
|
|
dark_result = dark_renderer.render("# Test", "doc", "space")
|
|
|
|
# Results should be different due to different themes
|
|
assert github_result.content != dark_result.content
|
|
assert "#24292e" in github_result.content # GitHub body color
|
|
assert "#c9d1d9" in dark_result.content # Dark mode text color
|
|
|
|
|
|
class TestHTMLRendererFactory:
|
|
"""Tests for HTMLRendererFactory."""
|
|
|
|
def test_create_default(self):
|
|
"""Test default renderer creation."""
|
|
renderer = HTMLRendererFactory.create_default()
|
|
assert isinstance(renderer, MarkdownToHTMLRenderer)
|
|
|
|
def test_create_github_style(self):
|
|
"""Test GitHub-style renderer."""
|
|
renderer = HTMLRendererFactory.create_github_style()
|
|
assert renderer.config.theme.name == "github"
|
|
|
|
def test_create_academic_style(self):
|
|
"""Test academic-style renderer."""
|
|
renderer = HTMLRendererFactory.create_academic_style()
|
|
assert renderer.config.theme.name == "academic"
|
|
assert renderer.config.include_toc is True
|
|
|
|
def test_create_minimal_style(self):
|
|
"""Test minimal-style renderer."""
|
|
renderer = HTMLRendererFactory.create_minimal_style()
|
|
assert renderer.config.theme.name == "minimal"
|
|
|
|
def test_create_dark_mode(self):
|
|
"""Test dark mode renderer."""
|
|
renderer = HTMLRendererFactory.create_dark_mode()
|
|
assert renderer.config.theme.name == "dark"
|
|
|
|
|
|
class TestCompositeRenderer:
|
|
"""Tests for CompositeRenderer."""
|
|
|
|
def test_register_renderer(self):
|
|
"""Test registering a renderer."""
|
|
composite = CompositeRenderer()
|
|
html_renderer = MarkdownToHTMLRenderer()
|
|
|
|
composite.register(html_renderer)
|
|
|
|
assert composite.get_renderer(RenderFormat.HTML) is html_renderer
|
|
|
|
def test_supported_formats(self):
|
|
"""Test listing supported formats."""
|
|
composite = CompositeRenderer()
|
|
html_renderer = MarkdownToHTMLRenderer()
|
|
|
|
composite.register(html_renderer)
|
|
|
|
formats = composite.supported_formats()
|
|
assert RenderFormat.HTML in formats
|
|
|
|
def test_render_via_composite(self):
|
|
"""Test rendering through composite."""
|
|
composite = CompositeRenderer()
|
|
composite.register(MarkdownToHTMLRenderer())
|
|
|
|
result = composite.render(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
format=RenderFormat.HTML,
|
|
)
|
|
|
|
assert result.format == RenderFormat.HTML
|
|
assert "<h1" in result.content
|
|
|
|
def test_no_renderer_for_format(self):
|
|
"""Test error when no renderer for format."""
|
|
composite = CompositeRenderer()
|
|
|
|
with pytest.raises(ValueError) as exc:
|
|
composite.render(
|
|
content="test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
format=RenderFormat.PDF,
|
|
)
|
|
|
|
assert "No renderer registered" in str(exc.value)
|
|
|
|
|
|
class TestSpaceRenderingService:
|
|
"""Tests for SpaceRenderingService."""
|
|
|
|
def test_default_initialization(self):
|
|
"""Test default service initialization."""
|
|
service = SpaceRenderingService()
|
|
|
|
assert service.renderer is not None
|
|
assert service.render_cache is not None
|
|
assert service.reference_graph is not None
|
|
|
|
def test_render_document(self):
|
|
"""Test basic document rendering."""
|
|
service = SpaceRenderingService()
|
|
|
|
result = service.render_document(
|
|
content="# Hello\n\nWorld",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert result.format == RenderFormat.HTML
|
|
assert "Hello" in result.content
|
|
assert result.document_id == "doc-1"
|
|
|
|
def test_render_caching(self):
|
|
"""Test that results are cached."""
|
|
service = SpaceRenderingService()
|
|
|
|
# First render
|
|
result1 = service.render_document(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
# Second render - should use cache
|
|
result2 = service.render_document(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
# Get cached entry
|
|
cached = service.get_cached_render("doc-1")
|
|
assert cached is not None
|
|
|
|
def test_force_refresh_bypasses_cache(self):
|
|
"""Test force refresh skips cache."""
|
|
service = SpaceRenderingService()
|
|
|
|
# First render
|
|
service.render_document(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
# Force refresh with different content
|
|
result = service.render_document(
|
|
content="# New Content",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
force_refresh=True,
|
|
)
|
|
|
|
assert "New Content" in result.content
|
|
|
|
def test_render_with_variables(self):
|
|
"""Test rendering with variable substitution."""
|
|
service = SpaceRenderingService()
|
|
|
|
result = service.render_document(
|
|
content="Hello {{name}}!",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
variables={"name": "World"},
|
|
)
|
|
|
|
assert "World" in result.content
|
|
|
|
def test_render_documents_batch(self):
|
|
"""Test batch rendering."""
|
|
service = SpaceRenderingService()
|
|
|
|
documents = [
|
|
{"id": "doc-1", "content": "# Doc 1"},
|
|
{"id": "doc-2", "content": "# Doc 2"},
|
|
{"id": "doc-3", "content": "# Doc 3"},
|
|
]
|
|
|
|
results = service.render_documents(
|
|
documents=documents,
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert len(results) == 3
|
|
assert "doc-1" in results
|
|
assert "doc-2" in results
|
|
assert "doc-3" in results
|
|
|
|
def test_invalidate_document(self):
|
|
"""Test cache invalidation."""
|
|
service = SpaceRenderingService()
|
|
|
|
# Render a document
|
|
service.render_document(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
# Invalidate
|
|
invalidated = service.invalidate_document("doc-1", "space-1")
|
|
|
|
assert "doc-1" in invalidated
|
|
assert service.get_cached_render("doc-1") is None
|
|
|
|
def test_invalidate_space(self):
|
|
"""Test invalidating entire space."""
|
|
service = SpaceRenderingService()
|
|
|
|
# Render multiple documents
|
|
service.render_document("# Doc 1", "doc-1", "space-1")
|
|
service.render_document("# Doc 2", "doc-2", "space-1")
|
|
|
|
# Invalidate space
|
|
count = service.invalidate_space("space-1")
|
|
|
|
assert count == 2
|
|
|
|
def test_render_statistics(self):
|
|
"""Test getting render statistics."""
|
|
service = SpaceRenderingService()
|
|
|
|
# Render some documents
|
|
service.render_document("# Doc 1", "doc-1", "space-1")
|
|
service.render_document("# Doc 2", "doc-2", "space-1")
|
|
|
|
stats = service.get_render_statistics("space-1")
|
|
|
|
assert "cached_documents" in stats
|
|
assert "render_format" in stats
|
|
assert stats["cached_documents"] == 2
|
|
|
|
def test_event_emission(self):
|
|
"""Test that events are emitted during rendering."""
|
|
event_bus = EventBus()
|
|
events = []
|
|
|
|
def capture_event(event):
|
|
events.append(event)
|
|
|
|
event_bus.subscribe(SpaceEventType.RENDER_STARTED, capture_event)
|
|
event_bus.subscribe(SpaceEventType.RENDER_COMPLETED, capture_event)
|
|
|
|
service = SpaceRenderingService(event_bus=event_bus)
|
|
|
|
service.render_document(
|
|
content="# Test",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
# Should have RENDER_STARTED and RENDER_COMPLETED events
|
|
event_types = [e.event_type for e in events]
|
|
assert SpaceEventType.RENDER_STARTED in event_types
|
|
assert SpaceEventType.RENDER_COMPLETED in event_types
|
|
|
|
|
|
class TestSpaceRenderingServiceBuilder:
|
|
"""Tests for SpaceRenderingServiceBuilder."""
|
|
|
|
def test_build_default(self):
|
|
"""Test building with defaults."""
|
|
service = SpaceRenderingServiceBuilder().build()
|
|
assert service.renderer is not None
|
|
|
|
def test_with_html_renderer(self):
|
|
"""Test building with HTML renderer."""
|
|
config = RenderConfig(theme=ThemeConfig(name="github"))
|
|
service = (
|
|
SpaceRenderingServiceBuilder()
|
|
.with_html_renderer(config)
|
|
.build()
|
|
)
|
|
assert service.renderer.config.theme.name == "github"
|
|
|
|
def test_with_event_bus(self):
|
|
"""Test building with event bus."""
|
|
bus = EventBus()
|
|
service = (
|
|
SpaceRenderingServiceBuilder()
|
|
.with_event_bus(bus)
|
|
.build()
|
|
)
|
|
assert service.event_bus is bus
|
|
|
|
def test_with_cache(self):
|
|
"""Test building with custom cache."""
|
|
cache = RenderCache()
|
|
service = (
|
|
SpaceRenderingServiceBuilder()
|
|
.with_cache(cache)
|
|
.build()
|
|
)
|
|
assert service.render_cache is cache
|
|
|
|
def test_with_reference_graph(self):
|
|
"""Test building with reference graph."""
|
|
graph = ReferenceGraph()
|
|
service = (
|
|
SpaceRenderingServiceBuilder()
|
|
.with_reference_graph(graph)
|
|
.build()
|
|
)
|
|
assert service.reference_graph is graph
|
|
|
|
def test_fluent_chaining(self):
|
|
"""Test fluent builder chaining."""
|
|
bus = EventBus()
|
|
cache = RenderCache()
|
|
graph = ReferenceGraph()
|
|
|
|
service = (
|
|
SpaceRenderingServiceBuilder()
|
|
.with_html_renderer()
|
|
.with_event_bus(bus)
|
|
.with_cache(cache)
|
|
.with_reference_graph(graph)
|
|
.build()
|
|
)
|
|
|
|
assert service.event_bus is bus
|
|
assert service.render_cache is cache
|
|
assert service.reference_graph is graph
|
|
|
|
|
|
class TestBasicMarkdownParsing:
|
|
"""Tests for basic markdown parsing fallback."""
|
|
|
|
def test_headings(self):
|
|
"""Test heading parsing."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
markdown = "# H1\n## H2\n### H3"
|
|
result = renderer.render(markdown, "doc", "space")
|
|
|
|
assert "<h1" in result.content
|
|
assert "<h2" in result.content
|
|
assert "<h3" in result.content
|
|
|
|
def test_bold_italic(self):
|
|
"""Test bold and italic."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
markdown = "**bold** and *italic*"
|
|
result = renderer.render(markdown, "doc", "space")
|
|
|
|
assert "<strong>bold</strong>" in result.content or "bold" in result.content
|
|
|
|
def test_code_blocks(self):
|
|
"""Test code block parsing."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
markdown = "```python\nprint('hello')\n```"
|
|
result = renderer.render(markdown, "doc", "space")
|
|
|
|
assert "<code" in result.content or "<pre" in result.content
|
|
|
|
def test_lists(self):
|
|
"""Test list parsing."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
markdown = "- Item 1\n- Item 2\n- Item 3"
|
|
result = renderer.render(markdown, "doc", "space")
|
|
|
|
assert "<li>" in result.content or "Item" in result.content
|
|
|
|
def test_blockquotes(self):
|
|
"""Test blockquote parsing."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
markdown = "> This is a quote"
|
|
result = renderer.render(markdown, "doc", "space")
|
|
|
|
assert "<blockquote" in result.content or "quote" in result.content
|
|
|
|
def test_horizontal_rule(self):
|
|
"""Test horizontal rule."""
|
|
renderer = MarkdownToHTMLRenderer()
|
|
markdown = "Text\n\n---\n\nMore text"
|
|
result = renderer.render(markdown, "doc", "space")
|
|
|
|
assert "<hr" in result.content
|
|
|
|
|
|
class TestThemeProperties:
|
|
"""Tests for theme property definitions."""
|
|
|
|
def test_default_theme_exists(self):
|
|
"""Test default theme is defined."""
|
|
assert "default" in THEME_PROPERTIES
|
|
|
|
def test_github_theme_exists(self):
|
|
"""Test GitHub theme is defined."""
|
|
assert "github" in THEME_PROPERTIES
|
|
|
|
def test_dark_theme_exists(self):
|
|
"""Test dark theme is defined."""
|
|
assert "dark" in THEME_PROPERTIES
|
|
|
|
def test_minimal_theme_exists(self):
|
|
"""Test minimal theme is defined."""
|
|
assert "minimal" in THEME_PROPERTIES
|
|
|
|
def test_academic_theme_exists(self):
|
|
"""Test academic theme is defined."""
|
|
assert "academic" in THEME_PROPERTIES
|
|
|
|
def test_theme_has_required_properties(self):
|
|
"""Test themes have required properties."""
|
|
required = ["font_family", "body_color", "body_background"]
|
|
for theme_name, props in THEME_PROPERTIES.items():
|
|
for prop in required:
|
|
assert prop in props, f"{theme_name} missing {prop}"
|
|
|
|
|
|
class TestRenderIntegration:
|
|
"""Integration tests for rendering pipeline."""
|
|
|
|
def test_full_render_pipeline(self):
|
|
"""Test complete render pipeline with cache."""
|
|
event_bus = EventBus()
|
|
cache = RenderCache()
|
|
graph = ReferenceGraph()
|
|
|
|
service = (
|
|
SpaceRenderingServiceBuilder()
|
|
.with_html_renderer()
|
|
.with_event_bus(event_bus)
|
|
.with_cache(cache)
|
|
.with_reference_graph(graph)
|
|
.build()
|
|
)
|
|
|
|
# First render
|
|
result1 = service.render_document(
|
|
content="# Hello World",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
assert result1.format == RenderFormat.HTML
|
|
assert "Hello World" in result1.content
|
|
|
|
# Check cached
|
|
assert cache.get("doc-1") is not None
|
|
|
|
# Second render should use cache
|
|
result2 = service.render_document(
|
|
content="# Hello World",
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
)
|
|
|
|
# Invalidate
|
|
invalidated = service.invalidate_document("doc-1", "space-1")
|
|
assert "doc-1" in invalidated
|
|
assert cache.get("doc-1") is None
|
|
|
|
def test_variable_substitution_in_render(self):
|
|
"""Test variable substitution during render."""
|
|
service = SpaceRenderingService()
|
|
|
|
content = "# Welcome {{user}}\n\nToday is {{day}}."
|
|
variables = {"user": "Alice", "day": "Monday"}
|
|
|
|
result = service.render_document(
|
|
content=content,
|
|
document_id="doc-1",
|
|
space_id="space-1",
|
|
variables=variables,
|
|
)
|
|
|
|
assert "Alice" in result.content
|
|
assert "Monday" in result.content
|