""" Unit tests for the new Page implementation to verify it meets the requirements: 1. Accepts a PageStyle that defines borders, line spacing and inter-block spacing 2. Makes an image canvas 3. Provides a method for accepting child objects 4. Provides methods for determining canvas size and border size 5. Has a method that calls render on all children 6. Has a method to query a point and determine which child it belongs to """ import unittest import numpy as np from PIL import Image, ImageDraw from pyWebLayout.concrete.page import Page from pyWebLayout.style.page_style import PageStyle from pyWebLayout.style.fonts import Font from pyWebLayout.core.base import Renderable, Queriable class SimpleTestRenderable(Renderable, Queriable): """A simple test renderable for testing the page system""" def __init__(self, text: str, size: tuple = (100, 50)): self._text = text self.size = size self.origin = np.array([0, 0]) def render(self): """Render returns None - drawing is done via the page's draw object""" return None class TestPageImplementation(unittest.TestCase): """Test cases for the Page class implementation""" def setUp(self): """Set up test fixtures""" self.basic_style = PageStyle( border_width=2, border_color=(255, 0, 0), line_spacing=8, inter_block_spacing=20, padding=(15, 15, 15, 15), background_color=(240, 240, 240) ) self.page_size = (800, 600) def test_page_creation_with_style(self): """Test creating a page with a PageStyle""" page = Page(size=self.page_size, style=self.basic_style) self.assertEqual(page.size, self.page_size) self.assertEqual(page.style, self.basic_style) self.assertEqual(page.border_size, 2) def test_page_creation_without_style(self): """Test creating a page without a PageStyle (should use defaults)""" page = Page(size=self.page_size) self.assertEqual(page.size, self.page_size) self.assertIsNotNone(page.style) def test_page_canvas_and_content_sizes(self): """Test that page correctly calculates canvas and content sizes""" style = PageStyle( border_width=5, padding=(10, 20, 30, 40) # top, right, bottom, left ) page = Page(size=self.page_size, style=style) # Canvas size should be page size minus borders expected_canvas_size = (790, 590) # 800-10, 600-10 (border on both sides) self.assertEqual(page.canvas_size, expected_canvas_size) # Content size should be canvas minus padding expected_content_size = (730, 550) # 790-60, 590-40 (padding left+right, top+bottom) self.assertEqual(page.content_size, expected_content_size) def test_page_add_remove_children(self): """Test adding and removing children from the page""" page = Page(size=self.page_size) # Initially no children self.assertEqual(len(page.children), 0) # Add children child1 = SimpleTestRenderable("Child 1") child2 = SimpleTestRenderable("Child 2") page.add_child(child1) self.assertEqual(len(page.children), 1) self.assertIn(child1, page.children) page.add_child(child2) self.assertEqual(len(page.children), 2) self.assertIn(child2, page.children) # Test method chaining child3 = SimpleTestRenderable("Child 3") result = page.add_child(child3) self.assertIs(result, page) # Should return self for chaining self.assertEqual(len(page.children), 3) self.assertIn(child3, page.children) # Remove childce you’ll notice is that responses don’t stream character-by-character like other providers. Instead, Claude Code processes your full request before sending back the complete response. removed = page.remove_child(child2) self.assertTrue(removed) self.assertEqual(len(page.children), 2) self.assertNotIn(child2, page.children) # Try to remove non-existent child removed = page.remove_child(child2) self.assertFalse(removed) # Clear all children page.clear_children() self.assertEqual(len(page.children), 0) def test_page_render(self): """Test that page renders and creates a canvas""" style = PageStyle( border_width=2, border_color=(255, 0, 0), background_color=(255, 255, 255) ) page = Page(size=(200, 150), style=style) # Add a child child = SimpleTestRenderable("Test child") page.add_child(child) # Render the page image = page.render() # Check that we got an image self.assertIsInstance(image, Image.Image) self.assertEqual(image.size, (200, 150)) self.assertEqual(image.mode, 'RGBA') # Check that draw object is available self.assertIsNotNone(page.draw) def test_page_query_point(self): """Test querying points to find children""" page = Page(size=(400, 300)) # Add children with known positions and sizes child1 = SimpleTestRenderable("Child 1", (100, 50)) child2 = SimpleTestRenderable("Child 2", (80, 40)) page.add_child(child1).add_child(child2) # Query points # Point within first child found_child = page.query_point((90, 30)) self.assertEqual(found_child, child1) # Point within second child found_child = page.query_point((30, 30)) self.assertEqual(found_child, child2) # Point outside any child found_child = page.query_point((300, 250)) self.assertIsNone(found_child) def test_page_in_object(self): """Test that page correctly implements in_object""" page = Page(size=(400, 300)) # Points within page bounds self.assertTrue(page.in_object((0, 0))) self.assertTrue(page.in_object((200, 150))) self.assertTrue(page.in_object((399, 299))) # Points outside page bounds self.assertFalse(page.in_object((-1, 0))) self.assertFalse(page.in_object((0, -1))) self.assertFalse(page.in_object((400, 299))) self.assertFalse(page.in_object((399, 300))) def test_page_with_borders(self): """Test page rendering with borders""" style = PageStyle( border_width=3, border_color=(128, 128, 128), background_color=(255, 255, 255) ) page = Page(size=(100, 100), style=style) image = page.render() # Check that image was created self.assertIsInstance(image, Image.Image) self.assertEqual(image.size, (100, 100)) # The border should be drawn but we can't easily test pixel values # Just verify the image exists and has the right properties def test_page_border_size_property(self): """Test that border_size property returns correct value""" # Test with border style_with_border = PageStyle(border_width=5) page_with_border = Page(size=self.page_size, style=style_with_border) self.assertEqual(page_with_border.border_size, 5) # Test without border style_no_border = PageStyle(border_width=0) page_no_border = Page(size=self.page_size, style=style_no_border) self.assertEqual(page_no_border.border_size, 0) def test_page_style_properties(self): """Test that page correctly exposes style properties""" page = Page(size=self.page_size, style=self.basic_style) # Test that style properties are accessible self.assertEqual(page.style.border_width, 2) self.assertEqual(page.style.border_color, (255, 0, 0)) self.assertEqual(page.style.line_spacing, 8) self.assertEqual(page.style.inter_block_spacing, 20) self.assertEqual(page.style.padding, (15, 15, 15, 15)) self.assertEqual(page.style.background_color, (240, 240, 240)) def test_page_children_list_operations(self): """Test that children list behaves correctly""" page = Page(size=self.page_size) # Test that children is initially empty list self.assertIsInstance(page.children, list) self.assertEqual(len(page.children), 0) # Test adding multiple children children = [ SimpleTestRenderable(f"Child {i}") for i in range(5) ] for child in children: page.add_child(child) self.assertEqual(len(page.children), 5) # Test that children are in the correct order for i, child in enumerate(page.children): self.assertEqual(child._text, f"Child {i}") def test_page_can_fit_line_boundary_checking(self): """Test that can_fit_line correctly checks bottom boundary""" # Create page with known dimensions # Page: 800x600, border: 40, padding: (10, 10, 10, 10) # Content area starts at y=50 (border + padding_top = 40 + 10) # Content area ends at y=550 (height - border - padding_bottom = 600 - 40 - 10) style = PageStyle( border_width=40, padding=(10, 10, 10, 10) ) page = Page(size=(800, 600), style=style) # Initial y_offset should be at border + padding_top = 50 self.assertEqual(page._current_y_offset, 50) # Test 1: Line that fits comfortably line_height = 20 max_y = 600 - 40 - 10 # 550 self.assertTrue(page.can_fit_line(line_height)) # Would end at 50 + 20 = 70, well within 550 # Test 2: Simulate adding lines to fill the page # Available height: 550 - 50 = 500 pixels # With 20-pixel lines, we can fit 25 lines exactly for i in range(24): # Add 24 lines self.assertTrue(page.can_fit_line(20), f"Line {i+1} should fit") # Simulate adding a line by updating y_offset page._current_y_offset += 20 # After 24 lines: y_offset = 50 + (24 * 20) = 530 self.assertEqual(page._current_y_offset, 530) # Test 3: One more 20-pixel line should fit (530 + 20 = 550, exactly at boundary) self.assertTrue(page.can_fit_line(20)) page._current_y_offset += 20 self.assertEqual(page._current_y_offset, 550) # Test 4: Now another line should NOT fit (550 + 20 = 570 > 550) self.assertFalse(page.can_fit_line(20)) # Test 5: Even a 1-pixel line should not fit (550 + 1 = 551 > 550) self.assertFalse(page.can_fit_line(1)) # Test 6: Edge case - exactly at boundary, 0-height line should fit self.assertTrue(page.can_fit_line(0)) def test_page_can_fit_line_with_different_styles(self): """Test can_fit_line with different page styles""" # Test with no border or padding style_no_border = PageStyle(border_width=0, padding=(0, 0, 0, 0)) page_no_border = Page(size=(100, 100), style=style_no_border) # With no border/padding, y_offset starts at 0 self.assertEqual(page_no_border._current_y_offset, 0) # Can fit a 100-pixel line exactly self.assertTrue(page_no_border.can_fit_line(100)) # Cannot fit a 101-pixel line self.assertFalse(page_no_border.can_fit_line(101)) # Test with large border and padding style_large = PageStyle(border_width=20, padding=(15, 15, 15, 15)) page_large = Page(size=(200, 200), style=style_large) # y_offset starts at border + padding_top = 20 + 15 = 35 self.assertEqual(page_large._current_y_offset, 35) # Max y = 200 - 20 - 15 = 165 # Available height = 165 - 35 = 130 pixels self.assertTrue(page_large.can_fit_line(130)) self.assertFalse(page_large.can_fit_line(131)) if __name__ == '__main__': unittest.main()