234 lines
7.5 KiB
Python
234 lines
7.5 KiB
Python
"""
|
|
Test the new abstract/concrete style system.
|
|
|
|
This test demonstrates how the new style system addresses the memory efficiency
|
|
concerns by using abstract styles that can be resolved to concrete styles
|
|
based on user preferences.
|
|
"""
|
|
|
|
import pytest
|
|
from pyWebLayout.style.abstract_style import (
|
|
AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize
|
|
)
|
|
from pyWebLayout.style.concrete_style import (
|
|
ConcreteStyleRegistry, RenderingContext, StyleResolver
|
|
)
|
|
from pyWebLayout.style.fonts import FontWeight
|
|
|
|
|
|
def test_abstract_style_is_hashable():
|
|
"""Test that AbstractStyle objects are hashable and can be used as dict keys."""
|
|
# Create two identical styles
|
|
style1 = AbstractStyle(
|
|
font_family=FontFamily.SERIF,
|
|
font_size=16,
|
|
font_weight=FontWeight.BOLD,
|
|
color="red"
|
|
)
|
|
|
|
style2 = AbstractStyle(
|
|
font_family=FontFamily.SERIF,
|
|
font_size=16,
|
|
font_weight=FontWeight.BOLD,
|
|
color="red"
|
|
)
|
|
|
|
# They should be equal and have the same hash
|
|
assert style1 == style2
|
|
assert hash(style1) == hash(style2)
|
|
|
|
# They should work as dictionary keys
|
|
style_dict = {style1: "first", style2: "second"}
|
|
assert len(style_dict) == 1 # Should be deduplicated
|
|
assert style_dict[style1] == "second" # Last value wins
|
|
|
|
|
|
def test_abstract_style_registry_deduplication():
|
|
"""Test that the registry prevents duplicate styles."""
|
|
registry = AbstractStyleRegistry()
|
|
|
|
# Create the same style twice
|
|
style1 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD)
|
|
style2 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD)
|
|
|
|
# Register both - should get same ID
|
|
id1, _ = registry.get_or_create_style(style1)
|
|
id2, _ = registry.get_or_create_style(style2)
|
|
|
|
assert id1 == id2 # Same style should get same ID
|
|
assert registry.get_style_count() == 2 # Only default + our style
|
|
|
|
|
|
def test_style_inheritance():
|
|
"""Test that style inheritance works properly."""
|
|
registry = AbstractStyleRegistry()
|
|
|
|
# Create base style
|
|
base_style = AbstractStyle(font_size=16, color="black")
|
|
base_id, _ = registry.get_or_create_style(base_style)
|
|
|
|
# Create derived style
|
|
derived_id, derived_style = registry.create_derived_style(
|
|
base_id,
|
|
font_weight=FontWeight.BOLD,
|
|
color="red"
|
|
)
|
|
|
|
# Resolve effective style
|
|
effective = registry.resolve_effective_style(derived_id)
|
|
|
|
assert effective.font_size == 16 # Inherited from base
|
|
assert effective.font_weight == FontWeight.BOLD # Overridden
|
|
assert effective.color == "red" # Overridden
|
|
|
|
|
|
def test_style_resolver_user_preferences():
|
|
"""Test that user preferences affect concrete style resolution."""
|
|
# Create rendering context with larger fonts
|
|
context = RenderingContext(
|
|
base_font_size=20, # Larger base size
|
|
font_scale_factor=1.5, # Additional scaling
|
|
large_text=True # Accessibility preference
|
|
)
|
|
|
|
resolver = StyleResolver(context)
|
|
|
|
# Create abstract style with medium size
|
|
abstract_style = AbstractStyle(font_size=FontSize.MEDIUM)
|
|
|
|
# Resolve to concrete style
|
|
concrete_style = resolver.resolve_style(abstract_style)
|
|
|
|
# Font size should be: 20 (base) * 1.0 (medium) * 1.5 (scale) * 1.2
|
|
# (large_text) = 36
|
|
expected_size = int(20 * 1.0 * 1.5 * 1.2)
|
|
assert concrete_style.font_size == expected_size
|
|
|
|
|
|
def test_style_resolver_color_resolution():
|
|
"""Test color name resolution."""
|
|
context = RenderingContext()
|
|
resolver = StyleResolver(context)
|
|
|
|
# Test named colors
|
|
red_style = AbstractStyle(color="red")
|
|
concrete_red = resolver.resolve_style(red_style)
|
|
assert concrete_red.color == (255, 0, 0)
|
|
|
|
# Test hex colors
|
|
hex_style = AbstractStyle(color="#ff0000")
|
|
concrete_hex = resolver.resolve_style(hex_style)
|
|
assert concrete_hex.color == (255, 0, 0)
|
|
|
|
# Test RGB tuple (should pass through)
|
|
rgb_style = AbstractStyle(color=(128, 64, 192))
|
|
concrete_rgb = resolver.resolve_style(rgb_style)
|
|
assert concrete_rgb.color == (128, 64, 192)
|
|
|
|
|
|
def test_concrete_style_caching():
|
|
"""Test that concrete styles are cached efficiently."""
|
|
context = RenderingContext()
|
|
registry = ConcreteStyleRegistry(StyleResolver(context))
|
|
|
|
# Create abstract style
|
|
abstract_style = AbstractStyle(font_size=16, color="blue")
|
|
|
|
# Get font twice - should be cached
|
|
font1 = registry.get_font(abstract_style)
|
|
font2 = registry.get_font(abstract_style)
|
|
|
|
# Should be the same object (cached)
|
|
assert font1 is font2
|
|
|
|
# Check cache stats
|
|
stats = registry.get_cache_stats()
|
|
assert stats["concrete_styles"] == 1
|
|
assert stats["fonts"] == 1
|
|
|
|
|
|
def test_global_font_scaling():
|
|
"""Test that global font scaling affects all text."""
|
|
# Create two contexts with different scaling
|
|
context_normal = RenderingContext(font_scale_factor=1.0)
|
|
context_large = RenderingContext(font_scale_factor=2.0)
|
|
|
|
resolver_normal = StyleResolver(context_normal)
|
|
resolver_large = StyleResolver(context_large)
|
|
|
|
# Same abstract style
|
|
abstract_style = AbstractStyle(font_size=16)
|
|
|
|
# Resolve with different contexts
|
|
concrete_normal = resolver_normal.resolve_style(abstract_style)
|
|
concrete_large = resolver_large.resolve_style(abstract_style)
|
|
|
|
# Large should be 2x the size
|
|
assert concrete_large.font_size == concrete_normal.font_size * 2
|
|
|
|
|
|
def test_memory_efficiency():
|
|
"""Test that the new system is more memory efficient."""
|
|
registry = AbstractStyleRegistry()
|
|
|
|
# Create many "different" styles that are actually the same
|
|
styles = []
|
|
for i in range(100):
|
|
# All these styles are identical
|
|
style = AbstractStyle(
|
|
font_size=16,
|
|
font_weight=FontWeight.NORMAL,
|
|
color="black"
|
|
)
|
|
style_id, _ = registry.get_or_create_style(style)
|
|
styles.append(style_id)
|
|
|
|
# All should reference the same style
|
|
assert len(set(styles)) == 1 # All IDs are the same
|
|
assert registry.get_style_count() == 2 # Only default + our style
|
|
|
|
# This demonstrates that we don't create duplicate styles
|
|
|
|
|
|
def test_word_style_reference_concept():
|
|
"""Demonstrate how words would reference styles instead of storing fonts."""
|
|
registry = AbstractStyleRegistry()
|
|
|
|
# Create paragraph style
|
|
para_style = AbstractStyle(font_size=16, color="black")
|
|
para_id, _ = registry.get_or_create_style(para_style)
|
|
|
|
# Create bold word style
|
|
bold_style = AbstractStyle(font_size=16, color="black", font_weight=FontWeight.BOLD)
|
|
bold_id, _ = registry.get_or_create_style(bold_style)
|
|
|
|
# Simulate words storing style IDs instead of full Font objects
|
|
words_data = [
|
|
{"text": "This", "style_id": para_id},
|
|
{"text": "is", "style_id": para_id},
|
|
{"text": "bold", "style_id": bold_id},
|
|
{"text": "text", "style_id": para_id},
|
|
]
|
|
|
|
# To get the actual font for rendering, we resolve through registry
|
|
context = RenderingContext()
|
|
concrete_registry = ConcreteStyleRegistry(StyleResolver(context))
|
|
|
|
for word_data in words_data:
|
|
abstract_style = registry.get_style_by_id(word_data["style_id"])
|
|
font = concrete_registry.get_font(abstract_style)
|
|
|
|
# Now we have the actual Font object for rendering
|
|
assert font is not None
|
|
assert hasattr(font, 'font_size')
|
|
|
|
# Bold word should have bold weight
|
|
if word_data["text"] == "bold":
|
|
assert font.weight == FontWeight.BOLD
|
|
else:
|
|
assert font.weight == FontWeight.NORMAL
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__])
|