""" 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 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()