482 lines
19 KiB
Python
482 lines
19 KiB
Python
"""
|
|
Unit tests for pyWebLayout.concrete.functional module.
|
|
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 pyWebLayout.concrete.functional import (
|
|
LinkText, ButtonText, FormFieldText,
|
|
create_link_text, create_button_text, create_form_field_text
|
|
)
|
|
from pyWebLayout.abstract.functional import (
|
|
Link, Button, Form, FormField, LinkType, FormFieldType
|
|
)
|
|
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
|
from pyWebLayout.style import Alignment
|
|
|
|
|
|
class TestLinkText(unittest.TestCase):
|
|
"""Test cases for the LinkText 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)
|
|
|
|
# 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
|
|
self.mock_draw.rectangle.assert_not_called()
|
|
|
|
def test_in_object(self):
|
|
"""Test in_object method"""
|
|
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
|
|
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)
|
|
|
|
self.assertIsInstance(renderable, LinkText)
|
|
self.assertEqual(renderable.text, link_text)
|
|
self.assertEqual(renderable.link, self.internal_link)
|
|
|
|
|
|
class TestButtonText(unittest.TestCase):
|
|
"""Test cases for the ButtonText 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)
|
|
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]))
|
|
|
|
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)
|
|
|
|
self.assertIsInstance(renderable, ButtonText)
|
|
self.assertEqual(renderable.text, "Click Me")
|
|
self.assertEqual(renderable.button, self.button)
|
|
self.assertEqual(renderable._padding, custom_padding)
|
|
|
|
|
|
class TestFormFieldText(unittest.TestCase):
|
|
"""Test cases for the FormFieldText 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")
|
|
|
|
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)
|
|
|
|
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]))
|
|
|
|
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()
|
|
# Should render masked text
|
|
self.mock_draw.text.assert_called_once()
|
|
# 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)
|
|
|
|
self.assertIsInstance(renderable, FormFieldText)
|
|
self.assertEqual(renderable.text, "Username")
|
|
self.assertEqual(renderable.field, self.text_field)
|
|
self.assertEqual(renderable._field_height, custom_height)
|
|
|
|
|
|
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))
|
|
self.mock_draw = Mock()
|
|
self.callback_result = "callback_executed"
|
|
|
|
# Link callback: receives (location, point, **params)
|
|
def link_callback(location, point, **params):
|
|
return "callback_executed"
|
|
self.link_callback = link_callback
|
|
|
|
# Button callback: receives (point, **params)
|
|
def button_callback(point, **params):
|
|
return "callback_executed"
|
|
self.button_callback = button_callback
|
|
|
|
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
|
|
link = Link("test_function", LinkType.FUNCTION, self.link_callback)
|
|
renderable = LinkText(link, "Test Link", self.font, self.mock_draw)
|
|
|
|
# Simulate interaction
|
|
result = renderable.interact(np.array([10, 10]))
|
|
|
|
# Should execute the link's callback
|
|
self.assertEqual(result, self.callback_result)
|
|
|
|
def test_button_text_interaction(self):
|
|
"""Test that ButtonText properly handles interaction"""
|
|
button = Button("Test Button", self.button_callback)
|
|
renderable = ButtonText(button, self.font, self.mock_draw)
|
|
|
|
# Simulate interaction
|
|
result = renderable.interact(np.array([10, 10]))
|
|
|
|
# Should execute the button's callback
|
|
self.assertEqual(result, self.callback_result)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|