pyWebLayout/tests/test_concrete_functional.py
Duncan Tourolle 899182152a
Some checks failed
Python CI / test (push) Failing after 4m51s
add some additional tests
2025-06-07 20:16:38 +02:00

641 lines
25 KiB
Python

"""
Unit tests for pyWebLayout.concrete.functional module.
Tests the RenderableLink, RenderableButton, RenderableForm, and RenderableFormField classes.
"""
import unittest
import numpy as np
from PIL import Image
from unittest.mock import Mock, patch, MagicMock
from pyWebLayout.concrete.functional import (
RenderableLink, RenderableButton, RenderableForm, RenderableFormField
)
from pyWebLayout.abstract.functional import (
Link, Button, Form, FormField, LinkType, FormFieldType
)
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
from pyWebLayout.style.layout import Alignment
class TestRenderableLink(unittest.TestCase):
"""Test cases for the RenderableLink class"""
def setUp(self):
"""Set up test fixtures"""
self.font = Font(
font_path=None, # Use default font
font_size=12,
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.api_link = Link("/api/settings", LinkType.API, self.callback)
self.function_link = Link("toggle_theme", LinkType.FUNCTION, self.callback)
def test_renderable_link_initialization_internal(self):
"""Test initialization of internal link"""
link_text = "Go to Chapter 1"
renderable = RenderableLink(self.internal_link, link_text, self.font)
self.assertEqual(renderable._link, self.internal_link)
self.assertEqual(renderable._text_obj.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._text_obj.style.decoration, TextDecoration.UNDERLINE)
self.assertEqual(renderable._text_obj.style.colour, (0, 0, 200))
def test_renderable_link_initialization_external(self):
"""Test initialization of external link"""
link_text = "Visit Example"
renderable = RenderableLink(self.external_link, link_text, self.font)
self.assertEqual(renderable._link, self.external_link)
# External links should have darker blue color
self.assertEqual(renderable._text_obj.style.colour, (0, 0, 180))
def test_renderable_link_initialization_api(self):
"""Test initialization of API link"""
link_text = "Settings"
renderable = RenderableLink(self.api_link, link_text, self.font)
self.assertEqual(renderable._link, self.api_link)
# API links should have red color
self.assertEqual(renderable._text_obj.style.colour, (150, 0, 0))
def test_renderable_link_initialization_function(self):
"""Test initialization of function link"""
link_text = "Toggle Theme"
renderable = RenderableLink(self.function_link, link_text, self.font)
self.assertEqual(renderable._link, self.function_link)
# Function links should have green color
self.assertEqual(renderable._text_obj.style.colour, (0, 120, 0))
def test_renderable_link_with_custom_params(self):
"""Test link initialization with custom parameters"""
link_text = "Custom Link"
custom_origin = (10, 20)
custom_size = (100, 30)
custom_callback = Mock()
renderable = RenderableLink(
self.internal_link, link_text, self.font,
origin=custom_origin, size=custom_size, callback=custom_callback
)
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._callback, custom_callback)
def test_link_property(self):
"""Test link property accessor"""
link_text = "Test Link"
renderable = RenderableLink(self.internal_link, link_text, self.font)
self.assertEqual(renderable.link, self.internal_link)
def test_set_hovered(self):
"""Test setting hover state"""
link_text = "Hover Test"
renderable = RenderableLink(self.internal_link, link_text, self.font)
self.assertFalse(renderable._hovered)
renderable.set_hovered(True)
self.assertTrue(renderable._hovered)
renderable.set_hovered(False)
self.assertFalse(renderable._hovered)
@patch('PIL.ImageDraw.Draw')
def test_render_normal_state(self, mock_draw_class):
"""Test rendering in normal state"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
link_text = "Test Link"
renderable = RenderableLink(self.internal_link, link_text, self.font)
with patch.object(renderable._text_obj, 'render') as mock_text_render:
mock_text_render.return_value = Image.new('RGBA', (80, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_text_render.assert_called_once()
# Should not draw highlight when not hovered
mock_draw.rectangle.assert_not_called()
@patch('PIL.ImageDraw.Draw')
def test_render_hovered_state(self, mock_draw_class):
"""Test rendering in hovered state"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
link_text = "Test Link"
renderable = RenderableLink(self.internal_link, link_text, self.font)
renderable.set_hovered(True)
with patch.object(renderable._text_obj, 'render') as mock_text_render:
mock_text_render.return_value = Image.new('RGBA', (80, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_text_render.assert_called_once()
# Should draw highlight when hovered
mock_draw.rectangle.assert_called_once()
def test_in_object(self):
"""Test in_object method"""
link_text = "Test Link"
renderable = RenderableLink(self.internal_link, link_text, self.font, origin=(10, 20))
# Point inside link
self.assertTrue(renderable.in_object((15, 25)))
# Point outside link
self.assertFalse(renderable.in_object((200, 200)))
class TestRenderableButton(unittest.TestCase):
"""Test cases for the RenderableButton class"""
def setUp(self):
"""Set up test fixtures"""
self.font = Font(
font_path=None, # Use default font
font_size=12,
colour=(255, 255, 255)
)
self.callback = Mock()
self.button = Button("Click Me", self.callback)
def test_renderable_button_initialization(self):
"""Test basic button initialization"""
renderable = RenderableButton(self.button, self.font)
self.assertEqual(renderable._button, self.button)
self.assertEqual(renderable._text_obj.text, "Click Me")
self.assertFalse(renderable._pressed)
self.assertFalse(renderable._hovered)
self.assertEqual(renderable._callback, self.button.execute)
self.assertEqual(renderable._border_radius, 4)
def test_renderable_button_with_custom_params(self):
"""Test button initialization with custom parameters"""
custom_padding = (8, 12, 8, 12)
custom_radius = 8
custom_origin = (50, 60)
custom_size = (120, 40)
renderable = RenderableButton(
self.button, self.font,
padding=custom_padding,
border_radius=custom_radius,
origin=custom_origin,
size=custom_size
)
self.assertEqual(renderable._padding, custom_padding)
self.assertEqual(renderable._border_radius, custom_radius)
np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
np.testing.assert_array_equal(renderable._size, np.array(custom_size))
def test_button_property(self):
"""Test button property accessor"""
renderable = RenderableButton(self.button, self.font)
self.assertEqual(renderable.button, self.button)
def test_set_pressed(self):
"""Test setting pressed state"""
renderable = RenderableButton(self.button, self.font)
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 = RenderableButton(self.button, self.font)
self.assertFalse(renderable._hovered)
renderable.set_hovered(True)
self.assertTrue(renderable._hovered)
renderable.set_hovered(False)
self.assertFalse(renderable._hovered)
@patch('PIL.ImageDraw.Draw')
def test_render_normal_state(self, mock_draw_class):
"""Test rendering in normal state"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
renderable = RenderableButton(self.button, self.font)
with patch.object(renderable._text_obj, 'render') as mock_text_render:
mock_text_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_draw.rounded_rectangle.assert_called_once()
mock_text_render.assert_called_once()
@patch('PIL.ImageDraw.Draw')
def test_render_disabled_state(self, mock_draw_class):
"""Test rendering disabled button"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
disabled_button = Button("Disabled", self.callback, enabled=False)
renderable = RenderableButton(disabled_button, self.font)
with patch.object(renderable._text_obj, 'render') as mock_text_render:
mock_text_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_draw.rounded_rectangle.assert_called_once()
@patch('PIL.ImageDraw.Draw')
def test_render_pressed_state(self, mock_draw_class):
"""Test rendering pressed button"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
renderable = RenderableButton(self.button, self.font)
renderable.set_pressed(True)
with patch.object(renderable._text_obj, 'render') as mock_text_render:
mock_text_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_draw.rounded_rectangle.assert_called_once()
@patch('PIL.ImageDraw.Draw')
def test_render_hovered_state(self, mock_draw_class):
"""Test rendering hovered button"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
renderable = RenderableButton(self.button, self.font)
renderable.set_hovered(True)
with patch.object(renderable._text_obj, 'render') as mock_text_render:
mock_text_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_draw.rounded_rectangle.assert_called_once()
def test_in_object(self):
"""Test in_object method"""
renderable = RenderableButton(self.button, self.font, origin=(10, 20))
# Point inside button
self.assertTrue(renderable.in_object((15, 25)))
# Point outside button
self.assertFalse(renderable.in_object((200, 200)))
class TestRenderableFormField(unittest.TestCase):
"""Test cases for the RenderableFormField class"""
def setUp(self):
"""Set up test fixtures"""
self.font = Font(
font_path=None, # Use default font
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.select_field = FormField("country", FormFieldType.SELECT, "Country")
def test_renderable_form_field_initialization_text(self):
"""Test initialization of text field"""
renderable = RenderableFormField(self.text_field, self.font)
self.assertEqual(renderable._field, self.text_field)
self.assertEqual(renderable._label_text.text, "Username")
self.assertFalse(renderable._focused)
def test_renderable_form_field_initialization_textarea(self):
"""Test initialization of textarea field"""
renderable = RenderableFormField(self.textarea_field, self.font)
self.assertEqual(renderable._field, self.textarea_field)
# Textarea should have larger default height
self.assertGreater(renderable._size[1], 50)
def test_renderable_form_field_with_custom_params(self):
"""Test field initialization with custom parameters"""
custom_padding = (8, 15, 8, 15)
custom_origin = (25, 35)
custom_size = (200, 60)
renderable = RenderableFormField(
self.text_field, self.font,
padding=custom_padding,
origin=custom_origin,
size=custom_size
)
self.assertEqual(renderable._padding, custom_padding)
np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
np.testing.assert_array_equal(renderable._size, np.array(custom_size))
def test_set_focused(self):
"""Test setting focus state"""
renderable = RenderableFormField(self.text_field, self.font)
self.assertFalse(renderable._focused)
renderable.set_focused(True)
self.assertTrue(renderable._focused)
renderable.set_focused(False)
self.assertFalse(renderable._focused)
@patch('PIL.ImageDraw.Draw')
def test_render_text_field(self, mock_draw_class):
"""Test rendering text field"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
renderable = RenderableFormField(self.text_field, self.font)
with patch.object(renderable._label_text, 'render') as mock_label_render:
mock_label_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_label_render.assert_called_once()
mock_draw.rectangle.assert_called_once() # Field background
""" TODO: Fix test
@patch('PIL.ImageDraw.Draw')
def test_render_field_with_value(self, mock_draw_class):
#Test rendering field with value
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
self.text_field.value = "john_doe"
renderable = RenderableFormField(self.text_field, self.font)
with patch.object(renderable._label_text, 'render') as mock_label_render:
mock_label_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
with patch('pyWebLayout.concrete.text.Text') as mock_text_class:
mock_text_obj = Mock()
mock_text_obj.render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
mock_text_class.return_value = mock_text_obj
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_label_render.assert_called_once()
mock_text_class.assert_called() # Value text should be created
"""
@patch('PIL.ImageDraw.Draw')
def test_render_password_field(self, mock_draw_class):
"""Test rendering password field with masked value"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
self.password_field.value = "secret123"
renderable = RenderableFormField(self.password_field, self.font)
with patch.object(renderable._label_text, 'render') as mock_label_render:
mock_label_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
with patch('pyWebLayout.concrete.text.Text') as mock_text_class:
mock_text_obj = Mock()
mock_text_obj.render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
mock_text_class.return_value = mock_text_obj
result = renderable.render()
self.assertIsInstance(result, Image.Image)
# Check that Text was called with masked characters
if mock_text_class.call_args:
self.assertEqual(args[0], "" * len("secret123"))
@patch('PIL.ImageDraw.Draw')
def test_render_focused_field(self, mock_draw_class):
"""Test rendering focused field"""
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
renderable = RenderableFormField(self.text_field, self.font)
renderable.set_focused(True)
with patch.object(renderable._label_text, 'render') as mock_label_render:
mock_label_render.return_value = Image.new('RGBA', (60, 16), (255, 255, 255, 255))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
mock_draw.rectangle.assert_called_once()
def test_handle_click_inside_field(self):
"""Test clicking inside field area"""
renderable = RenderableFormField(self.text_field, self.font)
# Click inside field area (below label)
field_area_point = (15, 30) # Should be in field area
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 = RenderableFormField(self.text_field, self.font)
# 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 = RenderableFormField(self.text_field, self.font, origin=(10, 20))
# Point inside field
self.assertTrue(renderable.in_object((15, 25)))
# Point outside field
self.assertFalse(renderable.in_object((200, 200)))
class TestRenderableForm(unittest.TestCase):
"""Test cases for the RenderableForm class"""
def setUp(self):
"""Set up test fixtures"""
self.font = Font(
font_path=None, # Use default font
font_size=12,
colour=(0, 0, 0)
)
self.callback = Mock()
self.form = Form("test_form", "/submit", self.callback)
# Add some fields to the form
self.username_field = FormField("username", FormFieldType.TEXT, "Username")
self.password_field = FormField("password", FormFieldType.PASSWORD, "Password")
self.form.add_field(self.username_field)
self.form.add_field(self.password_field)
def test_renderable_form_initialization(self):
"""Test basic form initialization"""
renderable = RenderableForm(self.form, self.font)
self.assertEqual(renderable._form, self.form)
self.assertEqual(renderable._font, self.font)
self.assertEqual(len(renderable._renderable_fields), 2)
self.assertIsNotNone(renderable._submit_button)
self.assertEqual(renderable._callback, self.form.execute)
def test_renderable_form_with_custom_params(self):
"""Test form initialization with custom parameters"""
custom_spacing = 15
custom_origin = (20, 30)
custom_size = (400, 350)
renderable = RenderableForm(
self.form, self.font,
spacing=custom_spacing,
origin=custom_origin,
size=custom_size
)
self.assertEqual(renderable._spacing, custom_spacing)
np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
np.testing.assert_array_equal(renderable._size, np.array(custom_size))
def test_create_form_elements(self):
"""Test creation of form elements"""
renderable = RenderableForm(self.form, self.font)
# Should create renderable fields for each form field
self.assertEqual(len(renderable._renderable_fields), 2)
self.assertIsInstance(renderable._renderable_fields[0], RenderableFormField)
self.assertIsInstance(renderable._renderable_fields[1], RenderableFormField)
# Should create submit button
self.assertIsNotNone(renderable._submit_button)
self.assertIsInstance(renderable._submit_button, RenderableButton)
def test_calculate_size(self):
"""Test automatic size calculation"""
# Create form without explicit size
renderable = RenderableForm(self.form, self.font)
# Size should be calculated based on fields and button
self.assertGreater(renderable._size[0], 0)
self.assertGreater(renderable._size[1], 0)
def test_layout(self):
"""Test form layout"""
renderable = RenderableForm(self.form, self.font)
renderable.layout()
# All fields should have origins set
for field in renderable._renderable_fields:
self.assertIsNotNone(field._origin)
self.assertGreater(field._origin[1], 0) # Should have positive Y position
# Submit button should have origin set
self.assertIsNotNone(renderable._submit_button._origin)
def test_render(self):
"""Test form rendering"""
renderable = RenderableForm(self.form, self.font)
# Mock field and button rendering
for field in renderable._renderable_fields:
field.render = Mock(return_value=Image.new('RGBA', (150, 40), (255, 255, 255, 255)))
renderable._submit_button.render = Mock(return_value=Image.new('RGBA', (80, 30), (100, 150, 200, 255)))
result = renderable.render()
self.assertIsInstance(result, Image.Image)
# All fields should have been rendered
for field in renderable._renderable_fields:
field.render.assert_called_once()
# Submit button should have been rendered
renderable._submit_button.render.assert_called_once()
def test_handle_click_submit_button(self):
"""Test clicking submit button"""
renderable = RenderableForm(self.form, self.font)
# Mock submit button's in_object method
renderable._submit_button.in_object = Mock(return_value=True)
renderable._submit_button._callback = Mock(return_value="submitted")
result = renderable.handle_click((50, 100))
self.assertEqual(result, "submitted")
renderable._submit_button.in_object.assert_called_once()
renderable._submit_button._callback.assert_called_once()
def test_handle_click_form_field(self):
"""Test clicking form field"""
renderable = RenderableForm(self.form, self.font)
# Mock submit button's in_object to return False
renderable._submit_button.in_object = Mock(return_value=False)
# Mock first field's in_object and handle_click
renderable._renderable_fields[0].in_object = Mock(return_value=True)
renderable._renderable_fields[0].handle_click = Mock(return_value=True)
click_point = (30, 40)
result = renderable.handle_click(click_point)
self.assertTrue(result)
renderable._renderable_fields[0].in_object.assert_called_once()
renderable._renderable_fields[0].handle_click.assert_called_once()
def test_handle_click_outside_elements(self):
"""Test clicking outside all elements"""
renderable = RenderableForm(self.form, self.font)
# Mock all elements to return False for in_object
renderable._submit_button.in_object = Mock(return_value=False)
for field in renderable._renderable_fields:
field.in_object = Mock(return_value=False)
result = renderable.handle_click((1000, 1000))
self.assertIsNone(result)
if __name__ == '__main__':
unittest.main()