@@ -0,0 +1,232 @@
|
||||
"""
|
||||
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, TextAlign
|
||||
)
|
||||
from pyWebLayout.style.concrete_style import (
|
||||
ConcreteStyle, ConcreteStyleRegistry, RenderingContext, StyleResolver
|
||||
)
|
||||
from pyWebLayout.style.fonts import FontWeight, FontStyle, TextDecoration
|
||||
|
||||
|
||||
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__])
|
||||
Reference in New Issue
Block a user