FormFieldText treats its origin as the control's top-left - size and in_object both measure down from it - but drew the label by calling Text.render at that origin, and Text anchors on the baseline. The label's glyphs therefore landed above the origin, outside the box the control claims, on top of whatever was there. In a stacked form that is the preceding field's input box, which is what example_10_forms.png showed: every label but the first crowding the box above it. The label is now offset down by its ascent, so it occupies the space the control accounts for. Height derives from the label's ink height rather than the nominal font size, which had also eaten into the 5px gap between label and box. LABEL_GAP names that gap and field_area_offset gives the distance from the origin to the top of the input box; render, handle_click and the height calculation now share it instead of each recomputing font_size + 5. Also recorded under S12: the broken process pool is not merely wasted work. It forks from a process that already has threads, and tests/layout/test_ereader_image_rendering.py hangs at interpreter exit roughly one run in four - every test passes, then the process never returns.
500 lines
18 KiB
Python
500 lines
18 KiB
Python
"""
|
|
Unit tests for pyWebLayout.concrete.functional module.
|
|
Tests the LinkText, ButtonText, and FormFieldText classes.
|
|
"""
|
|
|
|
import unittest
|
|
import numpy as np
|
|
from unittest.mock import Mock, patch
|
|
|
|
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, FormField, LinkType, FormFieldType
|
|
)
|
|
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(
|
|
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 - origin is at baseline (10, 20), so test at baseline Y
|
|
self.assertTrue(renderable.in_object((15, 20)))
|
|
|
|
# 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. The label's
|
|
# height is its ink height (ascent + descent), not the nominal font size.
|
|
ascent, descent = renderable._style.font.getmetrics()
|
|
expected_height = (ascent + descent) + FormFieldText.LABEL_GAP \
|
|
+ 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()
|