@@ -0,0 +1,182 @@
|
||||
"""
|
||||
Unit tests for pyWebLayout style objects.
|
||||
|
||||
Tests the Font class and style enums for proper functionality and immutability.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration, Alignment
|
||||
|
||||
|
||||
class TestStyleObjects(unittest.TestCase):
|
||||
"""Test cases for pyWebLayout style objects."""
|
||||
|
||||
def test_font_weight_enum(self):
|
||||
"""Test FontWeight enum values."""
|
||||
self.assertEqual(FontWeight.NORMAL.value, "normal")
|
||||
self.assertEqual(FontWeight.BOLD.value, "bold")
|
||||
|
||||
# Test that all expected values exist
|
||||
weights = [FontWeight.NORMAL, FontWeight.BOLD]
|
||||
self.assertEqual(len(weights), 2)
|
||||
|
||||
def test_font_style_enum(self):
|
||||
"""Test FontStyle enum values."""
|
||||
self.assertEqual(FontStyle.NORMAL.value, "normal")
|
||||
self.assertEqual(FontStyle.ITALIC.value, "italic")
|
||||
|
||||
# Test that all expected values exist
|
||||
styles = [FontStyle.NORMAL, FontStyle.ITALIC]
|
||||
self.assertEqual(len(styles), 2)
|
||||
|
||||
def test_text_decoration_enum(self):
|
||||
"""Test TextDecoration enum values."""
|
||||
self.assertEqual(TextDecoration.NONE.value, "none")
|
||||
self.assertEqual(TextDecoration.UNDERLINE.value, "underline")
|
||||
self.assertEqual(TextDecoration.STRIKETHROUGH.value, "strikethrough")
|
||||
|
||||
# Test that all expected values exist
|
||||
decorations = [TextDecoration.NONE, TextDecoration.UNDERLINE, TextDecoration.STRIKETHROUGH]
|
||||
self.assertEqual(len(decorations), 3)
|
||||
|
||||
def test_alignment_enum(self):
|
||||
"""Test Alignment enum values."""
|
||||
self.assertEqual(Alignment.LEFT.value, 1)
|
||||
self.assertEqual(Alignment.CENTER.value, 2)
|
||||
self.assertEqual(Alignment.RIGHT.value, 3)
|
||||
self.assertEqual(Alignment.TOP.value, 4)
|
||||
self.assertEqual(Alignment.BOTTOM.value, 5)
|
||||
self.assertEqual(Alignment.JUSTIFY.value, 6)
|
||||
|
||||
def test_font_initialization_defaults(self):
|
||||
"""Test Font initialization with default values."""
|
||||
font = Font()
|
||||
|
||||
self.assertIsNone(font._font_path)
|
||||
self.assertEqual(font.font_size, 16)
|
||||
self.assertEqual(font.colour, (0, 0, 0))
|
||||
self.assertEqual(font.color, (0, 0, 0)) # Alias
|
||||
self.assertEqual(font.weight, FontWeight.NORMAL)
|
||||
self.assertEqual(font.style, FontStyle.NORMAL)
|
||||
self.assertEqual(font.decoration, TextDecoration.NONE)
|
||||
self.assertEqual(font.background, (255, 255, 255, 0)) # Transparent
|
||||
self.assertEqual(font.language, "en_EN")
|
||||
|
||||
def test_font_initialization_custom(self):
|
||||
"""Test Font initialization with custom values."""
|
||||
font = Font(
|
||||
font_path="/path/to/font.ttf",
|
||||
font_size=16,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD,
|
||||
style=FontStyle.ITALIC,
|
||||
decoration=TextDecoration.UNDERLINE,
|
||||
background=(255, 255, 0, 255),
|
||||
language="fr_FR"
|
||||
)
|
||||
|
||||
self.assertEqual(font._font_path, "/path/to/font.ttf")
|
||||
self.assertEqual(font.font_size, 16)
|
||||
self.assertEqual(font.colour, (255, 0, 0))
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(font.background, (255, 255, 0, 255))
|
||||
self.assertEqual(font.language, "fr_FR")
|
||||
|
||||
def test_font_with_methods(self):
|
||||
"""Test Font immutable modification methods."""
|
||||
original_font = Font(
|
||||
font_size=12,
|
||||
colour=(0, 0, 0),
|
||||
weight=FontWeight.NORMAL,
|
||||
style=FontStyle.NORMAL,
|
||||
decoration=TextDecoration.NONE
|
||||
)
|
||||
|
||||
# Test with_size
|
||||
size_font = original_font.with_size(16)
|
||||
self.assertEqual(size_font.font_size, 16)
|
||||
self.assertEqual(original_font.font_size, 12) # Original unchanged
|
||||
self.assertEqual(size_font.colour, (0, 0, 0)) # Other properties preserved
|
||||
|
||||
# Test with_colour
|
||||
color_font = original_font.with_colour((255, 0, 0))
|
||||
self.assertEqual(color_font.colour, (255, 0, 0))
|
||||
self.assertEqual(original_font.colour, (0, 0, 0)) # Original unchanged
|
||||
self.assertEqual(color_font.font_size, 12) # Other properties preserved
|
||||
|
||||
# Test with_weight
|
||||
weight_font = original_font.with_weight(FontWeight.BOLD)
|
||||
self.assertEqual(weight_font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(original_font.weight, FontWeight.NORMAL) # Original unchanged
|
||||
|
||||
# Test with_style
|
||||
style_font = original_font.with_style(FontStyle.ITALIC)
|
||||
self.assertEqual(style_font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(original_font.style, FontStyle.NORMAL) # Original unchanged
|
||||
|
||||
# Test with_decoration
|
||||
decoration_font = original_font.with_decoration(TextDecoration.UNDERLINE)
|
||||
self.assertEqual(decoration_font.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(original_font.decoration, TextDecoration.NONE) # Original unchanged
|
||||
|
||||
def test_font_property_access(self):
|
||||
"""Test Font property access methods."""
|
||||
font = Font(
|
||||
font_size=20,
|
||||
colour=(128, 128, 128),
|
||||
weight=FontWeight.BOLD,
|
||||
style=FontStyle.ITALIC,
|
||||
decoration=TextDecoration.STRIKETHROUGH
|
||||
)
|
||||
|
||||
# Test all property getters
|
||||
self.assertEqual(font.font_size, 20)
|
||||
self.assertEqual(font.colour, (128, 128, 128))
|
||||
self.assertEqual(font.color, (128, 128, 128)) # Alias
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
# Test that font object is accessible
|
||||
self.assertIsNotNone(font.font)
|
||||
|
||||
def test_font_immutability(self):
|
||||
"""Test that Font objects behave immutably."""
|
||||
font1 = Font(font_size=12, colour=(0, 0, 0))
|
||||
font2 = font1.with_size(16)
|
||||
font3 = font2.with_colour((255, 0, 0))
|
||||
|
||||
# Each should be different objects
|
||||
self.assertIsNot(font1, font2)
|
||||
self.assertIsNot(font2, font3)
|
||||
self.assertIsNot(font1, font3)
|
||||
|
||||
# Original properties should be unchanged
|
||||
self.assertEqual(font1.font_size, 12)
|
||||
self.assertEqual(font1.colour, (0, 0, 0))
|
||||
|
||||
self.assertEqual(font2.font_size, 16)
|
||||
self.assertEqual(font2.colour, (0, 0, 0))
|
||||
|
||||
self.assertEqual(font3.font_size, 16)
|
||||
self.assertEqual(font3.colour, (255, 0, 0))
|
||||
|
||||
def test_background_handling(self):
|
||||
"""Test background color handling."""
|
||||
# Test default transparent background
|
||||
font1 = Font()
|
||||
self.assertEqual(font1.background, (255, 255, 255, 0))
|
||||
|
||||
# Test explicit background
|
||||
font2 = Font(background=(255, 0, 0, 128))
|
||||
self.assertEqual(font2.background, (255, 0, 0, 128))
|
||||
|
||||
# Test None background becomes transparent
|
||||
font3 = Font(background=None)
|
||||
self.assertEqual(font3.background, (255, 255, 255, 0))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -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