@@ -0,0 +1,572 @@
|
||||
"""
|
||||
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())
|
||||
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()
|
||||
@@ -0,0 +1,687 @@
|
||||
"""
|
||||
Unit tests for abstract document elements.
|
||||
|
||||
Tests the Document, Chapter, Book, and MetadataType classes that handle
|
||||
document structure and metadata management.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pyWebLayout.abstract.document import Document, Chapter, Book, MetadataType
|
||||
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel, BlockType
|
||||
from pyWebLayout.abstract.inline import Word, FormattedSpan
|
||||
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
|
||||
class TestMetadataType(unittest.TestCase):
|
||||
"""Test cases for MetadataType enum."""
|
||||
|
||||
def test_metadata_types(self):
|
||||
"""Test that all expected metadata types exist."""
|
||||
expected_types = [
|
||||
'TITLE', 'AUTHOR', 'DESCRIPTION', 'KEYWORDS', 'LANGUAGE',
|
||||
'PUBLICATION_DATE', 'MODIFIED_DATE', 'PUBLISHER', 'IDENTIFIER',
|
||||
'COVER_IMAGE', 'CUSTOM'
|
||||
]
|
||||
|
||||
for type_name in expected_types:
|
||||
self.assertTrue(hasattr(MetadataType, type_name))
|
||||
|
||||
# Test custom type has expected value
|
||||
self.assertEqual(MetadataType.CUSTOM.value, 100)
|
||||
|
||||
|
||||
class TestDocument(unittest.TestCase):
|
||||
"""Test cases for Document class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.doc = Document("Test Document", "en-US")
|
||||
self.font = Font()
|
||||
|
||||
def test_document_creation(self):
|
||||
"""Test document creation with basic parameters."""
|
||||
self.assertEqual(self.doc.get_title(), "Test Document")
|
||||
self.assertEqual(self.doc.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||
self.assertEqual(len(self.doc.blocks), 0)
|
||||
|
||||
def test_document_creation_minimal(self):
|
||||
"""Test document creation with minimal parameters."""
|
||||
doc = Document()
|
||||
self.assertIsNone(doc.get_title())
|
||||
self.assertEqual(doc.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||
|
||||
def test_metadata_management(self):
|
||||
"""Test setting and getting metadata."""
|
||||
# Set various metadata types
|
||||
self.doc.set_metadata(MetadataType.AUTHOR, "John Doe")
|
||||
self.doc.set_metadata(MetadataType.DESCRIPTION, "A test document")
|
||||
self.doc.set_metadata(MetadataType.KEYWORDS, ["test", "document"])
|
||||
|
||||
# Test retrieval
|
||||
self.assertEqual(self.doc.get_metadata(MetadataType.AUTHOR), "John Doe")
|
||||
self.assertEqual(self.doc.get_metadata(MetadataType.DESCRIPTION), "A test document")
|
||||
self.assertEqual(self.doc.get_metadata(MetadataType.KEYWORDS), ["test", "document"])
|
||||
|
||||
# Test non-existent metadata
|
||||
self.assertIsNone(self.doc.get_metadata(MetadataType.PUBLISHER))
|
||||
|
||||
def test_title_convenience_methods(self):
|
||||
"""Test title getter and setter convenience methods."""
|
||||
# Test setting title
|
||||
self.doc.set_title("New Title")
|
||||
self.assertEqual(self.doc.get_title(), "New Title")
|
||||
|
||||
# Test that it's also in metadata
|
||||
self.assertEqual(self.doc.get_metadata(MetadataType.TITLE), "New Title")
|
||||
|
||||
def test_block_management(self):
|
||||
"""Test adding and managing blocks."""
|
||||
# Create some blocks
|
||||
para1 = Paragraph()
|
||||
para2 = Paragraph()
|
||||
heading = Heading(HeadingLevel.H1)
|
||||
|
||||
# Add blocks
|
||||
self.doc.add_block(para1)
|
||||
self.doc.add_block(heading)
|
||||
self.doc.add_block(para2)
|
||||
|
||||
# Test blocks list
|
||||
self.assertEqual(len(self.doc.blocks), 3)
|
||||
self.assertEqual(self.doc.blocks[0], para1)
|
||||
self.assertEqual(self.doc.blocks[1], heading)
|
||||
self.assertEqual(self.doc.blocks[2], para2)
|
||||
|
||||
def test_anchor_management(self):
|
||||
"""Test named anchor functionality."""
|
||||
heading = Heading(HeadingLevel.H1)
|
||||
para = Paragraph()
|
||||
|
||||
# Add anchors
|
||||
self.doc.add_anchor("intro", heading)
|
||||
self.doc.add_anchor("content", para)
|
||||
|
||||
# Test retrieval
|
||||
self.assertEqual(self.doc.get_anchor("intro"), heading)
|
||||
self.assertEqual(self.doc.get_anchor("content"), para)
|
||||
self.assertIsNone(self.doc.get_anchor("nonexistent"))
|
||||
|
||||
def test_resource_management(self):
|
||||
"""Test document resource management."""
|
||||
# Add various resources
|
||||
self.doc.add_resource("image1", {"type": "image", "path": "test.jpg"})
|
||||
self.doc.add_resource("style1", {"type": "css", "content": "body {}"})
|
||||
|
||||
# Test retrieval
|
||||
image = self.doc.get_resource("image1")
|
||||
self.assertEqual(image["type"], "image")
|
||||
self.assertEqual(image["path"], "test.jpg")
|
||||
|
||||
style = self.doc.get_resource("style1")
|
||||
self.assertEqual(style["type"], "css")
|
||||
|
||||
# Test non-existent resource
|
||||
self.assertIsNone(self.doc.get_resource("nonexistent"))
|
||||
|
||||
def test_stylesheet_management(self):
|
||||
"""Test stylesheet addition."""
|
||||
# Add stylesheets
|
||||
css1 = {"href": "style.css", "type": "text/css"}
|
||||
css2 = {"href": "theme.css", "type": "text/css"}
|
||||
|
||||
self.doc.add_stylesheet(css1)
|
||||
self.doc.add_stylesheet(css2)
|
||||
|
||||
# Test that stylesheets are stored
|
||||
self.assertEqual(len(self.doc._stylesheets), 2)
|
||||
self.assertEqual(self.doc._stylesheets[0], css1)
|
||||
self.assertEqual(self.doc._stylesheets[1], css2)
|
||||
|
||||
def test_script_management(self):
|
||||
"""Test script addition."""
|
||||
# Add scripts
|
||||
script1 = "console.log('Hello');"
|
||||
script2 = "document.ready(function(){});"
|
||||
|
||||
self.doc.add_script(script1)
|
||||
self.doc.add_script(script2)
|
||||
|
||||
# Test that scripts are stored
|
||||
self.assertEqual(len(self.doc._scripts), 2)
|
||||
self.assertEqual(self.doc._scripts[0], script1)
|
||||
self.assertEqual(self.doc._scripts[1], script2)
|
||||
|
||||
def test_find_blocks_by_type(self):
|
||||
"""Test finding blocks by type."""
|
||||
# Create blocks of different types
|
||||
para1 = Paragraph()
|
||||
para2 = Paragraph()
|
||||
heading1 = Heading(HeadingLevel.H1)
|
||||
heading2 = Heading(HeadingLevel.H2)
|
||||
|
||||
# Add blocks to document
|
||||
self.doc.add_block(para1)
|
||||
self.doc.add_block(heading1)
|
||||
self.doc.add_block(para2)
|
||||
self.doc.add_block(heading2)
|
||||
|
||||
# Test finding paragraphs
|
||||
paragraphs = self.doc.find_blocks_by_type(BlockType.PARAGRAPH)
|
||||
self.assertEqual(len(paragraphs), 2)
|
||||
self.assertIn(para1, paragraphs)
|
||||
self.assertIn(para2, paragraphs)
|
||||
|
||||
# Test finding headings
|
||||
headings = self.doc.find_blocks_by_type(BlockType.HEADING)
|
||||
self.assertEqual(len(headings), 2)
|
||||
self.assertIn(heading1, headings)
|
||||
self.assertIn(heading2, headings)
|
||||
|
||||
def test_find_headings(self):
|
||||
"""Test finding heading blocks specifically."""
|
||||
# Create mixed blocks
|
||||
para = Paragraph()
|
||||
h1 = Heading(HeadingLevel.H1)
|
||||
h2 = Heading(HeadingLevel.H2)
|
||||
|
||||
# Add words to headings for title extraction
|
||||
word1 = Word("Chapter", self.font)
|
||||
word2 = Word("One", self.font)
|
||||
h1.add_word(word1)
|
||||
h1.add_word(word2)
|
||||
|
||||
word3 = Word("Section", self.font)
|
||||
h2.add_word(word3)
|
||||
|
||||
self.doc.add_block(para)
|
||||
self.doc.add_block(h1)
|
||||
self.doc.add_block(h2)
|
||||
|
||||
# Test finding headings
|
||||
headings = self.doc.find_headings()
|
||||
self.assertEqual(len(headings), 2)
|
||||
self.assertIn(h1, headings)
|
||||
self.assertIn(h2, headings)
|
||||
self.assertNotIn(para, headings)
|
||||
|
||||
def test_generate_table_of_contents(self):
|
||||
"""Test table of contents generation."""
|
||||
# Create headings with content
|
||||
h1 = Heading(HeadingLevel.H1)
|
||||
h2 = Heading(HeadingLevel.H2)
|
||||
h3 = Heading(HeadingLevel.H3)
|
||||
|
||||
# Add words to headings
|
||||
h1.add_word(Word("Introduction", self.font))
|
||||
h2.add_word(Word("Getting", self.font))
|
||||
h2.add_word(Word("Started", self.font))
|
||||
h3.add_word(Word("Installation", self.font))
|
||||
|
||||
self.doc.add_block(h1)
|
||||
self.doc.add_block(h2)
|
||||
self.doc.add_block(h3)
|
||||
|
||||
# Generate TOC
|
||||
toc = self.doc.generate_table_of_contents()
|
||||
|
||||
# Test TOC structure
|
||||
self.assertEqual(len(toc), 3)
|
||||
|
||||
# Test first entry
|
||||
level, title, block = toc[0]
|
||||
self.assertEqual(level, 1) # H1
|
||||
self.assertEqual(title, "Introduction")
|
||||
self.assertEqual(block, h1)
|
||||
|
||||
# Test second entry
|
||||
level, title, block = toc[1]
|
||||
self.assertEqual(level, 2) # H2
|
||||
self.assertEqual(title, "Getting Started")
|
||||
self.assertEqual(block, h2)
|
||||
|
||||
# Test third entry
|
||||
level, title, block = toc[2]
|
||||
self.assertEqual(level, 3) # H3
|
||||
self.assertEqual(title, "Installation")
|
||||
self.assertEqual(block, h3)
|
||||
|
||||
|
||||
class TestChapter(unittest.TestCase):
|
||||
"""Test cases for Chapter class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.chapter = Chapter("Test Chapter", 1)
|
||||
|
||||
def test_chapter_creation(self):
|
||||
"""Test chapter creation."""
|
||||
self.assertEqual(self.chapter.title, "Test Chapter")
|
||||
self.assertEqual(self.chapter.level, 1)
|
||||
self.assertEqual(len(self.chapter.blocks), 0)
|
||||
|
||||
def test_chapter_creation_minimal(self):
|
||||
"""Test chapter creation with minimal parameters."""
|
||||
chapter = Chapter()
|
||||
self.assertIsNone(chapter.title)
|
||||
self.assertEqual(chapter.level, 1)
|
||||
|
||||
def test_title_property(self):
|
||||
"""Test title property getter and setter."""
|
||||
# Test setter
|
||||
self.chapter.title = "New Chapter Title"
|
||||
self.assertEqual(self.chapter.title, "New Chapter Title")
|
||||
|
||||
# Test setting to None
|
||||
self.chapter.title = None
|
||||
self.assertIsNone(self.chapter.title)
|
||||
|
||||
def test_level_property(self):
|
||||
"""Test level property."""
|
||||
self.assertEqual(self.chapter.level, 1)
|
||||
|
||||
# Level should be read-only (no setter test)
|
||||
# This is by design based on the class definition
|
||||
|
||||
def test_block_management(self):
|
||||
"""Test adding blocks to chapter."""
|
||||
para1 = Paragraph()
|
||||
para2 = Paragraph()
|
||||
heading = Heading(HeadingLevel.H2)
|
||||
|
||||
# Add blocks
|
||||
self.chapter.add_block(para1)
|
||||
self.chapter.add_block(heading)
|
||||
self.chapter.add_block(para2)
|
||||
|
||||
# Test blocks list
|
||||
self.assertEqual(len(self.chapter.blocks), 3)
|
||||
self.assertEqual(self.chapter.blocks[0], para1)
|
||||
self.assertEqual(self.chapter.blocks[1], heading)
|
||||
self.assertEqual(self.chapter.blocks[2], para2)
|
||||
|
||||
def test_metadata_management(self):
|
||||
"""Test chapter metadata."""
|
||||
# Set metadata
|
||||
self.chapter.set_metadata("author", "Jane Doe")
|
||||
self.chapter.set_metadata("word_count", 1500)
|
||||
self.chapter.set_metadata("tags", ["intro", "basics"])
|
||||
|
||||
# Test retrieval
|
||||
self.assertEqual(self.chapter.get_metadata("author"), "Jane Doe")
|
||||
self.assertEqual(self.chapter.get_metadata("word_count"), 1500)
|
||||
self.assertEqual(self.chapter.get_metadata("tags"), ["intro", "basics"])
|
||||
|
||||
# Test non-existent metadata
|
||||
self.assertIsNone(self.chapter.get_metadata("nonexistent"))
|
||||
|
||||
|
||||
class TestBook(unittest.TestCase):
|
||||
"""Test cases for Book class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.book = Book("Test Book", "Author Name", "en-US")
|
||||
|
||||
def test_book_creation(self):
|
||||
"""Test book creation with all parameters."""
|
||||
self.assertEqual(self.book.get_title(), "Test Book")
|
||||
self.assertEqual(self.book.get_author(), "Author Name")
|
||||
self.assertEqual(self.book.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||
self.assertEqual(len(self.book.chapters), 0)
|
||||
|
||||
def test_book_creation_minimal(self):
|
||||
"""Test book creation with minimal parameters."""
|
||||
book = Book()
|
||||
self.assertIsNone(book.get_title())
|
||||
self.assertIsNone(book.get_author())
|
||||
self.assertEqual(book.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||
|
||||
def test_book_creation_partial(self):
|
||||
"""Test book creation with partial parameters."""
|
||||
book = Book(title="Just Title")
|
||||
self.assertEqual(book.get_title(), "Just Title")
|
||||
self.assertIsNone(book.get_author())
|
||||
|
||||
def test_author_convenience_methods(self):
|
||||
"""Test author getter and setter convenience methods."""
|
||||
# Test setting author
|
||||
self.book.set_author("New Author")
|
||||
self.assertEqual(self.book.get_author(), "New Author")
|
||||
|
||||
# Test that it's also in metadata
|
||||
self.assertEqual(self.book.get_metadata(MetadataType.AUTHOR), "New Author")
|
||||
|
||||
def test_chapter_management(self):
|
||||
"""Test adding and managing chapters."""
|
||||
# Create chapters
|
||||
ch1 = Chapter("Introduction", 1)
|
||||
ch2 = Chapter("Getting Started", 1)
|
||||
ch3 = Chapter("Advanced Topics", 1)
|
||||
|
||||
# Add chapters
|
||||
self.book.add_chapter(ch1)
|
||||
self.book.add_chapter(ch2)
|
||||
self.book.add_chapter(ch3)
|
||||
|
||||
# Test chapters list
|
||||
self.assertEqual(len(self.book.chapters), 3)
|
||||
self.assertEqual(self.book.chapters[0], ch1)
|
||||
self.assertEqual(self.book.chapters[1], ch2)
|
||||
self.assertEqual(self.book.chapters[2], ch3)
|
||||
|
||||
def test_create_chapter(self):
|
||||
"""Test creating chapters through the book."""
|
||||
# Create chapter with title and level
|
||||
ch1 = self.book.create_chapter("Chapter 1", 1)
|
||||
self.assertEqual(ch1.title, "Chapter 1")
|
||||
self.assertEqual(ch1.level, 1)
|
||||
self.assertEqual(len(self.book.chapters), 1)
|
||||
self.assertEqual(self.book.chapters[0], ch1)
|
||||
|
||||
# Create chapter with minimal parameters
|
||||
ch2 = self.book.create_chapter()
|
||||
self.assertIsNone(ch2.title)
|
||||
self.assertEqual(ch2.level, 1)
|
||||
self.assertEqual(len(self.book.chapters), 2)
|
||||
|
||||
def test_generate_book_toc(self):
|
||||
"""Test table of contents generation for book."""
|
||||
# Create chapters with different levels
|
||||
ch1 = Chapter("Introduction", 1)
|
||||
ch2 = Chapter("Getting Started", 1)
|
||||
ch3 = Chapter("Basic Concepts", 2)
|
||||
ch4 = Chapter("Advanced Topics", 1)
|
||||
ch5 = Chapter("Best Practices", 2)
|
||||
|
||||
# Add chapters to book
|
||||
self.book.add_chapter(ch1)
|
||||
self.book.add_chapter(ch2)
|
||||
self.book.add_chapter(ch3)
|
||||
self.book.add_chapter(ch4)
|
||||
self.book.add_chapter(ch5)
|
||||
|
||||
# Generate TOC
|
||||
toc = self.book.generate_table_of_contents()
|
||||
|
||||
# Test TOC structure
|
||||
self.assertEqual(len(toc), 5)
|
||||
|
||||
# Test entries
|
||||
expected = [
|
||||
(1, "Introduction", ch1),
|
||||
(1, "Getting Started", ch2),
|
||||
(2, "Basic Concepts", ch3),
|
||||
(1, "Advanced Topics", ch4),
|
||||
(2, "Best Practices", ch5)
|
||||
]
|
||||
|
||||
for i, (exp_level, exp_title, exp_chapter) in enumerate(expected):
|
||||
level, title, chapter = toc[i]
|
||||
self.assertEqual(level, exp_level)
|
||||
self.assertEqual(title, exp_title)
|
||||
self.assertEqual(chapter, exp_chapter)
|
||||
|
||||
def test_generate_book_toc_with_untitled_chapters(self):
|
||||
"""Test TOC generation with chapters that have no title."""
|
||||
# Create chapters, some without titles
|
||||
ch1 = Chapter("Introduction", 1)
|
||||
ch2 = Chapter(None, 1) # No title
|
||||
ch3 = Chapter("Conclusion", 1)
|
||||
|
||||
self.book.add_chapter(ch1)
|
||||
self.book.add_chapter(ch2)
|
||||
self.book.add_chapter(ch3)
|
||||
|
||||
# Generate TOC
|
||||
toc = self.book.generate_table_of_contents()
|
||||
|
||||
# Should only include chapters with titles
|
||||
self.assertEqual(len(toc), 2)
|
||||
|
||||
level, title, chapter = toc[0]
|
||||
self.assertEqual(title, "Introduction")
|
||||
self.assertEqual(chapter, ch1)
|
||||
|
||||
level, title, chapter = toc[1]
|
||||
self.assertEqual(title, "Conclusion")
|
||||
self.assertEqual(chapter, ch3)
|
||||
|
||||
def test_book_inherits_document_features(self):
|
||||
"""Test that Book inherits all Document functionality."""
|
||||
# Test that book can use all document methods
|
||||
# Add blocks directly to book
|
||||
para = Paragraph()
|
||||
self.book.add_block(para)
|
||||
self.assertEqual(len(self.book.blocks), 1)
|
||||
|
||||
# Test metadata
|
||||
self.book.set_metadata(MetadataType.PUBLISHER, "Test Publisher")
|
||||
self.assertEqual(self.book.get_metadata(MetadataType.PUBLISHER), "Test Publisher")
|
||||
|
||||
# Test anchors
|
||||
heading = Heading(HeadingLevel.H1)
|
||||
self.book.add_anchor("preface", heading)
|
||||
self.assertEqual(self.book.get_anchor("preface"), heading)
|
||||
|
||||
|
||||
class TestDocumentFontRegistry(unittest.TestCase):
|
||||
"""Test cases for Document font registry functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.doc = Document("Test Document", "en-US")
|
||||
|
||||
def test_get_or_create_font_creates_new_font(self):
|
||||
"""Test that get_or_create_font creates a new font when none exists."""
|
||||
font = self.doc.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
self.assertEqual(font.font_size, 14)
|
||||
self.assertEqual(font.colour, (255, 0, 0))
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
|
||||
# Check that font is stored in registry
|
||||
self.assertEqual(len(self.doc._fonts), 1)
|
||||
|
||||
def test_get_or_create_font_reuses_existing_font(self):
|
||||
"""Test that get_or_create_font reuses existing fonts."""
|
||||
# Create first font
|
||||
font1 = self.doc.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Create second font with same properties
|
||||
font2 = self.doc.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Should return the same font object
|
||||
self.assertIs(font1, font2)
|
||||
|
||||
# Should only have one font in registry
|
||||
self.assertEqual(len(self.doc._fonts), 1)
|
||||
|
||||
def test_get_or_create_font_creates_different_fonts(self):
|
||||
"""Test that different font properties create different fonts."""
|
||||
# Create first font
|
||||
font1 = self.doc.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Create font with different size
|
||||
font2 = self.doc.get_or_create_font(
|
||||
font_size=16,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Create font with different color
|
||||
font3 = self.doc.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(0, 255, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Create font with different weight
|
||||
font4 = self.doc.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.NORMAL
|
||||
)
|
||||
|
||||
# All should be different objects
|
||||
self.assertIsNot(font1, font2)
|
||||
self.assertIsNot(font1, font3)
|
||||
self.assertIsNot(font1, font4)
|
||||
self.assertIsNot(font2, font3)
|
||||
self.assertIsNot(font2, font4)
|
||||
self.assertIsNot(font3, font4)
|
||||
|
||||
# Should have four fonts in registry
|
||||
self.assertEqual(len(self.doc._fonts), 4)
|
||||
|
||||
def test_get_or_create_font_with_all_parameters(self):
|
||||
"""Test get_or_create_font with all parameters."""
|
||||
font = self.doc.get_or_create_font(
|
||||
font_path="path/to/font.ttf",
|
||||
font_size=18,
|
||||
colour=(128, 64, 192),
|
||||
weight=FontWeight.BOLD,
|
||||
style=FontStyle.ITALIC,
|
||||
decoration=TextDecoration.UNDERLINE,
|
||||
background=(255, 255, 255, 128),
|
||||
language="fr_FR",
|
||||
min_hyphenation_width=80
|
||||
)
|
||||
|
||||
self.assertEqual(font._font_path, "path/to/font.ttf")
|
||||
self.assertEqual(font.font_size, 18)
|
||||
self.assertEqual(font.colour, (128, 64, 192))
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(font.background, (255, 255, 255, 128))
|
||||
self.assertEqual(font.language, "fr_FR")
|
||||
self.assertEqual(font.min_hyphenation_width, 80)
|
||||
|
||||
def test_get_or_create_font_with_defaults(self):
|
||||
"""Test get_or_create_font with default values."""
|
||||
font = self.doc.get_or_create_font()
|
||||
|
||||
# Should create font with default values
|
||||
self.assertIsNotNone(font)
|
||||
self.assertEqual(font.font_size, 16) # Default font size
|
||||
self.assertEqual(font.colour, (0, 0, 0)) # Default black color
|
||||
self.assertEqual(font.weight, FontWeight.NORMAL)
|
||||
self.assertEqual(font.style, FontStyle.NORMAL)
|
||||
self.assertEqual(font.decoration, TextDecoration.NONE)
|
||||
|
||||
|
||||
class TestChapterFontRegistry(unittest.TestCase):
|
||||
"""Test cases for Chapter font registry functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.doc = Document("Test Document", "en-US")
|
||||
self.chapter = Chapter("Test Chapter", 1, parent=self.doc)
|
||||
|
||||
def test_chapter_uses_parent_font_registry(self):
|
||||
"""Test that chapter uses parent document's font registry."""
|
||||
# Create font through chapter - should delegate to parent
|
||||
font1 = self.chapter.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Create same font through document - should return same object
|
||||
font2 = self.doc.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Should be the same font object
|
||||
self.assertIs(font1, font2)
|
||||
|
||||
# Should be stored in document's registry, not chapter's
|
||||
self.assertEqual(len(self.doc._fonts), 1)
|
||||
self.assertEqual(len(self.chapter._fonts), 0)
|
||||
|
||||
def test_chapter_without_parent_manages_own_fonts(self):
|
||||
"""Test that chapter without parent manages its own fonts."""
|
||||
# Create chapter without parent
|
||||
standalone_chapter = Chapter("Standalone Chapter", 1)
|
||||
|
||||
# Create font through chapter
|
||||
font1 = standalone_chapter.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Create same font again - should reuse
|
||||
font2 = standalone_chapter.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Should be the same font object
|
||||
self.assertIs(font1, font2)
|
||||
|
||||
# Should be stored in chapter's own registry
|
||||
self.assertEqual(len(standalone_chapter._fonts), 1)
|
||||
|
||||
def test_chapter_parent_assignment(self):
|
||||
"""Test that chapter parent assignment works correctly."""
|
||||
# Create chapter with parent
|
||||
chapter_with_parent = Chapter("Chapter with Parent", 1, parent=self.doc)
|
||||
self.assertEqual(chapter_with_parent._parent, self.doc)
|
||||
|
||||
# Create chapter without parent
|
||||
chapter_without_parent = Chapter("Chapter without Parent", 1)
|
||||
self.assertIsNone(chapter_without_parent._parent)
|
||||
|
||||
|
||||
class TestBookFontRegistry(unittest.TestCase):
|
||||
"""Test cases for Book font registry functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.book = Book("Test Book", "Author Name", "en-US")
|
||||
|
||||
def test_book_inherits_document_font_registry(self):
|
||||
"""Test that Book inherits Document's font registry functionality."""
|
||||
# Create font through book
|
||||
font1 = self.book.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Create same font again - should reuse
|
||||
font2 = self.book.get_or_create_font(
|
||||
font_size=14,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Should be the same font object
|
||||
self.assertIs(font1, font2)
|
||||
|
||||
# Should have one font in registry
|
||||
self.assertEqual(len(self.book._fonts), 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,529 @@
|
||||
"""
|
||||
Unit tests for abstract functional elements.
|
||||
|
||||
Tests the Link, Button, Form, FormField, and related classes that handle
|
||||
interactive functionality and user interface elements.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
from pyWebLayout.abstract.functional import (
|
||||
Link, LinkType, Button, Form, FormField, FormFieldType
|
||||
)
|
||||
|
||||
|
||||
class TestLinkType(unittest.TestCase):
|
||||
"""Test cases for LinkType enum."""
|
||||
|
||||
def test_link_types(self):
|
||||
"""Test that all expected link types exist."""
|
||||
expected_types = ['INTERNAL', 'EXTERNAL', 'API', 'FUNCTION']
|
||||
|
||||
for type_name in expected_types:
|
||||
self.assertTrue(hasattr(LinkType, type_name))
|
||||
|
||||
# Test specific values
|
||||
self.assertEqual(LinkType.INTERNAL.value, 1)
|
||||
self.assertEqual(LinkType.EXTERNAL.value, 2)
|
||||
self.assertEqual(LinkType.API.value, 3)
|
||||
self.assertEqual(LinkType.FUNCTION.value, 4)
|
||||
|
||||
|
||||
class TestLink(unittest.TestCase):
|
||||
"""Test cases for Link class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_callback = Mock(return_value="callback_result")
|
||||
|
||||
def test_link_creation_minimal(self):
|
||||
"""Test link creation with minimal parameters."""
|
||||
link = Link("test-location")
|
||||
|
||||
self.assertEqual(link.location, "test-location")
|
||||
self.assertEqual(link.link_type, LinkType.INTERNAL) # Default
|
||||
self.assertEqual(link.params, {})
|
||||
self.assertIsNone(link.title)
|
||||
self.assertIsNone(link._callback)
|
||||
|
||||
def test_link_creation_full(self):
|
||||
"""Test link creation with all parameters."""
|
||||
params = {"param1": "value1", "param2": "value2"}
|
||||
link = Link(
|
||||
location="https://example.com",
|
||||
link_type=LinkType.EXTERNAL,
|
||||
callback=self.mock_callback,
|
||||
params=params,
|
||||
title="Example Link"
|
||||
)
|
||||
|
||||
self.assertEqual(link.location, "https://example.com")
|
||||
self.assertEqual(link.link_type, LinkType.EXTERNAL)
|
||||
self.assertEqual(link.params, params)
|
||||
self.assertEqual(link.title, "Example Link")
|
||||
self.assertEqual(link._callback, self.mock_callback)
|
||||
|
||||
def test_internal_link_execution(self):
|
||||
"""Test executing internal links."""
|
||||
link = Link("#section1", LinkType.INTERNAL)
|
||||
result = link.execute()
|
||||
|
||||
# Internal links should return the location
|
||||
self.assertEqual(result, "#section1")
|
||||
|
||||
def test_external_link_execution(self):
|
||||
"""Test executing external links."""
|
||||
link = Link("https://example.com", LinkType.EXTERNAL)
|
||||
result = link.execute()
|
||||
|
||||
# External links should return the location
|
||||
self.assertEqual(result, "https://example.com")
|
||||
|
||||
def test_api_link_execution(self):
|
||||
"""Test executing API links with callback."""
|
||||
params = {"action": "save", "id": 123}
|
||||
link = Link(
|
||||
location="/api/save",
|
||||
link_type=LinkType.API,
|
||||
callback=self.mock_callback,
|
||||
params=params
|
||||
)
|
||||
|
||||
result = link.execute()
|
||||
|
||||
# Should call callback with location and params
|
||||
self.mock_callback.assert_called_once_with("/api/save", action="save", id=123)
|
||||
self.assertEqual(result, "callback_result")
|
||||
|
||||
def test_function_link_execution(self):
|
||||
"""Test executing function links with callback."""
|
||||
params = {"data": "test"}
|
||||
link = Link(
|
||||
location="save_document",
|
||||
link_type=LinkType.FUNCTION,
|
||||
callback=self.mock_callback,
|
||||
params=params
|
||||
)
|
||||
|
||||
result = link.execute()
|
||||
|
||||
# Should call callback with location and params
|
||||
self.mock_callback.assert_called_once_with("save_document", data="test")
|
||||
self.assertEqual(result, "callback_result")
|
||||
|
||||
def test_api_link_without_callback(self):
|
||||
"""Test API link without callback returns location."""
|
||||
link = Link("/api/endpoint", LinkType.API)
|
||||
result = link.execute()
|
||||
|
||||
# Without callback, should return location
|
||||
self.assertEqual(result, "/api/endpoint")
|
||||
|
||||
def test_function_link_without_callback(self):
|
||||
"""Test function link without callback returns location."""
|
||||
link = Link("function_name", LinkType.FUNCTION)
|
||||
result = link.execute()
|
||||
|
||||
# Without callback, should return location
|
||||
self.assertEqual(result, "function_name")
|
||||
|
||||
def test_link_properties(self):
|
||||
"""Test link property access."""
|
||||
params = {"key": "value"}
|
||||
link = Link(
|
||||
location="test",
|
||||
link_type=LinkType.API,
|
||||
params=params,
|
||||
title="Test Title"
|
||||
)
|
||||
|
||||
# Test all property getters
|
||||
self.assertEqual(link.location, "test")
|
||||
self.assertEqual(link.link_type, LinkType.API)
|
||||
self.assertEqual(link.params, params)
|
||||
self.assertEqual(link.title, "Test Title")
|
||||
|
||||
|
||||
class TestButton(unittest.TestCase):
|
||||
"""Test cases for Button class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_callback = Mock(return_value="button_clicked")
|
||||
|
||||
def test_button_creation_minimal(self):
|
||||
"""Test button creation with minimal parameters."""
|
||||
button = Button("Click Me", self.mock_callback)
|
||||
|
||||
self.assertEqual(button.label, "Click Me")
|
||||
self.assertEqual(button._callback, self.mock_callback)
|
||||
self.assertEqual(button.params, {})
|
||||
self.assertTrue(button.enabled)
|
||||
|
||||
def test_button_creation_full(self):
|
||||
"""Test button creation with all parameters."""
|
||||
params = {"action": "submit", "form_id": "test_form"}
|
||||
button = Button(
|
||||
label="Submit",
|
||||
callback=self.mock_callback,
|
||||
params=params,
|
||||
enabled=False
|
||||
)
|
||||
|
||||
self.assertEqual(button.label, "Submit")
|
||||
self.assertEqual(button._callback, self.mock_callback)
|
||||
self.assertEqual(button.params, params)
|
||||
self.assertFalse(button.enabled)
|
||||
|
||||
def test_button_label_property(self):
|
||||
"""Test button label getter and setter."""
|
||||
button = Button("Original", self.mock_callback)
|
||||
|
||||
# Test getter
|
||||
self.assertEqual(button.label, "Original")
|
||||
|
||||
# Test setter
|
||||
button.label = "New Label"
|
||||
self.assertEqual(button.label, "New Label")
|
||||
|
||||
def test_button_enabled_property(self):
|
||||
"""Test button enabled getter and setter."""
|
||||
button = Button("Test", self.mock_callback, enabled=True)
|
||||
|
||||
# Test initial state
|
||||
self.assertTrue(button.enabled)
|
||||
|
||||
# Test setter
|
||||
button.enabled = False
|
||||
self.assertFalse(button.enabled)
|
||||
|
||||
button.enabled = True
|
||||
self.assertTrue(button.enabled)
|
||||
|
||||
def test_button_execute_enabled(self):
|
||||
"""Test executing enabled button."""
|
||||
params = {"data": "test_data"}
|
||||
button = Button("Test", self.mock_callback, params=params, enabled=True)
|
||||
|
||||
result = button.execute()
|
||||
|
||||
# Should call callback with params
|
||||
self.mock_callback.assert_called_once_with(data="test_data")
|
||||
self.assertEqual(result, "button_clicked")
|
||||
|
||||
def test_button_execute_disabled(self):
|
||||
"""Test executing disabled button."""
|
||||
button = Button("Test", self.mock_callback, enabled=False)
|
||||
|
||||
result = button.execute()
|
||||
|
||||
# Should not call callback and return None
|
||||
self.mock_callback.assert_not_called()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_button_execute_no_callback(self):
|
||||
"""Test executing button without callback."""
|
||||
button = Button("Test", None, enabled=True)
|
||||
|
||||
result = button.execute()
|
||||
|
||||
# Should return None
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestFormFieldType(unittest.TestCase):
|
||||
"""Test cases for FormFieldType enum."""
|
||||
|
||||
def test_form_field_types(self):
|
||||
"""Test that all expected form field types exist."""
|
||||
expected_types = [
|
||||
'TEXT', 'PASSWORD', 'CHECKBOX', 'RADIO', 'SELECT', 'TEXTAREA',
|
||||
'NUMBER', 'DATE', 'TIME', 'EMAIL', 'URL', 'COLOR', 'RANGE', 'HIDDEN'
|
||||
]
|
||||
|
||||
for type_name in expected_types:
|
||||
self.assertTrue(hasattr(FormFieldType, type_name))
|
||||
|
||||
# Test some specific values
|
||||
self.assertEqual(FormFieldType.TEXT.value, 1)
|
||||
self.assertEqual(FormFieldType.PASSWORD.value, 2)
|
||||
self.assertEqual(FormFieldType.HIDDEN.value, 14)
|
||||
|
||||
|
||||
class TestFormField(unittest.TestCase):
|
||||
"""Test cases for FormField class."""
|
||||
|
||||
def test_form_field_creation_minimal(self):
|
||||
"""Test form field creation with minimal parameters."""
|
||||
field = FormField("username", FormFieldType.TEXT)
|
||||
|
||||
self.assertEqual(field.name, "username")
|
||||
self.assertEqual(field.field_type, FormFieldType.TEXT)
|
||||
self.assertEqual(field.label, "username") # Default to name
|
||||
self.assertIsNone(field.value)
|
||||
self.assertFalse(field.required)
|
||||
self.assertEqual(field.options, [])
|
||||
self.assertIsNone(field.form)
|
||||
|
||||
def test_form_field_creation_full(self):
|
||||
"""Test form field creation with all parameters."""
|
||||
options = [("value1", "Label 1"), ("value2", "Label 2")]
|
||||
field = FormField(
|
||||
name="country",
|
||||
field_type=FormFieldType.SELECT,
|
||||
label="Country",
|
||||
value="value1",
|
||||
required=True,
|
||||
options=options
|
||||
)
|
||||
|
||||
self.assertEqual(field.name, "country")
|
||||
self.assertEqual(field.field_type, FormFieldType.SELECT)
|
||||
self.assertEqual(field.label, "Country")
|
||||
self.assertEqual(field.value, "value1")
|
||||
self.assertTrue(field.required)
|
||||
self.assertEqual(field.options, options)
|
||||
|
||||
def test_form_field_value_property(self):
|
||||
"""Test form field value getter and setter."""
|
||||
field = FormField("test", FormFieldType.TEXT, value="initial")
|
||||
|
||||
# Test getter
|
||||
self.assertEqual(field.value, "initial")
|
||||
|
||||
# Test setter
|
||||
field.value = "new_value"
|
||||
self.assertEqual(field.value, "new_value")
|
||||
|
||||
def test_form_field_form_property(self):
|
||||
"""Test form field form getter and setter."""
|
||||
field = FormField("test", FormFieldType.TEXT)
|
||||
mock_form = Mock()
|
||||
|
||||
# Initial state
|
||||
self.assertIsNone(field.form)
|
||||
|
||||
# Test setter
|
||||
field.form = mock_form
|
||||
self.assertEqual(field.form, mock_form)
|
||||
|
||||
def test_form_field_properties(self):
|
||||
"""Test all form field property getters."""
|
||||
options = [("opt1", "Option 1")]
|
||||
field = FormField(
|
||||
name="test_field",
|
||||
field_type=FormFieldType.CHECKBOX,
|
||||
label="Test Field",
|
||||
value=True,
|
||||
required=True,
|
||||
options=options
|
||||
)
|
||||
|
||||
# Test all getters
|
||||
self.assertEqual(field.name, "test_field")
|
||||
self.assertEqual(field.field_type, FormFieldType.CHECKBOX)
|
||||
self.assertEqual(field.label, "Test Field")
|
||||
self.assertTrue(field.value)
|
||||
self.assertTrue(field.required)
|
||||
self.assertEqual(field.options, options)
|
||||
|
||||
|
||||
class TestForm(unittest.TestCase):
|
||||
"""Test cases for Form class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_callback = Mock(return_value="form_submitted")
|
||||
|
||||
def test_form_creation_minimal(self):
|
||||
"""Test form creation with minimal parameters."""
|
||||
form = Form("test_form")
|
||||
|
||||
self.assertEqual(form.form_id, "test_form")
|
||||
self.assertIsNone(form.action)
|
||||
self.assertIsNone(form._callback)
|
||||
self.assertEqual(len(form._fields), 0)
|
||||
|
||||
def test_form_creation_full(self):
|
||||
"""Test form creation with all parameters."""
|
||||
form = Form(
|
||||
form_id="contact_form",
|
||||
action="/submit",
|
||||
callback=self.mock_callback
|
||||
)
|
||||
|
||||
self.assertEqual(form.form_id, "contact_form")
|
||||
self.assertEqual(form.action, "/submit")
|
||||
self.assertEqual(form._callback, self.mock_callback)
|
||||
|
||||
def test_form_field_management(self):
|
||||
"""Test adding and retrieving form fields."""
|
||||
form = Form("test_form")
|
||||
|
||||
# Create fields
|
||||
field1 = FormField("username", FormFieldType.TEXT, value="john")
|
||||
field2 = FormField("password", FormFieldType.PASSWORD, value="secret")
|
||||
field3 = FormField("email", FormFieldType.EMAIL, value="john@example.com")
|
||||
|
||||
# Add fields
|
||||
form.add_field(field1)
|
||||
form.add_field(field2)
|
||||
form.add_field(field3)
|
||||
|
||||
# Test that fields are stored correctly
|
||||
self.assertEqual(len(form._fields), 3)
|
||||
|
||||
# Test field retrieval
|
||||
self.assertEqual(form.get_field("username"), field1)
|
||||
self.assertEqual(form.get_field("password"), field2)
|
||||
self.assertEqual(form.get_field("email"), field3)
|
||||
self.assertIsNone(form.get_field("nonexistent"))
|
||||
|
||||
# Test that fields have form reference
|
||||
self.assertEqual(field1.form, form)
|
||||
self.assertEqual(field2.form, form)
|
||||
self.assertEqual(field3.form, form)
|
||||
|
||||
def test_form_get_values(self):
|
||||
"""Test getting form values."""
|
||||
form = Form("test_form")
|
||||
|
||||
# Add fields with values
|
||||
form.add_field(FormField("name", FormFieldType.TEXT, value="John Doe"))
|
||||
form.add_field(FormField("age", FormFieldType.NUMBER, value=30))
|
||||
form.add_field(FormField("subscribe", FormFieldType.CHECKBOX, value=True))
|
||||
|
||||
# Get values
|
||||
values = form.get_values()
|
||||
|
||||
expected = {
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"subscribe": True
|
||||
}
|
||||
|
||||
self.assertEqual(values, expected)
|
||||
|
||||
def test_form_get_values_empty(self):
|
||||
"""Test getting values from empty form."""
|
||||
form = Form("empty_form")
|
||||
values = form.get_values()
|
||||
|
||||
self.assertEqual(values, {})
|
||||
|
||||
def test_form_execute_with_callback(self):
|
||||
"""Test executing form with callback."""
|
||||
form = Form("test_form", callback=self.mock_callback)
|
||||
|
||||
# Add some fields
|
||||
form.add_field(FormField("field1", FormFieldType.TEXT, value="value1"))
|
||||
form.add_field(FormField("field2", FormFieldType.TEXT, value="value2"))
|
||||
|
||||
result = form.execute()
|
||||
|
||||
# Should call callback with form_id and values
|
||||
expected_values = {"field1": "value1", "field2": "value2"}
|
||||
self.mock_callback.assert_called_once_with("test_form", expected_values)
|
||||
self.assertEqual(result, "form_submitted")
|
||||
|
||||
def test_form_execute_without_callback(self):
|
||||
"""Test executing form without callback."""
|
||||
form = Form("test_form")
|
||||
|
||||
# Add a field
|
||||
form.add_field(FormField("test", FormFieldType.TEXT, value="test_value"))
|
||||
|
||||
result = form.execute()
|
||||
|
||||
# Should return the form values
|
||||
expected = {"test": "test_value"}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_form_properties(self):
|
||||
"""Test form property getters."""
|
||||
form = Form("test_form", action="/submit")
|
||||
|
||||
self.assertEqual(form.form_id, "test_form")
|
||||
self.assertEqual(form.action, "/submit")
|
||||
|
||||
|
||||
class TestFormIntegration(unittest.TestCase):
|
||||
"""Integration tests for form functionality."""
|
||||
|
||||
def test_complete_form_workflow(self):
|
||||
"""Test a complete form creation and submission workflow."""
|
||||
# Create form
|
||||
form = Form("registration_form", action="/register")
|
||||
|
||||
# Add various field types
|
||||
form.add_field(FormField(
|
||||
"username", FormFieldType.TEXT,
|
||||
label="Username", required=True, value="testuser"
|
||||
))
|
||||
|
||||
form.add_field(FormField(
|
||||
"password", FormFieldType.PASSWORD,
|
||||
label="Password", required=True, value="secret123"
|
||||
))
|
||||
|
||||
form.add_field(FormField(
|
||||
"email", FormFieldType.EMAIL,
|
||||
label="Email", required=True, value="test@example.com"
|
||||
))
|
||||
|
||||
form.add_field(FormField(
|
||||
"country", FormFieldType.SELECT,
|
||||
label="Country", value="US",
|
||||
options=[("US", "United States"), ("CA", "Canada"), ("UK", "United Kingdom")]
|
||||
))
|
||||
|
||||
form.add_field(FormField(
|
||||
"newsletter", FormFieldType.CHECKBOX,
|
||||
label="Subscribe to newsletter", value=True
|
||||
))
|
||||
|
||||
# Test form state
|
||||
self.assertEqual(len(form._fields), 5)
|
||||
|
||||
# Test individual field access
|
||||
username_field = form.get_field("username")
|
||||
self.assertEqual(username_field.value, "testuser")
|
||||
self.assertTrue(username_field.required)
|
||||
|
||||
# Test getting all values
|
||||
values = form.get_values()
|
||||
expected = {
|
||||
"username": "testuser",
|
||||
"password": "secret123",
|
||||
"email": "test@example.com",
|
||||
"country": "US",
|
||||
"newsletter": True
|
||||
}
|
||||
self.assertEqual(values, expected)
|
||||
|
||||
# Test form submission
|
||||
result = form.execute()
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_form_field_modification(self):
|
||||
"""Test modifying form fields after creation."""
|
||||
form = Form("test_form")
|
||||
|
||||
# Add field
|
||||
field = FormField("test", FormFieldType.TEXT, value="initial")
|
||||
form.add_field(field)
|
||||
|
||||
# Modify field value
|
||||
field.value = "modified"
|
||||
|
||||
# Test that form reflects the change
|
||||
values = form.get_values()
|
||||
self.assertEqual(values["test"], "modified")
|
||||
|
||||
# Test getting the modified field
|
||||
retrieved_field = form.get_field("test")
|
||||
self.assertEqual(retrieved_field.value, "modified")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,858 @@
|
||||
"""
|
||||
Unit tests for abstract inline elements.
|
||||
|
||||
Tests the Word and FormattedSpan classes that handle inline text elements
|
||||
and formatting within documents.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from pyWebLayout.abstract.inline import Word, FormattedSpan, LineBreak
|
||||
from pyWebLayout.style import Font
|
||||
|
||||
|
||||
class TestWord(unittest.TestCase):
|
||||
"""Test cases for Word class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.font = Font()
|
||||
# Note: Font background is a tuple (255, 255, 255, 0) by default
|
||||
# Note: Font language is set via constructor parameter (language - with typo)
|
||||
|
||||
def test_word_creation_minimal(self):
|
||||
"""Test word creation with minimal parameters."""
|
||||
word = Word("hello", self.font)
|
||||
|
||||
self.assertEqual(word.text, "hello")
|
||||
self.assertEqual(word.style, self.font)
|
||||
self.assertIsNone(word.previous)
|
||||
self.assertIsNone(word.next)
|
||||
self.assertEqual(len(word.possible_hyphenation()),0)
|
||||
|
||||
|
||||
def test_word_hyphenation(self):
|
||||
"""Test word creation with minimal parameters."""
|
||||
word = Word("amsterdam", self.font)
|
||||
|
||||
self.assertEqual(word.text, "amsterdam")
|
||||
self.assertEqual(word.style, self.font)
|
||||
self.assertIsNone(word.previous)
|
||||
self.assertIsNone(word.next)
|
||||
self.assertEqual(len(word.possible_hyphenation()),3)
|
||||
|
||||
def test_word_creation_with_previous(self):
|
||||
"""Test word creation with previous word reference."""
|
||||
word1 = Word("first", self.font)
|
||||
word2 = Word("second", self.font, previous=word1)
|
||||
|
||||
self.assertEqual(word2.previous, word1)
|
||||
self.assertIsNone(word1.previous)
|
||||
self.assertEqual(word1.next, word2)
|
||||
self.assertIsNone(word2.next)
|
||||
|
||||
def test_word_creation_with_background_override(self):
|
||||
"""Test word creation with background color override."""
|
||||
word = Word("test", self.font, background="yellow")
|
||||
|
||||
self.assertEqual(word.background, "yellow")
|
||||
# Original font background should be unchanged - it's a tuple
|
||||
self.assertEqual(word.style.background, (255, 255, 255, 0))
|
||||
|
||||
def test_word_properties(self):
|
||||
"""Test word property getters."""
|
||||
word1 = Word("first", self.font)
|
||||
word2 = Word("second", self.font, background="blue", previous=word1)
|
||||
|
||||
# Test all properties
|
||||
self.assertEqual(word2.text, "second")
|
||||
self.assertEqual(word2.style, self.font)
|
||||
self.assertEqual(word2.background, "blue")
|
||||
self.assertEqual(word2.previous, word1)
|
||||
self.assertIsNone(word2.next)
|
||||
|
||||
|
||||
def test_add_next_word(self):
|
||||
"""Test linking words with add_next method."""
|
||||
word1 = Word("first", self.font)
|
||||
word2 = Word("second", self.font)
|
||||
word3 = Word("third", self.font)
|
||||
|
||||
# Link words
|
||||
word1.add_next(word2)
|
||||
word2.add_next(word3)
|
||||
|
||||
# Test forward links
|
||||
self.assertEqual(word1.next, word2)
|
||||
self.assertEqual(word2.next, word3)
|
||||
self.assertIsNone(word3.next)
|
||||
|
||||
# Backward links should remain as set in constructor
|
||||
self.assertIsNone(word1.previous)
|
||||
self.assertIsNone(word2.previous)
|
||||
self.assertIsNone(word3.previous)
|
||||
|
||||
def test_word_chain(self):
|
||||
"""Test creating a chain of linked words."""
|
||||
word1 = Word("first", self.font)
|
||||
word2 = Word("second", self.font, previous=word1)
|
||||
word3 = Word("third", self.font, previous=word2)
|
||||
|
||||
|
||||
# Test complete chain
|
||||
self.assertIsNone(word1.previous)
|
||||
self.assertEqual(word1.next, word2)
|
||||
|
||||
self.assertEqual(word2.previous, word1)
|
||||
self.assertEqual(word2.next, word3)
|
||||
|
||||
self.assertEqual(word3.previous, word2)
|
||||
self.assertIsNone(word3.next)
|
||||
|
||||
|
||||
def test_word_create_and_add_to_with_style_override(self):
|
||||
"""Test Word.create_and_add_to with explicit style parameter."""
|
||||
# Create alternate font
|
||||
alt_font = Font()
|
||||
|
||||
# Create mock container
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.add_word = Mock()
|
||||
# Ensure _words doesn't interfere
|
||||
del mock_container._words
|
||||
|
||||
# Create word with style override
|
||||
word = Word.create_and_add_to("test", mock_container, style=alt_font)
|
||||
|
||||
# Test that word uses the override style, not container style
|
||||
self.assertEqual(word.style, alt_font)
|
||||
self.assertNotEqual(word.style, self.font)
|
||||
|
||||
def test_word_create_and_add_to_with_background_override(self):
|
||||
"""Test Word.create_and_add_to with explicit background parameter."""
|
||||
# Create mock container
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.background = "container_bg"
|
||||
mock_container.add_word = Mock()
|
||||
# Ensure _words doesn't interfere
|
||||
del mock_container._words
|
||||
|
||||
# Create word with background override
|
||||
word = Word.create_and_add_to("test", mock_container, background="override_bg")
|
||||
|
||||
# Test that word uses the override background
|
||||
self.assertEqual(word.background, "override_bg")
|
||||
|
||||
def test_word_create_and_add_to_inherit_container_background(self):
|
||||
"""Test Word.create_and_add_to inheriting background from container."""
|
||||
# Create mock container with background
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.background = "container_bg"
|
||||
mock_container.add_word = Mock()
|
||||
# Ensure _words doesn't interfere
|
||||
del mock_container._words
|
||||
|
||||
# Create word without background override
|
||||
word = Word.create_and_add_to("test", mock_container)
|
||||
|
||||
# Test that word inherits container background
|
||||
self.assertEqual(word.background, "container_bg")
|
||||
|
||||
def test_word_create_and_add_to_with_words_list_previous(self):
|
||||
"""Test Word.create_and_add_to linking with previous word from _words list."""
|
||||
# Create mock container with _words list
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.add_word = Mock()
|
||||
|
||||
# Create existing word and add to container's _words list
|
||||
existing_word = Word("previous", self.font)
|
||||
mock_container._words = [existing_word]
|
||||
|
||||
# Create new word
|
||||
word = Word.create_and_add_to("current", mock_container)
|
||||
|
||||
# Test that words are linked
|
||||
self.assertEqual(word.previous, existing_word)
|
||||
self.assertEqual(existing_word.next, word)
|
||||
|
||||
def test_word_create_and_add_to_with_words_method_previous(self):
|
||||
"""Test Word.create_and_add_to linking with previous word from words() method."""
|
||||
# Create a simple container that implements words() method
|
||||
class SimpleContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self.existing_word = Word("previous", font)
|
||||
|
||||
def words(self):
|
||||
yield ("span1", self.existing_word)
|
||||
|
||||
def add_word(self, word):
|
||||
pass # Simple implementation
|
||||
|
||||
container = SimpleContainer(self.font)
|
||||
|
||||
# Create new word
|
||||
word = Word.create_and_add_to("current", container)
|
||||
|
||||
# Test that words are linked
|
||||
self.assertEqual(word.previous, container.existing_word)
|
||||
self.assertEqual(container.existing_word.next, word)
|
||||
|
||||
def test_word_create_and_add_to_no_style_error(self):
|
||||
"""Test Word.create_and_add_to raises error when container has no style."""
|
||||
# Create container without style
|
||||
class BadContainer:
|
||||
def add_word(self, word):
|
||||
pass
|
||||
|
||||
container = BadContainer()
|
||||
|
||||
# Test that AttributeError is raised
|
||||
with self.assertRaises(AttributeError) as context:
|
||||
Word.create_and_add_to("test", container)
|
||||
|
||||
self.assertIn("must have a 'style' property", str(context.exception))
|
||||
|
||||
def test_word_create_and_add_to_no_add_word_error(self):
|
||||
"""Test Word.create_and_add_to raises error when container has no add_word method."""
|
||||
# Create container without add_word
|
||||
class BadContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
|
||||
container = BadContainer(self.font)
|
||||
|
||||
# Test that AttributeError is raised
|
||||
with self.assertRaises(AttributeError) as context:
|
||||
Word.create_and_add_to("test", container)
|
||||
|
||||
self.assertIn("must have an 'add_word' method", str(context.exception))
|
||||
|
||||
def test_word_create_and_add_to_parameter_inspection_word_object(self):
|
||||
"""Test Word.create_and_add_to with add_word method that expects Word object."""
|
||||
# Create container with add_word method that has 'word' parameter name
|
||||
class WordObjectContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self.added_words = []
|
||||
|
||||
def add_word(self, word): # Parameter named 'word' indicates it expects Word object
|
||||
self.added_words.append(word)
|
||||
|
||||
container = WordObjectContainer(self.font)
|
||||
|
||||
# Create and add word
|
||||
word = Word.create_and_add_to("test", container)
|
||||
|
||||
# Test that the Word object was passed to add_word
|
||||
self.assertEqual(len(container.added_words), 1)
|
||||
self.assertEqual(container.added_words[0], word)
|
||||
self.assertIsInstance(container.added_words[0], Word)
|
||||
|
||||
def test_word_create_and_add_to_parameter_inspection_word_obj(self):
|
||||
"""Test Word.create_and_add_to with add_word method that has 'word_obj' parameter."""
|
||||
class WordObjContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self.added_words = []
|
||||
|
||||
def add_word(self, word_obj): # Parameter named 'word_obj' indicates it expects Word object
|
||||
self.added_words.append(word_obj)
|
||||
|
||||
container = WordObjContainer(self.font)
|
||||
word = Word.create_and_add_to("test", container)
|
||||
|
||||
self.assertEqual(len(container.added_words), 1)
|
||||
self.assertEqual(container.added_words[0], word)
|
||||
|
||||
def test_word_create_and_add_to_parameter_inspection_word_object(self):
|
||||
"""Test Word.create_and_add_to with add_word method that has 'word_object' parameter."""
|
||||
class WordObjectContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self.added_words = []
|
||||
|
||||
def add_word(self, word_object): # Parameter named 'word_object' indicates it expects Word object
|
||||
self.added_words.append(word_object)
|
||||
|
||||
container = WordObjectContainer(self.font)
|
||||
word = Word.create_and_add_to("test", container)
|
||||
|
||||
self.assertEqual(len(container.added_words), 1)
|
||||
self.assertEqual(container.added_words[0], word)
|
||||
|
||||
def test_word_create_and_add_to_parameter_inspection_text_fallback_with_words_list(self):
|
||||
"""Test Word.create_and_add_to with add_word that expects text but container has _words list."""
|
||||
class TextExpectingContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self._words = [] # Has _words list
|
||||
self.add_word_calls = []
|
||||
|
||||
def add_word(self, text): # Parameter named 'text' suggests it expects string
|
||||
# This simulates FormattedSpan.add_word behavior
|
||||
self.add_word_calls.append(text)
|
||||
# In real FormattedSpan, this would create a Word internally
|
||||
|
||||
container = TextExpectingContainer(self.font)
|
||||
word = Word.create_and_add_to("test", container)
|
||||
|
||||
# Word should be added to _words list directly, not via add_word
|
||||
self.assertEqual(len(container._words), 1)
|
||||
self.assertEqual(container._words[0], word)
|
||||
# add_word should not have been called since it expects text
|
||||
self.assertEqual(len(container.add_word_calls), 0)
|
||||
|
||||
def test_word_create_and_add_to_parameter_inspection_fallback_without_words_list(self):
|
||||
"""Test Word.create_and_add_to fallback when container doesn't have _words list."""
|
||||
class TextExpectingContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
# No _words list
|
||||
self.added_words = []
|
||||
|
||||
def add_word(self, text): # Parameter suggests text but we'll pass Word as fallback
|
||||
self.added_words.append(text)
|
||||
|
||||
container = TextExpectingContainer(self.font)
|
||||
word = Word.create_and_add_to("test", container)
|
||||
|
||||
# Should fallback to calling add_word with Word object
|
||||
self.assertEqual(len(container.added_words), 1)
|
||||
self.assertEqual(container.added_words[0], word)
|
||||
|
||||
def test_word_create_and_add_to_no_parameters_edge_case(self):
|
||||
"""Test Word.create_and_add_to with add_word method that has no parameters."""
|
||||
class NoParamsContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self.add_word_called = False
|
||||
|
||||
def add_word(self): # No parameters - edge case
|
||||
self.add_word_called = True
|
||||
|
||||
container = NoParamsContainer(self.font)
|
||||
|
||||
# The current implementation will fail when calling add_word(word) with a no-parameter method
|
||||
with self.assertRaises(TypeError) as context:
|
||||
Word.create_and_add_to("test", container)
|
||||
|
||||
self.assertIn("takes 1 positional argument but 2 were given", str(context.exception))
|
||||
|
||||
def test_word_create_and_add_to_linking_behavior_with_existing_words(self):
|
||||
"""Test Word.create_and_add_to properly links with existing words in container."""
|
||||
# Create container with existing words
|
||||
class ContainerWithWords:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self._words = []
|
||||
# Add an existing word
|
||||
existing_word = Word("existing", font)
|
||||
self._words.append(existing_word)
|
||||
|
||||
def add_word(self, word):
|
||||
self._words.append(word)
|
||||
|
||||
container = ContainerWithWords(self.font)
|
||||
existing_word = container._words[0]
|
||||
|
||||
# Create new word
|
||||
new_word = Word.create_and_add_to("new", container)
|
||||
|
||||
# Test linking
|
||||
self.assertEqual(new_word.previous, existing_word)
|
||||
self.assertEqual(existing_word.next, new_word)
|
||||
self.assertEqual(len(container._words), 2)
|
||||
self.assertEqual(container._words[1], new_word)
|
||||
|
||||
def test_word_create_and_add_to_linking_behavior_with_words_method(self):
|
||||
"""Test Word.create_and_add_to properly links with words from container.words() method."""
|
||||
class ContainerWithWordsMethod:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
self.existing_word1 = Word("first", font)
|
||||
self.existing_word2 = Word("second", font)
|
||||
self.existing_word1.add_next(self.existing_word2)
|
||||
self.added_words = []
|
||||
|
||||
def words(self):
|
||||
yield ("span1", self.existing_word1)
|
||||
yield ("span2", self.existing_word2)
|
||||
|
||||
def add_word(self, word):
|
||||
self.added_words.append(word)
|
||||
|
||||
container = ContainerWithWordsMethod(self.font)
|
||||
|
||||
# Create new word
|
||||
new_word = Word.create_and_add_to("third", container)
|
||||
|
||||
# Should link to the last word returned by words() method
|
||||
self.assertEqual(new_word.previous, container.existing_word2)
|
||||
self.assertEqual(container.existing_word2.next, new_word)
|
||||
|
||||
def test_word_create_and_add_to_linking_behavior_empty_words_method(self):
|
||||
"""Test Word.create_and_add_to with empty words() method."""
|
||||
class EmptyWordsContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
|
||||
def words(self):
|
||||
# Empty iterator
|
||||
return iter([])
|
||||
|
||||
def add_word(self, word):
|
||||
pass
|
||||
|
||||
container = EmptyWordsContainer(self.font)
|
||||
|
||||
# Create word
|
||||
word = Word.create_and_add_to("test", container)
|
||||
|
||||
# Should have no previous word
|
||||
self.assertIsNone(word.previous)
|
||||
|
||||
def test_word_create_and_add_to_linking_behavior_words_method_exception(self):
|
||||
"""Test Word.create_and_add_to with words() method that raises exception."""
|
||||
class ExceptionWordsContainer:
|
||||
def __init__(self, font):
|
||||
self.style = font
|
||||
|
||||
def words(self):
|
||||
raise TypeError("Error in words method")
|
||||
|
||||
def add_word(self, word):
|
||||
pass
|
||||
|
||||
container = ExceptionWordsContainer(self.font)
|
||||
|
||||
# Create word - should handle exception gracefully
|
||||
word = Word.create_and_add_to("test", container)
|
||||
|
||||
# Should have no previous word due to exception
|
||||
self.assertIsNone(word.previous)
|
||||
|
||||
|
||||
class TestFormattedSpan(unittest.TestCase):
|
||||
"""Test cases for FormattedSpan class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.font = Font()
|
||||
# Font background is a tuple, not a string
|
||||
|
||||
def test_formatted_span_creation_minimal(self):
|
||||
"""Test formatted span creation with minimal parameters."""
|
||||
span = FormattedSpan(self.font)
|
||||
|
||||
self.assertEqual(span.style, self.font)
|
||||
self.assertEqual(span.background, self.font.background)
|
||||
self.assertEqual(len(span.words), 0)
|
||||
|
||||
def test_formatted_span_creation_with_background(self):
|
||||
"""Test formatted span creation with background override."""
|
||||
span = FormattedSpan(self.font, background="yellow")
|
||||
|
||||
self.assertEqual(span.style, self.font)
|
||||
self.assertEqual(span.background, "yellow")
|
||||
self.assertNotEqual(span.background, self.font.background)
|
||||
|
||||
def test_formatted_span_properties(self):
|
||||
"""Test formatted span property getters."""
|
||||
span = FormattedSpan(self.font, background="blue")
|
||||
|
||||
self.assertEqual(span.style, self.font)
|
||||
self.assertEqual(span.background, "blue")
|
||||
self.assertIsInstance(span.words, list)
|
||||
self.assertEqual(len(span.words), 0)
|
||||
|
||||
def test_add_single_word(self):
|
||||
"""Test adding a single word to formatted span."""
|
||||
span = FormattedSpan(self.font)
|
||||
|
||||
word = span.add_word("hello")
|
||||
|
||||
# Test returned word
|
||||
self.assertIsInstance(word, Word)
|
||||
self.assertEqual(word.text, "hello")
|
||||
self.assertEqual(word.style, self.font)
|
||||
self.assertEqual(word.background, self.font.background)
|
||||
self.assertIsNone(word.previous)
|
||||
|
||||
# Test span state
|
||||
self.assertEqual(len(span.words), 1)
|
||||
self.assertEqual(span.words[0], word)
|
||||
|
||||
def test_add_multiple_words(self):
|
||||
"""Test adding multiple words to formatted span."""
|
||||
span = FormattedSpan(self.font)
|
||||
|
||||
word1 = span.add_word("first")
|
||||
word2 = span.add_word("second")
|
||||
word3 = span.add_word("third")
|
||||
|
||||
# Test span contains all words
|
||||
self.assertEqual(len(span.words), 3)
|
||||
self.assertEqual(span.words[0], word1)
|
||||
self.assertEqual(span.words[1], word2)
|
||||
self.assertEqual(span.words[2], word3)
|
||||
|
||||
# Test word linking
|
||||
self.assertIsNone(word1.previous)
|
||||
self.assertEqual(word1.next, word2)
|
||||
|
||||
self.assertEqual(word2.previous, word1)
|
||||
self.assertEqual(word2.next, word3)
|
||||
|
||||
self.assertEqual(word3.previous, word2)
|
||||
self.assertIsNone(word3.next)
|
||||
|
||||
def test_add_word_with_background_override(self):
|
||||
"""Test that added words inherit span background."""
|
||||
span = FormattedSpan(self.font, background="red")
|
||||
|
||||
word = span.add_word("test")
|
||||
|
||||
# Word should inherit span's background
|
||||
self.assertEqual(word.background, "red")
|
||||
self.assertEqual(word.style, self.font)
|
||||
|
||||
def test_word_style_consistency(self):
|
||||
"""Test that all words in span have consistent style."""
|
||||
span = FormattedSpan(self.font, background="green")
|
||||
|
||||
words = []
|
||||
for text in ["this", "is", "a", "test"]:
|
||||
words.append(span.add_word(text))
|
||||
|
||||
# All words should have same style and background
|
||||
for word in words:
|
||||
self.assertEqual(word.style, self.font)
|
||||
self.assertEqual(word.background, "green")
|
||||
|
||||
def test_word_chain_integrity(self):
|
||||
"""Test that word chain is properly maintained."""
|
||||
span = FormattedSpan(self.font)
|
||||
|
||||
words = []
|
||||
for i in range(5):
|
||||
words.append(span.add_word(f"word{i}"))
|
||||
|
||||
# Test complete chain
|
||||
for i in range(5):
|
||||
word = words[i]
|
||||
|
||||
# Test previous link
|
||||
if i == 0:
|
||||
self.assertIsNone(word.previous)
|
||||
else:
|
||||
self.assertEqual(word.previous, words[i-1])
|
||||
|
||||
# Test next link
|
||||
if i == 4:
|
||||
self.assertIsNone(word.next)
|
||||
else:
|
||||
self.assertEqual(word.next, words[i+1])
|
||||
|
||||
def test_empty_span_operations(self):
|
||||
"""Test operations on empty formatted span."""
|
||||
span = FormattedSpan(self.font)
|
||||
|
||||
# Test empty state
|
||||
self.assertEqual(len(span.words), 0)
|
||||
self.assertEqual(span.words, [])
|
||||
|
||||
# Add first word
|
||||
first_word = span.add_word("first")
|
||||
self.assertIsNone(first_word.previous)
|
||||
self.assertIsNone(first_word.next)
|
||||
|
||||
def test_formatted_span_create_and_add_to_with_container_style(self):
|
||||
"""Test FormattedSpan.create_and_add_to with container that has style property."""
|
||||
# Create mock container with style and add_span method
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.add_span = Mock()
|
||||
# Remove background so it inherits from font
|
||||
del mock_container.background
|
||||
|
||||
# Create and add span
|
||||
span = FormattedSpan.create_and_add_to(mock_container)
|
||||
|
||||
# Test that span was created with correct properties
|
||||
self.assertIsInstance(span, FormattedSpan)
|
||||
self.assertEqual(span.style, self.font)
|
||||
self.assertEqual(span.background, self.font.background)
|
||||
|
||||
# Test that add_span was called
|
||||
mock_container.add_span.assert_called_once_with(span)
|
||||
|
||||
def test_formatted_span_create_and_add_to_with_style_override(self):
|
||||
"""Test FormattedSpan.create_and_add_to with explicit style parameter."""
|
||||
# Create alternate font
|
||||
alt_font = Font()
|
||||
|
||||
# Create mock container
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.add_span = Mock()
|
||||
|
||||
# Create span with style override
|
||||
span = FormattedSpan.create_and_add_to(mock_container, style=alt_font)
|
||||
|
||||
# Test that span uses the override style, not container style
|
||||
self.assertEqual(span.style, alt_font)
|
||||
self.assertNotEqual(span.style, self.font)
|
||||
|
||||
def test_formatted_span_create_and_add_to_with_background_override(self):
|
||||
"""Test FormattedSpan.create_and_add_to with explicit background parameter."""
|
||||
# Create mock container
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.background = "container_bg"
|
||||
mock_container.add_span = Mock()
|
||||
|
||||
# Create span with background override
|
||||
span = FormattedSpan.create_and_add_to(mock_container, background="override_bg")
|
||||
|
||||
# Test that span uses the override background
|
||||
self.assertEqual(span.background, "override_bg")
|
||||
|
||||
def test_formatted_span_create_and_add_to_inherit_container_background(self):
|
||||
"""Test FormattedSpan.create_and_add_to inheriting background from container."""
|
||||
# Create mock container with background
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
mock_container.background = "container_bg"
|
||||
mock_container.add_span = Mock()
|
||||
|
||||
# Create span without background override
|
||||
span = FormattedSpan.create_and_add_to(mock_container)
|
||||
|
||||
# Test that span inherits container background
|
||||
self.assertEqual(span.background, "container_bg")
|
||||
|
||||
def test_formatted_span_create_and_add_to_no_style_error(self):
|
||||
"""Test FormattedSpan.create_and_add_to raises error when container has no style."""
|
||||
# Create mock container without style
|
||||
mock_container = Mock()
|
||||
del mock_container.style
|
||||
mock_container.add_span = Mock()
|
||||
|
||||
# Test that AttributeError is raised
|
||||
with self.assertRaises(AttributeError) as context:
|
||||
FormattedSpan.create_and_add_to(mock_container)
|
||||
|
||||
self.assertIn("must have a 'style' property", str(context.exception))
|
||||
|
||||
def test_formatted_span_create_and_add_to_no_add_span_error(self):
|
||||
"""Test FormattedSpan.create_and_add_to raises error when container has no add_span method."""
|
||||
# Create mock container without add_span
|
||||
mock_container = Mock()
|
||||
mock_container.style = self.font
|
||||
del mock_container.add_span
|
||||
|
||||
# Test that AttributeError is raised
|
||||
with self.assertRaises(AttributeError) as context:
|
||||
FormattedSpan.create_and_add_to(mock_container)
|
||||
|
||||
self.assertIn("must have an 'add_span' method", str(context.exception))
|
||||
|
||||
|
||||
class TestWordFormattedSpanIntegration(unittest.TestCase):
|
||||
"""Integration tests between Word and FormattedSpan classes."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.font = Font()
|
||||
# Font background is a tuple, not a string
|
||||
|
||||
def test_sentence_creation(self):
|
||||
"""Test creating a complete sentence with formatted span."""
|
||||
span = FormattedSpan(self.font)
|
||||
|
||||
sentence_words = ["The", "quick", "brown", "fox", "jumps"]
|
||||
words = []
|
||||
|
||||
for word_text in sentence_words:
|
||||
words.append(span.add_word(word_text))
|
||||
|
||||
# Test sentence structure
|
||||
self.assertEqual(len(span.words), 5)
|
||||
|
||||
# Test word content
|
||||
for i, expected_text in enumerate(sentence_words):
|
||||
self.assertEqual(words[i].text, expected_text)
|
||||
|
||||
# Test linking
|
||||
for i in range(5):
|
||||
if i > 0:
|
||||
self.assertEqual(words[i].previous, words[i-1])
|
||||
if i < 4:
|
||||
self.assertEqual(words[i].next, words[i+1])
|
||||
|
||||
def test_multiple_spans_same_style(self):
|
||||
"""Test creating multiple spans with the same style."""
|
||||
font = Font()
|
||||
|
||||
span1 = FormattedSpan(font)
|
||||
span2 = FormattedSpan(font)
|
||||
|
||||
# Add words to both spans
|
||||
span1_words = [span1.add_word("First"), span1.add_word("span")]
|
||||
span2_words = [span2.add_word("Second"), span2.add_word("span")]
|
||||
|
||||
# Test that spans are independent
|
||||
self.assertEqual(len(span1.words), 2)
|
||||
self.assertEqual(len(span2.words), 2)
|
||||
|
||||
# Test that words in different spans are not linked
|
||||
self.assertIsNone(span1_words[1].next)
|
||||
self.assertIsNone(span2_words[0].previous)
|
||||
|
||||
# But words within spans are linked
|
||||
self.assertEqual(span1_words[0].next, span1_words[1])
|
||||
self.assertEqual(span2_words[1].previous, span2_words[0])
|
||||
|
||||
def test_span_style_inheritance(self):
|
||||
"""Test that words properly inherit span styling."""
|
||||
font = Font()
|
||||
# Font background is a tuple (255, 255, 255, 0)
|
||||
|
||||
# Test with span background override
|
||||
span = FormattedSpan(font, background="lightgreen")
|
||||
|
||||
word1 = span.add_word("styled")
|
||||
word2 = span.add_word("text")
|
||||
|
||||
# Both words should have span's background, not font's
|
||||
self.assertEqual(word1.background, "lightgreen")
|
||||
self.assertEqual(word2.background, "lightgreen")
|
||||
|
||||
# But they should have font's other properties
|
||||
self.assertEqual(word1.style, font)
|
||||
self.assertEqual(word2.style, font)
|
||||
|
||||
def test_word_modification_after_creation(self):
|
||||
"""Test modifying words after they've been added to span."""
|
||||
span = FormattedSpan(self.font)
|
||||
|
||||
word = span.add_word("original")
|
||||
|
||||
# Verify initial state
|
||||
self.assertEqual(word.text, "original")
|
||||
self.assertEqual(len(span.words), 1)
|
||||
|
||||
# Words are immutable by design (no setter for text property)
|
||||
# But we can test that the reference is maintained
|
||||
self.assertEqual(span.words[0], word)
|
||||
self.assertEqual(span.words[0].text, "original")
|
||||
|
||||
|
||||
class TestLineBreak(unittest.TestCase):
|
||||
"""Test cases for LineBreak class."""
|
||||
|
||||
def test_line_break_creation(self):
|
||||
"""Test line break creation."""
|
||||
line_break = LineBreak()
|
||||
|
||||
# Test initial state
|
||||
self.assertIsNotNone(line_break.block_type)
|
||||
self.assertIsNone(line_break.parent)
|
||||
|
||||
def test_line_break_block_type(self):
|
||||
"""Test line break block type property."""
|
||||
line_break = LineBreak()
|
||||
|
||||
# Import BlockType to verify the correct type
|
||||
from pyWebLayout.abstract.block import BlockType
|
||||
self.assertEqual(line_break.block_type, BlockType.LINE_BREAK)
|
||||
|
||||
def test_line_break_parent_property(self):
|
||||
"""Test line break parent property getter and setter."""
|
||||
line_break = LineBreak()
|
||||
|
||||
# Test initial state
|
||||
self.assertIsNone(line_break.parent)
|
||||
|
||||
# Test setter
|
||||
mock_parent = Mock()
|
||||
line_break.parent = mock_parent
|
||||
self.assertEqual(line_break.parent, mock_parent)
|
||||
|
||||
# Test setting to None
|
||||
line_break.parent = None
|
||||
self.assertIsNone(line_break.parent)
|
||||
|
||||
def test_line_break_create_and_add_to_with_add_line_break(self):
|
||||
"""Test LineBreak.create_and_add_to with container that has add_line_break method."""
|
||||
# Create mock container with add_line_break method
|
||||
mock_container = Mock()
|
||||
mock_container.add_line_break = Mock()
|
||||
|
||||
# Create and add line break
|
||||
line_break = LineBreak.create_and_add_to(mock_container)
|
||||
|
||||
# Test that line break was created
|
||||
self.assertIsInstance(line_break, LineBreak)
|
||||
|
||||
# Test that add_line_break was called
|
||||
mock_container.add_line_break.assert_called_once_with(line_break)
|
||||
|
||||
def test_line_break_create_and_add_to_with_add_element(self):
|
||||
"""Test LineBreak.create_and_add_to with container that has add_element method."""
|
||||
# Create mock container without add_line_break but with add_element
|
||||
mock_container = Mock()
|
||||
del mock_container.add_line_break # Ensure no add_line_break method
|
||||
mock_container.add_element = Mock()
|
||||
|
||||
# Create and add line break
|
||||
line_break = LineBreak.create_and_add_to(mock_container)
|
||||
|
||||
# Test that line break was created
|
||||
self.assertIsInstance(line_break, LineBreak)
|
||||
|
||||
# Test that add_element was called
|
||||
mock_container.add_element.assert_called_once_with(line_break)
|
||||
|
||||
def test_line_break_create_and_add_to_with_add_word(self):
|
||||
"""Test LineBreak.create_and_add_to with container that has add_word method."""
|
||||
# Create mock container with only add_word method
|
||||
mock_container = Mock()
|
||||
del mock_container.add_line_break # Ensure no add_line_break method
|
||||
del mock_container.add_element # Ensure no add_element method
|
||||
mock_container.add_word = Mock()
|
||||
|
||||
# Create and add line break
|
||||
line_break = LineBreak.create_and_add_to(mock_container)
|
||||
|
||||
# Test that line break was created
|
||||
self.assertIsInstance(line_break, LineBreak)
|
||||
|
||||
# Test that add_word was called
|
||||
mock_container.add_word.assert_called_once_with(line_break)
|
||||
|
||||
def test_line_break_create_and_add_to_fallback(self):
|
||||
"""Test LineBreak.create_and_add_to fallback behavior when no add methods available."""
|
||||
# Create mock container without any add methods
|
||||
mock_container = Mock()
|
||||
del mock_container.add_line_break
|
||||
del mock_container.add_element
|
||||
del mock_container.add_word
|
||||
|
||||
# Create and add line break
|
||||
line_break = LineBreak.create_and_add_to(mock_container)
|
||||
|
||||
# Test that line break was created
|
||||
self.assertIsInstance(line_break, LineBreak)
|
||||
|
||||
# Test that parent was set manually
|
||||
self.assertEqual(line_break.parent, mock_container)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user