573 lines
20 KiB
Python
573 lines
20 KiB
Python
"""
|
|
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."""
|
|
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')
|
|
|
|
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 start
|
|
time.sleep(1)
|
|
|
|
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()
|