auto flake and corrections
This commit is contained in:
@@ -5,22 +5,24 @@ Tests the various alignment handlers (Left, Center, Right, Justify) and their in
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.concrete.text import Line, Text, LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
|
||||
from pyWebLayout.concrete.text import (
|
||||
Line, Text, LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
|
||||
)
|
||||
from pyWebLayout.style import Alignment
|
||||
from pyWebLayout.style import Font
|
||||
from pyWebLayout.abstract import Word
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
class TestAlignmentHandlers(unittest.TestCase):
|
||||
"""Test cases for the alignment handler system"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font()
|
||||
self.test_words = [Word(text, self.font) for text in ["This", "is", "a", "test", "sentence"]]
|
||||
self.test_words = [Word(text, self.font)
|
||||
for text in ["This", "is", "a", "test", "sentence"]]
|
||||
self.line_width = 300
|
||||
self.line_height = 30
|
||||
self.spacing = (5, 20) # min_spacing, max_spacing
|
||||
@@ -29,46 +31,78 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
|
||||
# Create a real PIL image (canvas) for testing
|
||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||
|
||||
|
||||
# Create a real ImageDraw object
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
# Create a real Font object
|
||||
self.style = Font()
|
||||
|
||||
|
||||
def test_left_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns LeftAlignmentHandler for LEFT alignment"""
|
||||
left_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.LEFT)
|
||||
|
||||
left_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT)
|
||||
|
||||
self.assertIsInstance(left_line._alignment_handler, LeftAlignmentHandler)
|
||||
|
||||
|
||||
def test_center_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns CenterRightAlignmentHandler for CENTER alignment"""
|
||||
center_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.CENTER)
|
||||
|
||||
self.assertIsInstance(center_line._alignment_handler, CenterRightAlignmentHandler)
|
||||
center_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.CENTER)
|
||||
|
||||
self.assertIsInstance(
|
||||
center_line._alignment_handler,
|
||||
CenterRightAlignmentHandler)
|
||||
# Check that it's configured for CENTER alignment
|
||||
self.assertEqual(center_line._alignment_handler._alignment, Alignment.CENTER)
|
||||
|
||||
|
||||
def test_right_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns CenterRightAlignmentHandler for RIGHT alignment"""
|
||||
right_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.RIGHT)
|
||||
|
||||
self.assertIsInstance(right_line._alignment_handler, CenterRightAlignmentHandler)
|
||||
right_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.RIGHT)
|
||||
|
||||
self.assertIsInstance(
|
||||
right_line._alignment_handler,
|
||||
CenterRightAlignmentHandler)
|
||||
# Check that it's configured for RIGHT alignment
|
||||
self.assertEqual(right_line._alignment_handler._alignment, Alignment.RIGHT)
|
||||
|
||||
|
||||
def test_justify_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns JustifyAlignmentHandler for JUSTIFY alignment"""
|
||||
justify_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.JUSTIFY)
|
||||
|
||||
justify_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.JUSTIFY)
|
||||
|
||||
self.assertIsInstance(justify_line._alignment_handler, JustifyAlignmentHandler)
|
||||
|
||||
|
||||
def test_left_alignment_word_addition(self):
|
||||
"""Test adding words to a left-aligned line"""
|
||||
left_line = Line(self.spacing, self.origin, self.size, self.draw, halign=Alignment.LEFT)
|
||||
|
||||
left_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
halign=Alignment.LEFT)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -78,15 +112,21 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(left_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_center_alignment_word_addition(self):
|
||||
"""Test adding words to a center-aligned line"""
|
||||
center_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.CENTER)
|
||||
|
||||
center_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.CENTER)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -96,15 +136,21 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(center_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_right_alignment_word_addition(self):
|
||||
"""Test adding words to a right-aligned line"""
|
||||
right_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.RIGHT)
|
||||
|
||||
right_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.RIGHT)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -114,15 +160,21 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(right_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_justify_alignment_word_addition(self):
|
||||
"""Test adding words to a justified line"""
|
||||
justify_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.JUSTIFY)
|
||||
|
||||
justify_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.JUSTIFY)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -132,16 +184,17 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(justify_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_handler_spacing_and_position_calculations(self):
|
||||
"""Test spacing and position calculations for different alignment handlers"""
|
||||
# Create sample text objects
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
# Test each handler type
|
||||
handlers = [
|
||||
("Left", LeftAlignmentHandler()),
|
||||
@@ -149,108 +202,132 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
("Right", CenterRightAlignmentHandler(Alignment.RIGHT)),
|
||||
("Justify", JustifyAlignmentHandler())
|
||||
]
|
||||
|
||||
|
||||
for name, handler in handlers:
|
||||
with self.subTest(handler=name):
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Check that spacing is a valid number
|
||||
self.assertIsInstance(spacing_calc, (int, float))
|
||||
self.assertGreaterEqual(spacing_calc, 0)
|
||||
|
||||
|
||||
# Check that position is a valid number
|
||||
self.assertIsInstance(position, (int, float))
|
||||
self.assertGreaterEqual(position, 0)
|
||||
|
||||
|
||||
# Check that overflow is a boolean
|
||||
self.assertIsInstance(overflow, bool)
|
||||
|
||||
|
||||
# Position should be within line width (unless overflow)
|
||||
if not overflow:
|
||||
self.assertLessEqual(position, self.line_width)
|
||||
|
||||
|
||||
def test_left_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for left alignment"""
|
||||
handler = LeftAlignmentHandler()
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Left alignment should have position at 0
|
||||
self.assertEqual(position, 0)
|
||||
|
||||
|
||||
# Should not overflow with reasonable text
|
||||
self.assertFalse(overflow)
|
||||
|
||||
|
||||
def test_center_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for center alignment"""
|
||||
handler = CenterRightAlignmentHandler(Alignment.CENTER)
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Center alignment should have position > 0 (centered) if no overflow
|
||||
if not overflow:
|
||||
self.assertGreaterEqual(position, 0)
|
||||
|
||||
|
||||
def test_right_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for right alignment"""
|
||||
handler = CenterRightAlignmentHandler(Alignment.RIGHT)
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Right alignment should have position >= 0
|
||||
self.assertGreaterEqual(position, 0)
|
||||
|
||||
|
||||
def test_justify_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for justify alignment"""
|
||||
handler = JustifyAlignmentHandler()
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Justify alignment should have position at 0
|
||||
self.assertEqual(position, 0)
|
||||
|
||||
|
||||
# Check spacing is reasonable
|
||||
self.assertGreaterEqual(spacing_calc, 0)
|
||||
|
||||
|
||||
def test_empty_line_alignment_handlers(self):
|
||||
"""Test alignment handlers with empty lines"""
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
alignments = [
|
||||
Alignment.LEFT,
|
||||
Alignment.CENTER,
|
||||
Alignment.RIGHT,
|
||||
Alignment.JUSTIFY]
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=alignment)
|
||||
|
||||
line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=alignment)
|
||||
|
||||
# Empty line should still have a handler
|
||||
self.assertIsNotNone(line._alignment_handler)
|
||||
|
||||
|
||||
# Should be able to render empty line
|
||||
line.render()
|
||||
|
||||
|
||||
def test_single_word_line_alignment(self):
|
||||
"""Test alignment handlers with single word lines"""
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
alignments = [
|
||||
Alignment.LEFT,
|
||||
Alignment.CENTER,
|
||||
Alignment.RIGHT,
|
||||
Alignment.JUSTIFY]
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=alignment)
|
||||
|
||||
line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=alignment)
|
||||
|
||||
# Create a test word
|
||||
test_word = Word("test", self.style)
|
||||
|
||||
|
||||
# Add a single word
|
||||
result, part = line.add_word(test_word)
|
||||
self.assertTrue(result) # Should fit
|
||||
self.assertIsNone(part) # No overflow part
|
||||
|
||||
|
||||
# Should be able to render single word line
|
||||
line.render()
|
||||
self.assertEqual(len(line.text_objects), 1)
|
||||
|
||||
@@ -6,7 +6,7 @@ Tests the Box class which handles basic box model rendering with alignment.
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.concrete.box import Box
|
||||
from pyWebLayout.style import Alignment
|
||||
@@ -14,17 +14,17 @@ from pyWebLayout.style import Alignment
|
||||
|
||||
class TestBox(unittest.TestCase):
|
||||
"""Test cases for the Box class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.origin = (10, 20)
|
||||
self.size = (100, 50)
|
||||
self.callback = Mock()
|
||||
|
||||
|
||||
def test_box_initialization_basic(self):
|
||||
"""Test basic box initialization"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(box._origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(box._size, np.array([100, 50]))
|
||||
np.testing.assert_array_equal(box._end, np.array([110, 70]))
|
||||
@@ -33,66 +33,65 @@ class TestBox(unittest.TestCase):
|
||||
self.assertIsNone(box._mode)
|
||||
self.assertEqual(box._halign, Alignment.CENTER)
|
||||
self.assertEqual(box._valign, Alignment.CENTER)
|
||||
|
||||
|
||||
def test_box_initialization_with_callback(self):
|
||||
"""Test box initialization with callback"""
|
||||
box = Box(self.origin, self.size, callback=self.callback)
|
||||
|
||||
|
||||
self.assertEqual(box._callback, self.callback)
|
||||
|
||||
|
||||
def test_box_initialization_with_sheet(self):
|
||||
"""Test box initialization with image sheet"""
|
||||
sheet = Image.new('RGBA', (200, 100), (255, 255, 255, 255))
|
||||
box = Box(self.origin, self.size, sheet=sheet)
|
||||
|
||||
|
||||
self.assertEqual(box._sheet, sheet)
|
||||
self.assertEqual(box._mode, 'RGBA')
|
||||
|
||||
|
||||
def test_box_initialization_with_mode(self):
|
||||
"""Test box initialization with explicit mode"""
|
||||
box = Box(self.origin, self.size, mode='RGB')
|
||||
|
||||
|
||||
self.assertEqual(box._mode, 'RGB')
|
||||
|
||||
|
||||
def test_box_initialization_with_alignment(self):
|
||||
"""Test box initialization with custom alignment"""
|
||||
box = Box(self.origin, self.size, halign=Alignment.LEFT, valign=Alignment.TOP)
|
||||
|
||||
|
||||
self.assertEqual(box._halign, Alignment.LEFT)
|
||||
self.assertEqual(box._valign, Alignment.TOP)
|
||||
|
||||
|
||||
def test_in_shape_point_inside(self):
|
||||
"""Test in_shape method with point inside box"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
# Test point inside
|
||||
self.assertTrue(box.in_shape(np.array([50, 40])))
|
||||
self.assertTrue(box.in_shape(np.array([10, 20]))) # Top-left corner
|
||||
self.assertTrue(box.in_shape(np.array([109, 69]))) # Just inside bottom-right
|
||||
|
||||
|
||||
def test_in_shape_point_outside(self):
|
||||
"""Test in_shape method with point outside box"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
# Test points outside
|
||||
self.assertFalse(box.in_shape(np.array([5, 15]))) # Before origin
|
||||
self.assertFalse(box.in_shape(np.array([110, 70]))) # At end (exclusive)
|
||||
self.assertFalse(box.in_shape(np.array([150, 100]))) # Far outside
|
||||
|
||||
self.assertFalse(box.in_shape(np.array([110, 70]))) # At end (exclusive)
|
||||
self.assertFalse(box.in_shape(np.array([150, 100]))) # Far outside
|
||||
|
||||
def test_in_shape_multiple_points(self):
|
||||
"""Test in_shape method with array of points"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
points = np.array([[50, 40], [5, 15], [109, 69], [110, 70]])
|
||||
result = box.in_shape(points)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(result, [True, False, True, False])
|
||||
|
||||
|
||||
def test_properties_access(self):
|
||||
"""Test that properties can be accessed correctly"""
|
||||
box = Box(self.origin, self.size, callback=self.callback)
|
||||
|
||||
|
||||
# Test that origin property works (should be available via inheritance)
|
||||
np.testing.assert_array_equal(box._origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(box._size, np.array([100, 50]))
|
||||
|
||||
@@ -5,23 +5,21 @@ Tests the LinkText, ButtonText, and FormFieldText classes.
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from pyWebLayout.concrete.functional import (
|
||||
LinkText, ButtonText, FormFieldText,
|
||||
LinkText, ButtonText, FormFieldText,
|
||||
create_link_text, create_button_text, create_form_field_text
|
||||
)
|
||||
from pyWebLayout.abstract.functional import (
|
||||
Link, Button, Form, FormField, LinkType, FormFieldType
|
||||
Link, Button, FormField, LinkType, FormFieldType
|
||||
)
|
||||
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||
from pyWebLayout.style import Alignment
|
||||
from pyWebLayout.style import Font, TextDecoration
|
||||
|
||||
|
||||
class TestLinkText(unittest.TestCase):
|
||||
"""Test cases for the LinkText class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
@@ -30,86 +28,89 @@ class TestLinkText(unittest.TestCase):
|
||||
colour=(0, 0, 0)
|
||||
)
|
||||
self.callback = Mock()
|
||||
|
||||
|
||||
# Create different types of links
|
||||
self.internal_link = Link("chapter1", LinkType.INTERNAL, self.callback)
|
||||
self.external_link = Link("https://example.com", LinkType.EXTERNAL, self.callback)
|
||||
self.external_link = Link(
|
||||
"https://example.com",
|
||||
LinkType.EXTERNAL,
|
||||
self.callback)
|
||||
self.api_link = Link("/api/settings", LinkType.API, self.callback)
|
||||
self.function_link = Link("toggle_theme", LinkType.FUNCTION, self.callback)
|
||||
|
||||
|
||||
# Create a mock ImageDraw.Draw object
|
||||
self.mock_draw = Mock()
|
||||
|
||||
|
||||
def test_link_text_initialization_internal(self):
|
||||
"""Test initialization of internal link text"""
|
||||
link_text = "Go to Chapter 1"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.internal_link)
|
||||
self.assertEqual(renderable.text, link_text)
|
||||
self.assertFalse(renderable._hovered)
|
||||
self.assertEqual(renderable._callback, self.internal_link.execute)
|
||||
|
||||
|
||||
# Check that the font has underline decoration and blue color
|
||||
self.assertEqual(renderable.style.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(renderable.style.colour, (0, 0, 200))
|
||||
|
||||
|
||||
def test_link_text_initialization_external(self):
|
||||
"""Test initialization of external link text"""
|
||||
link_text = "Visit Example"
|
||||
renderable = LinkText(self.external_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.external_link)
|
||||
# External links should have darker blue color
|
||||
self.assertEqual(renderable.style.colour, (0, 0, 180))
|
||||
|
||||
|
||||
def test_link_text_initialization_api(self):
|
||||
"""Test initialization of API link text"""
|
||||
link_text = "Settings"
|
||||
renderable = LinkText(self.api_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.api_link)
|
||||
# API links should have red color
|
||||
self.assertEqual(renderable.style.colour, (150, 0, 0))
|
||||
|
||||
|
||||
def test_link_text_initialization_function(self):
|
||||
"""Test initialization of function link text"""
|
||||
link_text = "Toggle Theme"
|
||||
renderable = LinkText(self.function_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.function_link)
|
||||
# Function links should have green color
|
||||
self.assertEqual(renderable.style.colour, (0, 120, 0))
|
||||
|
||||
|
||||
def test_link_property(self):
|
||||
"""Test link property accessor"""
|
||||
link_text = "Test Link"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable.link, self.internal_link)
|
||||
|
||||
|
||||
def test_set_hovered(self):
|
||||
"""Test setting hover state"""
|
||||
link_text = "Hover Test"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(True)
|
||||
self.assertTrue(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(False)
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
def test_render_normal_state(self):
|
||||
"""Test rendering in normal state"""
|
||||
link_text = "Test Link"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Parent render should be called
|
||||
mock_parent_render.assert_called_once()
|
||||
# Should not draw highlight when not hovered
|
||||
@@ -120,21 +121,25 @@ class TestLinkText(unittest.TestCase):
|
||||
link_text = "Test Link"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
# Mock width property
|
||||
|
||||
# Mock width property
|
||||
renderable._width = 80
|
||||
|
||||
|
||||
# Point inside link
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside link
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_factory_function(self):
|
||||
"""Test the create_link_text factory function"""
|
||||
link_text = "Factory Test"
|
||||
renderable = create_link_text(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
renderable = create_link_text(
|
||||
self.internal_link,
|
||||
link_text,
|
||||
self.font,
|
||||
self.mock_draw)
|
||||
|
||||
self.assertIsInstance(renderable, LinkText)
|
||||
self.assertEqual(renderable.text, link_text)
|
||||
self.assertEqual(renderable.link, self.internal_link)
|
||||
@@ -142,7 +147,7 @@ class TestLinkText(unittest.TestCase):
|
||||
|
||||
class TestButtonText(unittest.TestCase):
|
||||
"""Test cases for the ButtonText class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
@@ -153,112 +158,114 @@ class TestButtonText(unittest.TestCase):
|
||||
self.callback = Mock()
|
||||
self.button = Button("Click Me", self.callback)
|
||||
self.mock_draw = Mock()
|
||||
|
||||
|
||||
def test_button_text_initialization(self):
|
||||
"""Test basic button text initialization"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._button, self.button)
|
||||
self.assertEqual(renderable.text, "Click Me")
|
||||
self.assertFalse(renderable._pressed)
|
||||
self.assertFalse(renderable._hovered)
|
||||
self.assertEqual(renderable._callback, self.button.execute)
|
||||
self.assertEqual(renderable._padding, (4, 8, 4, 8))
|
||||
|
||||
|
||||
def test_button_text_with_custom_padding(self):
|
||||
"""Test button text initialization with custom padding"""
|
||||
custom_padding = (8, 12, 8, 12)
|
||||
|
||||
|
||||
renderable = ButtonText(
|
||||
self.button, self.font, self.mock_draw,
|
||||
padding=custom_padding
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(renderable._padding, custom_padding)
|
||||
|
||||
|
||||
def test_button_property(self):
|
||||
"""Test button property accessor"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable.button, self.button)
|
||||
|
||||
|
||||
def test_set_pressed(self):
|
||||
"""Test setting pressed state"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._pressed)
|
||||
|
||||
|
||||
renderable.set_pressed(True)
|
||||
self.assertTrue(renderable._pressed)
|
||||
|
||||
|
||||
renderable.set_pressed(False)
|
||||
self.assertFalse(renderable._pressed)
|
||||
|
||||
|
||||
def test_set_hovered(self):
|
||||
"""Test setting hover state"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(True)
|
||||
self.assertTrue(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(False)
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
def test_size_property(self):
|
||||
"""Test size property includes padding"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# The size should be padded size, not just text size
|
||||
# Since we handle mocks in __init__, use the padded values directly
|
||||
expected_width = renderable._padded_width
|
||||
expected_height = renderable._padded_height
|
||||
|
||||
np.testing.assert_array_equal(renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
|
||||
np.testing.assert_array_equal(
|
||||
renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
def test_render_normal_state(self):
|
||||
"""Test rendering in normal state"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should draw rounded rectangle for button background
|
||||
self.mock_draw.rounded_rectangle.assert_called_once()
|
||||
# Parent render should be called for text
|
||||
mock_parent_render.assert_called_once()
|
||||
|
||||
|
||||
def test_render_disabled_state(self):
|
||||
"""Test rendering disabled button"""
|
||||
disabled_button = Button("Disabled", self.callback, enabled=False)
|
||||
renderable = ButtonText(disabled_button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should still draw button background
|
||||
self.mock_draw.rounded_rectangle.assert_called_once()
|
||||
mock_parent_render.assert_called_once()
|
||||
|
||||
|
||||
def test_in_object_with_padding(self):
|
||||
"""Test in_object method considers padding"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Point inside button (including padding)
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside button
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_factory_function(self):
|
||||
"""Test the create_button_text factory function"""
|
||||
custom_padding = (6, 10, 6, 10)
|
||||
renderable = create_button_text(self.button, self.font, self.mock_draw, custom_padding)
|
||||
|
||||
renderable = create_button_text(
|
||||
self.button, self.font, self.mock_draw, custom_padding)
|
||||
|
||||
self.assertIsInstance(renderable, ButtonText)
|
||||
self.assertEqual(renderable.text, "Click Me")
|
||||
self.assertEqual(renderable.button, self.button)
|
||||
@@ -267,7 +274,7 @@ class TestButtonText(unittest.TestCase):
|
||||
|
||||
class TestFormFieldText(unittest.TestCase):
|
||||
"""Test cases for the FormFieldText class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
@@ -275,96 +282,102 @@ class TestFormFieldText(unittest.TestCase):
|
||||
font_size=12,
|
||||
colour=(0, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
# Create different types of form fields
|
||||
self.text_field = FormField("username", FormFieldType.TEXT, "Username")
|
||||
self.password_field = FormField("password", FormFieldType.PASSWORD, "Password")
|
||||
self.textarea_field = FormField("description", FormFieldType.TEXTAREA, "Description")
|
||||
self.textarea_field = FormField(
|
||||
"description", FormFieldType.TEXTAREA, "Description")
|
||||
self.select_field = FormField("country", FormFieldType.SELECT, "Country")
|
||||
|
||||
|
||||
self.mock_draw = Mock()
|
||||
|
||||
|
||||
def test_form_field_text_initialization(self):
|
||||
"""Test initialization of form field text"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._field, self.text_field)
|
||||
self.assertEqual(renderable.text, "Username")
|
||||
self.assertFalse(renderable._focused)
|
||||
self.assertEqual(renderable._field_height, 24)
|
||||
|
||||
|
||||
def test_form_field_text_with_custom_height(self):
|
||||
"""Test form field text with custom field height"""
|
||||
custom_height = 40
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw, custom_height)
|
||||
|
||||
renderable = FormFieldText(
|
||||
self.text_field,
|
||||
self.font,
|
||||
self.mock_draw,
|
||||
custom_height)
|
||||
|
||||
self.assertEqual(renderable._field_height, custom_height)
|
||||
|
||||
|
||||
def test_field_property(self):
|
||||
"""Test field property accessor"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable.field, self.text_field)
|
||||
|
||||
|
||||
def test_set_focused(self):
|
||||
"""Test setting focus state"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._focused)
|
||||
|
||||
|
||||
renderable.set_focused(True)
|
||||
self.assertTrue(renderable._focused)
|
||||
|
||||
|
||||
renderable.set_focused(False)
|
||||
self.assertFalse(renderable._focused)
|
||||
|
||||
|
||||
def test_size_includes_field_area(self):
|
||||
"""Test size property includes field area"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Size should include label height + gap + field height
|
||||
expected_height = renderable._style.font_size + 5 + renderable._field_height
|
||||
expected_width = renderable._field_width # Use the calculated field width
|
||||
|
||||
np.testing.assert_array_equal(renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
|
||||
np.testing.assert_array_equal(
|
||||
renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
def test_render_text_field(self):
|
||||
"""Test rendering text field"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render label
|
||||
mock_parent_render.assert_called_once()
|
||||
# Should draw field background rectangle
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
|
||||
|
||||
def test_render_field_with_value(self):
|
||||
"""Test rendering field with value"""
|
||||
self.text_field.value = "john_doe"
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render label
|
||||
mock_parent_render.assert_called_once()
|
||||
# Should draw field background and value text
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
self.mock_draw.text.assert_called_once()
|
||||
|
||||
|
||||
def test_render_password_field(self):
|
||||
"""Test rendering password field with masked value"""
|
||||
self.password_field.value = "secret123"
|
||||
renderable = FormFieldText(self.password_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render label and field
|
||||
mock_parent_render.assert_called_once()
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
@@ -373,61 +386,62 @@ class TestFormFieldText(unittest.TestCase):
|
||||
# Check that the text call used masked characters
|
||||
call_args = self.mock_draw.text.call_args[0]
|
||||
self.assertEqual(call_args[1], "•" * len("secret123"))
|
||||
|
||||
|
||||
def test_render_focused_field(self):
|
||||
"""Test rendering focused field"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
renderable.set_focused(True)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render with focus styling
|
||||
mock_parent_render.assert_called_once()
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
|
||||
|
||||
def test_handle_click_inside_field(self):
|
||||
"""Test clicking inside field area"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Click inside field area (below label)
|
||||
field_area_y = renderable._style.font_size + 5 + 10 # Within field area
|
||||
field_area_point = (15, field_area_y)
|
||||
result = renderable.handle_click(field_area_point)
|
||||
|
||||
|
||||
# Should return True and set focused
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(renderable._focused)
|
||||
|
||||
|
||||
def test_handle_click_outside_field(self):
|
||||
"""Test clicking outside field area"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Click outside field area
|
||||
outside_point = (200, 200)
|
||||
result = renderable.handle_click(outside_point)
|
||||
|
||||
|
||||
# Should return False and not set focused
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(renderable._focused)
|
||||
|
||||
|
||||
def test_in_object(self):
|
||||
"""Test in_object method"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Point inside field (including label and input area)
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside field
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_factory_function(self):
|
||||
"""Test the create_form_field_text factory function"""
|
||||
custom_height = 30
|
||||
renderable = create_form_field_text(self.text_field, self.font, self.mock_draw, custom_height)
|
||||
|
||||
renderable = create_form_field_text(
|
||||
self.text_field, self.font, self.mock_draw, custom_height)
|
||||
|
||||
self.assertIsInstance(renderable, FormFieldText)
|
||||
self.assertEqual(renderable.text, "Username")
|
||||
self.assertEqual(renderable.field, self.text_field)
|
||||
@@ -436,7 +450,7 @@ class TestFormFieldText(unittest.TestCase):
|
||||
|
||||
class TestInteractionCallbacks(unittest.TestCase):
|
||||
"""Test cases for interaction functionality"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(font_size=12, colour=(0, 0, 0))
|
||||
@@ -455,7 +469,8 @@ class TestInteractionCallbacks(unittest.TestCase):
|
||||
|
||||
def test_link_text_interaction(self):
|
||||
"""Test that LinkText properly handles interaction"""
|
||||
# Use a FUNCTION link type which calls the callback, not INTERNAL which returns location
|
||||
# Use a FUNCTION link type which calls the callback, not INTERNAL which
|
||||
# returns location
|
||||
link = Link("test_function", LinkType.FUNCTION, self.link_callback)
|
||||
renderable = LinkText(link, "Test Link", self.font, self.mock_draw)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
import tempfile
|
||||
import numpy as np
|
||||
from PIL import Image as PILImage, ImageDraw
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from pyWebLayout.concrete.image import RenderableImage
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage
|
||||
@@ -17,67 +17,67 @@ from pyWebLayout.style import Alignment
|
||||
|
||||
class TestRenderableImage(unittest.TestCase):
|
||||
"""Test cases for the RenderableImage class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
# Create a temporary test image
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.test_image_path = os.path.join(self.temp_dir, "test_image.png")
|
||||
|
||||
|
||||
# Create a simple test image
|
||||
test_img = PILImage.new('RGB', (100, 80), (255, 0, 0)) # Red image
|
||||
test_img.save(self.test_image_path)
|
||||
|
||||
|
||||
# Create abstract image objects
|
||||
self.abstract_image = AbstractImage(self.test_image_path, "Test Image", 100, 80)
|
||||
self.abstract_image_no_dims = AbstractImage(self.test_image_path, "Test Image")
|
||||
|
||||
|
||||
# Create a canvas and draw object for testing
|
||||
self.canvas = PILImage.new('RGBA', (400, 300), (255, 255, 255, 255))
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures"""
|
||||
# Clean up temporary files
|
||||
try:
|
||||
os.unlink(self.test_image_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
def test_renderable_image_initialization_basic(self):
|
||||
"""Test basic image initialization"""
|
||||
renderable = RenderableImage(self.abstract_image, self.canvas)
|
||||
|
||||
|
||||
self.assertEqual(renderable._abstract_image, self.abstract_image)
|
||||
self.assertEqual(renderable._canvas, self.canvas)
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
self.assertEqual(renderable._halign, Alignment.CENTER)
|
||||
self.assertEqual(renderable._valign, Alignment.CENTER)
|
||||
|
||||
|
||||
def test_renderable_image_initialization_with_constraints(self):
|
||||
"""Test image initialization with size constraints"""
|
||||
max_width = 50
|
||||
max_height = 40
|
||||
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
self.draw,
|
||||
max_width=max_width,
|
||||
max_height=max_height
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(renderable._abstract_image, self.abstract_image)
|
||||
# Size should be constrained
|
||||
self.assertLessEqual(renderable._size[0], max_width)
|
||||
self.assertLessEqual(renderable._size[1], max_height)
|
||||
|
||||
|
||||
def test_renderable_image_initialization_with_custom_params(self):
|
||||
"""Test image initialization with custom parameters"""
|
||||
custom_origin = (20, 30)
|
||||
custom_size = (120, 90)
|
||||
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
self.draw,
|
||||
@@ -86,30 +86,30 @@ class TestRenderableImage(unittest.TestCase):
|
||||
halign=Alignment.LEFT,
|
||||
valign=Alignment.TOP
|
||||
)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
|
||||
np.testing.assert_array_equal(renderable._size, np.array(custom_size))
|
||||
self.assertEqual(renderable._halign, Alignment.LEFT)
|
||||
self.assertEqual(renderable._valign, Alignment.TOP)
|
||||
|
||||
|
||||
def test_load_image_local_file(self):
|
||||
"""Test loading image from local file"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Image should be loaded
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
self.assertEqual(renderable._pil_image.size, (100, 80))
|
||||
|
||||
|
||||
def test_load_image_nonexistent_file(self):
|
||||
"""Test loading image from nonexistent file"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.draw)
|
||||
|
||||
|
||||
# Should have error message, no PIL image
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
|
||||
@patch('requests.get')
|
||||
def test_load_image_url_success(self, mock_get):
|
||||
"""Test loading image from URL (success)"""
|
||||
@@ -118,14 +118,14 @@ class TestRenderableImage(unittest.TestCase):
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = open(self.test_image_path, 'rb').read()
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
|
||||
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
|
||||
renderable = RenderableImage(url_abstract, self.draw)
|
||||
|
||||
|
||||
# Should successfully load image
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
|
||||
|
||||
@patch('requests.get')
|
||||
def test_load_image_url_failure(self, mock_get):
|
||||
"""Test loading image from URL (failure)"""
|
||||
@@ -133,14 +133,16 @@ class TestRenderableImage(unittest.TestCase):
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
url_abstract = AbstractImage("https://example.com/notfound.png", "Bad URL Image")
|
||||
|
||||
url_abstract = AbstractImage(
|
||||
"https://example.com/notfound.png",
|
||||
"Bad URL Image")
|
||||
renderable = RenderableImage(url_abstract, self.draw)
|
||||
|
||||
|
||||
# Should have error message
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
|
||||
def test_load_image_no_requests_library(self):
|
||||
"""Test loading URL image when requests library is not available"""
|
||||
# Mock the import to raise ImportError for requests
|
||||
@@ -148,24 +150,24 @@ class TestRenderableImage(unittest.TestCase):
|
||||
if name == 'requests':
|
||||
raise ImportError("No module named 'requests'")
|
||||
return __import__(name, *args, **kwargs)
|
||||
|
||||
|
||||
with patch('builtins.__import__', side_effect=mock_import):
|
||||
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
|
||||
renderable = RenderableImage(url_abstract, self.draw)
|
||||
|
||||
|
||||
# Should have error message about missing requests
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
self.assertIn("Requests library not available", renderable._error_message)
|
||||
|
||||
|
||||
def test_resize_image_fit_within_bounds(self):
|
||||
"""Test image resizing to fit within bounds"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Original image is 100x80, resize to fit in 50x50
|
||||
renderable._size = np.array([50, 50])
|
||||
resized = renderable._resize_image()
|
||||
|
||||
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
# Should maintain aspect ratio and fit within bounds
|
||||
self.assertLessEqual(resized.width, 50)
|
||||
@@ -174,90 +176,90 @@ class TestRenderableImage(unittest.TestCase):
|
||||
original_ratio = 100 / 80
|
||||
new_ratio = resized.width / resized.height
|
||||
self.assertAlmostEqual(original_ratio, new_ratio, delta=0.1)
|
||||
|
||||
|
||||
def test_resize_image_larger_target(self):
|
||||
"""Test image resizing when target is larger than original"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Target size larger than original
|
||||
renderable._size = np.array([200, 160])
|
||||
resized = renderable._resize_image()
|
||||
|
||||
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
# Should scale up to fill the space while maintaining aspect ratio
|
||||
self.assertGreater(resized.width, 100)
|
||||
self.assertGreater(resized.height, 80)
|
||||
|
||||
|
||||
def test_resize_image_no_image(self):
|
||||
"""Test resize when no image is loaded"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.draw)
|
||||
|
||||
|
||||
resized = renderable._resize_image()
|
||||
|
||||
|
||||
# Should return a placeholder image
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
self.assertEqual(resized.mode, 'RGBA')
|
||||
|
||||
|
||||
def test_draw_error_placeholder(self):
|
||||
"""Test drawing error placeholder"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.canvas)
|
||||
renderable._error_message = "File not found"
|
||||
|
||||
|
||||
# Set origin for the placeholder
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Call the error placeholder method
|
||||
renderable._draw_error_placeholder()
|
||||
|
||||
|
||||
# We can't easily test the actual drawing without complex mocking,
|
||||
# but we can verify the method doesn't raise an exception
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
|
||||
def test_draw_error_placeholder_with_text(self):
|
||||
"""Test drawing error placeholder with error message"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.canvas)
|
||||
renderable._error_message = "File not found"
|
||||
|
||||
|
||||
# Set origin for the placeholder
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Call the error placeholder method
|
||||
renderable._draw_error_placeholder()
|
||||
|
||||
|
||||
# Verify error message is set
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
self.assertIn("File not found", renderable._error_message)
|
||||
|
||||
|
||||
def test_render_successful_image(self):
|
||||
"""Test rendering successfully loaded image"""
|
||||
renderable = RenderableImage(self.abstract_image, self.canvas)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Render returns nothing (draws directly into canvas)
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
# Verify image was loaded
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
|
||||
|
||||
def test_render_failed_image(self):
|
||||
"""Test rendering when image failed to load"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.canvas)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
with patch.object(renderable, '_draw_error_placeholder') as mock_draw_error:
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
mock_draw_error.assert_called_once()
|
||||
|
||||
|
||||
def test_render_with_left_alignment(self):
|
||||
"""Test rendering with left alignment"""
|
||||
renderable = RenderableImage(
|
||||
@@ -267,14 +269,14 @@ class TestRenderableImage(unittest.TestCase):
|
||||
valign=Alignment.TOP
|
||||
)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
self.assertEqual(renderable._halign, Alignment.LEFT)
|
||||
self.assertEqual(renderable._valign, Alignment.TOP)
|
||||
|
||||
|
||||
def test_render_with_right_alignment(self):
|
||||
"""Test rendering with right alignment"""
|
||||
renderable = RenderableImage(
|
||||
@@ -284,96 +286,97 @@ class TestRenderableImage(unittest.TestCase):
|
||||
valign=Alignment.BOTTOM
|
||||
)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
self.assertEqual(renderable._halign, Alignment.RIGHT)
|
||||
self.assertEqual(renderable._valign, Alignment.BOTTOM)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_render_rgb_image_conversion(self):
|
||||
"""Test rendering RGB image (should be converted to RGBA)"""
|
||||
# Our test image is RGB, so this should test the conversion path
|
||||
renderable = RenderableImage(self.abstract_image, self.canvas)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
|
||||
|
||||
def test_in_object(self):
|
||||
"""Test in_object method"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))
|
||||
|
||||
|
||||
# Point inside image
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside image
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_in_object_with_numpy_array(self):
|
||||
"""Test in_object with numpy array point"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))
|
||||
|
||||
|
||||
# Point inside image as numpy array
|
||||
point = np.array([15, 25])
|
||||
self.assertTrue(renderable.in_object(point))
|
||||
|
||||
|
||||
# Point outside image as numpy array
|
||||
point = np.array([200, 200])
|
||||
self.assertFalse(renderable.in_object(point))
|
||||
|
||||
|
||||
def test_image_size_calculation_with_abstract_image_dimensions(self):
|
||||
"""Test that size is calculated from abstract image when available"""
|
||||
# Abstract image has dimensions 100x80
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Size should match the calculated scaled dimensions
|
||||
expected_size = self.abstract_image.calculate_scaled_dimensions()
|
||||
np.testing.assert_array_equal(renderable._size, np.array(expected_size))
|
||||
|
||||
|
||||
def test_image_size_calculation_with_constraints(self):
|
||||
"""Test size calculation with max constraints"""
|
||||
max_width = 60
|
||||
max_height = 50
|
||||
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
self.draw,
|
||||
max_width=max_width,
|
||||
max_height=max_height
|
||||
)
|
||||
|
||||
|
||||
# Size should respect constraints
|
||||
self.assertLessEqual(renderable._size[0], max_width)
|
||||
self.assertLessEqual(renderable._size[1], max_height)
|
||||
|
||||
|
||||
def test_image_without_initial_dimensions(self):
|
||||
"""Test image without initial dimensions in abstract image"""
|
||||
renderable = RenderableImage(self.abstract_image_no_dims, self.draw)
|
||||
|
||||
|
||||
# Should still work, using default or calculated size
|
||||
self.assertIsInstance(renderable._size, np.ndarray)
|
||||
self.assertEqual(len(renderable._size), 2)
|
||||
|
||||
|
||||
def test_set_origin_method(self):
|
||||
"""Test the set_origin method"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
new_origin = np.array([50, 60])
|
||||
renderable.set_origin(new_origin)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(renderable.origin, new_origin)
|
||||
|
||||
|
||||
def test_properties(self):
|
||||
"""Test the property methods"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20), size=(100, 80))
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image, self.draw, origin=(
|
||||
10, 20), size=(
|
||||
100, 80))
|
||||
|
||||
np.testing.assert_array_equal(renderable.origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(renderable.size, np.array([100, 80]))
|
||||
self.assertEqual(renderable.width, 100)
|
||||
|
||||
@@ -6,30 +6,29 @@ Tests the Text and Line classes for text rendering functionality.
|
||||
import unittest
|
||||
import numpy as np
|
||||
import os
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from PIL import Image, ImageDraw
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.concrete.text import Text, Line
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
|
||||
from pyWebLayout.style import Alignment
|
||||
from tests.utils.test_fonts import create_default_test_font, ensure_consistent_font_in_tests
|
||||
|
||||
|
||||
class TestText(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Ensure consistent font usage across tests
|
||||
ensure_consistent_font_in_tests()
|
||||
|
||||
|
||||
# Create a real PIL image (canvas) for testing
|
||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||
|
||||
|
||||
# Create a real ImageDraw object
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
# Create a consistent test Font object using bundled font
|
||||
self.style = create_default_test_font()
|
||||
|
||||
|
||||
def test_init(self):
|
||||
text_instance = Text(text="Test", style=self.style, draw=self.draw)
|
||||
self.assertEqual(text_instance.text, "Test")
|
||||
@@ -59,10 +58,10 @@ class TestText(unittest.TestCase):
|
||||
text_instance = Text(text="Test", style=self.style, draw=self.draw)
|
||||
# Set a position so we can render without issues
|
||||
text_instance.set_origin(np.array([10, 50]))
|
||||
|
||||
|
||||
# This should not raise any exceptions with real objects
|
||||
text_instance.render()
|
||||
|
||||
|
||||
# We can verify the canvas was modified (pixel check)
|
||||
# After rendering, some pixels should have changed from pure white
|
||||
# This is a more realistic test than checking mock calls
|
||||
@@ -70,7 +69,7 @@ class TestText(unittest.TestCase):
|
||||
def test_text_dimensions(self):
|
||||
"""Test that text dimensions are calculated correctly with real font"""
|
||||
text_instance = Text(text="Test", style=self.style, draw=self.draw)
|
||||
|
||||
|
||||
# With real objects, we should get actual width measurements
|
||||
self.assertGreater(text_instance.width, 0)
|
||||
self.assertIsInstance(text_instance.width, (int, float))
|
||||
@@ -94,10 +93,10 @@ class TestText(unittest.TestCase):
|
||||
text_instance = Text(text="Hello World!", style=self.style, draw=self.draw)
|
||||
text_instance.set_origin(np.array([50, 100]))
|
||||
text_instance.render()
|
||||
|
||||
|
||||
# Optionally save the canvas for visual inspection
|
||||
self._save_test_image("rendered_text.png")
|
||||
|
||||
|
||||
# Verify that something was drawn (canvas is no longer pure white everywhere)
|
||||
# Convert to array and check if any pixels changed
|
||||
pixels = np.array(self.canvas)
|
||||
@@ -120,13 +119,13 @@ class TestLine(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Ensure consistent font usage across tests
|
||||
ensure_consistent_font_in_tests()
|
||||
|
||||
|
||||
# Create a real PIL image (canvas) for testing
|
||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||
|
||||
|
||||
# Create a real ImageDraw object
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
# Create a consistent test Font object using bundled font
|
||||
self.style = create_default_test_font()
|
||||
|
||||
@@ -135,7 +134,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15) # min_spacing, max_spacing
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -144,7 +143,7 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(line._spacing, spacing)
|
||||
np.testing.assert_array_equal(line._origin, origin)
|
||||
np.testing.assert_array_equal(line._size, size)
|
||||
@@ -155,7 +154,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -164,10 +163,10 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Create a word to add
|
||||
word = Word(text="Hello", style=self.style)
|
||||
|
||||
|
||||
# This test may need adjustment based on the actual implementation
|
||||
|
||||
success, overflow_part = line.add_word(word)
|
||||
@@ -181,7 +180,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -190,25 +189,26 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Add words until the line is full
|
||||
words_added = 0
|
||||
for i in range(100):
|
||||
word = Word(text="Amsterdam", style=self.style)
|
||||
success, overflow_part = line.add_word(word)
|
||||
|
||||
|
||||
if overflow_part:
|
||||
# Word was hyphenated - overflow occurred
|
||||
self.assertIsNotNone(overflow_part.text)
|
||||
return
|
||||
elif not success:
|
||||
# Line is full, word couldn't be added
|
||||
self.assertGreater(words_added, 0, "Should have added at least one word before line filled")
|
||||
self.assertGreater(
|
||||
words_added, 0, "Should have added at least one word before line filled")
|
||||
return
|
||||
else:
|
||||
# Word was added successfully
|
||||
words_added += 1
|
||||
|
||||
|
||||
self.fail("Expected line to fill or overflow to occur but reached max iterations")
|
||||
|
||||
def test_line_add_word_until_overflow_small(self):
|
||||
@@ -216,7 +216,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -225,20 +225,20 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Create a word to add
|
||||
|
||||
for i in range(100):
|
||||
word = Word(text="Aslan", style=self.style)
|
||||
|
||||
|
||||
# This test may need adjustment based on the actual implementation
|
||||
|
||||
success, overflow_part = line.add_word(word)
|
||||
# If successful, the word should be added
|
||||
if success == False:
|
||||
if not success:
|
||||
self.assertIsNone(overflow_part)
|
||||
return
|
||||
|
||||
|
||||
self.fail("Expected line to reach capacity but reached max iterations")
|
||||
|
||||
def test_line_add_word_until_overflow_long_brute(self):
|
||||
@@ -246,7 +246,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -256,13 +256,14 @@ class TestLine(unittest.TestCase):
|
||||
halign=Alignment.LEFT,
|
||||
min_word_length_for_brute_force=6 # Lower threshold to enable hyphenation for shorter words
|
||||
)
|
||||
|
||||
|
||||
# Use a longer word to trigger brute force hyphenation
|
||||
words_added = 0
|
||||
for i in range(100):
|
||||
word = Word(text="AAAAAAAA", style=self.style) # 8 A's to ensure it's long enough
|
||||
# 8 A's to ensure it's long enough
|
||||
word = Word(text="AAAAAAAA", style=self.style)
|
||||
success, overflow_part = line.add_word(word)
|
||||
|
||||
|
||||
if overflow_part:
|
||||
# Word was hyphenated - verify overflow part exists
|
||||
self.assertIsNotNone(overflow_part.text)
|
||||
@@ -270,20 +271,20 @@ class TestLine(unittest.TestCase):
|
||||
return
|
||||
elif not success:
|
||||
# Line is full, word couldn't be added
|
||||
self.assertGreater(words_added, 0, "Should have added at least one word before line filled")
|
||||
self.assertGreater(
|
||||
words_added, 0, "Should have added at least one word before line filled")
|
||||
return
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
self.fail("Expected line to fill or overflow to occur but reached max iterations")
|
||||
|
||||
self.fail("Expected line to fill or overflow to occur but reached max iterations")
|
||||
|
||||
def test_line_render(self):
|
||||
"""Test line rendering with real objects"""
|
||||
spacing = (5, 15)
|
||||
origin = np.array([50, 100])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -292,7 +293,7 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Try to render the line (even if empty)
|
||||
try:
|
||||
line.render()
|
||||
|
||||
@@ -88,8 +88,11 @@ class TestLinkedWordHyphenation(unittest.TestCase):
|
||||
|
||||
# Both parts should be LinkText (this is the bug we're testing for)
|
||||
for text_obj in line._text_objects:
|
||||
self.assertIsInstance(text_obj, LinkText,
|
||||
f"Hyphenated LinkedWord part should be LinkText, got {type(text_obj)}")
|
||||
self.assertIsInstance(
|
||||
text_obj,
|
||||
LinkText,
|
||||
f"Hyphenated LinkedWord part should be LinkText, got {
|
||||
type(text_obj)}")
|
||||
self.assertEqual(text_obj.link.location, linked_word.location)
|
||||
|
||||
# The overflow should also be LinkText if it's hyphenated
|
||||
|
||||
@@ -9,10 +9,9 @@ Unit tests for the new Page implementation to verify it meets the requirements:
|
||||
"""
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL import Image
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from pyWebLayout.style.fonts import Font
|
||||
from pyWebLayout.core.base import Renderable, Queriable
|
||||
|
||||
|
||||
@@ -23,7 +22,7 @@ class SimpleTestRenderable(Renderable, Queriable):
|
||||
self._text = text
|
||||
self.size = size
|
||||
self._origin = np.array([0, 0])
|
||||
|
||||
|
||||
def render(self):
|
||||
"""Render returns None - drawing is done via the page's draw object"""
|
||||
return None
|
||||
@@ -31,7 +30,7 @@ class SimpleTestRenderable(Renderable, Queriable):
|
||||
|
||||
class TestPageImplementation(unittest.TestCase):
|
||||
"""Test cases for the Page class implementation"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.basic_style = PageStyle(
|
||||
@@ -42,81 +41,84 @@ class TestPageImplementation(unittest.TestCase):
|
||||
padding=(15, 15, 15, 15),
|
||||
background_color=(240, 240, 240)
|
||||
)
|
||||
|
||||
|
||||
self.page_size = (800, 600)
|
||||
|
||||
|
||||
def test_page_creation_with_style(self):
|
||||
"""Test creating a page with a PageStyle"""
|
||||
page = Page(size=self.page_size, style=self.basic_style)
|
||||
|
||||
|
||||
self.assertEqual(page.size, self.page_size)
|
||||
self.assertEqual(page.style, self.basic_style)
|
||||
self.assertEqual(page.border_size, 2)
|
||||
|
||||
|
||||
def test_page_creation_without_style(self):
|
||||
"""Test creating a page without a PageStyle (should use defaults)"""
|
||||
page = Page(size=self.page_size)
|
||||
|
||||
|
||||
self.assertEqual(page.size, self.page_size)
|
||||
self.assertIsNotNone(page.style)
|
||||
|
||||
|
||||
def test_page_canvas_and_content_sizes(self):
|
||||
"""Test that page correctly calculates canvas and content sizes"""
|
||||
style = PageStyle(
|
||||
border_width=5,
|
||||
padding=(10, 20, 30, 40) # top, right, bottom, left
|
||||
)
|
||||
|
||||
|
||||
page = Page(size=self.page_size, style=style)
|
||||
|
||||
|
||||
# Canvas size should be page size minus borders
|
||||
expected_canvas_size = (790, 590) # 800-10, 600-10 (border on both sides)
|
||||
self.assertEqual(page.canvas_size, expected_canvas_size)
|
||||
|
||||
|
||||
# Content size should be canvas minus padding
|
||||
expected_content_size = (730, 550) # 790-60, 590-40 (padding left+right, top+bottom)
|
||||
# 790-60, 590-40 (padding left+right, top+bottom)
|
||||
expected_content_size = (730, 550)
|
||||
self.assertEqual(page.content_size, expected_content_size)
|
||||
|
||||
|
||||
def test_page_add_remove_children(self):
|
||||
"""Test adding and removing children from the page"""
|
||||
page = Page(size=self.page_size)
|
||||
|
||||
|
||||
# Initially no children
|
||||
self.assertEqual(len(page.children), 0)
|
||||
|
||||
|
||||
# Add children
|
||||
child1 = SimpleTestRenderable("Child 1")
|
||||
child2 = SimpleTestRenderable("Child 2")
|
||||
|
||||
|
||||
page.add_child(child1)
|
||||
self.assertEqual(len(page.children), 1)
|
||||
self.assertIn(child1, page.children)
|
||||
|
||||
|
||||
page.add_child(child2)
|
||||
self.assertEqual(len(page.children), 2)
|
||||
self.assertIn(child2, page.children)
|
||||
|
||||
|
||||
# Test method chaining
|
||||
child3 = SimpleTestRenderable("Child 3")
|
||||
result = page.add_child(child3)
|
||||
self.assertIs(result, page) # Should return self for chaining
|
||||
self.assertEqual(len(page.children), 3)
|
||||
self.assertIn(child3, page.children)
|
||||
|
||||
# Remove childce you’ll notice is that responses don’t stream character-by-character like other providers. Instead, Claude Code processes your full request before sending back the complete response.
|
||||
|
||||
# Remove childce you’ll notice is that responses don’t stream
|
||||
# character-by-character like other providers. Instead, Claude Code
|
||||
# processes your full request before sending back the complete response.
|
||||
removed = page.remove_child(child2)
|
||||
self.assertTrue(removed)
|
||||
self.assertEqual(len(page.children), 2)
|
||||
self.assertNotIn(child2, page.children)
|
||||
|
||||
|
||||
# Try to remove non-existent child
|
||||
removed = page.remove_child(child2)
|
||||
self.assertFalse(removed)
|
||||
|
||||
|
||||
# Clear all children
|
||||
page.clear_children()
|
||||
self.assertEqual(len(page.children), 0)
|
||||
|
||||
|
||||
def test_page_render(self):
|
||||
"""Test that page renders and creates a canvas"""
|
||||
style = PageStyle(
|
||||
@@ -124,24 +126,24 @@ class TestPageImplementation(unittest.TestCase):
|
||||
border_color=(255, 0, 0),
|
||||
background_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
|
||||
page = Page(size=(200, 150), style=style)
|
||||
|
||||
|
||||
# Add a child
|
||||
child = SimpleTestRenderable("Test child")
|
||||
page.add_child(child)
|
||||
|
||||
|
||||
# Render the page
|
||||
image = page.render()
|
||||
|
||||
|
||||
# Check that we got an image
|
||||
self.assertIsInstance(image, Image.Image)
|
||||
self.assertEqual(image.size, (200, 150))
|
||||
self.assertEqual(image.mode, 'RGBA')
|
||||
|
||||
|
||||
# Check that draw object is available
|
||||
self.assertIsNotNone(page.draw)
|
||||
|
||||
|
||||
def test_page_query_point(self):
|
||||
"""Test querying points to find children"""
|
||||
page = Page(size=(400, 300))
|
||||
@@ -167,22 +169,22 @@ class TestPageImplementation(unittest.TestCase):
|
||||
result = page.query_point((300, 250))
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result.object_type, "empty")
|
||||
|
||||
|
||||
def test_page_in_object(self):
|
||||
"""Test that page correctly implements in_object"""
|
||||
page = Page(size=(400, 300))
|
||||
|
||||
|
||||
# Points within page bounds
|
||||
self.assertTrue(page.in_object((0, 0)))
|
||||
self.assertTrue(page.in_object((200, 150)))
|
||||
self.assertTrue(page.in_object((399, 299)))
|
||||
|
||||
|
||||
# Points outside page bounds
|
||||
self.assertFalse(page.in_object((-1, 0)))
|
||||
self.assertFalse(page.in_object((0, -1)))
|
||||
self.assertFalse(page.in_object((400, 299)))
|
||||
self.assertFalse(page.in_object((399, 300)))
|
||||
|
||||
|
||||
def test_page_with_borders(self):
|
||||
"""Test page rendering with borders"""
|
||||
style = PageStyle(
|
||||
@@ -190,33 +192,33 @@ class TestPageImplementation(unittest.TestCase):
|
||||
border_color=(128, 128, 128),
|
||||
background_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
|
||||
page = Page(size=(100, 100), style=style)
|
||||
image = page.render()
|
||||
|
||||
|
||||
# Check that image was created
|
||||
self.assertIsInstance(image, Image.Image)
|
||||
self.assertEqual(image.size, (100, 100))
|
||||
|
||||
|
||||
# The border should be drawn but we can't easily test pixel values
|
||||
# Just verify the image exists and has the right properties
|
||||
|
||||
|
||||
def test_page_border_size_property(self):
|
||||
"""Test that border_size property returns correct value"""
|
||||
# Test with border
|
||||
style_with_border = PageStyle(border_width=5)
|
||||
page_with_border = Page(size=self.page_size, style=style_with_border)
|
||||
self.assertEqual(page_with_border.border_size, 5)
|
||||
|
||||
|
||||
# Test without border
|
||||
style_no_border = PageStyle(border_width=0)
|
||||
page_no_border = Page(size=self.page_size, style=style_no_border)
|
||||
self.assertEqual(page_no_border.border_size, 0)
|
||||
|
||||
|
||||
def test_page_style_properties(self):
|
||||
"""Test that page correctly exposes style properties"""
|
||||
page = Page(size=self.page_size, style=self.basic_style)
|
||||
|
||||
|
||||
# Test that style properties are accessible
|
||||
self.assertEqual(page.style.border_width, 2)
|
||||
self.assertEqual(page.style.border_color, (255, 0, 0))
|
||||
@@ -224,30 +226,30 @@ class TestPageImplementation(unittest.TestCase):
|
||||
self.assertEqual(page.style.inter_block_spacing, 20)
|
||||
self.assertEqual(page.style.padding, (15, 15, 15, 15))
|
||||
self.assertEqual(page.style.background_color, (240, 240, 240))
|
||||
|
||||
|
||||
def test_page_children_list_operations(self):
|
||||
"""Test that children list behaves correctly"""
|
||||
page = Page(size=self.page_size)
|
||||
|
||||
|
||||
# Test that children is initially empty list
|
||||
self.assertIsInstance(page.children, list)
|
||||
self.assertEqual(len(page.children), 0)
|
||||
|
||||
|
||||
# Test adding multiple children
|
||||
children = [
|
||||
SimpleTestRenderable(f"Child {i}")
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
|
||||
for child in children:
|
||||
page.add_child(child)
|
||||
|
||||
|
||||
self.assertEqual(len(page.children), 5)
|
||||
|
||||
|
||||
# Test that children are in the correct order
|
||||
for i, child in enumerate(page.children):
|
||||
self.assertEqual(child._text, f"Child {i}")
|
||||
|
||||
|
||||
def test_page_can_fit_line_boundary_checking(self):
|
||||
"""Test that can_fit_line correctly checks bottom boundary"""
|
||||
# Create page with known dimensions
|
||||
@@ -259,63 +261,64 @@ class TestPageImplementation(unittest.TestCase):
|
||||
padding=(10, 10, 10, 10)
|
||||
)
|
||||
page = Page(size=(800, 600), style=style)
|
||||
|
||||
|
||||
# Initial y_offset should be at border + padding_top = 50
|
||||
self.assertEqual(page._current_y_offset, 50)
|
||||
|
||||
|
||||
# Test 1: Line that fits comfortably
|
||||
line_height = 20
|
||||
max_y = 600 - 40 - 10 # 550
|
||||
_max_y = 600 - 40 - 10 # 550
|
||||
self.assertTrue(page.can_fit_line(line_height))
|
||||
# Would end at 50 + 20 = 70, well within 550
|
||||
|
||||
|
||||
# Test 2: Simulate adding lines to fill the page
|
||||
# Available height: 550 - 50 = 500 pixels
|
||||
# With 20-pixel lines, we can fit 25 lines exactly
|
||||
for i in range(24): # Add 24 lines
|
||||
self.assertTrue(page.can_fit_line(20), f"Line {i+1} should fit")
|
||||
self.assertTrue(page.can_fit_line(20), f"Line {i + 1} should fit")
|
||||
# Simulate adding a line by updating y_offset
|
||||
page._current_y_offset += 20
|
||||
|
||||
|
||||
# After 24 lines: y_offset = 50 + (24 * 20) = 530
|
||||
self.assertEqual(page._current_y_offset, 530)
|
||||
|
||||
# Test 3: One more 20-pixel line should fit (530 + 20 = 550, exactly at boundary)
|
||||
|
||||
# Test 3: One more 20-pixel line should fit (530 + 20 = 550, exactly at
|
||||
# boundary)
|
||||
self.assertTrue(page.can_fit_line(20))
|
||||
page._current_y_offset += 20
|
||||
self.assertEqual(page._current_y_offset, 550)
|
||||
|
||||
|
||||
# Test 4: Now another line should NOT fit (550 + 20 = 570 > 550)
|
||||
self.assertFalse(page.can_fit_line(20))
|
||||
|
||||
|
||||
# Test 5: Even a 1-pixel line should not fit (550 + 1 = 551 > 550)
|
||||
self.assertFalse(page.can_fit_line(1))
|
||||
|
||||
|
||||
# Test 6: Edge case - exactly at boundary, 0-height line should fit
|
||||
self.assertTrue(page.can_fit_line(0))
|
||||
|
||||
|
||||
def test_page_can_fit_line_with_different_styles(self):
|
||||
"""Test can_fit_line with different page styles"""
|
||||
# Test with no border or padding
|
||||
style_no_border = PageStyle(border_width=0, padding=(0, 0, 0, 0))
|
||||
page_no_border = Page(size=(100, 100), style=style_no_border)
|
||||
|
||||
|
||||
# With no border/padding, y_offset starts at 0
|
||||
self.assertEqual(page_no_border._current_y_offset, 0)
|
||||
|
||||
|
||||
# Can fit a 100-pixel line exactly
|
||||
self.assertTrue(page_no_border.can_fit_line(100))
|
||||
|
||||
|
||||
# Cannot fit a 101-pixel line
|
||||
self.assertFalse(page_no_border.can_fit_line(101))
|
||||
|
||||
|
||||
# Test with large border and padding
|
||||
style_large = PageStyle(border_width=20, padding=(15, 15, 15, 15))
|
||||
page_large = Page(size=(200, 200), style=style_large)
|
||||
|
||||
|
||||
# y_offset starts at border + padding_top = 20 + 15 = 35
|
||||
self.assertEqual(page_large._current_y_offset, 35)
|
||||
|
||||
|
||||
# Max y = 200 - 20 - 15 = 165
|
||||
# Available height = 165 - 35 = 130 pixels
|
||||
self.assertTrue(page_large.can_fit_line(130))
|
||||
|
||||
@@ -17,8 +17,7 @@ from pyWebLayout.concrete.table import (
|
||||
TableRenderer
|
||||
)
|
||||
from pyWebLayout.abstract.block import (
|
||||
Table, TableRow, TableCell, Paragraph, Heading, HeadingLevel,
|
||||
Image as AbstractImage
|
||||
Table, TableRow, TableCell, Paragraph, Heading, HeadingLevel
|
||||
)
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font
|
||||
@@ -193,7 +192,11 @@ class TestTableCellRenderer:
|
||||
assert cell_renderer._style == default_table_style
|
||||
assert cell_renderer._is_header_section is False
|
||||
|
||||
def test_initialization_with_header(self, sample_font, sample_draw, default_table_style):
|
||||
def test_initialization_with_header(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
default_table_style):
|
||||
"""Test TableCellRenderer initialization for header cell."""
|
||||
cell = TableCell(is_header=True)
|
||||
cell_renderer = TableCellRenderer(
|
||||
@@ -207,7 +210,12 @@ class TestTableCellRenderer:
|
||||
|
||||
assert cell_renderer._is_header_section is True
|
||||
|
||||
def test_render_empty_cell(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_empty_cell(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering an empty cell."""
|
||||
cell = TableCell()
|
||||
cell_renderer = TableCellRenderer(
|
||||
@@ -223,7 +231,12 @@ class TestTableCellRenderer:
|
||||
# Render returns None (draws directly on canvas)
|
||||
assert result is None
|
||||
|
||||
def test_render_cell_with_text(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_cell_with_text(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a cell with text content."""
|
||||
cell = TableCell()
|
||||
paragraph = Paragraph(sample_font)
|
||||
@@ -243,7 +256,12 @@ class TestTableCellRenderer:
|
||||
result = cell_renderer.render()
|
||||
assert result is None
|
||||
|
||||
def test_render_header_cell(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_header_cell(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a header cell with different styling."""
|
||||
cell = TableCell(is_header=True)
|
||||
paragraph = Paragraph(sample_font)
|
||||
@@ -263,7 +281,12 @@ class TestTableCellRenderer:
|
||||
result = cell_renderer.render()
|
||||
assert result is None
|
||||
|
||||
def test_render_cell_with_heading(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_cell_with_heading(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a cell with heading content."""
|
||||
cell = TableCell()
|
||||
heading = Heading(HeadingLevel.H2, sample_font)
|
||||
@@ -294,11 +317,11 @@ class TestTableCellRenderer:
|
||||
)
|
||||
|
||||
# Point inside cell
|
||||
assert cell_renderer.in_object((50, 30)) == True
|
||||
assert cell_renderer.in_object((50, 30))
|
||||
|
||||
# Point outside cell
|
||||
assert cell_renderer.in_object((150, 30)) == False
|
||||
assert cell_renderer.in_object((50, 100)) == False
|
||||
assert not cell_renderer.in_object((150, 30))
|
||||
assert not cell_renderer.in_object((50, 100))
|
||||
|
||||
def test_properties_access(self, sample_font, sample_draw, default_table_style):
|
||||
"""Test accessing cell renderer properties."""
|
||||
@@ -361,7 +384,12 @@ class TestTableRowRenderer:
|
||||
result = row_renderer.render()
|
||||
assert result is None
|
||||
|
||||
def test_render_row_with_cells(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_row_with_cells(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a row with multiple cells."""
|
||||
row = TableRow()
|
||||
|
||||
@@ -388,7 +416,12 @@ class TestTableRowRenderer:
|
||||
# Verify cells were created
|
||||
assert len(row_renderer._cell_renderers) == 3
|
||||
|
||||
def test_render_row_with_colspan(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_row_with_colspan(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a row with cells that span multiple columns."""
|
||||
row = TableRow()
|
||||
|
||||
@@ -444,7 +477,11 @@ class TestTableRenderer:
|
||||
assert table_renderer._draw == sample_draw
|
||||
assert table_renderer._style == default_table_style
|
||||
|
||||
def test_dimension_calculation(self, simple_table, sample_draw, default_table_style):
|
||||
def test_dimension_calculation(
|
||||
self,
|
||||
simple_table,
|
||||
sample_draw,
|
||||
default_table_style):
|
||||
"""Test table dimension calculation."""
|
||||
table_renderer = TableRenderer(
|
||||
simple_table,
|
||||
@@ -459,7 +496,12 @@ class TestTableRenderer:
|
||||
assert len(table_renderer._row_heights) == 3 # header, body, footer
|
||||
assert all(width > 0 for width in table_renderer._column_widths)
|
||||
|
||||
def test_render_simple_table(self, simple_table, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_simple_table(
|
||||
self,
|
||||
simple_table,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a complete simple table."""
|
||||
table_renderer = TableRenderer(
|
||||
simple_table,
|
||||
@@ -475,7 +517,12 @@ class TestTableRenderer:
|
||||
# Verify rows were created
|
||||
assert len(table_renderer._row_renderers) == 2 # 1 header + 1 body
|
||||
|
||||
def test_render_table_with_caption(self, simple_table, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_table_with_caption(
|
||||
self,
|
||||
simple_table,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a table with caption."""
|
||||
simple_table.caption = "Test Table Caption"
|
||||
|
||||
@@ -535,7 +582,12 @@ class TestTableRenderer:
|
||||
# Should handle gracefully
|
||||
assert table_renderer is not None
|
||||
|
||||
def test_table_with_footer(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_table_with_footer(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a table with footer rows."""
|
||||
table = Table()
|
||||
|
||||
@@ -590,10 +642,10 @@ class TestTableRenderer:
|
||||
)
|
||||
|
||||
# Point inside table
|
||||
assert table_renderer.in_object((50, 50)) == True
|
||||
assert table_renderer.in_object((50, 50))
|
||||
|
||||
# Point outside table
|
||||
assert table_renderer.in_object((1000, 1000)) == False
|
||||
assert not table_renderer.in_object((1000, 1000))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user