""" Unit tests for abstract block elements. Tests the core abstract block classes that form the foundation of the document model. """ import unittest import os import tempfile import shutil import threading import time from PIL import Image as PILImage from pyWebLayout.abstract.block import ( Block, BlockType, Paragraph, Heading, HeadingLevel, Quote, CodeBlock, HList, ListStyle, ListItem, Table, TableRow, TableCell, HorizontalRule, Image ) from pyWebLayout.abstract.inline import Word, LineBreak from pyWebLayout.style import Font # Flask server for testing URL functionality try: from flask import Flask, send_file FLASK_AVAILABLE = True except ImportError: FLASK_AVAILABLE = False class TestBlockElements(unittest.TestCase): """Test cases for basic block elements.""" def test_paragraph_creation(self): """Test creating and using paragraphs.""" paragraph = Paragraph() self.assertEqual(paragraph.block_type, BlockType.PARAGRAPH) self.assertEqual(paragraph.word_count, 0) self.assertIsNone(paragraph.parent) # Add words font = Font() word1 = Word("Hello", font) word2 = Word("World", font) paragraph.add_word(word1) paragraph.add_word(word2) self.assertEqual(paragraph.word_count, 2) # Test word iteration words = list(paragraph.words_iter()) self.assertEqual(len(words), 2) self.assertEqual(words[0][1].text, "Hello") self.assertEqual(words[1][1].text, "World") def test_heading_levels(self): """Test heading creation with different levels.""" h1 = Heading(HeadingLevel.H1) h3 = Heading(HeadingLevel.H3) h6 = Heading(HeadingLevel.H6) self.assertEqual(h1.level, HeadingLevel.H1) self.assertEqual(h3.level, HeadingLevel.H3) self.assertEqual(h6.level, HeadingLevel.H6) self.assertEqual(h1.block_type, BlockType.HEADING) # Test level modification h1.level = HeadingLevel.H2 self.assertEqual(h1.level, HeadingLevel.H2) def test_quote_nesting(self): """Test blockquote with nested content.""" quote = Quote() # Add nested paragraphs p1 = Paragraph() p2 = Paragraph() quote.add_block(p1) quote.add_block(p2) self.assertEqual(p1.parent, quote) self.assertEqual(p2.parent, quote) # Test block iteration blocks = list(quote.blocks()) self.assertEqual(len(blocks), 2) self.assertEqual(blocks[0], p1) self.assertEqual(blocks[1], p2) def test_code_block(self): """Test code block functionality.""" code = CodeBlock("python") self.assertEqual(code.language, "python") self.assertEqual(code.line_count, 0) # Add code lines code.add_line("def hello():") code.add_line(" print('Hello!')") self.assertEqual(code.line_count, 2) # Test line iteration lines = list(code.lines()) self.assertEqual(len(lines), 2) self.assertEqual(lines[0][1], "def hello():") self.assertEqual(lines[1][1], " print('Hello!')") # Test language modification code.language = "javascript" self.assertEqual(code.language, "javascript") def test_list_creation(self): """Test list creation and item management.""" # Unordered list ul = HList(ListStyle.UNORDERED) self.assertEqual(ul.style, ListStyle.UNORDERED) self.assertEqual(ul.item_count, 0) # Add list items item1 = ListItem() item2 = ListItem() ul.add_item(item1) ul.add_item(item2) self.assertEqual(ul.item_count, 2) self.assertEqual(item1.parent, ul) self.assertEqual(item2.parent, ul) # Test item iteration items = list(ul.items()) self.assertEqual(len(items), 2) # Test list style change ul.style = ListStyle.ORDERED self.assertEqual(ul.style, ListStyle.ORDERED) def test_definition_list(self): """Test definition list with terms.""" dl = HList(ListStyle.DEFINITION) # Add definition items with terms dt1 = ListItem(term="Python") dt2 = ListItem(term="JavaScript") dl.add_item(dt1) dl.add_item(dt2) self.assertEqual(dt1.term, "Python") self.assertEqual(dt2.term, "JavaScript") # Test term modification dt1.term = "Python 3" self.assertEqual(dt1.term, "Python 3") def test_table_structure(self): """Test table, row, and cell structure.""" table = Table(caption="Test Table") self.assertEqual(table.caption, "Test Table") self.assertEqual(table.row_count["total"], 0) # Create rows and cells header_row = TableRow() data_row = TableRow() # Header cells h1 = TableCell(is_header=True) h2 = TableCell(is_header=True) header_row.add_cell(h1) header_row.add_cell(h2) # Data cells d1 = TableCell(is_header=False) d2 = TableCell(is_header=False, colspan=2) data_row.add_cell(d1) data_row.add_cell(d2) # Add rows to table table.add_row(header_row, "header") table.add_row(data_row, "body") # Test structure self.assertEqual(table.row_count["header"], 1) self.assertEqual(table.row_count["body"], 1) self.assertEqual(table.row_count["total"], 2) # Test cell properties self.assertTrue(h1.is_header) self.assertFalse(d1.is_header) self.assertEqual(d2.colspan, 2) self.assertEqual(d2.rowspan, 1) # Default # Test row cell count self.assertEqual(header_row.cell_count, 2) self.assertEqual(data_row.cell_count, 2) def test_table_sections(self): """Test table header, body, and footer sections.""" table = Table() # Add rows to different sections header = TableRow() body1 = TableRow() body2 = TableRow() footer = TableRow() table.add_row(header, "header") table.add_row(body1, "body") table.add_row(body2, "body") table.add_row(footer, "footer") # Test section iteration header_rows = list(table.header_rows()) body_rows = list(table.body_rows()) footer_rows = list(table.footer_rows()) self.assertEqual(len(header_rows), 1) self.assertEqual(len(body_rows), 2) self.assertEqual(len(footer_rows), 1) # Test all_rows iteration all_rows = list(table.all_rows()) self.assertEqual(len(all_rows), 4) # Check section labels sections = [section for section, row in all_rows] self.assertEqual(sections, ["header", "body", "body", "footer"]) def test_image_loading(self): """Test image element properties.""" # Test with basic properties img = Image("test.jpg", "Test image", 100, 200) self.assertEqual(img.source, "test.jpg") self.assertEqual(img.alt_text, "Test image") self.assertEqual(img.width, 100) self.assertEqual(img.height, 200) # Test property modification img.source = "new.png" img.alt_text = "New image" img.width = 150 img.height = 300 self.assertEqual(img.source, "new.png") self.assertEqual(img.alt_text, "New image") self.assertEqual(img.width, 150) self.assertEqual(img.height, 300) # Test dimensions tuple self.assertEqual(img.get_dimensions(), (150, 300)) def test_aspect_ratio_calculation(self): """Test image aspect ratio calculations.""" # Test with specified dimensions img = Image("test.jpg", width=400, height=200) self.assertEqual(img.get_aspect_ratio(), 2.0) # 400/200 # Test with only one dimension img2 = Image("test.jpg", width=300) self.assertIsNone(img2.get_aspect_ratio()) # No height specified # Test scaled dimensions scaled = img.calculate_scaled_dimensions(max_width=200, max_height=150) # Should scale down proportionally self.assertEqual(scaled[0], 200) # Width limited by max_width self.assertEqual(scaled[1], 100) # Height scaled proportionally def test_simple_elements(self): """Test simple block elements.""" hr = HorizontalRule() br = LineBreak() self.assertEqual(hr.block_type, BlockType.HORIZONTAL_RULE) self.assertEqual(br.block_type, BlockType.LINE_BREAK) # These elements have no additional properties self.assertIsNone(hr.parent) self.assertIsNone(br.parent) class TestImagePIL(unittest.TestCase): """Test cases for Image class with PIL functionality.""" @classmethod def setUpClass(cls): """Set up temporary directory and test images.""" cls.temp_dir = tempfile.mkdtemp() cls.sample_image_path = "tests/data/sample_image.jpg" # Create test images in different formats cls._create_test_images() # Start Flask server for URL testing if Flask is available if FLASK_AVAILABLE: cls._start_flask_server() @classmethod def tearDownClass(cls): """Clean up temporary directory and stop Flask server.""" shutil.rmtree(cls.temp_dir, ignore_errors=True) if FLASK_AVAILABLE and hasattr(cls, 'flask_thread'): cls.flask_server_running = False cls.flask_thread.join(timeout=2) @classmethod def _create_test_images(cls): """Create test images in different formats.""" # Load the sample image if os.path.exists(cls.sample_image_path): with PILImage.open(cls.sample_image_path) as img: cls.original_size = img.size # Save in different formats cls.jpg_path = os.path.join(cls.temp_dir, "test.jpg") cls.png_path = os.path.join(cls.temp_dir, "test.png") cls.bmp_path = os.path.join(cls.temp_dir, "test.bmp") cls.gif_path = os.path.join(cls.temp_dir, "test.gif") img.save(cls.jpg_path, "JPEG") img.save(cls.png_path, "PNG") img.save(cls.bmp_path, "BMP") # Convert to RGB for GIF (GIF doesn't support transparency from RGBA) rgb_img = img.convert("RGB") rgb_img.save(cls.gif_path, "GIF") else: # Create a simple test image if sample doesn't exist cls.original_size = (100, 100) test_img = PILImage.new("RGB", cls.original_size, (255, 0, 0)) cls.jpg_path = os.path.join(cls.temp_dir, "test.jpg") cls.png_path = os.path.join(cls.temp_dir, "test.png") cls.bmp_path = os.path.join(cls.temp_dir, "test.bmp") cls.gif_path = os.path.join(cls.temp_dir, "test.gif") test_img.save(cls.jpg_path, "JPEG") test_img.save(cls.png_path, "PNG") test_img.save(cls.bmp_path, "BMP") test_img.save(cls.gif_path, "GIF") @classmethod def _start_flask_server(cls): """Start a Flask server for URL testing.""" import urllib.request import urllib.error cls.flask_app = Flask(__name__) cls.flask_port = 5555 # Use a specific port for testing cls.flask_server_running = True @cls.flask_app.route('/test.jpg') def serve_test_image(): return send_file(cls.jpg_path, mimetype='image/jpeg') @cls.flask_app.route('/health') def health_check(): return 'OK', 200 def run_flask(): cls.flask_app.run(host='127.0.0.1', port=cls.flask_port, debug=False, use_reloader=False, threaded=True) cls.flask_thread = threading.Thread(target=run_flask, daemon=True) cls.flask_thread.start() # Wait for server to be ready with health check max_wait = 5 # Maximum 5 seconds wait_interval = 0.1 # Check every 100ms elapsed = 0 while elapsed < max_wait: try: with urllib.request.urlopen(f'http://127.0.0.1:{cls.flask_port}/health', timeout=1) as response: if response.status == 200: break except (urllib.error.URLError, ConnectionRefusedError, OSError): pass time.sleep(wait_interval) elapsed += wait_interval def test_image_url_detection(self): """Test URL detection functionality.""" img = Image() # Test URL detection self.assertTrue(img._is_url("http://example.com/image.jpg")) self.assertTrue(img._is_url("https://example.com/image.png")) self.assertTrue(img._is_url("ftp://example.com/image.gif")) # Test non-URL detection self.assertFalse(img._is_url("image.jpg")) self.assertFalse(img._is_url("/path/to/image.png")) self.assertFalse(img._is_url("../relative/path.gif")) self.assertFalse(img._is_url("")) def test_load_local_image_jpg(self): """Test loading local JPG image.""" img = Image(self.jpg_path) file_path, pil_img = img.load_image_data() self.assertIsNotNone(pil_img) self.assertEqual(file_path, self.jpg_path) self.assertEqual(pil_img.size, self.original_size) self.assertEqual(img.width, self.original_size[0]) self.assertEqual(img.height, self.original_size[1]) def test_load_local_image_png(self): """Test loading local PNG image.""" img = Image(self.png_path) file_path, pil_img = img.load_image_data() self.assertIsNotNone(pil_img) self.assertEqual(file_path, self.png_path) self.assertEqual(pil_img.size, self.original_size) def test_load_local_image_bmp(self): """Test loading local BMP image.""" img = Image(self.bmp_path) file_path, pil_img = img.load_image_data() self.assertIsNotNone(pil_img) self.assertEqual(file_path, self.bmp_path) self.assertEqual(pil_img.size, self.original_size) def test_load_local_image_gif(self): """Test loading local GIF image.""" img = Image(self.gif_path) file_path, pil_img = img.load_image_data() self.assertIsNotNone(pil_img) self.assertEqual(file_path, self.gif_path) self.assertEqual(pil_img.size, self.original_size) def test_load_nonexistent_image(self): """Test loading non-existent image.""" img = Image("nonexistent.jpg") file_path, pil_img = img.load_image_data() self.assertIsNone(pil_img) self.assertIsNone(file_path) def test_load_empty_source(self): """Test loading with empty source.""" img = Image("") file_path, pil_img = img.load_image_data() self.assertIsNone(pil_img) self.assertIsNone(file_path) def test_auto_update_dimensions(self): """Test automatic dimension updating.""" img = Image(self.jpg_path, width=50, height=50) # Wrong initial dimensions # Test with auto-update enabled (default) file_path, pil_img = img.load_image_data(auto_update_dimensions=True) self.assertEqual(img.width, self.original_size[0]) self.assertEqual(img.height, self.original_size[1]) def test_no_auto_update_dimensions(self): """Test loading without automatic dimension updating.""" original_width, original_height = 50, 50 img = Image(self.jpg_path, width=original_width, height=original_height) # Test with auto-update disabled file_path, pil_img = img.load_image_data(auto_update_dimensions=False) self.assertEqual(img.width, original_width) # Should remain unchanged self.assertEqual(img.height, original_height) # Should remain unchanged def test_get_image_info(self): """Test getting detailed image information.""" img = Image(self.jpg_path) info = img.get_image_info() self.assertIsInstance(info, dict) self.assertIn('format', info) self.assertIn('mode', info) self.assertIn('size', info) self.assertIn('width', info) self.assertIn('height', info) self.assertEqual(info['size'], self.original_size) self.assertEqual(info['width'], self.original_size[0]) self.assertEqual(info['height'], self.original_size[1]) def test_get_image_info_different_formats(self): """Test getting image info for different formats.""" formats_and_paths = [ ('JPEG', self.jpg_path), ('PNG', self.png_path), ('BMP', self.bmp_path), ('GIF', self.gif_path), ] for expected_format, path in formats_and_paths: with self.subTest(format=expected_format): img = Image(path) info = img.get_image_info() self.assertEqual(info['format'], expected_format) self.assertEqual(info['size'], self.original_size) def test_get_image_info_nonexistent(self): """Test getting image info for non-existent image.""" img = Image("nonexistent.jpg") info = img.get_image_info() self.assertEqual(info, {}) @unittest.skipUnless(FLASK_AVAILABLE, "Flask not available for URL testing") def test_load_image_from_url(self): """Test loading image from URL.""" url = f"http://127.0.0.1:{self.flask_port}/test.jpg" img = Image(url) file_path, pil_img = img.load_image_data() self.assertIsNotNone(pil_img) self.assertIsNotNone(file_path) self.assertTrue(file_path.endswith('.tmp')) # Should be a temp file self.assertEqual(pil_img.size, self.original_size) # Check that dimensions were updated self.assertEqual(img.width, self.original_size[0]) self.assertEqual(img.height, self.original_size[1]) @unittest.skipUnless(FLASK_AVAILABLE, "Flask not available for URL testing") def test_get_image_info_from_url(self): """Test getting image info from URL.""" url = f"http://127.0.0.1:{self.flask_port}/test.jpg" img = Image(url) info = img.get_image_info() self.assertIsInstance(info, dict) self.assertEqual(info['format'], 'JPEG') self.assertEqual(info['size'], self.original_size) def test_load_invalid_url(self): """Test loading from invalid URL.""" img = Image("http://nonexistent.domain/image.jpg") file_path, pil_img = img.load_image_data() self.assertIsNone(pil_img) self.assertIsNone(file_path) def test_multiple_loads_cleanup(self): """Test that multiple loads don't leave temp files.""" img = Image(self.jpg_path) # Load multiple times for _ in range(3): file_path, pil_img = img.load_image_data() self.assertIsNotNone(pil_img) def test_original_sample_image(self): """Test loading the original sample image if it exists.""" if os.path.exists(self.sample_image_path): img = Image(self.sample_image_path) file_path, pil_img = img.load_image_data() self.assertIsNotNone(pil_img) self.assertEqual(file_path, self.sample_image_path) # Test that we can get image info info = img.get_image_info() self.assertIsInstance(info, dict) self.assertIn('format', info) self.assertIn('size', info) if __name__ == '__main__': unittest.main()