""" 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 @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.functional.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.functional.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 mock_text_class.assert_called() self.assertEqual(mock_text_class.call_args[0][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()