pyWebLayout/tests/style/test_new_style_system.py

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__])