auto flake and corrections
This commit is contained in:
@@ -12,9 +12,9 @@ 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
|
||||
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
|
||||
@@ -29,256 +29,256 @@ except ImportError:
|
||||
|
||||
class TestBlockElements(unittest.TestCase):
|
||||
"""Test cases for basic block elements."""
|
||||
|
||||
|
||||
def test_paragraph_creation(self):
|
||||
"""Test creating and using paragraphs."""
|
||||
paragraph = Paragraph()
|
||||
|
||||
|
||||
self.assertEqual(paragraph.block_type, BlockType.PARAGRAPH)
|
||||
self.assertEqual(paragraph.word_count, 0)
|
||||
self.assertIsNone(paragraph.parent)
|
||||
|
||||
|
||||
# Add words
|
||||
font = Font()
|
||||
word1 = Word("Hello", font)
|
||||
word2 = Word("World", font)
|
||||
|
||||
|
||||
paragraph.add_word(word1)
|
||||
paragraph.add_word(word2)
|
||||
|
||||
|
||||
self.assertEqual(paragraph.word_count, 2)
|
||||
|
||||
|
||||
# Test word iteration
|
||||
words = list(paragraph.words_iter())
|
||||
self.assertEqual(len(words), 2)
|
||||
self.assertEqual(words[0][1].text, "Hello")
|
||||
self.assertEqual(words[1][1].text, "World")
|
||||
|
||||
|
||||
def test_heading_levels(self):
|
||||
"""Test heading creation with different levels."""
|
||||
h1 = Heading(HeadingLevel.H1)
|
||||
h3 = Heading(HeadingLevel.H3)
|
||||
h6 = Heading(HeadingLevel.H6)
|
||||
|
||||
|
||||
self.assertEqual(h1.level, HeadingLevel.H1)
|
||||
self.assertEqual(h3.level, HeadingLevel.H3)
|
||||
self.assertEqual(h6.level, HeadingLevel.H6)
|
||||
|
||||
|
||||
self.assertEqual(h1.block_type, BlockType.HEADING)
|
||||
|
||||
|
||||
# Test level modification
|
||||
h1.level = HeadingLevel.H2
|
||||
self.assertEqual(h1.level, HeadingLevel.H2)
|
||||
|
||||
|
||||
def test_quote_nesting(self):
|
||||
"""Test blockquote with nested content."""
|
||||
quote = Quote()
|
||||
|
||||
|
||||
# Add nested paragraphs
|
||||
p1 = Paragraph()
|
||||
p2 = Paragraph()
|
||||
|
||||
|
||||
quote.add_block(p1)
|
||||
quote.add_block(p2)
|
||||
|
||||
|
||||
self.assertEqual(p1.parent, quote)
|
||||
self.assertEqual(p2.parent, quote)
|
||||
|
||||
|
||||
# Test block iteration
|
||||
blocks = list(quote.blocks())
|
||||
self.assertEqual(len(blocks), 2)
|
||||
self.assertEqual(blocks[0], p1)
|
||||
self.assertEqual(blocks[1], p2)
|
||||
|
||||
|
||||
def test_code_block(self):
|
||||
"""Test code block functionality."""
|
||||
code = CodeBlock("python")
|
||||
|
||||
|
||||
self.assertEqual(code.language, "python")
|
||||
self.assertEqual(code.line_count, 0)
|
||||
|
||||
|
||||
# Add code lines
|
||||
code.add_line("def hello():")
|
||||
code.add_line(" print('Hello!')")
|
||||
|
||||
|
||||
self.assertEqual(code.line_count, 2)
|
||||
|
||||
|
||||
# Test line iteration
|
||||
lines = list(code.lines())
|
||||
self.assertEqual(len(lines), 2)
|
||||
self.assertEqual(lines[0][1], "def hello():")
|
||||
self.assertEqual(lines[1][1], " print('Hello!')")
|
||||
|
||||
|
||||
# Test language modification
|
||||
code.language = "javascript"
|
||||
self.assertEqual(code.language, "javascript")
|
||||
|
||||
|
||||
def test_list_creation(self):
|
||||
"""Test list creation and item management."""
|
||||
# Unordered list
|
||||
ul = HList(ListStyle.UNORDERED)
|
||||
self.assertEqual(ul.style, ListStyle.UNORDERED)
|
||||
self.assertEqual(ul.item_count, 0)
|
||||
|
||||
|
||||
# Add list items
|
||||
item1 = ListItem()
|
||||
item2 = ListItem()
|
||||
|
||||
|
||||
ul.add_item(item1)
|
||||
ul.add_item(item2)
|
||||
|
||||
|
||||
self.assertEqual(ul.item_count, 2)
|
||||
self.assertEqual(item1.parent, ul)
|
||||
self.assertEqual(item2.parent, ul)
|
||||
|
||||
|
||||
# Test item iteration
|
||||
items = list(ul.items())
|
||||
self.assertEqual(len(items), 2)
|
||||
|
||||
|
||||
# Test list style change
|
||||
ul.style = ListStyle.ORDERED
|
||||
self.assertEqual(ul.style, ListStyle.ORDERED)
|
||||
|
||||
|
||||
def test_definition_list(self):
|
||||
"""Test definition list with terms."""
|
||||
dl = HList(ListStyle.DEFINITION)
|
||||
|
||||
|
||||
# Add definition items with terms
|
||||
dt1 = ListItem(term="Python")
|
||||
dt2 = ListItem(term="JavaScript")
|
||||
|
||||
|
||||
dl.add_item(dt1)
|
||||
dl.add_item(dt2)
|
||||
|
||||
|
||||
self.assertEqual(dt1.term, "Python")
|
||||
self.assertEqual(dt2.term, "JavaScript")
|
||||
|
||||
|
||||
# Test term modification
|
||||
dt1.term = "Python 3"
|
||||
self.assertEqual(dt1.term, "Python 3")
|
||||
|
||||
|
||||
def test_table_structure(self):
|
||||
"""Test table, row, and cell structure."""
|
||||
table = Table(caption="Test Table")
|
||||
|
||||
|
||||
self.assertEqual(table.caption, "Test Table")
|
||||
self.assertEqual(table.row_count["total"], 0)
|
||||
|
||||
|
||||
# Create rows and cells
|
||||
header_row = TableRow()
|
||||
data_row = TableRow()
|
||||
|
||||
|
||||
# Header cells
|
||||
h1 = TableCell(is_header=True)
|
||||
h2 = TableCell(is_header=True)
|
||||
header_row.add_cell(h1)
|
||||
header_row.add_cell(h2)
|
||||
|
||||
|
||||
# Data cells
|
||||
d1 = TableCell(is_header=False)
|
||||
d2 = TableCell(is_header=False, colspan=2)
|
||||
data_row.add_cell(d1)
|
||||
data_row.add_cell(d2)
|
||||
|
||||
|
||||
# Add rows to table
|
||||
table.add_row(header_row, "header")
|
||||
table.add_row(data_row, "body")
|
||||
|
||||
|
||||
# Test structure
|
||||
self.assertEqual(table.row_count["header"], 1)
|
||||
self.assertEqual(table.row_count["body"], 1)
|
||||
self.assertEqual(table.row_count["total"], 2)
|
||||
|
||||
|
||||
# Test cell properties
|
||||
self.assertTrue(h1.is_header)
|
||||
self.assertFalse(d1.is_header)
|
||||
self.assertEqual(d2.colspan, 2)
|
||||
self.assertEqual(d2.rowspan, 1) # Default
|
||||
|
||||
|
||||
# Test row cell count
|
||||
self.assertEqual(header_row.cell_count, 2)
|
||||
self.assertEqual(data_row.cell_count, 2)
|
||||
|
||||
|
||||
def test_table_sections(self):
|
||||
"""Test table header, body, and footer sections."""
|
||||
table = Table()
|
||||
|
||||
|
||||
# Add rows to different sections
|
||||
header = TableRow()
|
||||
body1 = TableRow()
|
||||
body2 = TableRow()
|
||||
footer = TableRow()
|
||||
|
||||
|
||||
table.add_row(header, "header")
|
||||
table.add_row(body1, "body")
|
||||
table.add_row(body2, "body")
|
||||
table.add_row(footer, "footer")
|
||||
|
||||
|
||||
# Test section iteration
|
||||
header_rows = list(table.header_rows())
|
||||
body_rows = list(table.body_rows())
|
||||
footer_rows = list(table.footer_rows())
|
||||
|
||||
|
||||
self.assertEqual(len(header_rows), 1)
|
||||
self.assertEqual(len(body_rows), 2)
|
||||
self.assertEqual(len(footer_rows), 1)
|
||||
|
||||
|
||||
# Test all_rows iteration
|
||||
all_rows = list(table.all_rows())
|
||||
self.assertEqual(len(all_rows), 4)
|
||||
|
||||
|
||||
# Check section labels
|
||||
sections = [section for section, row in all_rows]
|
||||
self.assertEqual(sections, ["header", "body", "body", "footer"])
|
||||
|
||||
|
||||
def test_image_loading(self):
|
||||
"""Test image element properties."""
|
||||
# Test with basic properties
|
||||
img = Image("test.jpg", "Test image", 100, 200)
|
||||
|
||||
|
||||
self.assertEqual(img.source, "test.jpg")
|
||||
self.assertEqual(img.alt_text, "Test image")
|
||||
self.assertEqual(img.width, 100)
|
||||
self.assertEqual(img.height, 200)
|
||||
|
||||
|
||||
# Test property modification
|
||||
img.source = "new.png"
|
||||
img.alt_text = "New image"
|
||||
img.width = 150
|
||||
img.height = 300
|
||||
|
||||
|
||||
self.assertEqual(img.source, "new.png")
|
||||
self.assertEqual(img.alt_text, "New image")
|
||||
self.assertEqual(img.width, 150)
|
||||
self.assertEqual(img.height, 300)
|
||||
|
||||
|
||||
# Test dimensions tuple
|
||||
self.assertEqual(img.get_dimensions(), (150, 300))
|
||||
|
||||
|
||||
def test_aspect_ratio_calculation(self):
|
||||
"""Test image aspect ratio calculations."""
|
||||
# Test with specified dimensions
|
||||
img = Image("test.jpg", width=400, height=200)
|
||||
self.assertEqual(img.get_aspect_ratio(), 2.0) # 400/200
|
||||
|
||||
|
||||
# Test with only one dimension
|
||||
img2 = Image("test.jpg", width=300)
|
||||
self.assertIsNone(img2.get_aspect_ratio()) # No height specified
|
||||
|
||||
|
||||
# Test scaled dimensions
|
||||
scaled = img.calculate_scaled_dimensions(max_width=200, max_height=150)
|
||||
# Should scale down proportionally
|
||||
self.assertEqual(scaled[0], 200) # Width limited by max_width
|
||||
self.assertEqual(scaled[1], 100) # Height scaled proportionally
|
||||
|
||||
|
||||
def test_simple_elements(self):
|
||||
"""Test simple block elements."""
|
||||
hr = HorizontalRule()
|
||||
br = LineBreak()
|
||||
|
||||
|
||||
self.assertEqual(hr.block_type, BlockType.HORIZONTAL_RULE)
|
||||
self.assertEqual(br.block_type, BlockType.LINE_BREAK)
|
||||
|
||||
|
||||
# These elements have no additional properties
|
||||
self.assertIsNone(hr.parent)
|
||||
self.assertIsNone(br.parent)
|
||||
@@ -286,29 +286,29 @@ class TestBlockElements(unittest.TestCase):
|
||||
|
||||
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."""
|
||||
@@ -316,17 +316,17 @@ class TestImagePIL(unittest.TestCase):
|
||||
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")
|
||||
@@ -334,17 +334,17 @@ class TestImagePIL(unittest.TestCase):
|
||||
# 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."""
|
||||
@@ -365,7 +365,7 @@ class TestImagePIL(unittest.TestCase):
|
||||
|
||||
def run_flask():
|
||||
cls.flask_app.run(host='127.0.0.1', port=cls.flask_port, debug=False,
|
||||
use_reloader=False, threaded=True)
|
||||
use_reloader=False, threaded=True)
|
||||
|
||||
cls.flask_thread = threading.Thread(target=run_flask, daemon=True)
|
||||
cls.flask_thread.start()
|
||||
@@ -384,120 +384,120 @@ class TestImagePIL(unittest.TestCase):
|
||||
pass
|
||||
time.sleep(wait_interval)
|
||||
elapsed += wait_interval
|
||||
|
||||
|
||||
def test_image_url_detection(self):
|
||||
"""Test URL detection functionality."""
|
||||
img = Image()
|
||||
|
||||
|
||||
# Test URL detection
|
||||
self.assertTrue(img._is_url("http://example.com/image.jpg"))
|
||||
self.assertTrue(img._is_url("https://example.com/image.png"))
|
||||
self.assertTrue(img._is_url("ftp://example.com/image.gif"))
|
||||
|
||||
|
||||
# Test non-URL detection
|
||||
self.assertFalse(img._is_url("image.jpg"))
|
||||
self.assertFalse(img._is_url("/path/to/image.png"))
|
||||
self.assertFalse(img._is_url("../relative/path.gif"))
|
||||
self.assertFalse(img._is_url(""))
|
||||
|
||||
|
||||
def test_load_local_image_jpg(self):
|
||||
"""Test loading local JPG image."""
|
||||
img = Image(self.jpg_path)
|
||||
|
||||
|
||||
file_path, pil_img = img.load_image_data()
|
||||
|
||||
|
||||
self.assertIsNotNone(pil_img)
|
||||
self.assertEqual(file_path, self.jpg_path)
|
||||
self.assertEqual(pil_img.size, self.original_size)
|
||||
self.assertEqual(img.width, self.original_size[0])
|
||||
self.assertEqual(img.height, self.original_size[1])
|
||||
|
||||
|
||||
def test_load_local_image_png(self):
|
||||
"""Test loading local PNG image."""
|
||||
img = Image(self.png_path)
|
||||
|
||||
|
||||
file_path, pil_img = img.load_image_data()
|
||||
|
||||
|
||||
self.assertIsNotNone(pil_img)
|
||||
self.assertEqual(file_path, self.png_path)
|
||||
self.assertEqual(pil_img.size, self.original_size)
|
||||
|
||||
|
||||
def test_load_local_image_bmp(self):
|
||||
"""Test loading local BMP image."""
|
||||
img = Image(self.bmp_path)
|
||||
|
||||
|
||||
file_path, pil_img = img.load_image_data()
|
||||
|
||||
|
||||
self.assertIsNotNone(pil_img)
|
||||
self.assertEqual(file_path, self.bmp_path)
|
||||
self.assertEqual(pil_img.size, self.original_size)
|
||||
|
||||
|
||||
def test_load_local_image_gif(self):
|
||||
"""Test loading local GIF image."""
|
||||
img = Image(self.gif_path)
|
||||
|
||||
|
||||
file_path, pil_img = img.load_image_data()
|
||||
|
||||
|
||||
self.assertIsNotNone(pil_img)
|
||||
self.assertEqual(file_path, self.gif_path)
|
||||
self.assertEqual(pil_img.size, self.original_size)
|
||||
|
||||
|
||||
def test_load_nonexistent_image(self):
|
||||
"""Test loading non-existent image."""
|
||||
img = Image("nonexistent.jpg")
|
||||
|
||||
|
||||
file_path, pil_img = img.load_image_data()
|
||||
|
||||
|
||||
self.assertIsNone(pil_img)
|
||||
self.assertIsNone(file_path)
|
||||
|
||||
|
||||
def test_load_empty_source(self):
|
||||
"""Test loading with empty source."""
|
||||
img = Image("")
|
||||
|
||||
|
||||
file_path, pil_img = img.load_image_data()
|
||||
|
||||
|
||||
self.assertIsNone(pil_img)
|
||||
self.assertIsNone(file_path)
|
||||
|
||||
|
||||
def test_auto_update_dimensions(self):
|
||||
"""Test automatic dimension updating."""
|
||||
img = Image(self.jpg_path, width=50, height=50) # Wrong initial dimensions
|
||||
|
||||
|
||||
# Test with auto-update enabled (default)
|
||||
file_path, pil_img = img.load_image_data(auto_update_dimensions=True)
|
||||
|
||||
|
||||
self.assertEqual(img.width, self.original_size[0])
|
||||
self.assertEqual(img.height, self.original_size[1])
|
||||
|
||||
|
||||
def test_no_auto_update_dimensions(self):
|
||||
"""Test loading without automatic dimension updating."""
|
||||
original_width, original_height = 50, 50
|
||||
img = Image(self.jpg_path, width=original_width, height=original_height)
|
||||
|
||||
|
||||
# Test with auto-update disabled
|
||||
file_path, pil_img = img.load_image_data(auto_update_dimensions=False)
|
||||
|
||||
|
||||
self.assertEqual(img.width, original_width) # Should remain unchanged
|
||||
self.assertEqual(img.height, original_height) # Should remain unchanged
|
||||
|
||||
|
||||
def test_get_image_info(self):
|
||||
"""Test getting detailed image information."""
|
||||
img = Image(self.jpg_path)
|
||||
|
||||
|
||||
info = img.get_image_info()
|
||||
|
||||
|
||||
self.assertIsInstance(info, dict)
|
||||
self.assertIn('format', info)
|
||||
self.assertIn('mode', info)
|
||||
self.assertIn('size', info)
|
||||
self.assertIn('width', info)
|
||||
self.assertIn('height', info)
|
||||
|
||||
|
||||
self.assertEqual(info['size'], self.original_size)
|
||||
self.assertEqual(info['width'], self.original_size[0])
|
||||
self.assertEqual(info['height'], self.original_size[1])
|
||||
|
||||
|
||||
def test_get_image_info_different_formats(self):
|
||||
"""Test getting image info for different formats."""
|
||||
formats_and_paths = [
|
||||
@@ -506,80 +506,80 @@ class TestImagePIL(unittest.TestCase):
|
||||
('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)
|
||||
|
||||
@@ -8,13 +8,13 @@ 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.abstract.inline import Word
|
||||
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 = [
|
||||
@@ -22,135 +22,141 @@ class TestMetadataType(unittest.TestCase):
|
||||
'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"])
|
||||
|
||||
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
|
||||
@@ -158,87 +164,87 @@ class TestDocument(unittest.TestCase):
|
||||
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
|
||||
@@ -248,127 +254,127 @@ class TestDocument(unittest.TestCase):
|
||||
|
||||
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
|
||||
@@ -377,13 +383,13 @@ class TestBook(unittest.TestCase):
|
||||
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
|
||||
@@ -392,20 +398,20 @@ class TestBook(unittest.TestCase):
|
||||
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),
|
||||
@@ -414,38 +420,38 @@ class TestBook(unittest.TestCase):
|
||||
(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
|
||||
@@ -453,11 +459,14 @@ class TestBook(unittest.TestCase):
|
||||
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")
|
||||
|
||||
self.assertEqual(
|
||||
self.book.get_metadata(
|
||||
MetadataType.PUBLISHER),
|
||||
"Test Publisher")
|
||||
|
||||
# Test anchors
|
||||
heading = Heading(HeadingLevel.H1)
|
||||
self.book.add_anchor("preface", heading)
|
||||
@@ -466,11 +475,11 @@ class TestBook(unittest.TestCase):
|
||||
|
||||
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(
|
||||
@@ -478,14 +487,14 @@ class TestDocumentFontRegistry(unittest.TestCase):
|
||||
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
|
||||
@@ -494,20 +503,20 @@ class TestDocumentFontRegistry(unittest.TestCase):
|
||||
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
|
||||
@@ -516,28 +525,28 @@ class TestDocumentFontRegistry(unittest.TestCase):
|
||||
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)
|
||||
@@ -545,10 +554,10 @@ class TestDocumentFontRegistry(unittest.TestCase):
|
||||
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(
|
||||
@@ -562,7 +571,7 @@ class TestDocumentFontRegistry(unittest.TestCase):
|
||||
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))
|
||||
@@ -572,11 +581,11 @@ class TestDocumentFontRegistry(unittest.TestCase):
|
||||
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
|
||||
@@ -588,12 +597,12 @@ class TestDocumentFontRegistry(unittest.TestCase):
|
||||
|
||||
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
|
||||
@@ -602,52 +611,52 @@ class TestChapterFontRegistry(unittest.TestCase):
|
||||
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)
|
||||
@@ -655,11 +664,11 @@ class TestChapterFontRegistry(unittest.TestCase):
|
||||
|
||||
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
|
||||
@@ -668,17 +677,17 @@ class TestBookFontRegistry(unittest.TestCase):
|
||||
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)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ interactive functionality and user interface elements.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock
|
||||
from pyWebLayout.abstract.functional import (
|
||||
Link, LinkType, Button, Form, FormField, FormFieldType
|
||||
)
|
||||
@@ -14,14 +14,14 @@ from pyWebLayout.abstract.functional import (
|
||||
|
||||
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)
|
||||
@@ -31,21 +31,21 @@ class TestLinkType(unittest.TestCase):
|
||||
|
||||
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"}
|
||||
@@ -56,29 +56,29 @@ class TestLink(unittest.TestCase):
|
||||
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}
|
||||
@@ -92,9 +92,10 @@ class TestLink(unittest.TestCase):
|
||||
result = link.execute()
|
||||
|
||||
# Should call callback with location, point (None when not provided), and params
|
||||
self.mock_callback.assert_called_once_with("/api/save", None, action="save", id=123)
|
||||
self.mock_callback.assert_called_once_with(
|
||||
"/api/save", None, action="save", id=123)
|
||||
self.assertEqual(result, "callback_result")
|
||||
|
||||
|
||||
def test_function_link_execution(self):
|
||||
"""Test executing function links with callback."""
|
||||
params = {"data": "test"}
|
||||
@@ -110,23 +111,23 @@ class TestLink(unittest.TestCase):
|
||||
# Should call callback with location, point (None when not provided), and params
|
||||
self.mock_callback.assert_called_once_with("save_document", None, 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"}
|
||||
@@ -136,7 +137,7 @@ class TestLink(unittest.TestCase):
|
||||
params=params,
|
||||
title="Test Title"
|
||||
)
|
||||
|
||||
|
||||
# Test all property getters
|
||||
self.assertEqual(link.location, "test")
|
||||
self.assertEqual(link.link_type, LinkType.API)
|
||||
@@ -146,20 +147,20 @@ class TestLink(unittest.TestCase):
|
||||
|
||||
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"}
|
||||
@@ -169,37 +170,37 @@ class TestButton(unittest.TestCase):
|
||||
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"}
|
||||
@@ -210,40 +211,40 @@ class TestButton(unittest.TestCase):
|
||||
# Should call callback with point (None when not provided) and params
|
||||
self.mock_callback.assert_called_once_with(None, 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)
|
||||
@@ -252,11 +253,11 @@ class TestFormFieldType(unittest.TestCase):
|
||||
|
||||
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
|
||||
@@ -264,7 +265,7 @@ class TestFormField(unittest.TestCase):
|
||||
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")]
|
||||
@@ -276,37 +277,37 @@ class TestFormField(unittest.TestCase):
|
||||
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")]
|
||||
@@ -318,7 +319,7 @@ class TestFormField(unittest.TestCase):
|
||||
required=True,
|
||||
options=options
|
||||
)
|
||||
|
||||
|
||||
# Test all getters
|
||||
self.assertEqual(field.name, "test_field")
|
||||
self.assertEqual(field.field_type, FormFieldType.CHECKBOX)
|
||||
@@ -330,20 +331,20 @@ class TestFormField(unittest.TestCase):
|
||||
|
||||
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(
|
||||
@@ -351,145 +352,144 @@ class TestForm(unittest.TestCase):
|
||||
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,
|
||||
"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(
|
||||
"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 = {
|
||||
@@ -500,26 +500,26 @@ class TestFormIntegration(unittest.TestCase):
|
||||
"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")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,9 @@ class TestChapterFontRegistry(FontRegistryTestMixin, unittest.TestCase):
|
||||
return Chapter("Test Chapter", level=1)
|
||||
|
||||
|
||||
class TestChapterFontRegistryParentDelegation(FontRegistryParentDelegationTestMixin, unittest.TestCase):
|
||||
class TestChapterFontRegistryParentDelegation(
|
||||
FontRegistryParentDelegationTestMixin,
|
||||
unittest.TestCase):
|
||||
"""Test FontRegistry parent delegation for Chapter - simplified with mixin."""
|
||||
|
||||
def create_parent(self):
|
||||
|
||||
@@ -3,20 +3,20 @@ Unit tests for LinkedWord and LinkedImage classes.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pyWebLayout.abstract.inline import Word, LinkedWord
|
||||
from pyWebLayout.abstract.block import Image, LinkedImage
|
||||
from pyWebLayout.abstract.inline import LinkedWord
|
||||
from pyWebLayout.abstract.block import LinkedImage
|
||||
from pyWebLayout.abstract.functional import LinkType
|
||||
from pyWebLayout.style import Font
|
||||
|
||||
|
||||
class TestLinkedWord(unittest.TestCase):
|
||||
"""Test cases for LinkedWord class."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.font = Font(font_size=16)
|
||||
self.location = "https://example.com"
|
||||
|
||||
|
||||
def test_linked_word_creation(self):
|
||||
"""Test creating a LinkedWord."""
|
||||
linked_word = LinkedWord(
|
||||
@@ -25,12 +25,12 @@ class TestLinkedWord(unittest.TestCase):
|
||||
location=self.location,
|
||||
link_type=LinkType.EXTERNAL
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(linked_word.text, "example")
|
||||
self.assertEqual(linked_word.location, self.location)
|
||||
self.assertEqual(linked_word.link_type, LinkType.EXTERNAL)
|
||||
self.assertIsNone(linked_word.link_callback)
|
||||
|
||||
|
||||
def test_linked_word_inherits_from_word(self):
|
||||
"""Test that LinkedWord inherits Word properties."""
|
||||
linked_word = LinkedWord(
|
||||
@@ -38,21 +38,21 @@ class TestLinkedWord(unittest.TestCase):
|
||||
style=self.font,
|
||||
location=self.location
|
||||
)
|
||||
|
||||
|
||||
# Should have Word properties
|
||||
self.assertEqual(linked_word.text, "test")
|
||||
self.assertEqual(linked_word.style, self.font)
|
||||
self.assertIsNone(linked_word.previous)
|
||||
self.assertIsNone(linked_word.next)
|
||||
|
||||
|
||||
def test_linked_word_with_callback(self):
|
||||
"""Test LinkedWord with a callback function."""
|
||||
callback_called = []
|
||||
|
||||
|
||||
def test_callback(location, **params):
|
||||
callback_called.append((location, params))
|
||||
return "navigated"
|
||||
|
||||
|
||||
linked_word = LinkedWord(
|
||||
text="click",
|
||||
style=self.font,
|
||||
@@ -61,15 +61,15 @@ class TestLinkedWord(unittest.TestCase):
|
||||
callback=test_callback,
|
||||
params={"source": "test"}
|
||||
)
|
||||
|
||||
result = linked_word.execute_link()
|
||||
|
||||
|
||||
linked_word.execute_link()
|
||||
|
||||
self.assertEqual(len(callback_called), 1)
|
||||
self.assertEqual(callback_called[0][0], self.location)
|
||||
self.assertIn("text", callback_called[0][1])
|
||||
self.assertEqual(callback_called[0][1]["text"], "click")
|
||||
self.assertEqual(callback_called[0][1]["source"], "test")
|
||||
|
||||
|
||||
def test_linked_word_execute_external_link(self):
|
||||
"""Test executing an external link returns the location."""
|
||||
linked_word = LinkedWord(
|
||||
@@ -78,10 +78,10 @@ class TestLinkedWord(unittest.TestCase):
|
||||
location=self.location,
|
||||
link_type=LinkType.EXTERNAL
|
||||
)
|
||||
|
||||
|
||||
result = linked_word.execute_link()
|
||||
self.assertEqual(result, self.location)
|
||||
|
||||
|
||||
def test_linked_word_with_title(self):
|
||||
"""Test LinkedWord with title/tooltip."""
|
||||
linked_word = LinkedWord(
|
||||
@@ -90,9 +90,9 @@ class TestLinkedWord(unittest.TestCase):
|
||||
location=self.location,
|
||||
title="Click to visit example.com"
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(linked_word.link_title, "Click to visit example.com")
|
||||
|
||||
|
||||
def test_linked_word_chain(self):
|
||||
"""Test chaining multiple LinkedWords."""
|
||||
word1 = LinkedWord(
|
||||
@@ -100,14 +100,14 @@ class TestLinkedWord(unittest.TestCase):
|
||||
style=self.font,
|
||||
location=self.location
|
||||
)
|
||||
|
||||
|
||||
word2 = LinkedWord(
|
||||
text="here",
|
||||
style=self.font,
|
||||
location=self.location,
|
||||
previous=word1
|
||||
)
|
||||
|
||||
|
||||
# Check chain
|
||||
self.assertEqual(word1.next, word2)
|
||||
self.assertEqual(word2.previous, word1)
|
||||
@@ -115,13 +115,13 @@ class TestLinkedWord(unittest.TestCase):
|
||||
|
||||
class TestLinkedImage(unittest.TestCase):
|
||||
"""Test cases for LinkedImage class."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.source = "logo.png"
|
||||
self.alt_text = "Company Logo"
|
||||
self.location = "https://example.com/home"
|
||||
|
||||
|
||||
def test_linked_image_creation(self):
|
||||
"""Test creating a LinkedImage."""
|
||||
linked_image = LinkedImage(
|
||||
@@ -132,14 +132,14 @@ class TestLinkedImage(unittest.TestCase):
|
||||
height=50,
|
||||
link_type=LinkType.EXTERNAL
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(linked_image.source, self.source)
|
||||
self.assertEqual(linked_image.alt_text, self.alt_text)
|
||||
self.assertEqual(linked_image.location, self.location)
|
||||
self.assertEqual(linked_image.width, 100)
|
||||
self.assertEqual(linked_image.height, 50)
|
||||
self.assertEqual(linked_image.link_type, LinkType.EXTERNAL)
|
||||
|
||||
|
||||
def test_linked_image_inherits_from_image(self):
|
||||
"""Test that LinkedImage inherits Image properties."""
|
||||
linked_image = LinkedImage(
|
||||
@@ -147,20 +147,20 @@ class TestLinkedImage(unittest.TestCase):
|
||||
alt_text=self.alt_text,
|
||||
location=self.location
|
||||
)
|
||||
|
||||
|
||||
# Should have Image properties and methods
|
||||
self.assertEqual(linked_image.source, self.source)
|
||||
self.assertEqual(linked_image.alt_text, self.alt_text)
|
||||
self.assertIsNotNone(linked_image.get_dimensions)
|
||||
|
||||
|
||||
def test_linked_image_with_callback(self):
|
||||
"""Test LinkedImage with a callback function."""
|
||||
callback_called = []
|
||||
|
||||
|
||||
def image_callback(location, **params):
|
||||
callback_called.append((location, params))
|
||||
return "image_clicked"
|
||||
|
||||
|
||||
linked_image = LinkedImage(
|
||||
source=self.source,
|
||||
alt_text=self.alt_text,
|
||||
@@ -168,15 +168,15 @@ class TestLinkedImage(unittest.TestCase):
|
||||
link_type=LinkType.FUNCTION,
|
||||
callback=image_callback
|
||||
)
|
||||
|
||||
result = linked_image.execute_link()
|
||||
|
||||
|
||||
linked_image.execute_link()
|
||||
|
||||
self.assertEqual(len(callback_called), 1)
|
||||
self.assertEqual(callback_called[0][0], self.location)
|
||||
self.assertIn("alt_text", callback_called[0][1])
|
||||
self.assertEqual(callback_called[0][1]["alt_text"], self.alt_text)
|
||||
self.assertIn("source", callback_called[0][1])
|
||||
|
||||
|
||||
def test_linked_image_execute_internal_link(self):
|
||||
"""Test executing an internal link returns the location."""
|
||||
linked_image = LinkedImage(
|
||||
@@ -185,7 +185,7 @@ class TestLinkedImage(unittest.TestCase):
|
||||
location="#section2",
|
||||
link_type=LinkType.INTERNAL
|
||||
)
|
||||
|
||||
|
||||
result = linked_image.execute_link()
|
||||
self.assertEqual(result, "#section2")
|
||||
|
||||
|
||||
@@ -5,22 +5,24 @@ Tests the various alignment handlers (Left, Center, Right, Justify) and their in
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.concrete.text import Line, Text, LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
|
||||
from pyWebLayout.concrete.text import (
|
||||
Line, Text, LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
|
||||
)
|
||||
from pyWebLayout.style import Alignment
|
||||
from pyWebLayout.style import Font
|
||||
from pyWebLayout.abstract import Word
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
class TestAlignmentHandlers(unittest.TestCase):
|
||||
"""Test cases for the alignment handler system"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font()
|
||||
self.test_words = [Word(text, self.font) for text in ["This", "is", "a", "test", "sentence"]]
|
||||
self.test_words = [Word(text, self.font)
|
||||
for text in ["This", "is", "a", "test", "sentence"]]
|
||||
self.line_width = 300
|
||||
self.line_height = 30
|
||||
self.spacing = (5, 20) # min_spacing, max_spacing
|
||||
@@ -29,46 +31,78 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
|
||||
# Create a real PIL image (canvas) for testing
|
||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||
|
||||
|
||||
# Create a real ImageDraw object
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
# Create a real Font object
|
||||
self.style = Font()
|
||||
|
||||
|
||||
def test_left_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns LeftAlignmentHandler for LEFT alignment"""
|
||||
left_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.LEFT)
|
||||
|
||||
left_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT)
|
||||
|
||||
self.assertIsInstance(left_line._alignment_handler, LeftAlignmentHandler)
|
||||
|
||||
|
||||
def test_center_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns CenterRightAlignmentHandler for CENTER alignment"""
|
||||
center_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.CENTER)
|
||||
|
||||
self.assertIsInstance(center_line._alignment_handler, CenterRightAlignmentHandler)
|
||||
center_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.CENTER)
|
||||
|
||||
self.assertIsInstance(
|
||||
center_line._alignment_handler,
|
||||
CenterRightAlignmentHandler)
|
||||
# Check that it's configured for CENTER alignment
|
||||
self.assertEqual(center_line._alignment_handler._alignment, Alignment.CENTER)
|
||||
|
||||
|
||||
def test_right_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns CenterRightAlignmentHandler for RIGHT alignment"""
|
||||
right_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.RIGHT)
|
||||
|
||||
self.assertIsInstance(right_line._alignment_handler, CenterRightAlignmentHandler)
|
||||
right_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.RIGHT)
|
||||
|
||||
self.assertIsInstance(
|
||||
right_line._alignment_handler,
|
||||
CenterRightAlignmentHandler)
|
||||
# Check that it's configured for RIGHT alignment
|
||||
self.assertEqual(right_line._alignment_handler._alignment, Alignment.RIGHT)
|
||||
|
||||
|
||||
def test_justify_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns JustifyAlignmentHandler for JUSTIFY alignment"""
|
||||
justify_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.JUSTIFY)
|
||||
|
||||
justify_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.JUSTIFY)
|
||||
|
||||
self.assertIsInstance(justify_line._alignment_handler, JustifyAlignmentHandler)
|
||||
|
||||
|
||||
def test_left_alignment_word_addition(self):
|
||||
"""Test adding words to a left-aligned line"""
|
||||
left_line = Line(self.spacing, self.origin, self.size, self.draw, halign=Alignment.LEFT)
|
||||
|
||||
left_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
halign=Alignment.LEFT)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -78,15 +112,21 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(left_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_center_alignment_word_addition(self):
|
||||
"""Test adding words to a center-aligned line"""
|
||||
center_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.CENTER)
|
||||
|
||||
center_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.CENTER)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -96,15 +136,21 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(center_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_right_alignment_word_addition(self):
|
||||
"""Test adding words to a right-aligned line"""
|
||||
right_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.RIGHT)
|
||||
|
||||
right_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.RIGHT)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -114,15 +160,21 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(right_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_justify_alignment_word_addition(self):
|
||||
"""Test adding words to a justified line"""
|
||||
justify_line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=Alignment.JUSTIFY)
|
||||
|
||||
justify_line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=Alignment.JUSTIFY)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
@@ -132,16 +184,17 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(justify_line.text_objects), words_added)
|
||||
|
||||
|
||||
def test_handler_spacing_and_position_calculations(self):
|
||||
"""Test spacing and position calculations for different alignment handlers"""
|
||||
# Create sample text objects
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
# Test each handler type
|
||||
handlers = [
|
||||
("Left", LeftAlignmentHandler()),
|
||||
@@ -149,108 +202,132 @@ class TestAlignmentHandlers(unittest.TestCase):
|
||||
("Right", CenterRightAlignmentHandler(Alignment.RIGHT)),
|
||||
("Justify", JustifyAlignmentHandler())
|
||||
]
|
||||
|
||||
|
||||
for name, handler in handlers:
|
||||
with self.subTest(handler=name):
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Check that spacing is a valid number
|
||||
self.assertIsInstance(spacing_calc, (int, float))
|
||||
self.assertGreaterEqual(spacing_calc, 0)
|
||||
|
||||
|
||||
# Check that position is a valid number
|
||||
self.assertIsInstance(position, (int, float))
|
||||
self.assertGreaterEqual(position, 0)
|
||||
|
||||
|
||||
# Check that overflow is a boolean
|
||||
self.assertIsInstance(overflow, bool)
|
||||
|
||||
|
||||
# Position should be within line width (unless overflow)
|
||||
if not overflow:
|
||||
self.assertLessEqual(position, self.line_width)
|
||||
|
||||
|
||||
def test_left_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for left alignment"""
|
||||
handler = LeftAlignmentHandler()
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Left alignment should have position at 0
|
||||
self.assertEqual(position, 0)
|
||||
|
||||
|
||||
# Should not overflow with reasonable text
|
||||
self.assertFalse(overflow)
|
||||
|
||||
|
||||
def test_center_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for center alignment"""
|
||||
handler = CenterRightAlignmentHandler(Alignment.CENTER)
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Center alignment should have position > 0 (centered) if no overflow
|
||||
if not overflow:
|
||||
self.assertGreaterEqual(position, 0)
|
||||
|
||||
|
||||
def test_right_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for right alignment"""
|
||||
handler = CenterRightAlignmentHandler(Alignment.RIGHT)
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Right alignment should have position >= 0
|
||||
self.assertGreaterEqual(position, 0)
|
||||
|
||||
|
||||
def test_justify_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for justify alignment"""
|
||||
handler = JustifyAlignmentHandler()
|
||||
text_objects = [Text(word, self.style, self.draw) for word in ["Hello", "World"]]
|
||||
|
||||
text_objects = [Text(word, self.style, self.draw)
|
||||
for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position, overflow = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
|
||||
# Justify alignment should have position at 0
|
||||
self.assertEqual(position, 0)
|
||||
|
||||
|
||||
# Check spacing is reasonable
|
||||
self.assertGreaterEqual(spacing_calc, 0)
|
||||
|
||||
|
||||
def test_empty_line_alignment_handlers(self):
|
||||
"""Test alignment handlers with empty lines"""
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
alignments = [
|
||||
Alignment.LEFT,
|
||||
Alignment.CENTER,
|
||||
Alignment.RIGHT,
|
||||
Alignment.JUSTIFY]
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=alignment)
|
||||
|
||||
line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=alignment)
|
||||
|
||||
# Empty line should still have a handler
|
||||
self.assertIsNotNone(line._alignment_handler)
|
||||
|
||||
|
||||
# Should be able to render empty line
|
||||
line.render()
|
||||
|
||||
|
||||
def test_single_word_line_alignment(self):
|
||||
"""Test alignment handlers with single word lines"""
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
alignments = [
|
||||
Alignment.LEFT,
|
||||
Alignment.CENTER,
|
||||
Alignment.RIGHT,
|
||||
Alignment.JUSTIFY]
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
line = Line(self.spacing, self.origin, self.size, self.draw, font=self.style, halign=alignment)
|
||||
|
||||
line = Line(
|
||||
self.spacing,
|
||||
self.origin,
|
||||
self.size,
|
||||
self.draw,
|
||||
font=self.style,
|
||||
halign=alignment)
|
||||
|
||||
# Create a test word
|
||||
test_word = Word("test", self.style)
|
||||
|
||||
|
||||
# Add a single word
|
||||
result, part = line.add_word(test_word)
|
||||
self.assertTrue(result) # Should fit
|
||||
self.assertIsNone(part) # No overflow part
|
||||
|
||||
|
||||
# Should be able to render single word line
|
||||
line.render()
|
||||
self.assertEqual(len(line.text_objects), 1)
|
||||
|
||||
@@ -6,7 +6,7 @@ Tests the Box class which handles basic box model rendering with alignment.
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.concrete.box import Box
|
||||
from pyWebLayout.style import Alignment
|
||||
@@ -14,17 +14,17 @@ from pyWebLayout.style import Alignment
|
||||
|
||||
class TestBox(unittest.TestCase):
|
||||
"""Test cases for the Box class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.origin = (10, 20)
|
||||
self.size = (100, 50)
|
||||
self.callback = Mock()
|
||||
|
||||
|
||||
def test_box_initialization_basic(self):
|
||||
"""Test basic box initialization"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(box._origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(box._size, np.array([100, 50]))
|
||||
np.testing.assert_array_equal(box._end, np.array([110, 70]))
|
||||
@@ -33,66 +33,65 @@ class TestBox(unittest.TestCase):
|
||||
self.assertIsNone(box._mode)
|
||||
self.assertEqual(box._halign, Alignment.CENTER)
|
||||
self.assertEqual(box._valign, Alignment.CENTER)
|
||||
|
||||
|
||||
def test_box_initialization_with_callback(self):
|
||||
"""Test box initialization with callback"""
|
||||
box = Box(self.origin, self.size, callback=self.callback)
|
||||
|
||||
|
||||
self.assertEqual(box._callback, self.callback)
|
||||
|
||||
|
||||
def test_box_initialization_with_sheet(self):
|
||||
"""Test box initialization with image sheet"""
|
||||
sheet = Image.new('RGBA', (200, 100), (255, 255, 255, 255))
|
||||
box = Box(self.origin, self.size, sheet=sheet)
|
||||
|
||||
|
||||
self.assertEqual(box._sheet, sheet)
|
||||
self.assertEqual(box._mode, 'RGBA')
|
||||
|
||||
|
||||
def test_box_initialization_with_mode(self):
|
||||
"""Test box initialization with explicit mode"""
|
||||
box = Box(self.origin, self.size, mode='RGB')
|
||||
|
||||
|
||||
self.assertEqual(box._mode, 'RGB')
|
||||
|
||||
|
||||
def test_box_initialization_with_alignment(self):
|
||||
"""Test box initialization with custom alignment"""
|
||||
box = Box(self.origin, self.size, halign=Alignment.LEFT, valign=Alignment.TOP)
|
||||
|
||||
|
||||
self.assertEqual(box._halign, Alignment.LEFT)
|
||||
self.assertEqual(box._valign, Alignment.TOP)
|
||||
|
||||
|
||||
def test_in_shape_point_inside(self):
|
||||
"""Test in_shape method with point inside box"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
# Test point inside
|
||||
self.assertTrue(box.in_shape(np.array([50, 40])))
|
||||
self.assertTrue(box.in_shape(np.array([10, 20]))) # Top-left corner
|
||||
self.assertTrue(box.in_shape(np.array([109, 69]))) # Just inside bottom-right
|
||||
|
||||
|
||||
def test_in_shape_point_outside(self):
|
||||
"""Test in_shape method with point outside box"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
# Test points outside
|
||||
self.assertFalse(box.in_shape(np.array([5, 15]))) # Before origin
|
||||
self.assertFalse(box.in_shape(np.array([110, 70]))) # At end (exclusive)
|
||||
self.assertFalse(box.in_shape(np.array([150, 100]))) # Far outside
|
||||
|
||||
self.assertFalse(box.in_shape(np.array([110, 70]))) # At end (exclusive)
|
||||
self.assertFalse(box.in_shape(np.array([150, 100]))) # Far outside
|
||||
|
||||
def test_in_shape_multiple_points(self):
|
||||
"""Test in_shape method with array of points"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
|
||||
points = np.array([[50, 40], [5, 15], [109, 69], [110, 70]])
|
||||
result = box.in_shape(points)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(result, [True, False, True, False])
|
||||
|
||||
|
||||
def test_properties_access(self):
|
||||
"""Test that properties can be accessed correctly"""
|
||||
box = Box(self.origin, self.size, callback=self.callback)
|
||||
|
||||
|
||||
# Test that origin property works (should be available via inheritance)
|
||||
np.testing.assert_array_equal(box._origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(box._size, np.array([100, 50]))
|
||||
|
||||
@@ -5,23 +5,21 @@ Tests the LinkText, ButtonText, and FormFieldText classes.
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from pyWebLayout.concrete.functional import (
|
||||
LinkText, ButtonText, FormFieldText,
|
||||
LinkText, ButtonText, FormFieldText,
|
||||
create_link_text, create_button_text, create_form_field_text
|
||||
)
|
||||
from pyWebLayout.abstract.functional import (
|
||||
Link, Button, Form, FormField, LinkType, FormFieldType
|
||||
Link, Button, FormField, LinkType, FormFieldType
|
||||
)
|
||||
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||
from pyWebLayout.style import Alignment
|
||||
from pyWebLayout.style import Font, TextDecoration
|
||||
|
||||
|
||||
class TestLinkText(unittest.TestCase):
|
||||
"""Test cases for the LinkText class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
@@ -30,86 +28,89 @@ class TestLinkText(unittest.TestCase):
|
||||
colour=(0, 0, 0)
|
||||
)
|
||||
self.callback = Mock()
|
||||
|
||||
|
||||
# Create different types of links
|
||||
self.internal_link = Link("chapter1", LinkType.INTERNAL, self.callback)
|
||||
self.external_link = Link("https://example.com", LinkType.EXTERNAL, self.callback)
|
||||
self.external_link = Link(
|
||||
"https://example.com",
|
||||
LinkType.EXTERNAL,
|
||||
self.callback)
|
||||
self.api_link = Link("/api/settings", LinkType.API, self.callback)
|
||||
self.function_link = Link("toggle_theme", LinkType.FUNCTION, self.callback)
|
||||
|
||||
|
||||
# Create a mock ImageDraw.Draw object
|
||||
self.mock_draw = Mock()
|
||||
|
||||
|
||||
def test_link_text_initialization_internal(self):
|
||||
"""Test initialization of internal link text"""
|
||||
link_text = "Go to Chapter 1"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.internal_link)
|
||||
self.assertEqual(renderable.text, link_text)
|
||||
self.assertFalse(renderable._hovered)
|
||||
self.assertEqual(renderable._callback, self.internal_link.execute)
|
||||
|
||||
|
||||
# Check that the font has underline decoration and blue color
|
||||
self.assertEqual(renderable.style.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(renderable.style.colour, (0, 0, 200))
|
||||
|
||||
|
||||
def test_link_text_initialization_external(self):
|
||||
"""Test initialization of external link text"""
|
||||
link_text = "Visit Example"
|
||||
renderable = LinkText(self.external_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.external_link)
|
||||
# External links should have darker blue color
|
||||
self.assertEqual(renderable.style.colour, (0, 0, 180))
|
||||
|
||||
|
||||
def test_link_text_initialization_api(self):
|
||||
"""Test initialization of API link text"""
|
||||
link_text = "Settings"
|
||||
renderable = LinkText(self.api_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.api_link)
|
||||
# API links should have red color
|
||||
self.assertEqual(renderable.style.colour, (150, 0, 0))
|
||||
|
||||
|
||||
def test_link_text_initialization_function(self):
|
||||
"""Test initialization of function link text"""
|
||||
link_text = "Toggle Theme"
|
||||
renderable = LinkText(self.function_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._link, self.function_link)
|
||||
# Function links should have green color
|
||||
self.assertEqual(renderable.style.colour, (0, 120, 0))
|
||||
|
||||
|
||||
def test_link_property(self):
|
||||
"""Test link property accessor"""
|
||||
link_text = "Test Link"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable.link, self.internal_link)
|
||||
|
||||
|
||||
def test_set_hovered(self):
|
||||
"""Test setting hover state"""
|
||||
link_text = "Hover Test"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(True)
|
||||
self.assertTrue(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(False)
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
def test_render_normal_state(self):
|
||||
"""Test rendering in normal state"""
|
||||
link_text = "Test Link"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Parent render should be called
|
||||
mock_parent_render.assert_called_once()
|
||||
# Should not draw highlight when not hovered
|
||||
@@ -120,21 +121,25 @@ class TestLinkText(unittest.TestCase):
|
||||
link_text = "Test Link"
|
||||
renderable = LinkText(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
# Mock width property
|
||||
|
||||
# Mock width property
|
||||
renderable._width = 80
|
||||
|
||||
|
||||
# Point inside link
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside link
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_factory_function(self):
|
||||
"""Test the create_link_text factory function"""
|
||||
link_text = "Factory Test"
|
||||
renderable = create_link_text(self.internal_link, link_text, self.font, self.mock_draw)
|
||||
|
||||
renderable = create_link_text(
|
||||
self.internal_link,
|
||||
link_text,
|
||||
self.font,
|
||||
self.mock_draw)
|
||||
|
||||
self.assertIsInstance(renderable, LinkText)
|
||||
self.assertEqual(renderable.text, link_text)
|
||||
self.assertEqual(renderable.link, self.internal_link)
|
||||
@@ -142,7 +147,7 @@ class TestLinkText(unittest.TestCase):
|
||||
|
||||
class TestButtonText(unittest.TestCase):
|
||||
"""Test cases for the ButtonText class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
@@ -153,112 +158,114 @@ class TestButtonText(unittest.TestCase):
|
||||
self.callback = Mock()
|
||||
self.button = Button("Click Me", self.callback)
|
||||
self.mock_draw = Mock()
|
||||
|
||||
|
||||
def test_button_text_initialization(self):
|
||||
"""Test basic button text initialization"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._button, self.button)
|
||||
self.assertEqual(renderable.text, "Click Me")
|
||||
self.assertFalse(renderable._pressed)
|
||||
self.assertFalse(renderable._hovered)
|
||||
self.assertEqual(renderable._callback, self.button.execute)
|
||||
self.assertEqual(renderable._padding, (4, 8, 4, 8))
|
||||
|
||||
|
||||
def test_button_text_with_custom_padding(self):
|
||||
"""Test button text initialization with custom padding"""
|
||||
custom_padding = (8, 12, 8, 12)
|
||||
|
||||
|
||||
renderable = ButtonText(
|
||||
self.button, self.font, self.mock_draw,
|
||||
padding=custom_padding
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(renderable._padding, custom_padding)
|
||||
|
||||
|
||||
def test_button_property(self):
|
||||
"""Test button property accessor"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable.button, self.button)
|
||||
|
||||
|
||||
def test_set_pressed(self):
|
||||
"""Test setting pressed state"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._pressed)
|
||||
|
||||
|
||||
renderable.set_pressed(True)
|
||||
self.assertTrue(renderable._pressed)
|
||||
|
||||
|
||||
renderable.set_pressed(False)
|
||||
self.assertFalse(renderable._pressed)
|
||||
|
||||
|
||||
def test_set_hovered(self):
|
||||
"""Test setting hover state"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(True)
|
||||
self.assertTrue(renderable._hovered)
|
||||
|
||||
|
||||
renderable.set_hovered(False)
|
||||
self.assertFalse(renderable._hovered)
|
||||
|
||||
|
||||
def test_size_property(self):
|
||||
"""Test size property includes padding"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# The size should be padded size, not just text size
|
||||
# Since we handle mocks in __init__, use the padded values directly
|
||||
expected_width = renderable._padded_width
|
||||
expected_height = renderable._padded_height
|
||||
|
||||
np.testing.assert_array_equal(renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
|
||||
np.testing.assert_array_equal(
|
||||
renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
def test_render_normal_state(self):
|
||||
"""Test rendering in normal state"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should draw rounded rectangle for button background
|
||||
self.mock_draw.rounded_rectangle.assert_called_once()
|
||||
# Parent render should be called for text
|
||||
mock_parent_render.assert_called_once()
|
||||
|
||||
|
||||
def test_render_disabled_state(self):
|
||||
"""Test rendering disabled button"""
|
||||
disabled_button = Button("Disabled", self.callback, enabled=False)
|
||||
renderable = ButtonText(disabled_button, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should still draw button background
|
||||
self.mock_draw.rounded_rectangle.assert_called_once()
|
||||
mock_parent_render.assert_called_once()
|
||||
|
||||
|
||||
def test_in_object_with_padding(self):
|
||||
"""Test in_object method considers padding"""
|
||||
renderable = ButtonText(self.button, self.font, self.mock_draw)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Point inside button (including padding)
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside button
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_factory_function(self):
|
||||
"""Test the create_button_text factory function"""
|
||||
custom_padding = (6, 10, 6, 10)
|
||||
renderable = create_button_text(self.button, self.font, self.mock_draw, custom_padding)
|
||||
|
||||
renderable = create_button_text(
|
||||
self.button, self.font, self.mock_draw, custom_padding)
|
||||
|
||||
self.assertIsInstance(renderable, ButtonText)
|
||||
self.assertEqual(renderable.text, "Click Me")
|
||||
self.assertEqual(renderable.button, self.button)
|
||||
@@ -267,7 +274,7 @@ class TestButtonText(unittest.TestCase):
|
||||
|
||||
class TestFormFieldText(unittest.TestCase):
|
||||
"""Test cases for the FormFieldText class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
@@ -275,96 +282,102 @@ class TestFormFieldText(unittest.TestCase):
|
||||
font_size=12,
|
||||
colour=(0, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
# Create different types of form fields
|
||||
self.text_field = FormField("username", FormFieldType.TEXT, "Username")
|
||||
self.password_field = FormField("password", FormFieldType.PASSWORD, "Password")
|
||||
self.textarea_field = FormField("description", FormFieldType.TEXTAREA, "Description")
|
||||
self.textarea_field = FormField(
|
||||
"description", FormFieldType.TEXTAREA, "Description")
|
||||
self.select_field = FormField("country", FormFieldType.SELECT, "Country")
|
||||
|
||||
|
||||
self.mock_draw = Mock()
|
||||
|
||||
|
||||
def test_form_field_text_initialization(self):
|
||||
"""Test initialization of form field text"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable._field, self.text_field)
|
||||
self.assertEqual(renderable.text, "Username")
|
||||
self.assertFalse(renderable._focused)
|
||||
self.assertEqual(renderable._field_height, 24)
|
||||
|
||||
|
||||
def test_form_field_text_with_custom_height(self):
|
||||
"""Test form field text with custom field height"""
|
||||
custom_height = 40
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw, custom_height)
|
||||
|
||||
renderable = FormFieldText(
|
||||
self.text_field,
|
||||
self.font,
|
||||
self.mock_draw,
|
||||
custom_height)
|
||||
|
||||
self.assertEqual(renderable._field_height, custom_height)
|
||||
|
||||
|
||||
def test_field_property(self):
|
||||
"""Test field property accessor"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertEqual(renderable.field, self.text_field)
|
||||
|
||||
|
||||
def test_set_focused(self):
|
||||
"""Test setting focus state"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
self.assertFalse(renderable._focused)
|
||||
|
||||
|
||||
renderable.set_focused(True)
|
||||
self.assertTrue(renderable._focused)
|
||||
|
||||
|
||||
renderable.set_focused(False)
|
||||
self.assertFalse(renderable._focused)
|
||||
|
||||
|
||||
def test_size_includes_field_area(self):
|
||||
"""Test size property includes field area"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Size should include label height + gap + field height
|
||||
expected_height = renderable._style.font_size + 5 + renderable._field_height
|
||||
expected_width = renderable._field_width # Use the calculated field width
|
||||
|
||||
np.testing.assert_array_equal(renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
|
||||
np.testing.assert_array_equal(
|
||||
renderable.size, np.array([expected_width, expected_height]))
|
||||
|
||||
def test_render_text_field(self):
|
||||
"""Test rendering text field"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render label
|
||||
mock_parent_render.assert_called_once()
|
||||
# Should draw field background rectangle
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
|
||||
|
||||
def test_render_field_with_value(self):
|
||||
"""Test rendering field with value"""
|
||||
self.text_field.value = "john_doe"
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render label
|
||||
mock_parent_render.assert_called_once()
|
||||
# Should draw field background and value text
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
self.mock_draw.text.assert_called_once()
|
||||
|
||||
|
||||
def test_render_password_field(self):
|
||||
"""Test rendering password field with masked value"""
|
||||
self.password_field.value = "secret123"
|
||||
renderable = FormFieldText(self.password_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render label and field
|
||||
mock_parent_render.assert_called_once()
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
@@ -373,61 +386,62 @@ class TestFormFieldText(unittest.TestCase):
|
||||
# Check that the text call used masked characters
|
||||
call_args = self.mock_draw.text.call_args[0]
|
||||
self.assertEqual(call_args[1], "•" * len("secret123"))
|
||||
|
||||
|
||||
def test_render_focused_field(self):
|
||||
"""Test rendering focused field"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
renderable.set_focused(True)
|
||||
|
||||
|
||||
# Mock the parent Text render method
|
||||
with patch('pyWebLayout.concrete.text.Text.render') as mock_parent_render:
|
||||
renderable.render()
|
||||
|
||||
|
||||
# Should render with focus styling
|
||||
mock_parent_render.assert_called_once()
|
||||
self.mock_draw.rectangle.assert_called_once()
|
||||
|
||||
|
||||
def test_handle_click_inside_field(self):
|
||||
"""Test clicking inside field area"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Click inside field area (below label)
|
||||
field_area_y = renderable._style.font_size + 5 + 10 # Within field area
|
||||
field_area_point = (15, field_area_y)
|
||||
result = renderable.handle_click(field_area_point)
|
||||
|
||||
|
||||
# Should return True and set focused
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(renderable._focused)
|
||||
|
||||
|
||||
def test_handle_click_outside_field(self):
|
||||
"""Test clicking outside field area"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
|
||||
|
||||
# Click outside field area
|
||||
outside_point = (200, 200)
|
||||
result = renderable.handle_click(outside_point)
|
||||
|
||||
|
||||
# Should return False and not set focused
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(renderable._focused)
|
||||
|
||||
|
||||
def test_in_object(self):
|
||||
"""Test in_object method"""
|
||||
renderable = FormFieldText(self.text_field, self.font, self.mock_draw)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Point inside field (including label and input area)
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside field
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_factory_function(self):
|
||||
"""Test the create_form_field_text factory function"""
|
||||
custom_height = 30
|
||||
renderable = create_form_field_text(self.text_field, self.font, self.mock_draw, custom_height)
|
||||
|
||||
renderable = create_form_field_text(
|
||||
self.text_field, self.font, self.mock_draw, custom_height)
|
||||
|
||||
self.assertIsInstance(renderable, FormFieldText)
|
||||
self.assertEqual(renderable.text, "Username")
|
||||
self.assertEqual(renderable.field, self.text_field)
|
||||
@@ -436,7 +450,7 @@ class TestFormFieldText(unittest.TestCase):
|
||||
|
||||
class TestInteractionCallbacks(unittest.TestCase):
|
||||
"""Test cases for interaction functionality"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(font_size=12, colour=(0, 0, 0))
|
||||
@@ -455,7 +469,8 @@ class TestInteractionCallbacks(unittest.TestCase):
|
||||
|
||||
def test_link_text_interaction(self):
|
||||
"""Test that LinkText properly handles interaction"""
|
||||
# Use a FUNCTION link type which calls the callback, not INTERNAL which returns location
|
||||
# Use a FUNCTION link type which calls the callback, not INTERNAL which
|
||||
# returns location
|
||||
link = Link("test_function", LinkType.FUNCTION, self.link_callback)
|
||||
renderable = LinkText(link, "Test Link", self.font, self.mock_draw)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
import tempfile
|
||||
import numpy as np
|
||||
from PIL import Image as PILImage, ImageDraw
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from pyWebLayout.concrete.image import RenderableImage
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage
|
||||
@@ -17,67 +17,67 @@ from pyWebLayout.style import Alignment
|
||||
|
||||
class TestRenderableImage(unittest.TestCase):
|
||||
"""Test cases for the RenderableImage class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
# Create a temporary test image
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.test_image_path = os.path.join(self.temp_dir, "test_image.png")
|
||||
|
||||
|
||||
# Create a simple test image
|
||||
test_img = PILImage.new('RGB', (100, 80), (255, 0, 0)) # Red image
|
||||
test_img.save(self.test_image_path)
|
||||
|
||||
|
||||
# Create abstract image objects
|
||||
self.abstract_image = AbstractImage(self.test_image_path, "Test Image", 100, 80)
|
||||
self.abstract_image_no_dims = AbstractImage(self.test_image_path, "Test Image")
|
||||
|
||||
|
||||
# Create a canvas and draw object for testing
|
||||
self.canvas = PILImage.new('RGBA', (400, 300), (255, 255, 255, 255))
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures"""
|
||||
# Clean up temporary files
|
||||
try:
|
||||
os.unlink(self.test_image_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
def test_renderable_image_initialization_basic(self):
|
||||
"""Test basic image initialization"""
|
||||
renderable = RenderableImage(self.abstract_image, self.canvas)
|
||||
|
||||
|
||||
self.assertEqual(renderable._abstract_image, self.abstract_image)
|
||||
self.assertEqual(renderable._canvas, self.canvas)
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
self.assertEqual(renderable._halign, Alignment.CENTER)
|
||||
self.assertEqual(renderable._valign, Alignment.CENTER)
|
||||
|
||||
|
||||
def test_renderable_image_initialization_with_constraints(self):
|
||||
"""Test image initialization with size constraints"""
|
||||
max_width = 50
|
||||
max_height = 40
|
||||
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
self.draw,
|
||||
max_width=max_width,
|
||||
max_height=max_height
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(renderable._abstract_image, self.abstract_image)
|
||||
# Size should be constrained
|
||||
self.assertLessEqual(renderable._size[0], max_width)
|
||||
self.assertLessEqual(renderable._size[1], max_height)
|
||||
|
||||
|
||||
def test_renderable_image_initialization_with_custom_params(self):
|
||||
"""Test image initialization with custom parameters"""
|
||||
custom_origin = (20, 30)
|
||||
custom_size = (120, 90)
|
||||
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
self.draw,
|
||||
@@ -86,30 +86,30 @@ class TestRenderableImage(unittest.TestCase):
|
||||
halign=Alignment.LEFT,
|
||||
valign=Alignment.TOP
|
||||
)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
|
||||
np.testing.assert_array_equal(renderable._size, np.array(custom_size))
|
||||
self.assertEqual(renderable._halign, Alignment.LEFT)
|
||||
self.assertEqual(renderable._valign, Alignment.TOP)
|
||||
|
||||
|
||||
def test_load_image_local_file(self):
|
||||
"""Test loading image from local file"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Image should be loaded
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
self.assertEqual(renderable._pil_image.size, (100, 80))
|
||||
|
||||
|
||||
def test_load_image_nonexistent_file(self):
|
||||
"""Test loading image from nonexistent file"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.draw)
|
||||
|
||||
|
||||
# Should have error message, no PIL image
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
|
||||
@patch('requests.get')
|
||||
def test_load_image_url_success(self, mock_get):
|
||||
"""Test loading image from URL (success)"""
|
||||
@@ -118,14 +118,14 @@ class TestRenderableImage(unittest.TestCase):
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = open(self.test_image_path, 'rb').read()
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
|
||||
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
|
||||
renderable = RenderableImage(url_abstract, self.draw)
|
||||
|
||||
|
||||
# Should successfully load image
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
|
||||
|
||||
@patch('requests.get')
|
||||
def test_load_image_url_failure(self, mock_get):
|
||||
"""Test loading image from URL (failure)"""
|
||||
@@ -133,14 +133,16 @@ class TestRenderableImage(unittest.TestCase):
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
url_abstract = AbstractImage("https://example.com/notfound.png", "Bad URL Image")
|
||||
|
||||
url_abstract = AbstractImage(
|
||||
"https://example.com/notfound.png",
|
||||
"Bad URL Image")
|
||||
renderable = RenderableImage(url_abstract, self.draw)
|
||||
|
||||
|
||||
# Should have error message
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
|
||||
def test_load_image_no_requests_library(self):
|
||||
"""Test loading URL image when requests library is not available"""
|
||||
# Mock the import to raise ImportError for requests
|
||||
@@ -148,24 +150,24 @@ class TestRenderableImage(unittest.TestCase):
|
||||
if name == 'requests':
|
||||
raise ImportError("No module named 'requests'")
|
||||
return __import__(name, *args, **kwargs)
|
||||
|
||||
|
||||
with patch('builtins.__import__', side_effect=mock_import):
|
||||
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
|
||||
renderable = RenderableImage(url_abstract, self.draw)
|
||||
|
||||
|
||||
# Should have error message about missing requests
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
self.assertIn("Requests library not available", renderable._error_message)
|
||||
|
||||
|
||||
def test_resize_image_fit_within_bounds(self):
|
||||
"""Test image resizing to fit within bounds"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Original image is 100x80, resize to fit in 50x50
|
||||
renderable._size = np.array([50, 50])
|
||||
resized = renderable._resize_image()
|
||||
|
||||
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
# Should maintain aspect ratio and fit within bounds
|
||||
self.assertLessEqual(resized.width, 50)
|
||||
@@ -174,90 +176,90 @@ class TestRenderableImage(unittest.TestCase):
|
||||
original_ratio = 100 / 80
|
||||
new_ratio = resized.width / resized.height
|
||||
self.assertAlmostEqual(original_ratio, new_ratio, delta=0.1)
|
||||
|
||||
|
||||
def test_resize_image_larger_target(self):
|
||||
"""Test image resizing when target is larger than original"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Target size larger than original
|
||||
renderable._size = np.array([200, 160])
|
||||
resized = renderable._resize_image()
|
||||
|
||||
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
# Should scale up to fill the space while maintaining aspect ratio
|
||||
self.assertGreater(resized.width, 100)
|
||||
self.assertGreater(resized.height, 80)
|
||||
|
||||
|
||||
def test_resize_image_no_image(self):
|
||||
"""Test resize when no image is loaded"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.draw)
|
||||
|
||||
|
||||
resized = renderable._resize_image()
|
||||
|
||||
|
||||
# Should return a placeholder image
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
self.assertEqual(resized.mode, 'RGBA')
|
||||
|
||||
|
||||
def test_draw_error_placeholder(self):
|
||||
"""Test drawing error placeholder"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.canvas)
|
||||
renderable._error_message = "File not found"
|
||||
|
||||
|
||||
# Set origin for the placeholder
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Call the error placeholder method
|
||||
renderable._draw_error_placeholder()
|
||||
|
||||
|
||||
# We can't easily test the actual drawing without complex mocking,
|
||||
# but we can verify the method doesn't raise an exception
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
|
||||
def test_draw_error_placeholder_with_text(self):
|
||||
"""Test drawing error placeholder with error message"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.canvas)
|
||||
renderable._error_message = "File not found"
|
||||
|
||||
|
||||
# Set origin for the placeholder
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Call the error placeholder method
|
||||
renderable._draw_error_placeholder()
|
||||
|
||||
|
||||
# Verify error message is set
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
self.assertIn("File not found", renderable._error_message)
|
||||
|
||||
|
||||
def test_render_successful_image(self):
|
||||
"""Test rendering successfully loaded image"""
|
||||
renderable = RenderableImage(self.abstract_image, self.canvas)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
# Render returns nothing (draws directly into canvas)
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
# Verify image was loaded
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
|
||||
|
||||
def test_render_failed_image(self):
|
||||
"""Test rendering when image failed to load"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract, self.canvas)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
with patch.object(renderable, '_draw_error_placeholder') as mock_draw_error:
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
mock_draw_error.assert_called_once()
|
||||
|
||||
|
||||
def test_render_with_left_alignment(self):
|
||||
"""Test rendering with left alignment"""
|
||||
renderable = RenderableImage(
|
||||
@@ -267,14 +269,14 @@ class TestRenderableImage(unittest.TestCase):
|
||||
valign=Alignment.TOP
|
||||
)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
self.assertEqual(renderable._halign, Alignment.LEFT)
|
||||
self.assertEqual(renderable._valign, Alignment.TOP)
|
||||
|
||||
|
||||
def test_render_with_right_alignment(self):
|
||||
"""Test rendering with right alignment"""
|
||||
renderable = RenderableImage(
|
||||
@@ -284,96 +286,97 @@ class TestRenderableImage(unittest.TestCase):
|
||||
valign=Alignment.BOTTOM
|
||||
)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
self.assertEqual(renderable._halign, Alignment.RIGHT)
|
||||
self.assertEqual(renderable._valign, Alignment.BOTTOM)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_render_rgb_image_conversion(self):
|
||||
"""Test rendering RGB image (should be converted to RGBA)"""
|
||||
# Our test image is RGB, so this should test the conversion path
|
||||
renderable = RenderableImage(self.abstract_image, self.canvas)
|
||||
renderable.set_origin(np.array([10, 20]))
|
||||
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
|
||||
# Result should be None as it draws directly
|
||||
self.assertIsNone(result)
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
|
||||
|
||||
def test_in_object(self):
|
||||
"""Test in_object method"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))
|
||||
|
||||
|
||||
# Point inside image
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
|
||||
# Point outside image
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
|
||||
def test_in_object_with_numpy_array(self):
|
||||
"""Test in_object with numpy array point"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))
|
||||
|
||||
|
||||
# Point inside image as numpy array
|
||||
point = np.array([15, 25])
|
||||
self.assertTrue(renderable.in_object(point))
|
||||
|
||||
|
||||
# Point outside image as numpy array
|
||||
point = np.array([200, 200])
|
||||
self.assertFalse(renderable.in_object(point))
|
||||
|
||||
|
||||
def test_image_size_calculation_with_abstract_image_dimensions(self):
|
||||
"""Test that size is calculated from abstract image when available"""
|
||||
# Abstract image has dimensions 100x80
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
# Size should match the calculated scaled dimensions
|
||||
expected_size = self.abstract_image.calculate_scaled_dimensions()
|
||||
np.testing.assert_array_equal(renderable._size, np.array(expected_size))
|
||||
|
||||
|
||||
def test_image_size_calculation_with_constraints(self):
|
||||
"""Test size calculation with max constraints"""
|
||||
max_width = 60
|
||||
max_height = 50
|
||||
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
self.draw,
|
||||
max_width=max_width,
|
||||
max_height=max_height
|
||||
)
|
||||
|
||||
|
||||
# Size should respect constraints
|
||||
self.assertLessEqual(renderable._size[0], max_width)
|
||||
self.assertLessEqual(renderable._size[1], max_height)
|
||||
|
||||
|
||||
def test_image_without_initial_dimensions(self):
|
||||
"""Test image without initial dimensions in abstract image"""
|
||||
renderable = RenderableImage(self.abstract_image_no_dims, self.draw)
|
||||
|
||||
|
||||
# Should still work, using default or calculated size
|
||||
self.assertIsInstance(renderable._size, np.ndarray)
|
||||
self.assertEqual(len(renderable._size), 2)
|
||||
|
||||
|
||||
def test_set_origin_method(self):
|
||||
"""Test the set_origin method"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw)
|
||||
|
||||
|
||||
new_origin = np.array([50, 60])
|
||||
renderable.set_origin(new_origin)
|
||||
|
||||
|
||||
np.testing.assert_array_equal(renderable.origin, new_origin)
|
||||
|
||||
|
||||
def test_properties(self):
|
||||
"""Test the property methods"""
|
||||
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20), size=(100, 80))
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image, self.draw, origin=(
|
||||
10, 20), size=(
|
||||
100, 80))
|
||||
|
||||
np.testing.assert_array_equal(renderable.origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(renderable.size, np.array([100, 80]))
|
||||
self.assertEqual(renderable.width, 100)
|
||||
|
||||
@@ -6,30 +6,29 @@ Tests the Text and Line classes for text rendering functionality.
|
||||
import unittest
|
||||
import numpy as np
|
||||
import os
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from PIL import Image, ImageDraw
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.concrete.text import Text, Line
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
|
||||
from pyWebLayout.style import Alignment
|
||||
from tests.utils.test_fonts import create_default_test_font, ensure_consistent_font_in_tests
|
||||
|
||||
|
||||
class TestText(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Ensure consistent font usage across tests
|
||||
ensure_consistent_font_in_tests()
|
||||
|
||||
|
||||
# Create a real PIL image (canvas) for testing
|
||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||
|
||||
|
||||
# Create a real ImageDraw object
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
# Create a consistent test Font object using bundled font
|
||||
self.style = create_default_test_font()
|
||||
|
||||
|
||||
def test_init(self):
|
||||
text_instance = Text(text="Test", style=self.style, draw=self.draw)
|
||||
self.assertEqual(text_instance.text, "Test")
|
||||
@@ -59,10 +58,10 @@ class TestText(unittest.TestCase):
|
||||
text_instance = Text(text="Test", style=self.style, draw=self.draw)
|
||||
# Set a position so we can render without issues
|
||||
text_instance.set_origin(np.array([10, 50]))
|
||||
|
||||
|
||||
# This should not raise any exceptions with real objects
|
||||
text_instance.render()
|
||||
|
||||
|
||||
# We can verify the canvas was modified (pixel check)
|
||||
# After rendering, some pixels should have changed from pure white
|
||||
# This is a more realistic test than checking mock calls
|
||||
@@ -70,7 +69,7 @@ class TestText(unittest.TestCase):
|
||||
def test_text_dimensions(self):
|
||||
"""Test that text dimensions are calculated correctly with real font"""
|
||||
text_instance = Text(text="Test", style=self.style, draw=self.draw)
|
||||
|
||||
|
||||
# With real objects, we should get actual width measurements
|
||||
self.assertGreater(text_instance.width, 0)
|
||||
self.assertIsInstance(text_instance.width, (int, float))
|
||||
@@ -94,10 +93,10 @@ class TestText(unittest.TestCase):
|
||||
text_instance = Text(text="Hello World!", style=self.style, draw=self.draw)
|
||||
text_instance.set_origin(np.array([50, 100]))
|
||||
text_instance.render()
|
||||
|
||||
|
||||
# Optionally save the canvas for visual inspection
|
||||
self._save_test_image("rendered_text.png")
|
||||
|
||||
|
||||
# Verify that something was drawn (canvas is no longer pure white everywhere)
|
||||
# Convert to array and check if any pixels changed
|
||||
pixels = np.array(self.canvas)
|
||||
@@ -120,13 +119,13 @@ class TestLine(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Ensure consistent font usage across tests
|
||||
ensure_consistent_font_in_tests()
|
||||
|
||||
|
||||
# Create a real PIL image (canvas) for testing
|
||||
self.canvas = Image.new('RGB', (800, 600), color='white')
|
||||
|
||||
|
||||
# Create a real ImageDraw object
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
|
||||
# Create a consistent test Font object using bundled font
|
||||
self.style = create_default_test_font()
|
||||
|
||||
@@ -135,7 +134,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15) # min_spacing, max_spacing
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -144,7 +143,7 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(line._spacing, spacing)
|
||||
np.testing.assert_array_equal(line._origin, origin)
|
||||
np.testing.assert_array_equal(line._size, size)
|
||||
@@ -155,7 +154,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -164,10 +163,10 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Create a word to add
|
||||
word = Word(text="Hello", style=self.style)
|
||||
|
||||
|
||||
# This test may need adjustment based on the actual implementation
|
||||
|
||||
success, overflow_part = line.add_word(word)
|
||||
@@ -181,7 +180,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -190,25 +189,26 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Add words until the line is full
|
||||
words_added = 0
|
||||
for i in range(100):
|
||||
word = Word(text="Amsterdam", style=self.style)
|
||||
success, overflow_part = line.add_word(word)
|
||||
|
||||
|
||||
if overflow_part:
|
||||
# Word was hyphenated - overflow occurred
|
||||
self.assertIsNotNone(overflow_part.text)
|
||||
return
|
||||
elif not success:
|
||||
# Line is full, word couldn't be added
|
||||
self.assertGreater(words_added, 0, "Should have added at least one word before line filled")
|
||||
self.assertGreater(
|
||||
words_added, 0, "Should have added at least one word before line filled")
|
||||
return
|
||||
else:
|
||||
# Word was added successfully
|
||||
words_added += 1
|
||||
|
||||
|
||||
self.fail("Expected line to fill or overflow to occur but reached max iterations")
|
||||
|
||||
def test_line_add_word_until_overflow_small(self):
|
||||
@@ -216,7 +216,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -225,20 +225,20 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Create a word to add
|
||||
|
||||
for i in range(100):
|
||||
word = Word(text="Aslan", style=self.style)
|
||||
|
||||
|
||||
# This test may need adjustment based on the actual implementation
|
||||
|
||||
success, overflow_part = line.add_word(word)
|
||||
# If successful, the word should be added
|
||||
if success == False:
|
||||
if not success:
|
||||
self.assertIsNone(overflow_part)
|
||||
return
|
||||
|
||||
|
||||
self.fail("Expected line to reach capacity but reached max iterations")
|
||||
|
||||
def test_line_add_word_until_overflow_long_brute(self):
|
||||
@@ -246,7 +246,7 @@ class TestLine(unittest.TestCase):
|
||||
spacing = (5, 15)
|
||||
origin = np.array([0, 0])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -256,13 +256,14 @@ class TestLine(unittest.TestCase):
|
||||
halign=Alignment.LEFT,
|
||||
min_word_length_for_brute_force=6 # Lower threshold to enable hyphenation for shorter words
|
||||
)
|
||||
|
||||
|
||||
# Use a longer word to trigger brute force hyphenation
|
||||
words_added = 0
|
||||
for i in range(100):
|
||||
word = Word(text="AAAAAAAA", style=self.style) # 8 A's to ensure it's long enough
|
||||
# 8 A's to ensure it's long enough
|
||||
word = Word(text="AAAAAAAA", style=self.style)
|
||||
success, overflow_part = line.add_word(word)
|
||||
|
||||
|
||||
if overflow_part:
|
||||
# Word was hyphenated - verify overflow part exists
|
||||
self.assertIsNotNone(overflow_part.text)
|
||||
@@ -270,20 +271,20 @@ class TestLine(unittest.TestCase):
|
||||
return
|
||||
elif not success:
|
||||
# Line is full, word couldn't be added
|
||||
self.assertGreater(words_added, 0, "Should have added at least one word before line filled")
|
||||
self.assertGreater(
|
||||
words_added, 0, "Should have added at least one word before line filled")
|
||||
return
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
self.fail("Expected line to fill or overflow to occur but reached max iterations")
|
||||
|
||||
self.fail("Expected line to fill or overflow to occur but reached max iterations")
|
||||
|
||||
def test_line_render(self):
|
||||
"""Test line rendering with real objects"""
|
||||
spacing = (5, 15)
|
||||
origin = np.array([50, 100])
|
||||
size = np.array([400, 50])
|
||||
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=origin,
|
||||
@@ -292,7 +293,7 @@ class TestLine(unittest.TestCase):
|
||||
font=self.style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
|
||||
# Try to render the line (even if empty)
|
||||
try:
|
||||
line.render()
|
||||
|
||||
@@ -88,8 +88,11 @@ class TestLinkedWordHyphenation(unittest.TestCase):
|
||||
|
||||
# Both parts should be LinkText (this is the bug we're testing for)
|
||||
for text_obj in line._text_objects:
|
||||
self.assertIsInstance(text_obj, LinkText,
|
||||
f"Hyphenated LinkedWord part should be LinkText, got {type(text_obj)}")
|
||||
self.assertIsInstance(
|
||||
text_obj,
|
||||
LinkText,
|
||||
f"Hyphenated LinkedWord part should be LinkText, got {
|
||||
type(text_obj)}")
|
||||
self.assertEqual(text_obj.link.location, linked_word.location)
|
||||
|
||||
# The overflow should also be LinkText if it's hyphenated
|
||||
|
||||
@@ -9,10 +9,9 @@ Unit tests for the new Page implementation to verify it meets the requirements:
|
||||
"""
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL import Image
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from pyWebLayout.style.fonts import Font
|
||||
from pyWebLayout.core.base import Renderable, Queriable
|
||||
|
||||
|
||||
@@ -23,7 +22,7 @@ class SimpleTestRenderable(Renderable, Queriable):
|
||||
self._text = text
|
||||
self.size = size
|
||||
self._origin = np.array([0, 0])
|
||||
|
||||
|
||||
def render(self):
|
||||
"""Render returns None - drawing is done via the page's draw object"""
|
||||
return None
|
||||
@@ -31,7 +30,7 @@ class SimpleTestRenderable(Renderable, Queriable):
|
||||
|
||||
class TestPageImplementation(unittest.TestCase):
|
||||
"""Test cases for the Page class implementation"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.basic_style = PageStyle(
|
||||
@@ -42,81 +41,84 @@ class TestPageImplementation(unittest.TestCase):
|
||||
padding=(15, 15, 15, 15),
|
||||
background_color=(240, 240, 240)
|
||||
)
|
||||
|
||||
|
||||
self.page_size = (800, 600)
|
||||
|
||||
|
||||
def test_page_creation_with_style(self):
|
||||
"""Test creating a page with a PageStyle"""
|
||||
page = Page(size=self.page_size, style=self.basic_style)
|
||||
|
||||
|
||||
self.assertEqual(page.size, self.page_size)
|
||||
self.assertEqual(page.style, self.basic_style)
|
||||
self.assertEqual(page.border_size, 2)
|
||||
|
||||
|
||||
def test_page_creation_without_style(self):
|
||||
"""Test creating a page without a PageStyle (should use defaults)"""
|
||||
page = Page(size=self.page_size)
|
||||
|
||||
|
||||
self.assertEqual(page.size, self.page_size)
|
||||
self.assertIsNotNone(page.style)
|
||||
|
||||
|
||||
def test_page_canvas_and_content_sizes(self):
|
||||
"""Test that page correctly calculates canvas and content sizes"""
|
||||
style = PageStyle(
|
||||
border_width=5,
|
||||
padding=(10, 20, 30, 40) # top, right, bottom, left
|
||||
)
|
||||
|
||||
|
||||
page = Page(size=self.page_size, style=style)
|
||||
|
||||
|
||||
# Canvas size should be page size minus borders
|
||||
expected_canvas_size = (790, 590) # 800-10, 600-10 (border on both sides)
|
||||
self.assertEqual(page.canvas_size, expected_canvas_size)
|
||||
|
||||
|
||||
# Content size should be canvas minus padding
|
||||
expected_content_size = (730, 550) # 790-60, 590-40 (padding left+right, top+bottom)
|
||||
# 790-60, 590-40 (padding left+right, top+bottom)
|
||||
expected_content_size = (730, 550)
|
||||
self.assertEqual(page.content_size, expected_content_size)
|
||||
|
||||
|
||||
def test_page_add_remove_children(self):
|
||||
"""Test adding and removing children from the page"""
|
||||
page = Page(size=self.page_size)
|
||||
|
||||
|
||||
# Initially no children
|
||||
self.assertEqual(len(page.children), 0)
|
||||
|
||||
|
||||
# Add children
|
||||
child1 = SimpleTestRenderable("Child 1")
|
||||
child2 = SimpleTestRenderable("Child 2")
|
||||
|
||||
|
||||
page.add_child(child1)
|
||||
self.assertEqual(len(page.children), 1)
|
||||
self.assertIn(child1, page.children)
|
||||
|
||||
|
||||
page.add_child(child2)
|
||||
self.assertEqual(len(page.children), 2)
|
||||
self.assertIn(child2, page.children)
|
||||
|
||||
|
||||
# Test method chaining
|
||||
child3 = SimpleTestRenderable("Child 3")
|
||||
result = page.add_child(child3)
|
||||
self.assertIs(result, page) # Should return self for chaining
|
||||
self.assertEqual(len(page.children), 3)
|
||||
self.assertIn(child3, page.children)
|
||||
|
||||
# Remove childce you’ll notice is that responses don’t stream character-by-character like other providers. Instead, Claude Code processes your full request before sending back the complete response.
|
||||
|
||||
# Remove childce you’ll notice is that responses don’t stream
|
||||
# character-by-character like other providers. Instead, Claude Code
|
||||
# processes your full request before sending back the complete response.
|
||||
removed = page.remove_child(child2)
|
||||
self.assertTrue(removed)
|
||||
self.assertEqual(len(page.children), 2)
|
||||
self.assertNotIn(child2, page.children)
|
||||
|
||||
|
||||
# Try to remove non-existent child
|
||||
removed = page.remove_child(child2)
|
||||
self.assertFalse(removed)
|
||||
|
||||
|
||||
# Clear all children
|
||||
page.clear_children()
|
||||
self.assertEqual(len(page.children), 0)
|
||||
|
||||
|
||||
def test_page_render(self):
|
||||
"""Test that page renders and creates a canvas"""
|
||||
style = PageStyle(
|
||||
@@ -124,24 +126,24 @@ class TestPageImplementation(unittest.TestCase):
|
||||
border_color=(255, 0, 0),
|
||||
background_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
|
||||
page = Page(size=(200, 150), style=style)
|
||||
|
||||
|
||||
# Add a child
|
||||
child = SimpleTestRenderable("Test child")
|
||||
page.add_child(child)
|
||||
|
||||
|
||||
# Render the page
|
||||
image = page.render()
|
||||
|
||||
|
||||
# Check that we got an image
|
||||
self.assertIsInstance(image, Image.Image)
|
||||
self.assertEqual(image.size, (200, 150))
|
||||
self.assertEqual(image.mode, 'RGBA')
|
||||
|
||||
|
||||
# Check that draw object is available
|
||||
self.assertIsNotNone(page.draw)
|
||||
|
||||
|
||||
def test_page_query_point(self):
|
||||
"""Test querying points to find children"""
|
||||
page = Page(size=(400, 300))
|
||||
@@ -167,22 +169,22 @@ class TestPageImplementation(unittest.TestCase):
|
||||
result = page.query_point((300, 250))
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result.object_type, "empty")
|
||||
|
||||
|
||||
def test_page_in_object(self):
|
||||
"""Test that page correctly implements in_object"""
|
||||
page = Page(size=(400, 300))
|
||||
|
||||
|
||||
# Points within page bounds
|
||||
self.assertTrue(page.in_object((0, 0)))
|
||||
self.assertTrue(page.in_object((200, 150)))
|
||||
self.assertTrue(page.in_object((399, 299)))
|
||||
|
||||
|
||||
# Points outside page bounds
|
||||
self.assertFalse(page.in_object((-1, 0)))
|
||||
self.assertFalse(page.in_object((0, -1)))
|
||||
self.assertFalse(page.in_object((400, 299)))
|
||||
self.assertFalse(page.in_object((399, 300)))
|
||||
|
||||
|
||||
def test_page_with_borders(self):
|
||||
"""Test page rendering with borders"""
|
||||
style = PageStyle(
|
||||
@@ -190,33 +192,33 @@ class TestPageImplementation(unittest.TestCase):
|
||||
border_color=(128, 128, 128),
|
||||
background_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
|
||||
page = Page(size=(100, 100), style=style)
|
||||
image = page.render()
|
||||
|
||||
|
||||
# Check that image was created
|
||||
self.assertIsInstance(image, Image.Image)
|
||||
self.assertEqual(image.size, (100, 100))
|
||||
|
||||
|
||||
# The border should be drawn but we can't easily test pixel values
|
||||
# Just verify the image exists and has the right properties
|
||||
|
||||
|
||||
def test_page_border_size_property(self):
|
||||
"""Test that border_size property returns correct value"""
|
||||
# Test with border
|
||||
style_with_border = PageStyle(border_width=5)
|
||||
page_with_border = Page(size=self.page_size, style=style_with_border)
|
||||
self.assertEqual(page_with_border.border_size, 5)
|
||||
|
||||
|
||||
# Test without border
|
||||
style_no_border = PageStyle(border_width=0)
|
||||
page_no_border = Page(size=self.page_size, style=style_no_border)
|
||||
self.assertEqual(page_no_border.border_size, 0)
|
||||
|
||||
|
||||
def test_page_style_properties(self):
|
||||
"""Test that page correctly exposes style properties"""
|
||||
page = Page(size=self.page_size, style=self.basic_style)
|
||||
|
||||
|
||||
# Test that style properties are accessible
|
||||
self.assertEqual(page.style.border_width, 2)
|
||||
self.assertEqual(page.style.border_color, (255, 0, 0))
|
||||
@@ -224,30 +226,30 @@ class TestPageImplementation(unittest.TestCase):
|
||||
self.assertEqual(page.style.inter_block_spacing, 20)
|
||||
self.assertEqual(page.style.padding, (15, 15, 15, 15))
|
||||
self.assertEqual(page.style.background_color, (240, 240, 240))
|
||||
|
||||
|
||||
def test_page_children_list_operations(self):
|
||||
"""Test that children list behaves correctly"""
|
||||
page = Page(size=self.page_size)
|
||||
|
||||
|
||||
# Test that children is initially empty list
|
||||
self.assertIsInstance(page.children, list)
|
||||
self.assertEqual(len(page.children), 0)
|
||||
|
||||
|
||||
# Test adding multiple children
|
||||
children = [
|
||||
SimpleTestRenderable(f"Child {i}")
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
|
||||
for child in children:
|
||||
page.add_child(child)
|
||||
|
||||
|
||||
self.assertEqual(len(page.children), 5)
|
||||
|
||||
|
||||
# Test that children are in the correct order
|
||||
for i, child in enumerate(page.children):
|
||||
self.assertEqual(child._text, f"Child {i}")
|
||||
|
||||
|
||||
def test_page_can_fit_line_boundary_checking(self):
|
||||
"""Test that can_fit_line correctly checks bottom boundary"""
|
||||
# Create page with known dimensions
|
||||
@@ -259,63 +261,64 @@ class TestPageImplementation(unittest.TestCase):
|
||||
padding=(10, 10, 10, 10)
|
||||
)
|
||||
page = Page(size=(800, 600), style=style)
|
||||
|
||||
|
||||
# Initial y_offset should be at border + padding_top = 50
|
||||
self.assertEqual(page._current_y_offset, 50)
|
||||
|
||||
|
||||
# Test 1: Line that fits comfortably
|
||||
line_height = 20
|
||||
max_y = 600 - 40 - 10 # 550
|
||||
_max_y = 600 - 40 - 10 # 550
|
||||
self.assertTrue(page.can_fit_line(line_height))
|
||||
# Would end at 50 + 20 = 70, well within 550
|
||||
|
||||
|
||||
# Test 2: Simulate adding lines to fill the page
|
||||
# Available height: 550 - 50 = 500 pixels
|
||||
# With 20-pixel lines, we can fit 25 lines exactly
|
||||
for i in range(24): # Add 24 lines
|
||||
self.assertTrue(page.can_fit_line(20), f"Line {i+1} should fit")
|
||||
self.assertTrue(page.can_fit_line(20), f"Line {i + 1} should fit")
|
||||
# Simulate adding a line by updating y_offset
|
||||
page._current_y_offset += 20
|
||||
|
||||
|
||||
# After 24 lines: y_offset = 50 + (24 * 20) = 530
|
||||
self.assertEqual(page._current_y_offset, 530)
|
||||
|
||||
# Test 3: One more 20-pixel line should fit (530 + 20 = 550, exactly at boundary)
|
||||
|
||||
# Test 3: One more 20-pixel line should fit (530 + 20 = 550, exactly at
|
||||
# boundary)
|
||||
self.assertTrue(page.can_fit_line(20))
|
||||
page._current_y_offset += 20
|
||||
self.assertEqual(page._current_y_offset, 550)
|
||||
|
||||
|
||||
# Test 4: Now another line should NOT fit (550 + 20 = 570 > 550)
|
||||
self.assertFalse(page.can_fit_line(20))
|
||||
|
||||
|
||||
# Test 5: Even a 1-pixel line should not fit (550 + 1 = 551 > 550)
|
||||
self.assertFalse(page.can_fit_line(1))
|
||||
|
||||
|
||||
# Test 6: Edge case - exactly at boundary, 0-height line should fit
|
||||
self.assertTrue(page.can_fit_line(0))
|
||||
|
||||
|
||||
def test_page_can_fit_line_with_different_styles(self):
|
||||
"""Test can_fit_line with different page styles"""
|
||||
# Test with no border or padding
|
||||
style_no_border = PageStyle(border_width=0, padding=(0, 0, 0, 0))
|
||||
page_no_border = Page(size=(100, 100), style=style_no_border)
|
||||
|
||||
|
||||
# With no border/padding, y_offset starts at 0
|
||||
self.assertEqual(page_no_border._current_y_offset, 0)
|
||||
|
||||
|
||||
# Can fit a 100-pixel line exactly
|
||||
self.assertTrue(page_no_border.can_fit_line(100))
|
||||
|
||||
|
||||
# Cannot fit a 101-pixel line
|
||||
self.assertFalse(page_no_border.can_fit_line(101))
|
||||
|
||||
|
||||
# Test with large border and padding
|
||||
style_large = PageStyle(border_width=20, padding=(15, 15, 15, 15))
|
||||
page_large = Page(size=(200, 200), style=style_large)
|
||||
|
||||
|
||||
# y_offset starts at border + padding_top = 20 + 15 = 35
|
||||
self.assertEqual(page_large._current_y_offset, 35)
|
||||
|
||||
|
||||
# Max y = 200 - 20 - 15 = 165
|
||||
# Available height = 165 - 35 = 130 pixels
|
||||
self.assertTrue(page_large.can_fit_line(130))
|
||||
|
||||
@@ -17,8 +17,7 @@ from pyWebLayout.concrete.table import (
|
||||
TableRenderer
|
||||
)
|
||||
from pyWebLayout.abstract.block import (
|
||||
Table, TableRow, TableCell, Paragraph, Heading, HeadingLevel,
|
||||
Image as AbstractImage
|
||||
Table, TableRow, TableCell, Paragraph, Heading, HeadingLevel
|
||||
)
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font
|
||||
@@ -193,7 +192,11 @@ class TestTableCellRenderer:
|
||||
assert cell_renderer._style == default_table_style
|
||||
assert cell_renderer._is_header_section is False
|
||||
|
||||
def test_initialization_with_header(self, sample_font, sample_draw, default_table_style):
|
||||
def test_initialization_with_header(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
default_table_style):
|
||||
"""Test TableCellRenderer initialization for header cell."""
|
||||
cell = TableCell(is_header=True)
|
||||
cell_renderer = TableCellRenderer(
|
||||
@@ -207,7 +210,12 @@ class TestTableCellRenderer:
|
||||
|
||||
assert cell_renderer._is_header_section is True
|
||||
|
||||
def test_render_empty_cell(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_empty_cell(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering an empty cell."""
|
||||
cell = TableCell()
|
||||
cell_renderer = TableCellRenderer(
|
||||
@@ -223,7 +231,12 @@ class TestTableCellRenderer:
|
||||
# Render returns None (draws directly on canvas)
|
||||
assert result is None
|
||||
|
||||
def test_render_cell_with_text(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_cell_with_text(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a cell with text content."""
|
||||
cell = TableCell()
|
||||
paragraph = Paragraph(sample_font)
|
||||
@@ -243,7 +256,12 @@ class TestTableCellRenderer:
|
||||
result = cell_renderer.render()
|
||||
assert result is None
|
||||
|
||||
def test_render_header_cell(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_header_cell(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a header cell with different styling."""
|
||||
cell = TableCell(is_header=True)
|
||||
paragraph = Paragraph(sample_font)
|
||||
@@ -263,7 +281,12 @@ class TestTableCellRenderer:
|
||||
result = cell_renderer.render()
|
||||
assert result is None
|
||||
|
||||
def test_render_cell_with_heading(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_cell_with_heading(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a cell with heading content."""
|
||||
cell = TableCell()
|
||||
heading = Heading(HeadingLevel.H2, sample_font)
|
||||
@@ -294,11 +317,11 @@ class TestTableCellRenderer:
|
||||
)
|
||||
|
||||
# Point inside cell
|
||||
assert cell_renderer.in_object((50, 30)) == True
|
||||
assert cell_renderer.in_object((50, 30))
|
||||
|
||||
# Point outside cell
|
||||
assert cell_renderer.in_object((150, 30)) == False
|
||||
assert cell_renderer.in_object((50, 100)) == False
|
||||
assert not cell_renderer.in_object((150, 30))
|
||||
assert not cell_renderer.in_object((50, 100))
|
||||
|
||||
def test_properties_access(self, sample_font, sample_draw, default_table_style):
|
||||
"""Test accessing cell renderer properties."""
|
||||
@@ -361,7 +384,12 @@ class TestTableRowRenderer:
|
||||
result = row_renderer.render()
|
||||
assert result is None
|
||||
|
||||
def test_render_row_with_cells(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_row_with_cells(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a row with multiple cells."""
|
||||
row = TableRow()
|
||||
|
||||
@@ -388,7 +416,12 @@ class TestTableRowRenderer:
|
||||
# Verify cells were created
|
||||
assert len(row_renderer._cell_renderers) == 3
|
||||
|
||||
def test_render_row_with_colspan(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_row_with_colspan(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a row with cells that span multiple columns."""
|
||||
row = TableRow()
|
||||
|
||||
@@ -444,7 +477,11 @@ class TestTableRenderer:
|
||||
assert table_renderer._draw == sample_draw
|
||||
assert table_renderer._style == default_table_style
|
||||
|
||||
def test_dimension_calculation(self, simple_table, sample_draw, default_table_style):
|
||||
def test_dimension_calculation(
|
||||
self,
|
||||
simple_table,
|
||||
sample_draw,
|
||||
default_table_style):
|
||||
"""Test table dimension calculation."""
|
||||
table_renderer = TableRenderer(
|
||||
simple_table,
|
||||
@@ -459,7 +496,12 @@ class TestTableRenderer:
|
||||
assert len(table_renderer._row_heights) == 3 # header, body, footer
|
||||
assert all(width > 0 for width in table_renderer._column_widths)
|
||||
|
||||
def test_render_simple_table(self, simple_table, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_simple_table(
|
||||
self,
|
||||
simple_table,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a complete simple table."""
|
||||
table_renderer = TableRenderer(
|
||||
simple_table,
|
||||
@@ -475,7 +517,12 @@ class TestTableRenderer:
|
||||
# Verify rows were created
|
||||
assert len(table_renderer._row_renderers) == 2 # 1 header + 1 body
|
||||
|
||||
def test_render_table_with_caption(self, simple_table, sample_draw, sample_canvas, default_table_style):
|
||||
def test_render_table_with_caption(
|
||||
self,
|
||||
simple_table,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a table with caption."""
|
||||
simple_table.caption = "Test Table Caption"
|
||||
|
||||
@@ -535,7 +582,12 @@ class TestTableRenderer:
|
||||
# Should handle gracefully
|
||||
assert table_renderer is not None
|
||||
|
||||
def test_table_with_footer(self, sample_font, sample_draw, sample_canvas, default_table_style):
|
||||
def test_table_with_footer(
|
||||
self,
|
||||
sample_font,
|
||||
sample_draw,
|
||||
sample_canvas,
|
||||
default_table_style):
|
||||
"""Test rendering a table with footer rows."""
|
||||
table = Table()
|
||||
|
||||
@@ -590,10 +642,10 @@ class TestTableRenderer:
|
||||
)
|
||||
|
||||
# Point inside table
|
||||
assert table_renderer.in_object((50, 50)) == True
|
||||
assert table_renderer.in_object((50, 50))
|
||||
|
||||
# Point outside table
|
||||
assert table_renderer.in_object((1000, 1000)) == False
|
||||
assert not table_renderer.in_object((1000, 1000))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -8,7 +8,6 @@ import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
from pyWebLayout.core.highlight import (
|
||||
Highlight,
|
||||
|
||||
@@ -15,7 +15,6 @@ from pyWebLayout.concrete.text import Text, Line
|
||||
from pyWebLayout.concrete.functional import LinkText
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.abstract.functional import Link, LinkType
|
||||
from pyWebLayout.style import Font, Alignment
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from tests.utils.test_fonts import create_default_test_font, ensure_consistent_font_in_tests
|
||||
|
||||
@@ -369,7 +368,8 @@ class TestPageQueryRange(unittest.TestCase):
|
||||
start_text = line._text_objects[0]
|
||||
end_text = line._text_objects[1]
|
||||
|
||||
start_point = (int(start_text._origin[0] + 5), int(start_text._origin[1] + 5))
|
||||
start_point = (
|
||||
int(start_text._origin[0] + 5), int(start_text._origin[1] + 5))
|
||||
end_point = (int(end_text._origin[0] + 5), int(end_text._origin[1] + 5))
|
||||
|
||||
sel_range = self.page.query_range(start_point, end_point)
|
||||
|
||||
+204
-167
@@ -9,7 +9,6 @@ import unittest
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
# Import ebooklib for creating test EPUB files
|
||||
try:
|
||||
@@ -21,8 +20,8 @@ except ImportError:
|
||||
from pyWebLayout.io.readers.epub_reader import read_epub, EPUBReader
|
||||
from pyWebLayout.abstract.document import Book
|
||||
from pyWebLayout.abstract.block import (
|
||||
Paragraph, Heading, HeadingLevel, Quote, CodeBlock,
|
||||
HList, ListStyle, Table, HorizontalRule, Image
|
||||
Paragraph, Heading, Quote, CodeBlock, HList,
|
||||
ListStyle, Table, Image
|
||||
)
|
||||
from pyWebLayout.style import FontWeight, FontStyle, TextDecoration
|
||||
|
||||
@@ -30,12 +29,12 @@ from pyWebLayout.style import FontWeight, FontStyle, TextDecoration
|
||||
@unittest.skipUnless(EBOOKLIB_AVAILABLE, "ebooklib not available")
|
||||
class TestEPUBReader(unittest.TestCase):
|
||||
"""Test cases for EPUB reader functionality."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.epub_files = []
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
# Clean up test EPUB files
|
||||
@@ -44,21 +43,21 @@ class TestEPUBReader(unittest.TestCase):
|
||||
os.remove(epub_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# Clean up test directory
|
||||
if os.path.exists(self.test_dir):
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def create_simple_epub(self, title="Test Book", author="Test Author"):
|
||||
"""Create a simple EPUB file for testing."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
|
||||
# Set metadata
|
||||
book.set_identifier('test-id-123')
|
||||
book.set_title(title)
|
||||
book.set_language('en')
|
||||
book.add_author(author)
|
||||
|
||||
|
||||
# Create a simple chapter
|
||||
chapter1 = epub.EpubHtml(
|
||||
title='Chapter 1',
|
||||
@@ -75,31 +74,32 @@ class TestEPUBReader(unittest.TestCase):
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
# Add chapter to book
|
||||
book.add_item(chapter1)
|
||||
|
||||
|
||||
# Define table of contents
|
||||
book.toc = (epub.Link("chapter1.xhtml", "Chapter 1", "ch1"),)
|
||||
|
||||
|
||||
# Add navigation files
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
|
||||
|
||||
# Define spine
|
||||
book.spine = ['nav', chapter1]
|
||||
|
||||
|
||||
# Create temporary file
|
||||
epub_path = os.path.join(self.test_dir, f'test_simple_{len(self.epub_files)}.epub')
|
||||
epub_path = os.path.join(self.test_dir,
|
||||
f'test_simple_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
|
||||
return epub_path
|
||||
|
||||
|
||||
def create_complex_epub(self):
|
||||
"""Create a more complex EPUB file with multiple chapters and content types."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
|
||||
# Set metadata
|
||||
book.set_identifier('complex-test-id-456')
|
||||
book.set_title('Complex Test Book')
|
||||
@@ -109,7 +109,7 @@ class TestEPUBReader(unittest.TestCase):
|
||||
book.add_metadata('DC', 'subject', 'Testing')
|
||||
book.add_metadata('DC', 'date', '2024-01-01')
|
||||
book.add_metadata('DC', 'publisher', 'Test Publisher')
|
||||
|
||||
|
||||
# Chapter 1: Basic content
|
||||
chapter1 = epub.EpubHtml(
|
||||
title='Introduction',
|
||||
@@ -126,7 +126,7 @@ class TestEPUBReader(unittest.TestCase):
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
# Chapter 2: Styled content
|
||||
chapter2 = epub.EpubHtml(
|
||||
title='Styled Content',
|
||||
@@ -138,7 +138,7 @@ class TestEPUBReader(unittest.TestCase):
|
||||
<head><title>Styled Content</title></head>
|
||||
<body>
|
||||
<h1>Styled Content</h1>
|
||||
<p>This chapter contains various <strong>bold text</strong>, <em>italic text</em>,
|
||||
<p>This chapter contains various <strong>bold text</strong>, <em>italic text</em>,
|
||||
and <span style="color: red; font-weight: bold;">colored text</span>.</p>
|
||||
<h2>Subsection</h2>
|
||||
<p>Text with <u>underline</u> and <s>strikethrough</s>.</p>
|
||||
@@ -147,7 +147,7 @@ class TestEPUBReader(unittest.TestCase):
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
# Chapter 3: Lists and quotes
|
||||
chapter3 = epub.EpubHtml(
|
||||
title='Lists and Quotes',
|
||||
@@ -159,30 +159,30 @@ class TestEPUBReader(unittest.TestCase):
|
||||
<head><title>Lists and Quotes</title></head>
|
||||
<body>
|
||||
<h1>Lists and Quotes</h1>
|
||||
|
||||
|
||||
<h2>Unordered List</h2>
|
||||
<ul>
|
||||
<li>First item</li>
|
||||
<li><strong>Bold item</strong></li>
|
||||
<li>Item with <em>italic text</em></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h2>Ordered List</h2>
|
||||
<ol>
|
||||
<li>First numbered item</li>
|
||||
<li>Second numbered item</li>
|
||||
<li>Third numbered item</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<h2>Quote</h2>
|
||||
<blockquote>
|
||||
<p>This is a <span style="font-style: italic;">quoted paragraph</span>
|
||||
<p>This is a <span style="font-style: italic;">quoted paragraph</span>
|
||||
with some styling.</p>
|
||||
</blockquote>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
# Chapter 4: Tables and code
|
||||
chapter4 = epub.EpubHtml(
|
||||
title='Tables and Code',
|
||||
@@ -194,7 +194,7 @@ class TestEPUBReader(unittest.TestCase):
|
||||
<head><title>Tables and Code</title></head>
|
||||
<body>
|
||||
<h1>Tables and Code</h1>
|
||||
|
||||
|
||||
<h2>Simple Table</h2>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -214,25 +214,25 @@ class TestEPUBReader(unittest.TestCase):
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<h2>Code Block</h2>
|
||||
<pre><code>function test() {
|
||||
console.log("Hello, world!");
|
||||
return true;
|
||||
}</code></pre>
|
||||
|
||||
|
||||
<h2>Inline Code</h2>
|
||||
<p>Use the <code>print()</code> function to output text.</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
# Add chapters to book
|
||||
book.add_item(chapter1)
|
||||
book.add_item(chapter2)
|
||||
book.add_item(chapter3)
|
||||
book.add_item(chapter4)
|
||||
|
||||
|
||||
# Define table of contents
|
||||
book.toc = (
|
||||
epub.Link("chapter1.xhtml", "Introduction", "intro"),
|
||||
@@ -240,31 +240,32 @@ class TestEPUBReader(unittest.TestCase):
|
||||
epub.Link("chapter3.xhtml", "Lists and Quotes", "lists"),
|
||||
epub.Link("chapter4.xhtml", "Tables and Code", "tables")
|
||||
)
|
||||
|
||||
|
||||
# Add navigation files
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
|
||||
|
||||
# Define spine
|
||||
book.spine = ['nav', chapter1, chapter2, chapter3, chapter4]
|
||||
|
||||
|
||||
# Create temporary file
|
||||
epub_path = os.path.join(self.test_dir, f'test_complex_{len(self.epub_files)}.epub')
|
||||
epub_path = os.path.join(self.test_dir,
|
||||
f'test_complex_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
|
||||
return epub_path
|
||||
|
||||
|
||||
def create_epub_with_nested_content(self):
|
||||
"""Create an EPUB with nested content structures."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
|
||||
# Set metadata
|
||||
book.set_identifier('nested-test-id-789')
|
||||
book.set_title('Nested Content Test')
|
||||
book.set_language('en')
|
||||
book.add_author('Test Author')
|
||||
|
||||
|
||||
# Chapter with nested content
|
||||
chapter = epub.EpubHtml(
|
||||
title='Nested Content',
|
||||
@@ -276,17 +277,17 @@ class TestEPUBReader(unittest.TestCase):
|
||||
<head><title>Nested Content</title></head>
|
||||
<body>
|
||||
<h1>Nested Content Examples</h1>
|
||||
|
||||
|
||||
<div>
|
||||
<h2>Section in Div</h2>
|
||||
<p>Paragraph inside div.</p>
|
||||
|
||||
|
||||
<section>
|
||||
<h3>Subsection</h3>
|
||||
<article>
|
||||
<h4>Article Header</h4>
|
||||
<p>Article content with <strong>nested <em>formatting</em></strong>.</p>
|
||||
|
||||
|
||||
<aside>
|
||||
<p>Sidebar content in aside element.</p>
|
||||
<ul>
|
||||
@@ -297,268 +298,284 @@ class TestEPUBReader(unittest.TestCase):
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
<footer>
|
||||
<p>Footer content with <span style="font-size: 12px; color: gray;">small gray text</span>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
# Add chapter to book
|
||||
book.add_item(chapter)
|
||||
|
||||
|
||||
# Define table of contents
|
||||
book.toc = (epub.Link("nested.xhtml", "Nested Content", "nested"),)
|
||||
|
||||
|
||||
# Add navigation files
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
|
||||
|
||||
# Define spine
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
|
||||
# Create temporary file
|
||||
epub_path = os.path.join(self.test_dir, f'test_nested_{len(self.epub_files)}.epub')
|
||||
epub_path = os.path.join(self.test_dir,
|
||||
f'test_nested_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
|
||||
return epub_path
|
||||
|
||||
|
||||
def test_simple_epub_reading(self):
|
||||
"""Test reading a simple EPUB file."""
|
||||
epub_path = self.create_simple_epub()
|
||||
|
||||
|
||||
# Read the EPUB
|
||||
book = read_epub(epub_path)
|
||||
|
||||
|
||||
# Verify it's a Book object
|
||||
self.assertIsInstance(book, Book)
|
||||
|
||||
|
||||
# Check metadata
|
||||
self.assertEqual(book.title, "Test Book")
|
||||
|
||||
|
||||
# Check chapters
|
||||
chapters = list(book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
|
||||
# Check chapter content
|
||||
chapter = chapters[0]
|
||||
blocks = list(chapter.blocks)
|
||||
self.assertGreater(len(blocks), 0)
|
||||
|
||||
|
||||
# Should have a heading and paragraphs
|
||||
has_heading = any(isinstance(block, Heading) for block in blocks)
|
||||
has_paragraph = any(isinstance(block, Paragraph) for block in blocks)
|
||||
|
||||
|
||||
self.assertTrue(has_heading, "Should contain at least one heading")
|
||||
self.assertTrue(has_paragraph, "Should contain at least one paragraph")
|
||||
|
||||
|
||||
def test_complex_epub_reading(self):
|
||||
"""Test reading a complex EPUB file with multiple chapters."""
|
||||
epub_path = self.create_complex_epub()
|
||||
|
||||
|
||||
# Read the EPUB
|
||||
book = read_epub(epub_path)
|
||||
|
||||
|
||||
# Verify it's a Book object
|
||||
self.assertIsInstance(book, Book)
|
||||
|
||||
|
||||
# Check metadata
|
||||
self.assertEqual(book.title, "Complex Test Book")
|
||||
|
||||
|
||||
# Check chapters
|
||||
chapters = list(book.chapters)
|
||||
self.assertEqual(len(chapters), 4)
|
||||
|
||||
|
||||
# Test each chapter has content
|
||||
for i, chapter in enumerate(chapters):
|
||||
blocks = list(chapter.blocks)
|
||||
self.assertGreater(len(blocks), 0, f"Chapter {i+1} should have blocks")
|
||||
|
||||
self.assertGreater(len(blocks), 0, f"Chapter {i + 1} should have blocks")
|
||||
|
||||
# Each chapter should start with a heading
|
||||
first_block = blocks[0]
|
||||
self.assertIsInstance(first_block, Heading, f"Chapter {i+1} should start with heading")
|
||||
|
||||
self.assertIsInstance(
|
||||
first_block, Heading, f"Chapter {
|
||||
i + 1} should start with heading")
|
||||
|
||||
def test_epub_styled_content(self):
|
||||
"""Test that styled content in EPUB is properly parsed."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
|
||||
chapters = list(book.chapters)
|
||||
|
||||
|
||||
# Check styled content in chapter 2 (index 1)
|
||||
if len(chapters) > 1:
|
||||
chapter2_blocks = list(chapters[1].blocks)
|
||||
|
||||
|
||||
# Find paragraphs with styled text
|
||||
styled_words_found = False
|
||||
for block in chapter2_blocks:
|
||||
if isinstance(block, Paragraph):
|
||||
words = list(block.words_iter())
|
||||
for _, word in words:
|
||||
if (word.style.weight == FontWeight.BOLD or
|
||||
if (word.style.weight == FontWeight.BOLD or
|
||||
word.style.style == FontStyle.ITALIC or
|
||||
word.style.colour != (0, 0, 0)): # Non-black color
|
||||
word.style.colour != (0, 0, 0)): # Non-black color
|
||||
styled_words_found = True
|
||||
break
|
||||
if styled_words_found:
|
||||
break
|
||||
|
||||
|
||||
self.assertTrue(styled_words_found, "Should find styled words in chapter 2")
|
||||
|
||||
|
||||
def test_epub_lists(self):
|
||||
"""Test that lists in EPUB are properly parsed."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
|
||||
chapters = list(book.chapters)
|
||||
|
||||
|
||||
# Check lists in chapter 3 (index 2)
|
||||
if len(chapters) > 2:
|
||||
chapter3_blocks = list(chapters[2].blocks)
|
||||
|
||||
|
||||
# Find list blocks
|
||||
unordered_list_found = False
|
||||
ordered_list_found = False
|
||||
quote_found = False
|
||||
|
||||
|
||||
for block in chapter3_blocks:
|
||||
if isinstance(block, HList):
|
||||
if block.style == ListStyle.UNORDERED:
|
||||
unordered_list_found = True
|
||||
|
||||
|
||||
# Check list items
|
||||
items = list(block.items())
|
||||
self.assertGreater(len(items), 0, "Unordered list should have items")
|
||||
|
||||
self.assertGreater(
|
||||
len(items), 0, "Unordered list should have items")
|
||||
|
||||
elif block.style == ListStyle.ORDERED:
|
||||
ordered_list_found = True
|
||||
|
||||
|
||||
# Check list items
|
||||
items = list(block.items())
|
||||
self.assertGreater(len(items), 0, "Ordered list should have items")
|
||||
|
||||
self.assertGreater(
|
||||
len(items), 0, "Ordered list should have items")
|
||||
|
||||
elif isinstance(block, Quote):
|
||||
quote_found = True
|
||||
|
||||
self.assertTrue(unordered_list_found, "Should find unordered list in chapter 3")
|
||||
|
||||
self.assertTrue(
|
||||
unordered_list_found,
|
||||
"Should find unordered list in chapter 3")
|
||||
self.assertTrue(ordered_list_found, "Should find ordered list in chapter 3")
|
||||
self.assertTrue(quote_found, "Should find quote in chapter 3")
|
||||
|
||||
|
||||
def test_epub_tables(self):
|
||||
"""Test that tables in EPUB are properly parsed."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
|
||||
chapters = list(book.chapters)
|
||||
|
||||
|
||||
# Check tables in chapter 4 (index 3)
|
||||
if len(chapters) > 3:
|
||||
chapter4_blocks = list(chapters[3].blocks)
|
||||
|
||||
|
||||
# Find table blocks
|
||||
table_found = False
|
||||
code_block_found = False
|
||||
|
||||
|
||||
for block in chapter4_blocks:
|
||||
if isinstance(block, Table):
|
||||
table_found = True
|
||||
|
||||
|
||||
# Check table has rows
|
||||
rows = list(block.all_rows())
|
||||
self.assertGreater(len(rows), 0, "Table should have rows")
|
||||
|
||||
|
||||
elif isinstance(block, CodeBlock):
|
||||
code_block_found = True
|
||||
|
||||
|
||||
# Check code block has lines
|
||||
lines = list(block.lines())
|
||||
self.assertGreater(len(lines), 0, "Code block should have lines")
|
||||
|
||||
|
||||
self.assertTrue(table_found, "Should find table in chapter 4")
|
||||
self.assertTrue(code_block_found, "Should find code block in chapter 4")
|
||||
|
||||
|
||||
def test_epub_nested_content(self):
|
||||
"""Test that nested content structures are properly parsed."""
|
||||
epub_path = self.create_epub_with_nested_content()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
|
||||
chapters = list(book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
|
||||
chapter_blocks = list(chapters[0].blocks)
|
||||
self.assertGreater(len(chapter_blocks), 0)
|
||||
|
||||
|
||||
# Should have multiple headings (h1, h2, h3, h4)
|
||||
headings = [block for block in chapter_blocks if isinstance(block, Heading)]
|
||||
self.assertGreater(len(headings), 2, "Should have multiple headings from nested content")
|
||||
|
||||
self.assertGreater(
|
||||
len(headings),
|
||||
2,
|
||||
"Should have multiple headings from nested content")
|
||||
|
||||
# Should have paragraphs and lists from nested content
|
||||
paragraphs = [block for block in chapter_blocks if isinstance(block, Paragraph)]
|
||||
lists = [block for block in chapter_blocks if isinstance(block, HList)]
|
||||
|
||||
self.assertGreater(len(paragraphs), 0, "Should have paragraphs from nested content")
|
||||
|
||||
self.assertGreater(
|
||||
len(paragraphs),
|
||||
0,
|
||||
"Should have paragraphs from nested content")
|
||||
self.assertGreater(len(lists), 0, "Should have lists from nested content")
|
||||
|
||||
|
||||
def test_epub_metadata_extraction(self):
|
||||
"""Test that EPUB metadata is properly extracted."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
|
||||
# Check basic metadata
|
||||
self.assertEqual(book.title, "Complex Test Book")
|
||||
|
||||
|
||||
# Check that metadata was set (implementation may vary)
|
||||
# This tests that the metadata parsing doesn't crash
|
||||
self.assertIsNotNone(book.title)
|
||||
|
||||
|
||||
def test_epub_reader_class_direct(self):
|
||||
"""Test EPUBReader class directly."""
|
||||
epub_path = self.create_simple_epub()
|
||||
|
||||
|
||||
reader = EPUBReader(epub_path)
|
||||
book = reader.read()
|
||||
|
||||
|
||||
self.assertIsInstance(book, Book)
|
||||
self.assertEqual(book.title, "Test Book")
|
||||
|
||||
|
||||
def test_invalid_epub_handling(self):
|
||||
"""Test handling of invalid EPUB files."""
|
||||
# Create a non-EPUB file
|
||||
invalid_path = os.path.join(self.test_dir, 'invalid.epub')
|
||||
with open(invalid_path, 'w') as f:
|
||||
f.write("This is not an EPUB file")
|
||||
|
||||
|
||||
# Should raise an exception or handle gracefully
|
||||
with self.assertRaises(Exception):
|
||||
read_epub(invalid_path)
|
||||
|
||||
|
||||
def test_nonexistent_epub_handling(self):
|
||||
"""Test handling of nonexistent EPUB files."""
|
||||
nonexistent_path = os.path.join(self.test_dir, 'nonexistent.epub')
|
||||
|
||||
|
||||
# Should raise an exception
|
||||
with self.assertRaises(Exception):
|
||||
read_epub(nonexistent_path)
|
||||
|
||||
|
||||
def test_epub_with_custom_metadata(self):
|
||||
"""Test EPUB with various metadata fields."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
|
||||
# Set comprehensive metadata
|
||||
book.set_identifier('custom-metadata-test')
|
||||
book.set_title('Custom Metadata Test')
|
||||
book.set_language('en')
|
||||
book.add_author('Primary Author')
|
||||
book.add_author('Secondary Author')
|
||||
book.add_metadata('DC', 'description', 'A comprehensive test of metadata extraction')
|
||||
book.add_metadata(
|
||||
'DC',
|
||||
'description',
|
||||
'A comprehensive test of metadata extraction')
|
||||
book.add_metadata('DC', 'subject', 'Testing')
|
||||
book.add_metadata('DC', 'subject', 'EPUB')
|
||||
book.add_metadata('DC', 'date', '2024-06-07')
|
||||
book.add_metadata('DC', 'publisher', 'Test Publishing House')
|
||||
book.add_metadata('DC', 'rights', 'Public Domain')
|
||||
|
||||
|
||||
# Simple chapter
|
||||
chapter = epub.EpubHtml(
|
||||
title='Metadata Test',
|
||||
@@ -574,22 +591,23 @@ class TestEPUBReader(unittest.TestCase):
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
book.add_item(chapter)
|
||||
book.toc = (epub.Link("metadata.xhtml", "Metadata Test", "meta"),)
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
|
||||
# Write and test
|
||||
epub_path = os.path.join(self.test_dir, f'test_metadata_{len(self.epub_files)}.epub')
|
||||
epub_path = os.path.join(self.test_dir,
|
||||
f'test_metadata_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
|
||||
# Read and verify
|
||||
parsed_book = read_epub(epub_path)
|
||||
self.assertEqual(parsed_book.title, "Custom Metadata Test")
|
||||
|
||||
|
||||
# Verify chapters were created
|
||||
chapters = list(parsed_book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
@@ -597,12 +615,12 @@ class TestEPUBReader(unittest.TestCase):
|
||||
|
||||
class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
"""Test cases that specifically verify EPUB reader uses html_extraction properly."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.epub_files = []
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
for epub_file in self.epub_files:
|
||||
@@ -610,10 +628,10 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
os.remove(epub_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if os.path.exists(self.test_dir):
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@unittest.skipUnless(EBOOKLIB_AVAILABLE, "ebooklib not available")
|
||||
def test_html_extraction_integration(self):
|
||||
"""Test that EPUB reader properly uses html_extraction functionality."""
|
||||
@@ -623,7 +641,7 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
book.set_title('HTML Extraction Test')
|
||||
book.set_language('en')
|
||||
book.add_author('Test Author')
|
||||
|
||||
|
||||
# Chapter that exercises html_extraction features
|
||||
chapter = epub.EpubHtml(
|
||||
title='HTML Features',
|
||||
@@ -635,22 +653,22 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
<head><title>HTML Features</title></head>
|
||||
<body>
|
||||
<h1>HTML Extraction Test</h1>
|
||||
|
||||
|
||||
<!-- Test paragraph with inline formatting -->
|
||||
<p>This paragraph has <strong>bold</strong>, <em>italic</em>,
|
||||
<p>This paragraph has <strong>bold</strong>, <em>italic</em>,
|
||||
<u>underlined</u>, and <span style="color: #ff0000; font-weight: bold;">styled</span> text.</p>
|
||||
|
||||
|
||||
<!-- Test headings -->
|
||||
<h2>Second Level Heading</h2>
|
||||
<h3>Third Level Heading</h3>
|
||||
|
||||
|
||||
<!-- Test lists with styled content -->
|
||||
<ul>
|
||||
<li>Plain list item</li>
|
||||
<li><strong>Bold list item</strong></li>
|
||||
<li>List item with <em>italic text</em></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- Test table with styled cells -->
|
||||
<table>
|
||||
<tr>
|
||||
@@ -662,19 +680,19 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
<td>Normal text</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- Test blockquote -->
|
||||
<blockquote>
|
||||
<p>This is a quoted paragraph with <strong>bold text</strong>.</p>
|
||||
</blockquote>
|
||||
|
||||
|
||||
<!-- Test code block -->
|
||||
<pre><code>def test_function():
|
||||
return "Hello, World!"</code></pre>
|
||||
|
||||
|
||||
<!-- Test nested formatting -->
|
||||
<p>Nested formatting: <strong>bold with <em>italic nested</em> inside</strong>.</p>
|
||||
|
||||
|
||||
<!-- Test color variations -->
|
||||
<p>
|
||||
<span style="color: red;">Red text</span>,
|
||||
@@ -684,26 +702,26 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
book.add_item(chapter)
|
||||
book.toc = (epub.Link("html_features.xhtml", "HTML Features", "html"),)
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
|
||||
# Write EPUB
|
||||
epub_path = os.path.join(self.test_dir, 'html_extraction_test.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
|
||||
# Read and analyze
|
||||
parsed_book = read_epub(epub_path)
|
||||
chapters = list(parsed_book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
|
||||
blocks = list(chapters[0].blocks)
|
||||
self.assertGreater(len(blocks), 5) # Should have multiple blocks
|
||||
|
||||
|
||||
# Test that we get the expected block types
|
||||
block_types = [type(block).__name__ for block in blocks]
|
||||
self.assertIn('Heading', block_types, "Should have heading blocks")
|
||||
@@ -712,29 +730,31 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
self.assertIn('Table', block_types, "Should have table blocks")
|
||||
self.assertIn('Quote', block_types, "Should have quote blocks")
|
||||
self.assertIn('CodeBlock', block_types, "Should have code blocks")
|
||||
|
||||
|
||||
# Test styled content was preserved
|
||||
styled_content_found = False
|
||||
for block in blocks:
|
||||
if isinstance(block, Paragraph):
|
||||
words = list(block.words_iter())
|
||||
for _, word in words:
|
||||
if (word.style.weight == FontWeight.BOLD or
|
||||
if (word.style.weight == FontWeight.BOLD or
|
||||
word.style.style == FontStyle.ITALIC or
|
||||
word.style.decoration == TextDecoration.UNDERLINE or
|
||||
word.style.colour != (0, 0, 0)):
|
||||
word.style.colour != (0, 0, 0)):
|
||||
styled_content_found = True
|
||||
break
|
||||
if styled_content_found:
|
||||
break
|
||||
|
||||
self.assertTrue(styled_content_found, "Should find styled content in parsed blocks")
|
||||
|
||||
|
||||
self.assertTrue(
|
||||
styled_content_found,
|
||||
"Should find styled content in parsed blocks")
|
||||
|
||||
# Test specific color parsing
|
||||
red_text_found = False
|
||||
green_text_found = False
|
||||
blue_text_found = False
|
||||
|
||||
|
||||
for block in blocks:
|
||||
if isinstance(block, (Paragraph, Table)):
|
||||
if isinstance(block, Paragraph):
|
||||
@@ -746,12 +766,11 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
green_text_found = True
|
||||
elif word.style.colour == (0, 0, 255): # Blue
|
||||
blue_text_found = True
|
||||
|
||||
|
||||
# At least one color should be found (depending on implementation)
|
||||
color_found = red_text_found or green_text_found or blue_text_found
|
||||
self.assertTrue(color_found, "Should find at least one colored text")
|
||||
|
||||
|
||||
def test_epub_with_image(self):
|
||||
"""Test that images in EPUB are properly parsed."""
|
||||
book = epub.EpubBook()
|
||||
@@ -759,20 +778,31 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
book.set_title('Image Test Book')
|
||||
book.set_language('en')
|
||||
book.add_author('Test Author')
|
||||
|
||||
|
||||
# Create minimal JPEG data for testing
|
||||
img_data = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342\xff\xc0\x00\x11\x08\x00d\x00d\x01\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00\xaa\xff\xd9'
|
||||
|
||||
img_data = (
|
||||
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00'
|
||||
b'\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t'
|
||||
b'\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a'
|
||||
b'\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342'
|
||||
b'\xff\xc0\x00\x11\x08\x00d\x00d\x01\x01\x11\x00\x02\x11\x01\x03'
|
||||
b'\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x10\x01'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00'
|
||||
b'\xaa\xff\xd9'
|
||||
)
|
||||
|
||||
# Create an EpubImage item
|
||||
image_item = epub.EpubImage()
|
||||
image_item.id = 'test_img'
|
||||
image_item.file_name = 'images/test_image.jpg'
|
||||
image_item.media_type = 'image/jpeg'
|
||||
image_item.content = img_data
|
||||
|
||||
|
||||
# Add image to book
|
||||
book.add_item(image_item)
|
||||
|
||||
|
||||
# Create a chapter that references the image
|
||||
chapter = epub.EpubHtml(
|
||||
title='Image Chapter',
|
||||
@@ -788,36 +818,43 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
<p>Text after the image.</p>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
book.add_item(chapter)
|
||||
book.toc = (epub.Link("image_chapter.xhtml", "Image Chapter", "img_ch"),)
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
|
||||
# Write EPUB
|
||||
epub_path = os.path.join(self.test_dir, f'test_image_{len(self.epub_files)}.epub')
|
||||
epub_path = os.path.join(self.test_dir,
|
||||
f'test_image_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
|
||||
# Read and analyze
|
||||
parsed_book = read_epub(epub_path)
|
||||
chapters = list(parsed_book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
|
||||
blocks = list(chapters[0].blocks)
|
||||
self.assertGreater(len(blocks), 0)
|
||||
|
||||
|
||||
# Find blocks by type
|
||||
heading_blocks = [block for block in blocks if isinstance(block, Heading)]
|
||||
paragraph_blocks = [block for block in blocks if isinstance(block, Paragraph)]
|
||||
image_blocks = [block for block in blocks if isinstance(block, Image)]
|
||||
|
||||
|
||||
# Verify we have the expected blocks
|
||||
self.assertEqual(len(heading_blocks), 1, "Should find exactly one heading block")
|
||||
self.assertGreaterEqual(len(paragraph_blocks), 2, "Should find at least two paragraph blocks")
|
||||
self.assertEqual(
|
||||
len(heading_blocks),
|
||||
1,
|
||||
"Should find exactly one heading block")
|
||||
self.assertGreaterEqual(
|
||||
len(paragraph_blocks),
|
||||
2,
|
||||
"Should find at least two paragraph blocks")
|
||||
self.assertEqual(len(image_blocks), 1, "Should find exactly one image block")
|
||||
|
||||
|
||||
# Verify image properties
|
||||
image_block = image_blocks[0]
|
||||
self.assertEqual(image_block.alt_text, "Test image")
|
||||
|
||||
@@ -14,14 +14,15 @@ from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
class TestHTMLParagraph(unittest.TestCase):
|
||||
"""Test cases for basic paragraph parsing."""
|
||||
|
||||
|
||||
def test_simple(self):
|
||||
text = "<p>This is a paragraph.</p>"
|
||||
paragraphs = parse_html_string(text)
|
||||
self.assertEqual(len(paragraphs), 1)
|
||||
self.assertEqual(len(paragraphs[0]), 4)
|
||||
|
||||
for w1, t1 in zip(paragraphs[0].words_iter(), "This is a paragraph.".split(" ")):
|
||||
for w1, t1 in zip(paragraphs[0].words_iter(),
|
||||
"This is a paragraph.".split(" ")):
|
||||
self.assertEqual(w1[1].text, t1)
|
||||
|
||||
def test_multiple(self):
|
||||
@@ -31,26 +32,28 @@ class TestHTMLParagraph(unittest.TestCase):
|
||||
self.assertEqual(len(paragraphs[0]), 4)
|
||||
self.assertEqual(len(paragraphs[1]), 4)
|
||||
|
||||
for w1, t1 in zip(paragraphs[0].words_iter(), "This is a paragraph.".split(" ")):
|
||||
for w1, t1 in zip(paragraphs[0].words_iter(),
|
||||
"This is a paragraph.".split(" ")):
|
||||
self.assertEqual(w1[1].text, t1)
|
||||
|
||||
for w1, t1 in zip(paragraphs[1].words_iter(), "This is another paragraph.".split(" ")):
|
||||
for w1, t1 in zip(paragraphs[1].words_iter(),
|
||||
"This is another paragraph.".split(" ")):
|
||||
self.assertEqual(w1[1].text, t1)
|
||||
|
||||
|
||||
class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
"""Test cases for paragraphs with inline styling."""
|
||||
|
||||
|
||||
def test_bold_text(self):
|
||||
"""Test paragraphs with bold text using <strong> and <b> tags."""
|
||||
text = "<p>This is <strong>bold text</strong> in a paragraph.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
self.assertEqual(len(words), 7) # "This is bold text in a paragraph."
|
||||
|
||||
|
||||
# Check that 'bold' and 'text' words have bold font weight
|
||||
bold_word = words[2][1] # 'bold'
|
||||
text_word = words[3][1] # 'text'
|
||||
@@ -58,7 +61,7 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
self.assertEqual(bold_word.style.weight, FontWeight.BOLD)
|
||||
self.assertEqual(text_word.text, "text")
|
||||
self.assertEqual(text_word.style.weight, FontWeight.BOLD)
|
||||
|
||||
|
||||
# Check that other words are not bold
|
||||
normal_word = words[0][1] # 'This'
|
||||
self.assertEqual(normal_word.text, "This")
|
||||
@@ -70,9 +73,9 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
|
||||
|
||||
# Check that 'italic' and 'text' words have italic font style
|
||||
italic_word = words[2][1] # 'italic'
|
||||
text_word = words[3][1] # 'text'
|
||||
@@ -86,7 +89,7 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
text = "<p>This is <u>underlined text</u> here.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
underlined_word = words[2][1] # 'underlined'
|
||||
self.assertEqual(underlined_word.style.decoration, TextDecoration.UNDERLINE)
|
||||
@@ -96,50 +99,60 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
text = "<p>This is <s>strikethrough text</s> here.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
strike_word = words[2][1] # 'strikethrough'
|
||||
self.assertEqual(strike_word.style.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
def test_span_with_inline_styles(self):
|
||||
"""Test paragraphs with span elements containing inline CSS styles."""
|
||||
text = '<p>This text is normal, but <span style="color: red; font-weight: bold;">this part is red and bold</span>.</p>'
|
||||
text = (
|
||||
'<p>This text is normal, but <span style="color: red; font-weight: bold;">'
|
||||
'this part is red and bold</span>.</p>'
|
||||
)
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
|
||||
|
||||
# Find the styled words
|
||||
styled_words = []
|
||||
for _, word in words:
|
||||
if word.text in ["this", "part", "is", "red", "and", "bold"]:
|
||||
if word.style.weight == FontWeight.BOLD:
|
||||
styled_words.append(word)
|
||||
|
||||
self.assertGreater(len(styled_words), 0, "Should have bold words in styled span")
|
||||
|
||||
|
||||
self.assertGreater(
|
||||
len(styled_words),
|
||||
0,
|
||||
"Should have bold words in styled span")
|
||||
|
||||
# Check that at least one word has the red color
|
||||
red_words = [w for w in styled_words if w.style.colour == (255, 0, 0)]
|
||||
self.assertGreater(len(red_words), 0, "Should have red colored words")
|
||||
|
||||
def test_mixed_formatting(self):
|
||||
"""Test paragraphs with multiple formatting elements combined."""
|
||||
text = "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style=\"color: blue;\">blue</span>, and <mark>highlighted</mark> text all together.</p>"
|
||||
text = (
|
||||
'<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, '
|
||||
'<span style="color: blue;">blue</span>, and <mark>highlighted</mark> '
|
||||
'text all together.</p>'
|
||||
)
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
|
||||
|
||||
# Check for bold word
|
||||
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
|
||||
|
||||
# Check for italic word
|
||||
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(italic_words), 0, "Should have italic words")
|
||||
|
||||
|
||||
# Check for blue colored word
|
||||
blue_words = [w for _, w in words if w.style.colour == (0, 0, 255)]
|
||||
self.assertGreater(len(blue_words), 0, "Should have blue colored words")
|
||||
@@ -149,26 +162,29 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
text = "<p>This has <strong>bold with <em>italic inside</em></strong> formatting.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
|
||||
|
||||
# Find words that should be both bold and italic
|
||||
bold_italic_words = [w for _, w in words
|
||||
if w.style.weight == FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(bold_italic_words), 0, "Should have words that are both bold and italic")
|
||||
bold_italic_words = [w for _, w in words if w.style.weight ==
|
||||
FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(
|
||||
len(bold_italic_words),
|
||||
0,
|
||||
"Should have words that are both bold and italic")
|
||||
|
||||
def test_color_variations(self):
|
||||
"""Test different color formats in CSS."""
|
||||
text = '<p><span style="color: #ff0000;">Hex red</span> and <span style="color: green;">Named green</span>.</p>'
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
|
||||
words = list(blocks[0].words_iter())
|
||||
|
||||
|
||||
# Check for hex red color
|
||||
hex_red_words = [w for _, w in words if w.style.colour == (255, 0, 0)]
|
||||
self.assertGreater(len(hex_red_words), 0, "Should have hex red colored words")
|
||||
|
||||
|
||||
# Check for named green color
|
||||
green_words = [w for _, w in words if w.style.colour == (0, 255, 0)]
|
||||
self.assertGreater(len(green_words), 0, "Should have green colored words")
|
||||
@@ -176,7 +192,7 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
|
||||
class TestHTMLBlockElements(unittest.TestCase):
|
||||
"""Test cases for block-level HTML elements."""
|
||||
|
||||
|
||||
def test_body_element(self):
|
||||
"""Test parsing of body element containing other elements."""
|
||||
text = "<body><p>Paragraph one.</p><p>Paragraph two.</p></body>"
|
||||
@@ -195,17 +211,20 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
|
||||
def test_headings(self):
|
||||
"""Test all heading levels h1-h6."""
|
||||
text = "<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
||||
text = (
|
||||
"<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
|
||||
"<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
||||
)
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 6)
|
||||
|
||||
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
|
||||
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
|
||||
|
||||
|
||||
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
|
||||
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
|
||||
|
||||
for i, block in enumerate(blocks):
|
||||
self.assertIsInstance(block, Heading)
|
||||
self.assertEqual(block.level, expected_levels[i])
|
||||
|
||||
|
||||
words = list(block.words_iter())
|
||||
self.assertEqual(len(words), 2) # "Heading" and number
|
||||
self.assertEqual(words[0][1].text, "Heading")
|
||||
@@ -216,7 +235,7 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Quote)
|
||||
|
||||
|
||||
# Check that the quote contains a paragraph
|
||||
quote_blocks = list(blocks[0].blocks())
|
||||
self.assertEqual(len(quote_blocks), 1)
|
||||
@@ -228,7 +247,7 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], CodeBlock)
|
||||
|
||||
|
||||
lines = list(blocks[0].lines())
|
||||
self.assertGreater(len(lines), 0)
|
||||
|
||||
@@ -239,7 +258,7 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], HList)
|
||||
self.assertEqual(blocks[0].style, ListStyle.UNORDERED)
|
||||
|
||||
|
||||
items = list(blocks[0].items())
|
||||
self.assertEqual(len(items), 3)
|
||||
|
||||
@@ -257,10 +276,10 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], HList)
|
||||
|
||||
|
||||
items = list(blocks[0].items())
|
||||
self.assertEqual(len(items), 3)
|
||||
|
||||
|
||||
# Check second item has bold text
|
||||
second_item_blocks = list(items[1].blocks())
|
||||
if second_item_blocks:
|
||||
@@ -306,12 +325,12 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
"""
|
||||
blocks = parse_html_string(text)
|
||||
self.assertGreater(len(blocks), 2)
|
||||
|
||||
|
||||
# Should have at least a heading, paragraph, and quote
|
||||
has_heading = any(isinstance(b, Heading) for b in blocks)
|
||||
has_paragraph = any(isinstance(b, Paragraph) for b in blocks)
|
||||
has_quote = any(isinstance(b, Quote) for b in blocks)
|
||||
|
||||
|
||||
self.assertTrue(has_heading, "Should contain a heading")
|
||||
self.assertTrue(has_paragraph, "Should contain a paragraph")
|
||||
self.assertTrue(has_quote, "Should contain a quote")
|
||||
@@ -322,7 +341,7 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
blocks = parse_html_string(text)
|
||||
# Empty elements may not create blocks, which is acceptable behavior
|
||||
self.assertGreaterEqual(len(blocks), 0)
|
||||
|
||||
|
||||
# Test that empty paragraph with some content does create a block
|
||||
text_with_content = "<p> </p>" # Contains whitespace
|
||||
blocks_with_content = parse_html_string(text_with_content)
|
||||
@@ -332,7 +351,7 @@ class TestHTMLBlockElements(unittest.TestCase):
|
||||
|
||||
class TestHTMLComplexStructures(unittest.TestCase):
|
||||
"""Test cases for complex HTML structures combining multiple features."""
|
||||
|
||||
|
||||
def test_article_with_mixed_content(self):
|
||||
"""Test a realistic article structure with mixed content."""
|
||||
text = """
|
||||
@@ -350,7 +369,7 @@ class TestHTMLComplexStructures(unittest.TestCase):
|
||||
"""
|
||||
blocks = parse_html_string(text)
|
||||
self.assertGreater(len(blocks), 3)
|
||||
|
||||
|
||||
# Verify we have the expected block types
|
||||
block_types = [type(b).__name__ for b in blocks]
|
||||
self.assertIn('Heading', block_types)
|
||||
@@ -383,12 +402,12 @@ class TestHTMLComplexStructures(unittest.TestCase):
|
||||
|
||||
class TestHTMLFontRegistryIntegration(unittest.TestCase):
|
||||
"""Test cases for font registry integration with HTML extraction."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.doc = Document("Test Document", "en-US")
|
||||
self.base_font = Font(font_size=16, colour=(0, 0, 0))
|
||||
|
||||
|
||||
def test_font_registry_creates_fonts(self):
|
||||
"""Test that HTML parsing with document context creates fonts in registry."""
|
||||
html_content = """
|
||||
@@ -397,21 +416,21 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
|
||||
<h1>Main Header</h1>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
# Initially empty style registry
|
||||
initial_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
|
||||
# Parse HTML with document context
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
|
||||
|
||||
# Should have created styles for different formatting
|
||||
final_style_count = self.doc.get_style_registry().get_style_count()
|
||||
self.assertGreater(final_style_count, initial_style_count,
|
||||
"Should have created styles in registry")
|
||||
|
||||
self.assertGreater(final_style_count, initial_style_count,
|
||||
"Should have created styles in registry")
|
||||
|
||||
# Should have created blocks
|
||||
self.assertGreater(len(blocks), 0, "Should have created blocks")
|
||||
|
||||
|
||||
def test_font_registry_reuses_fonts(self):
|
||||
"""Test that parsing same content reuses existing styles."""
|
||||
html_content = """
|
||||
@@ -420,23 +439,23 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
|
||||
<h1>Main Header</h1>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
# First parse
|
||||
blocks1 = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
first_parse_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
|
||||
# Second parse with same content
|
||||
blocks2 = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
second_parse_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
|
||||
# Style count should not increase on second parse
|
||||
self.assertEqual(first_parse_style_count, second_parse_style_count,
|
||||
"Should reuse existing styles instead of creating new ones")
|
||||
|
||||
"Should reuse existing styles instead of creating new ones")
|
||||
|
||||
# Both parses should create same number of blocks
|
||||
self.assertEqual(len(blocks1), len(blocks2),
|
||||
"Should create same structure on both parses")
|
||||
|
||||
"Should create same structure on both parses")
|
||||
|
||||
def test_font_registry_different_styles_create_different_fonts(self):
|
||||
"""Test that different styles create different style objects."""
|
||||
# Create styles with different properties
|
||||
@@ -449,65 +468,71 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
|
||||
style_id3, style3 = self.doc.get_or_create_style(
|
||||
font_size=14, color=(0, 255, 0), font_weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
|
||||
# Should be different style IDs
|
||||
self.assertNotEqual(style_id1, style_id2, "Different sizes should create different styles")
|
||||
self.assertNotEqual(style_id1, style_id3, "Different colors should create different styles")
|
||||
self.assertNotEqual(
|
||||
style_id1,
|
||||
style_id2,
|
||||
"Different sizes should create different styles")
|
||||
self.assertNotEqual(
|
||||
style_id1,
|
||||
style_id3,
|
||||
"Different colors should create different styles")
|
||||
self.assertNotEqual(style_id2, style_id3, "All styles should be different")
|
||||
|
||||
|
||||
# Should have multiple styles in registry
|
||||
self.assertGreaterEqual(self.doc.get_style_registry().get_style_count(), 3)
|
||||
|
||||
|
||||
def test_font_registry_integration_with_html_styles(self):
|
||||
"""Test that HTML parsing uses style registry for styled content."""
|
||||
html_content = """
|
||||
<p>Normal text with <strong>bold</strong> and <em>italic</em> and
|
||||
<p>Normal text with <strong>bold</strong> and <em>italic</em> and
|
||||
<span style="color: red;">red text</span>.</p>
|
||||
"""
|
||||
|
||||
|
||||
# Parse content
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
|
||||
|
||||
# Extract all words from the paragraph
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words_iter())
|
||||
|
||||
|
||||
# Find words with different styles
|
||||
normal_words = [w for _, w in words if w.style.weight == FontWeight.NORMAL
|
||||
and w.style.style == FontStyle.NORMAL]
|
||||
normal_words = [w for _, w in words if w.style.weight == FontWeight.NORMAL
|
||||
and w.style.style == FontStyle.NORMAL]
|
||||
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
|
||||
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
|
||||
red_words = [w for _, w in words if w.style.colour == (255, 0, 0)]
|
||||
|
||||
|
||||
# Should have words with different styles
|
||||
self.assertGreater(len(normal_words), 0, "Should have normal words")
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
self.assertGreater(len(italic_words), 0, "Should have italic words")
|
||||
self.assertGreater(len(red_words), 0, "Should have red words")
|
||||
|
||||
|
||||
# Style registry should contain multiple styles for different formatting
|
||||
self.assertGreater(self.doc.get_style_registry().get_style_count(), 1,
|
||||
"Should have multiple styles for different formatting")
|
||||
|
||||
self.assertGreater(self.doc.get_style_registry().get_style_count(), 1,
|
||||
"Should have multiple styles for different formatting")
|
||||
|
||||
def test_font_registry_without_document_context(self):
|
||||
"""Test that parsing without document context works (fallback behavior)."""
|
||||
html_content = "<p>This is <strong>bold text</strong>.</p>"
|
||||
|
||||
|
||||
# Get initial style count (should include default style)
|
||||
initial_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
|
||||
# Parse without document context
|
||||
blocks = parse_html_string(html_content, self.base_font)
|
||||
|
||||
|
||||
# Should still create blocks successfully
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
|
||||
# Should not affect document's style registry
|
||||
final_style_count = self.doc.get_style_registry().get_style_count()
|
||||
self.assertEqual(final_style_count, initial_style_count,
|
||||
"Document style registry should remain unchanged")
|
||||
|
||||
self.assertEqual(final_style_count, initial_style_count,
|
||||
"Document style registry should remain unchanged")
|
||||
|
||||
def test_complex_html_font_reuse(self):
|
||||
"""Test style reuse with complex HTML containing repeated styles."""
|
||||
html_content = """
|
||||
@@ -518,46 +543,46 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
|
||||
<p>Another paragraph with <strong>bold</strong> text.</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
# Parse content
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
style_count_after_parse = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
|
||||
# Parse same content again
|
||||
blocks2 = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
style_count_after_second_parse = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
|
||||
# Style count should not increase on second parse
|
||||
self.assertEqual(style_count_after_parse, style_count_after_second_parse,
|
||||
"Styles should be reused for repeated formatting")
|
||||
|
||||
"Styles should be reused for repeated formatting")
|
||||
|
||||
# Both should create same structure
|
||||
self.assertEqual(len(blocks), len(blocks2))
|
||||
|
||||
|
||||
def test_font_registry_with_nested_styles(self):
|
||||
"""Test style registry with nested HTML styles."""
|
||||
html_content = """
|
||||
<p>Text with <strong>bold and <em>bold italic</em> nested</strong> styles.</p>
|
||||
"""
|
||||
|
||||
|
||||
# Parse content
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
|
||||
|
||||
# Should create styles for different style combinations
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words_iter())
|
||||
|
||||
|
||||
# Find words that are both bold and italic
|
||||
bold_italic_words = [w for _, w in words
|
||||
if w.style.weight == FontWeight.BOLD
|
||||
and w.style.style == FontStyle.ITALIC]
|
||||
|
||||
self.assertGreater(len(bold_italic_words), 0,
|
||||
"Should have words with combined bold+italic style")
|
||||
|
||||
bold_italic_words = [w for _, w in words
|
||||
if w.style.weight == FontWeight.BOLD
|
||||
and w.style.style == FontStyle.ITALIC]
|
||||
|
||||
self.assertGreater(len(bold_italic_words), 0,
|
||||
"Should have words with combined bold+italic style")
|
||||
|
||||
# Should have multiple styles in registry for different combinations
|
||||
self.assertGreater(self.doc.get_style_registry().get_style_count(), 1,
|
||||
"Should create separate styles for style combinations")
|
||||
"Should create separate styles for style combinations")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -6,7 +6,7 @@ reusing test patterns from test_html_extraction.py that are known to pass.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from bs4 import BeautifulSoup
|
||||
from pyWebLayout.io.readers.html_extraction import (
|
||||
create_base_context,
|
||||
apply_element_styling,
|
||||
@@ -50,11 +50,11 @@ from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
class TestUtilityFunctions(unittest.TestCase):
|
||||
"""Test cases for utility functions."""
|
||||
|
||||
|
||||
def test_create_base_context(self):
|
||||
"""Test creation of base style context."""
|
||||
context = create_base_context()
|
||||
|
||||
|
||||
self.assertIsInstance(context, StyleContext)
|
||||
self.assertIsInstance(context.font, Font)
|
||||
self.assertIsNone(context.background)
|
||||
@@ -62,113 +62,119 @@ class TestUtilityFunctions(unittest.TestCase):
|
||||
self.assertEqual(context.css_styles, {})
|
||||
self.assertEqual(context.element_attributes, {})
|
||||
self.assertEqual(context.parent_elements, [])
|
||||
|
||||
|
||||
def test_parse_inline_styles_from_existing_tests(self):
|
||||
"""Test parsing CSS inline styles - adapted from test_span_with_inline_styles."""
|
||||
# From: '<span style="color: red; font-weight: bold;">this part is red and bold</span>'
|
||||
style_text = "color: red; font-weight: bold;"
|
||||
styles = parse_inline_styles(style_text)
|
||||
|
||||
|
||||
expected = {
|
||||
"color": "red",
|
||||
"font-weight": "bold"
|
||||
}
|
||||
self.assertEqual(styles, expected)
|
||||
|
||||
|
||||
def test_parse_inline_styles_color_variations(self):
|
||||
"""Test parsing different color formats - adapted from test_color_variations."""
|
||||
# Test hex color parsing
|
||||
hex_style = "color: #ff0000;"
|
||||
styles = parse_inline_styles(hex_style)
|
||||
self.assertEqual(styles.get("color"), "#ff0000")
|
||||
|
||||
# Test named color parsing
|
||||
|
||||
# Test named color parsing
|
||||
named_style = "color: green;"
|
||||
styles = parse_inline_styles(named_style)
|
||||
self.assertEqual(styles.get("color"), "green")
|
||||
|
||||
|
||||
def test_apply_element_font_styles_bold_elements(self):
|
||||
"""Test font style application for bold elements - adapted from test_bold_text."""
|
||||
base_font = Font()
|
||||
|
||||
|
||||
# Test <strong> tag - from "<strong>bold text</strong>"
|
||||
font = apply_element_font_styles(base_font, "strong", {})
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
|
||||
|
||||
# Test <b> tag
|
||||
font = apply_element_font_styles(base_font, "b", {})
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
|
||||
|
||||
def test_apply_element_font_styles_italic_elements(self):
|
||||
"""Test font style application for italic elements - adapted from test_italic_text."""
|
||||
base_font = Font()
|
||||
|
||||
|
||||
# Test <em> tag - from "<em>italic text</em>"
|
||||
font = apply_element_font_styles(base_font, "em", {})
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
|
||||
|
||||
# Test <i> tag
|
||||
font = apply_element_font_styles(base_font, "i", {})
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
|
||||
|
||||
def test_apply_element_font_styles_decoration_elements(self):
|
||||
"""Test font decoration - adapted from test_underlined_text and test_strikethrough_text."""
|
||||
base_font = Font()
|
||||
|
||||
|
||||
# Test <u> tag - from "<u>underlined text</u>"
|
||||
font = apply_element_font_styles(base_font, "u", {})
|
||||
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||
|
||||
|
||||
# Test <s> tag - from "<s>strikethrough text</s>"
|
||||
font = apply_element_font_styles(base_font, "s", {})
|
||||
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
|
||||
# Test <del> tag
|
||||
font = apply_element_font_styles(base_font, "del", {})
|
||||
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
|
||||
def test_apply_element_font_styles_headings(self):
|
||||
"""Test heading font styles - adapted from test_headings."""
|
||||
base_font = Font()
|
||||
|
||||
|
||||
# Test heading sizes and weights - from test_headings which tests h1-h6
|
||||
headings = [("h1", 24), ("h2", 20), ("h3", 18), ("h4", 16), ("h5", 14), ("h6", 12)]
|
||||
|
||||
headings = [("h1", 24), ("h2", 20), ("h3", 18),
|
||||
("h4", 16), ("h5", 14), ("h6", 12)]
|
||||
|
||||
for tag, expected_size in headings:
|
||||
font = apply_element_font_styles(base_font, tag, {})
|
||||
self.assertEqual(font.font_size, expected_size, f"Size mismatch for {tag}")
|
||||
self.assertEqual(font.weight, FontWeight.BOLD, f"Weight should be bold for {tag}")
|
||||
|
||||
self.assertEqual(
|
||||
font.weight,
|
||||
FontWeight.BOLD,
|
||||
f"Weight should be bold for {tag}")
|
||||
|
||||
def test_apply_element_font_styles_color_parsing(self):
|
||||
"""Test color parsing - adapted from test_color_variations."""
|
||||
base_font = Font()
|
||||
|
||||
|
||||
# Test named colors - from '<span style="color: green;">Named green</span>'
|
||||
css_styles = {"color": "green"}
|
||||
font = apply_element_font_styles(base_font, "span", css_styles)
|
||||
self.assertEqual(font.colour, (0, 255, 0))
|
||||
|
||||
|
||||
# Test hex colors - from '<span style="color: #ff0000;">Hex red</span>'
|
||||
css_styles = {"color": "#ff0000"}
|
||||
font = apply_element_font_styles(base_font, "span", css_styles)
|
||||
self.assertEqual(font.colour, (255, 0, 0))
|
||||
|
||||
|
||||
def test_apply_element_styling_with_classes_and_styles(self):
|
||||
"""Test complete element styling - adapted from test_span_with_inline_styles."""
|
||||
# From: '<span style="color: red; font-weight: bold;">this part is red and bold</span>'
|
||||
soup = BeautifulSoup('<span class="highlight" style="color: red; font-weight: bold;">text</span>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<span class="highlight" style="color: red; font-weight: bold;">text</span>',
|
||||
'html.parser')
|
||||
element = soup.find('span')
|
||||
base_context = create_base_context()
|
||||
|
||||
|
||||
styled_context = apply_element_styling(base_context, element)
|
||||
|
||||
|
||||
# Check CSS classes
|
||||
self.assertIn("highlight", styled_context.css_classes)
|
||||
|
||||
|
||||
# Check CSS styles
|
||||
self.assertEqual(styled_context.css_styles.get("color"), "red")
|
||||
self.assertEqual(styled_context.css_styles.get("font-weight"), "bold")
|
||||
|
||||
|
||||
# Check font styling
|
||||
self.assertEqual(styled_context.font.colour, (255, 0, 0))
|
||||
self.assertEqual(styled_context.font.weight, FontWeight.BOLD)
|
||||
@@ -176,170 +182,185 @@ class TestUtilityFunctions(unittest.TestCase):
|
||||
|
||||
class TestExtractTextContent(unittest.TestCase):
|
||||
"""Test cases for text content extraction."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_context = create_base_context()
|
||||
|
||||
|
||||
def test_extract_simple_text(self):
|
||||
"""Test extracting simple text - adapted from test_simple."""
|
||||
# From: "<p>This is a paragraph.</p>"
|
||||
soup = BeautifulSoup('<p>This is a paragraph.</p>', 'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
|
||||
words = extract_text_content(element, self.base_context)
|
||||
|
||||
|
||||
# Should match the expected word count from original test
|
||||
self.assertEqual(len(words), 4) # "This", "is", "a", "paragraph."
|
||||
self.assertIsInstance(words[0], Word)
|
||||
self.assertEqual(words[0].text, "This")
|
||||
|
||||
|
||||
def test_extract_styled_text_bold(self):
|
||||
"""Test extracting bold styled text - adapted from test_bold_text."""
|
||||
# From: "<p>This is <strong>bold text</strong> in a paragraph.</p>"
|
||||
soup = BeautifulSoup('<span>This is <strong>bold text</strong> in a paragraph.</span>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<span>This is <strong>bold text</strong> in a paragraph.</span>',
|
||||
'html.parser')
|
||||
element = soup.find('span')
|
||||
|
||||
|
||||
words = extract_text_content(element, self.base_context)
|
||||
|
||||
|
||||
# Find the bold words
|
||||
bold_words = [w for w in words if w.style.weight == FontWeight.BOLD]
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
|
||||
|
||||
# Check specific words are bold (from original test expectations)
|
||||
bold_word_texts = [w.text for w in bold_words]
|
||||
self.assertIn("bold", bold_word_texts)
|
||||
self.assertIn("text", bold_word_texts)
|
||||
|
||||
|
||||
def test_extract_nested_formatting(self):
|
||||
"""Test nested formatting - adapted from test_nested_formatting."""
|
||||
# From: "<p>This has <strong>bold with <em>italic inside</em></strong> formatting.</p>"
|
||||
soup = BeautifulSoup('<span>This has <strong>bold with <em>italic inside</em></strong> formatting.</span>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<span>This has <strong>bold with <em>italic inside</em></strong> formatting.</span>',
|
||||
'html.parser')
|
||||
element = soup.find('span')
|
||||
|
||||
|
||||
words = extract_text_content(element, self.base_context)
|
||||
|
||||
|
||||
# Find words that should be both bold and italic
|
||||
bold_italic_words = [w for w in words
|
||||
if w.style.weight == FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(bold_italic_words), 0, "Should have words that are both bold and italic")
|
||||
bold_italic_words = [w for w in words if w.style.weight ==
|
||||
FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(
|
||||
len(bold_italic_words),
|
||||
0,
|
||||
"Should have words that are both bold and italic")
|
||||
|
||||
|
||||
class TestHandlerFunctions(unittest.TestCase):
|
||||
"""Test cases for HTML element handler functions using known working patterns."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_context = create_base_context()
|
||||
|
||||
|
||||
def test_paragraph_handler_simple(self):
|
||||
"""Test paragraph handler - adapted from test_simple."""
|
||||
# From: "<p>This is a paragraph.</p>"
|
||||
soup = BeautifulSoup('<p>This is a paragraph.</p>', 'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
|
||||
result = paragraph_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, Paragraph)
|
||||
# Should match original test expectations
|
||||
self.assertEqual(len(result), 4) # 4 words
|
||||
|
||||
|
||||
words = list(result.words_iter())
|
||||
expected_texts = ["This", "is", "a", "paragraph."]
|
||||
for i, expected_text in enumerate(expected_texts):
|
||||
self.assertEqual(words[i][1].text, expected_text)
|
||||
|
||||
|
||||
def test_heading_handler_all_levels(self):
|
||||
"""Test heading handler - adapted from test_headings."""
|
||||
# From: "<h1>Heading 1</h1><h2>Heading 2</h2>..."
|
||||
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
|
||||
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
|
||||
|
||||
# From: "<h1>Heading 1</h1><h2>Heading 2</h2>..."
|
||||
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
|
||||
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
|
||||
|
||||
for i, expected_level in enumerate(expected_levels, 1):
|
||||
tag = f"h{i}"
|
||||
soup = BeautifulSoup(f'<{tag}>Heading {i}</{tag}>', 'html.parser')
|
||||
element = soup.find(tag)
|
||||
|
||||
|
||||
result = heading_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, Heading)
|
||||
self.assertEqual(result.level, expected_level)
|
||||
|
||||
|
||||
# Should match original test word expectations
|
||||
words = list(result.words_iter())
|
||||
self.assertEqual(len(words), 2) # "Heading" and number
|
||||
self.assertEqual(words[0][1].text, "Heading")
|
||||
|
||||
|
||||
def test_blockquote_handler(self):
|
||||
"""Test blockquote handler - adapted from test_blockquote."""
|
||||
# From: "<blockquote><p>This is a quoted paragraph.</p></blockquote>"
|
||||
soup = BeautifulSoup('<blockquote><p>This is a quoted paragraph.</p></blockquote>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<blockquote><p>This is a quoted paragraph.</p></blockquote>',
|
||||
'html.parser')
|
||||
element = soup.find('blockquote')
|
||||
|
||||
|
||||
result = blockquote_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, Quote)
|
||||
|
||||
|
||||
# Check that the quote contains a paragraph (from original test)
|
||||
quote_blocks = list(result.blocks())
|
||||
self.assertEqual(len(quote_blocks), 1)
|
||||
self.assertIsInstance(quote_blocks[0], Paragraph)
|
||||
|
||||
|
||||
def test_preformatted_handler(self):
|
||||
"""Test preformatted handler - adapted from test_preformatted_code."""
|
||||
# From: "<pre><code>function hello() {\n console.log('Hello');\n}</code></pre>"
|
||||
soup = BeautifulSoup('<pre><code>function hello() {\n console.log(\'Hello\');\n}</code></pre>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<pre><code>function hello() {\n console.log(\'Hello\');\n}</code></pre>',
|
||||
'html.parser')
|
||||
element = soup.find('pre')
|
||||
|
||||
|
||||
result = preformatted_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, CodeBlock)
|
||||
|
||||
|
||||
# Should have lines (from original test expectation)
|
||||
lines = list(result.lines())
|
||||
self.assertGreater(len(lines), 0)
|
||||
|
||||
|
||||
def test_unordered_list_handler(self):
|
||||
"""Test unordered list handler - adapted from test_unordered_list."""
|
||||
# From: "<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>"
|
||||
soup = BeautifulSoup('<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>',
|
||||
'html.parser')
|
||||
element = soup.find('ul')
|
||||
|
||||
|
||||
result = unordered_list_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, HList)
|
||||
self.assertEqual(result.style, ListStyle.UNORDERED)
|
||||
|
||||
|
||||
# Should match original test expectations
|
||||
items = list(result.items())
|
||||
self.assertEqual(len(items), 3)
|
||||
|
||||
|
||||
def test_ordered_list_handler(self):
|
||||
"""Test ordered list handler - adapted from test_ordered_list."""
|
||||
# From: "<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>"
|
||||
soup = BeautifulSoup('<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>',
|
||||
'html.parser')
|
||||
element = soup.find('ol')
|
||||
|
||||
|
||||
result = ordered_list_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, HList)
|
||||
self.assertEqual(result.style, ListStyle.ORDERED)
|
||||
|
||||
|
||||
# Should match original test expectations
|
||||
items = list(result.items())
|
||||
self.assertEqual(len(items), 3) # "First item", "Second item", "Third item"
|
||||
|
||||
|
||||
def test_list_item_handler(self):
|
||||
"""Test list item handler."""
|
||||
soup = BeautifulSoup('<li>List item content</li>', 'html.parser')
|
||||
element = soup.find('li')
|
||||
|
||||
|
||||
result = list_item_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, ListItem)
|
||||
blocks = list(result.blocks())
|
||||
self.assertGreater(len(blocks), 0)
|
||||
|
||||
|
||||
def test_table_handler(self):
|
||||
"""Test table handler - adapted from test_table_basic."""
|
||||
# From test_table_basic structure
|
||||
@@ -356,76 +377,80 @@ class TestHandlerFunctions(unittest.TestCase):
|
||||
</table>
|
||||
''', 'html.parser')
|
||||
element = soup.find('table')
|
||||
|
||||
|
||||
result = table_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, Table)
|
||||
|
||||
|
||||
def test_table_row_handler(self):
|
||||
"""Test table row handler."""
|
||||
soup = BeautifulSoup('<tr><td>Cell 1</td><td>Cell 2</td></tr>', 'html.parser')
|
||||
element = soup.find('tr')
|
||||
|
||||
|
||||
result = table_row_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, TableRow)
|
||||
|
||||
|
||||
def test_table_cell_handler(self):
|
||||
"""Test table cell handler."""
|
||||
soup = BeautifulSoup('<td>Cell content</td>', 'html.parser')
|
||||
element = soup.find('td')
|
||||
|
||||
|
||||
# Apply styling to get attributes
|
||||
styled_context = apply_element_styling(self.base_context, element)
|
||||
result = table_cell_handler(element, styled_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, TableCell)
|
||||
self.assertEqual(result.is_header, False)
|
||||
|
||||
|
||||
def test_table_header_cell_handler(self):
|
||||
"""Test table header cell handler."""
|
||||
soup = BeautifulSoup('<th>Header content</th>', 'html.parser')
|
||||
element = soup.find('th')
|
||||
|
||||
|
||||
# Apply styling to get attributes
|
||||
styled_context = apply_element_styling(self.base_context, element)
|
||||
result = table_header_cell_handler(element, styled_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, TableCell)
|
||||
self.assertEqual(result.is_header, True)
|
||||
|
||||
|
||||
def test_horizontal_rule_handler(self):
|
||||
"""Test horizontal rule handler."""
|
||||
soup = BeautifulSoup('<hr>', 'html.parser')
|
||||
element = soup.find('hr')
|
||||
|
||||
|
||||
result = horizontal_rule_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, HorizontalRule)
|
||||
|
||||
|
||||
def test_image_handler(self):
|
||||
"""Test image handler."""
|
||||
soup = BeautifulSoup('<img src="test.jpg" alt="Test image" width="100" height="50">', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<img src="test.jpg" alt="Test image" width="100" height="50">',
|
||||
'html.parser')
|
||||
element = soup.find('img')
|
||||
|
||||
|
||||
# Need to apply styling first to get attributes
|
||||
styled_context = apply_element_styling(self.base_context, element)
|
||||
result = image_handler(element, styled_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, Image)
|
||||
self.assertEqual(result.source, "test.jpg")
|
||||
self.assertEqual(result.alt_text, "Test image")
|
||||
self.assertEqual(result.width, 100)
|
||||
self.assertEqual(result.height, 50)
|
||||
|
||||
|
||||
def test_div_handler_container(self):
|
||||
"""Test div handler - adapted from test_div_container."""
|
||||
# From: "<div><p>First paragraph.</p><p>Second paragraph.</p></div>"
|
||||
soup = BeautifulSoup('<div><p>First paragraph.</p><p>Second paragraph.</p></div>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<div><p>First paragraph.</p><p>Second paragraph.</p></div>',
|
||||
'html.parser')
|
||||
element = soup.find('div')
|
||||
|
||||
|
||||
result = div_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, list)
|
||||
# Should match original test expectations
|
||||
self.assertEqual(len(result), 2)
|
||||
@@ -435,23 +460,25 @@ class TestHandlerFunctions(unittest.TestCase):
|
||||
|
||||
class TestStyledContentHandling(unittest.TestCase):
|
||||
"""Test styled content handling using patterns from existing tests."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_context = create_base_context()
|
||||
|
||||
|
||||
def test_paragraph_with_bold_content(self):
|
||||
"""Test paragraph with bold content - adapted from test_bold_text."""
|
||||
# From: "<p>This is <strong>bold text</strong> in a paragraph.</p>"
|
||||
soup = BeautifulSoup('<p>This is <strong>bold text</strong> in a paragraph.</p>', 'html.parser')
|
||||
soup = BeautifulSoup(
|
||||
'<p>This is <strong>bold text</strong> in a paragraph.</p>',
|
||||
'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
|
||||
result = paragraph_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, Paragraph)
|
||||
words = list(result.words_iter())
|
||||
self.assertEqual(len(words), 7) # From original test expectation
|
||||
|
||||
|
||||
# Check that 'bold' and 'text' words have bold font weight (from original test)
|
||||
bold_word = words[2][1] # 'bold'
|
||||
text_word = words[3][1] # 'text'
|
||||
@@ -459,31 +486,35 @@ class TestStyledContentHandling(unittest.TestCase):
|
||||
self.assertEqual(bold_word.style.weight, FontWeight.BOLD)
|
||||
self.assertEqual(text_word.text, "text")
|
||||
self.assertEqual(text_word.style.weight, FontWeight.BOLD)
|
||||
|
||||
|
||||
# Check that other words are not bold (from original test)
|
||||
normal_word = words[0][1] # 'This'
|
||||
self.assertEqual(normal_word.text, "This")
|
||||
self.assertNotEqual(normal_word.style.weight, FontWeight.BOLD)
|
||||
|
||||
|
||||
def test_paragraph_with_mixed_formatting(self):
|
||||
"""Test mixed formatting - adapted from test_mixed_formatting."""
|
||||
# From: "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style=\"color: blue;\">blue</span>..."
|
||||
soup = BeautifulSoup('<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style="color: blue;">blue</span> text.</p>', 'html.parser')
|
||||
# From: "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>..."
|
||||
html_str = (
|
||||
'<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, '
|
||||
'<span style="color: blue;">blue</span> text.</p>'
|
||||
)
|
||||
soup = BeautifulSoup(html_str, 'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
|
||||
result = paragraph_handler(element, self.base_context)
|
||||
|
||||
|
||||
self.assertIsInstance(result, Paragraph)
|
||||
words = list(result.words_iter())
|
||||
|
||||
|
||||
# Check for bold word (from original test pattern)
|
||||
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
|
||||
|
||||
# Check for italic word (from original test pattern)
|
||||
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(italic_words), 0, "Should have italic words")
|
||||
|
||||
|
||||
# Check for blue colored word (from original test pattern)
|
||||
blue_words = [w for _, w in words if w.style.colour == (0, 0, 255)]
|
||||
self.assertGreater(len(blue_words), 0, "Should have blue colored words")
|
||||
|
||||
@@ -18,97 +18,115 @@ class TestHTMLFileLoader(unittest.TestCase):
|
||||
def test_load_html_file(self):
|
||||
"""Test loading and parsing an HTML file from disk."""
|
||||
# Path to the test HTML file
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
html_file_path = os.path.join(
|
||||
"tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Verify the test file exists
|
||||
self.assertTrue(os.path.exists(html_file_path), f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
self.assertTrue(
|
||||
os.path.exists(html_file_path),
|
||||
f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
# Read the HTML file
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
|
||||
# Verify we got some content
|
||||
self.assertGreater(len(html_content), 0, "HTML file should not be empty")
|
||||
|
||||
|
||||
# Parse the HTML content using the html_extraction module
|
||||
try:
|
||||
blocks = parse_html_string(html_content)
|
||||
except Exception as e:
|
||||
self.fail(f"Failed to parse HTML file: {e}")
|
||||
|
||||
|
||||
# Verify we got some blocks
|
||||
self.assertIsInstance(blocks, list, "parse_html_string should return a list")
|
||||
self.assertGreater(len(blocks), 0, "Should extract at least one block from the HTML file")
|
||||
|
||||
self.assertGreater(
|
||||
len(blocks),
|
||||
0,
|
||||
"Should extract at least one block from the HTML file")
|
||||
|
||||
# Verify all returned items are Block instances
|
||||
for i, block in enumerate(blocks):
|
||||
self.assertIsInstance(block, Block, f"Item {i} should be a Block instance, got {type(block)}")
|
||||
|
||||
self.assertIsInstance(
|
||||
block,
|
||||
Block,
|
||||
f"Item {i} should be a Block instance, got {
|
||||
type(block)}")
|
||||
|
||||
print(f"Successfully loaded and parsed HTML file with {len(blocks)} blocks")
|
||||
|
||||
def test_load_html_file_with_custom_font(self):
|
||||
"""Test loading HTML file with a custom base font."""
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
html_file_path = os.path.join(
|
||||
"tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Skip if file doesn't exist
|
||||
if not os.path.exists(html_file_path):
|
||||
self.skipTest(f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
|
||||
# Create a custom font
|
||||
custom_font = Font(font_size=14, colour=(100, 100, 100))
|
||||
|
||||
|
||||
# Read and parse with custom font
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
|
||||
blocks = parse_html_string(html_content, base_font=custom_font)
|
||||
|
||||
|
||||
# Verify we got blocks
|
||||
self.assertGreater(len(blocks), 0, "Should extract blocks with custom font")
|
||||
|
||||
print(f"Successfully parsed HTML file with custom font, got {len(blocks)} blocks")
|
||||
|
||||
print(
|
||||
f"Successfully parsed HTML file with custom font, got {
|
||||
len(blocks)} blocks")
|
||||
|
||||
def test_load_html_file_content_types(self):
|
||||
"""Test that the loaded HTML file contains expected content types."""
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
html_file_path = os.path.join(
|
||||
"tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Skip if file doesn't exist
|
||||
if not os.path.exists(html_file_path):
|
||||
self.skipTest(f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
|
||||
blocks = parse_html_string(html_content)
|
||||
|
||||
|
||||
# Check that we have different types of blocks
|
||||
block_type_names = [type(block).__name__ for block in blocks]
|
||||
unique_types = set(block_type_names)
|
||||
|
||||
|
||||
# A Wikipedia page should contain multiple types of content
|
||||
self.assertGreater(len(unique_types), 1, "Should have multiple types of blocks in Wikipedia page")
|
||||
|
||||
self.assertGreater(
|
||||
len(unique_types),
|
||||
1,
|
||||
"Should have multiple types of blocks in Wikipedia page")
|
||||
|
||||
print(f"Found block types: {sorted(unique_types)}")
|
||||
|
||||
def test_html_file_size_handling(self):
|
||||
"""Test that large HTML files can be handled gracefully."""
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
html_file_path = os.path.join(
|
||||
"tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Skip if file doesn't exist
|
||||
if not os.path.exists(html_file_path):
|
||||
self.skipTest(f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
|
||||
# Get file size
|
||||
file_size = os.path.getsize(html_file_path)
|
||||
print(f"HTML file size: {file_size} bytes")
|
||||
|
||||
|
||||
# Read and parse
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
|
||||
# This should not raise an exception even for large files
|
||||
blocks = parse_html_string(html_content)
|
||||
|
||||
|
||||
# Basic verification
|
||||
self.assertIsInstance(blocks, list)
|
||||
print(f"Successfully processed {file_size} byte file into {len(blocks)} blocks")
|
||||
|
||||
@@ -112,13 +112,14 @@ class TestHTMLLinkEndToEnd(unittest.TestCase):
|
||||
rendered = page.render()
|
||||
self.assertIsNotNone(rendered)
|
||||
|
||||
print(f"\nSettings overlay test:")
|
||||
print("\nSettings overlay test:")
|
||||
print(f" Found {len(all_linked_words)} linked words")
|
||||
print(f" Actions: {actions}")
|
||||
print(f" Rendered: {rendered.size}")
|
||||
|
||||
# The links are successfully created and rendered!
|
||||
# In a real application, query_point would be used to detect clicks on these links
|
||||
# In a real application, query_point would be used to detect clicks on
|
||||
# these links
|
||||
|
||||
def test_link_metadata_preserved(self):
|
||||
"""Test that link metadata (title, type) is preserved through the workflow."""
|
||||
|
||||
@@ -155,8 +155,11 @@ class TestHTMLLinkInteractivity(unittest.TestCase):
|
||||
html = '''
|
||||
<div>
|
||||
<h2 style="text-align: center; font-size: 18px; font-weight: bold; margin: 10px 0;">Settings</h2>
|
||||
<p style="padding: 15px; margin: 5px 0; background-color: #dc3545; text-align: center; border-radius: 5px;">
|
||||
<a href="action:back_to_library" style="text-decoration: none; color: white; font-weight: bold; font-size: 14px;">◄ Back to Library</a>
|
||||
<p style="padding: 15px; margin: 5px 0; background-color: #dc3545; text-align: center;
|
||||
border-radius: 5px;">
|
||||
<a href="action:back_to_library"
|
||||
style="text-decoration: none; color: white; font-weight: bold; font-size: 14px;">
|
||||
◄ Back to Library</a>
|
||||
</p>
|
||||
<p style="padding: 10px; margin: 5px 0; background-color: #f8f9fa; border-radius: 5px;">
|
||||
<span style="font-weight: bold;">Font Size: 100%</span><br>
|
||||
@@ -177,23 +180,26 @@ class TestHTMLLinkInteractivity(unittest.TestCase):
|
||||
all_linked_words.append(word)
|
||||
|
||||
# Verify we found the expected links
|
||||
self.assertGreater(len(all_linked_words), 0, "Should find LinkedWords in settings HTML")
|
||||
self.assertGreater(
|
||||
len(all_linked_words),
|
||||
0,
|
||||
"Should find LinkedWords in settings HTML")
|
||||
|
||||
# Check for specific link targets
|
||||
link_targets = {word.location for word in all_linked_words}
|
||||
|
||||
self.assertIn("action:back_to_library", link_targets,
|
||||
"Should find 'Back to Library' link")
|
||||
"Should find 'Back to Library' link")
|
||||
self.assertIn("setting:font_decrease", link_targets,
|
||||
"Should find font decrease link")
|
||||
"Should find font decrease link")
|
||||
self.assertIn("setting:font_increase", link_targets,
|
||||
"Should find font increase link")
|
||||
"Should find font increase link")
|
||||
|
||||
# Verify the link texts
|
||||
back_to_library_words = [w for w in all_linked_words
|
||||
if w.location == "action:back_to_library"]
|
||||
if w.location == "action:back_to_library"]
|
||||
self.assertGreater(len(back_to_library_words), 0,
|
||||
"Should have words linked to back_to_library action")
|
||||
"Should have words linked to back_to_library action")
|
||||
|
||||
# Print debug info
|
||||
print(f"\nFound {len(all_linked_words)} linked words:")
|
||||
|
||||
@@ -7,173 +7,172 @@ from bs4 import BeautifulSoup
|
||||
from pyWebLayout.io.readers.html_extraction import (
|
||||
parse_html_string,
|
||||
extract_text_content,
|
||||
create_base_context,
|
||||
apply_element_styling
|
||||
create_base_context
|
||||
)
|
||||
from pyWebLayout.abstract.inline import LinkedWord
|
||||
from pyWebLayout.abstract.functional import LinkType
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
from pyWebLayout.style import Font
|
||||
|
||||
|
||||
class TestHTMLLinkExtraction(unittest.TestCase):
|
||||
"""Test cases for HTML hyperlink extraction."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_context = create_base_context()
|
||||
|
||||
|
||||
def test_simple_external_link(self):
|
||||
"""Test extracting a simple external link."""
|
||||
html = '<p>Visit <a href="https://example.com">this site</a> for more.</p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
# Should have: "Visit", "this", "site", "for", "more."
|
||||
self.assertEqual(len(words), 5)
|
||||
|
||||
|
||||
# Check that "this" and "site" are LinkedWords
|
||||
self.assertIsInstance(words[1], LinkedWord)
|
||||
self.assertIsInstance(words[2], LinkedWord)
|
||||
|
||||
|
||||
# Check link properties
|
||||
self.assertEqual(words[1].location, "https://example.com")
|
||||
self.assertEqual(words[1].link_type, LinkType.EXTERNAL)
|
||||
self.assertEqual(words[2].location, "https://example.com")
|
||||
self.assertEqual(words[2].link_type, LinkType.EXTERNAL)
|
||||
|
||||
|
||||
def test_internal_link(self):
|
||||
"""Test extracting an internal anchor link."""
|
||||
html = '<p>Go to <a href="#section2">section 2</a> below.</p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
# Find LinkedWords
|
||||
linked_words = [w for w in words if isinstance(w, LinkedWord)]
|
||||
self.assertEqual(len(linked_words), 2) # "section" and "2"
|
||||
|
||||
|
||||
# Check they're internal links
|
||||
for word in linked_words:
|
||||
self.assertEqual(word.link_type, LinkType.INTERNAL)
|
||||
self.assertEqual(word.location, "#section2")
|
||||
|
||||
|
||||
def test_multi_word_link(self):
|
||||
"""Test that multi-word links create separate LinkedWords."""
|
||||
html = '<p><a href="/next">click here for next page</a></p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
# All words should be LinkedWords
|
||||
self.assertEqual(len(words), 5)
|
||||
for word in words:
|
||||
self.assertIsInstance(word, LinkedWord)
|
||||
self.assertEqual(word.location, "/next")
|
||||
self.assertEqual(word.link_type, LinkType.INTERNAL)
|
||||
|
||||
|
||||
def test_link_with_title(self):
|
||||
"""Test extracting link with title attribute."""
|
||||
html = '<p><a href="https://example.com" title="Visit Example">click</a></p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
self.assertEqual(len(words), 1)
|
||||
self.assertIsInstance(words[0], LinkedWord)
|
||||
self.assertEqual(words[0].link_title, "Visit Example")
|
||||
|
||||
|
||||
def test_mixed_linked_and_normal_text(self):
|
||||
"""Test paragraph with both linked and normal text."""
|
||||
html = '<p>Some <a href="/page">linked text</a> and normal text.</p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
# "Some" - normal
|
||||
# "linked" - LinkedWord
|
||||
# "text" - LinkedWord
|
||||
# "text" - LinkedWord
|
||||
# "and" - normal
|
||||
# "normal" - normal
|
||||
# "text." - normal
|
||||
|
||||
|
||||
self.assertNotIsInstance(words[0], LinkedWord) # "Some"
|
||||
self.assertIsInstance(words[1], LinkedWord) # "linked"
|
||||
self.assertIsInstance(words[2], LinkedWord) # "text"
|
||||
self.assertNotIsInstance(words[3], LinkedWord) # "and"
|
||||
|
||||
|
||||
def test_link_without_href(self):
|
||||
"""Test that <a> without href is treated as normal text."""
|
||||
html = '<p><a>not a link</a></p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
# Should be regular Words, not LinkedWords
|
||||
for word in words:
|
||||
self.assertNotIsInstance(word, LinkedWord)
|
||||
|
||||
|
||||
def test_javascript_link(self):
|
||||
"""Test that javascript: links are detected as API type."""
|
||||
html = '<p><a href="javascript:alert()">click</a></p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
self.assertIsInstance(words[0], LinkedWord)
|
||||
self.assertEqual(words[0].link_type, LinkType.API)
|
||||
|
||||
|
||||
def test_nested_formatting_in_link(self):
|
||||
"""Test link with nested formatting."""
|
||||
html = '<p><a href="/page">text with <strong>bold</strong> word</a></p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
# All should be LinkedWords regardless of formatting
|
||||
for word in words:
|
||||
self.assertIsInstance(word, LinkedWord)
|
||||
self.assertEqual(word.location, "/page")
|
||||
|
||||
|
||||
def test_multiple_links_in_paragraph(self):
|
||||
"""Test paragraph with multiple separate links."""
|
||||
html = '<p><a href="/page1">first</a> and <a href="/page2">second</a> link</p>'
|
||||
blocks = parse_html_string(html)
|
||||
|
||||
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words)
|
||||
|
||||
|
||||
# Find LinkedWords and their locations
|
||||
linked_words = [(w.text, w.location) for w in words if isinstance(w, LinkedWord)]
|
||||
|
||||
linked_words = [(w.text, w.location)
|
||||
for w in words if isinstance(w, LinkedWord)]
|
||||
|
||||
# Should have "first" linked to /page1 and "second" linked to /page2
|
||||
self.assertIn(("first", "/page1"), linked_words)
|
||||
self.assertIn(("second", "/page2"), linked_words)
|
||||
|
||||
|
||||
def test_extract_text_content_with_links(self):
|
||||
"""Test extract_text_content directly with link elements."""
|
||||
html = '<span>Visit <a href="https://example.com">our site</a> today</span>'
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
element = soup.find('span')
|
||||
|
||||
|
||||
context = create_base_context()
|
||||
words = extract_text_content(element, context)
|
||||
|
||||
|
||||
# Should have: "Visit", "our", "site", "today"
|
||||
self.assertEqual(len(words), 4)
|
||||
|
||||
|
||||
# Check types
|
||||
self.assertNotIsInstance(words[0], LinkedWord) # "Visit"
|
||||
self.assertIsInstance(words[1], LinkedWord) # "our"
|
||||
|
||||
@@ -16,7 +16,7 @@ from pyWebLayout.layout.ereader_layout import (
|
||||
FontScaler,
|
||||
BidirectionalLayouter
|
||||
)
|
||||
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel, Table, HList
|
||||
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
@@ -210,7 +210,7 @@ class TestRenderingPosition:
|
||||
|
||||
assert pos != "not a position"
|
||||
assert pos != 42
|
||||
assert pos != None
|
||||
assert pos is not None
|
||||
|
||||
def test_hashability(self):
|
||||
"""Test that RenderingPosition is hashable and can be used in sets/dicts."""
|
||||
@@ -594,7 +594,6 @@ class TestBidirectionalLayouter:
|
||||
# Larger font should estimate fewer blocks
|
||||
assert est_large.block_index >= est_normal.block_index
|
||||
|
||||
|
||||
def test_scale_block_fonts_paragraph(self, sample_font):
|
||||
"""Test scaling fonts in a paragraph block."""
|
||||
layouter = BidirectionalLayouter([], PageStyle())
|
||||
@@ -610,7 +609,12 @@ class TestBidirectionalLayouter:
|
||||
assert scaled != paragraph
|
||||
|
||||
# Check that words were scaled (words is a list, not a method)
|
||||
words = scaled.words if hasattr(scaled, 'words') and isinstance(scaled.words, list) else list(scaled.words_iter())
|
||||
words = scaled.words if hasattr(
|
||||
scaled,
|
||||
'words') and isinstance(
|
||||
scaled.words,
|
||||
list) else list(
|
||||
scaled.words_iter())
|
||||
assert len(words) >= 2
|
||||
|
||||
def test_scale_block_fonts_heading(self, sample_font):
|
||||
@@ -639,7 +643,8 @@ class TestBidirectionalLayouter:
|
||||
# Use a simple block (not Paragraph, Heading, Table, or HList)
|
||||
unknown_block = Block(BlockType.HORIZONTAL_RULE)
|
||||
|
||||
success, new_pos = layouter._layout_block_on_page(unknown_block, page, position, 1.0)
|
||||
success, new_pos = layouter._layout_block_on_page(
|
||||
unknown_block, page, position, 1.0)
|
||||
|
||||
# Should skip and move to next block
|
||||
assert success is True
|
||||
@@ -682,7 +687,10 @@ class TestBidirectionalLayouter:
|
||||
assert new_pos.block_index == 1
|
||||
assert new_pos.list_item_index == 0
|
||||
|
||||
def test_render_page_forward_simple(self, sample_blocks_with_headings, sample_page_style):
|
||||
def test_render_page_forward_simple(
|
||||
self,
|
||||
sample_blocks_with_headings,
|
||||
sample_page_style):
|
||||
"""Test forward page rendering with simple blocks."""
|
||||
layouter = BidirectionalLayouter(
|
||||
sample_blocks_with_headings,
|
||||
@@ -700,7 +708,8 @@ class TestBidirectionalLayouter:
|
||||
# Position should advance
|
||||
assert next_pos.block_index >= position.block_index
|
||||
|
||||
def test_render_page_forward_with_font_scale(self, sample_blocks_with_headings, sample_page_style):
|
||||
def test_render_page_forward_with_font_scale(
|
||||
self, sample_blocks_with_headings, sample_page_style):
|
||||
"""Test forward rendering with font scaling."""
|
||||
layouter = BidirectionalLayouter(
|
||||
sample_blocks_with_headings,
|
||||
@@ -720,7 +729,10 @@ class TestBidirectionalLayouter:
|
||||
assert page1 is not None
|
||||
assert page2 is not None
|
||||
|
||||
def test_render_page_forward_at_end(self, sample_blocks_with_headings, sample_page_style):
|
||||
def test_render_page_forward_at_end(
|
||||
self,
|
||||
sample_blocks_with_headings,
|
||||
sample_page_style):
|
||||
"""Test forward rendering at end of document."""
|
||||
layouter = BidirectionalLayouter(
|
||||
sample_blocks_with_headings,
|
||||
@@ -735,7 +747,8 @@ class TestBidirectionalLayouter:
|
||||
# Should still render a page
|
||||
assert page is not None
|
||||
|
||||
def test_render_page_forward_beyond_end(self, sample_blocks_with_headings, sample_page_style):
|
||||
def test_render_page_forward_beyond_end(
|
||||
self, sample_blocks_with_headings, sample_page_style):
|
||||
"""Test forward rendering beyond document end."""
|
||||
layouter = BidirectionalLayouter(
|
||||
sample_blocks_with_headings,
|
||||
@@ -750,7 +763,10 @@ class TestBidirectionalLayouter:
|
||||
# Should handle gracefully
|
||||
assert page is not None
|
||||
|
||||
def test_render_page_backward_simple(self, sample_blocks_with_headings, sample_page_style):
|
||||
def test_render_page_backward_simple(
|
||||
self,
|
||||
sample_blocks_with_headings,
|
||||
sample_page_style):
|
||||
"""Test backward page rendering."""
|
||||
layouter = BidirectionalLayouter(
|
||||
sample_blocks_with_headings,
|
||||
@@ -776,7 +792,8 @@ class TestBidirectionalLayouter:
|
||||
target_end = RenderingPosition(block_index=10)
|
||||
actual_end = RenderingPosition(block_index=12) # Overshot
|
||||
|
||||
adjusted = layouter._adjust_start_estimate(current_start, target_end, actual_end)
|
||||
adjusted = layouter._adjust_start_estimate(
|
||||
current_start, target_end, actual_end)
|
||||
|
||||
# Should move start forward (increase block_index)
|
||||
assert adjusted.block_index > current_start.block_index
|
||||
@@ -789,7 +806,8 @@ class TestBidirectionalLayouter:
|
||||
target_end = RenderingPosition(block_index=10)
|
||||
actual_end = RenderingPosition(block_index=8) # Undershot
|
||||
|
||||
adjusted = layouter._adjust_start_estimate(current_start, target_end, actual_end)
|
||||
adjusted = layouter._adjust_start_estimate(
|
||||
current_start, target_end, actual_end)
|
||||
|
||||
# Should move start backward (decrease block_index)
|
||||
assert adjusted.block_index <= current_start.block_index
|
||||
@@ -802,12 +820,14 @@ class TestBidirectionalLayouter:
|
||||
target_end = RenderingPosition(block_index=10)
|
||||
actual_end = RenderingPosition(block_index=10) # Exact
|
||||
|
||||
adjusted = layouter._adjust_start_estimate(current_start, target_end, actual_end)
|
||||
adjusted = layouter._adjust_start_estimate(
|
||||
current_start, target_end, actual_end)
|
||||
|
||||
# Should return same or similar position
|
||||
assert adjusted.block_index >= 0
|
||||
|
||||
def test_layout_paragraph_on_page_with_pretext(self, sample_font, sample_page_style):
|
||||
def test_layout_paragraph_on_page_with_pretext(
|
||||
self, sample_font, sample_page_style):
|
||||
"""Test paragraph layout with pretext (hyphenated word continuation)."""
|
||||
layouter = BidirectionalLayouter([], sample_page_style, page_size=(800, 600))
|
||||
|
||||
@@ -819,7 +839,8 @@ class TestBidirectionalLayouter:
|
||||
page = Page(size=(800, 600), style=sample_page_style)
|
||||
position = RenderingPosition(remaining_pretext="pre-")
|
||||
|
||||
success, new_pos = layouter._layout_paragraph_on_page(paragraph, page, position, 1.0)
|
||||
success, new_pos = layouter._layout_paragraph_on_page(
|
||||
paragraph, page, position, 1.0)
|
||||
|
||||
# Should attempt to layout
|
||||
assert isinstance(success, bool)
|
||||
@@ -838,7 +859,8 @@ class TestBidirectionalLayouter:
|
||||
page = Page(size=(800, 600), style=sample_page_style)
|
||||
position = RenderingPosition()
|
||||
|
||||
success, new_pos = layouter._layout_paragraph_on_page(paragraph, page, position, 1.0)
|
||||
success, new_pos = layouter._layout_paragraph_on_page(
|
||||
paragraph, page, position, 1.0)
|
||||
|
||||
# Should complete successfully
|
||||
assert isinstance(success, bool)
|
||||
@@ -856,7 +878,8 @@ class TestBidirectionalLayouter:
|
||||
page = Page(size=(800, 600), style=sample_page_style)
|
||||
position = RenderingPosition()
|
||||
|
||||
success, new_pos = layouter._layout_heading_on_page(heading, page, position, 1.0)
|
||||
success, new_pos = layouter._layout_heading_on_page(
|
||||
heading, page, position, 1.0)
|
||||
|
||||
# Should attempt to layout like a paragraph
|
||||
assert isinstance(success, bool)
|
||||
|
||||
@@ -9,9 +9,7 @@ This module tests:
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
|
||||
from pyWebLayout.layout.ereader_manager import (
|
||||
BookmarkManager,
|
||||
@@ -57,7 +55,7 @@ def sample_blocks(sample_font):
|
||||
# Paragraphs
|
||||
for i in range(5):
|
||||
p = Paragraph(sample_font)
|
||||
p.add_word(Word(f"Paragraph", sample_font))
|
||||
p.add_word(Word("Paragraph", sample_font))
|
||||
p.add_word(Word(f"{i}", sample_font))
|
||||
blocks.append(p)
|
||||
|
||||
@@ -94,7 +92,7 @@ class TestBookmarkManager:
|
||||
"""Test that initialization creates bookmarks directory if needed."""
|
||||
bookmarks_dir = str(tmp_path / "new_bookmarks")
|
||||
|
||||
manager = BookmarkManager("test_doc", bookmarks_dir)
|
||||
BookmarkManager("test_doc", bookmarks_dir)
|
||||
|
||||
assert Path(bookmarks_dir).exists()
|
||||
assert Path(bookmarks_dir).is_dir()
|
||||
@@ -296,7 +294,8 @@ class TestEreaderLayoutManager:
|
||||
assert manager.font_scale == 1.0
|
||||
assert isinstance(manager.current_position, RenderingPosition)
|
||||
|
||||
def test_initialization_with_custom_page_style(self, sample_blocks, temp_bookmarks_dir):
|
||||
def test_initialization_with_custom_page_style(
|
||||
self, sample_blocks, temp_bookmarks_dir):
|
||||
"""Test initialization with custom page style."""
|
||||
custom_style = PageStyle()
|
||||
|
||||
@@ -309,7 +308,8 @@ class TestEreaderLayoutManager:
|
||||
|
||||
assert manager.page_style == custom_style
|
||||
|
||||
def test_initialization_loads_saved_position(self, sample_blocks, temp_bookmarks_dir):
|
||||
def test_initialization_loads_saved_position(
|
||||
self, sample_blocks, temp_bookmarks_dir):
|
||||
"""Test that initialization loads saved reading position."""
|
||||
# Save a position first
|
||||
bookmark_mgr = BookmarkManager("test_doc", temp_bookmarks_dir)
|
||||
@@ -493,7 +493,7 @@ class TestEreaderLayoutManager:
|
||||
bookmarks_dir=temp_bookmarks_dir
|
||||
)
|
||||
|
||||
page = manager.set_font_scale(1.0)
|
||||
manager.set_font_scale(1.0)
|
||||
|
||||
assert manager.font_scale == 1.0
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@ class TestHTMLLinksInEreader(unittest.TestCase):
|
||||
if isinstance(word, LinkedWord):
|
||||
all_linked_words.append(word)
|
||||
|
||||
self.assertGreater(len(all_linked_words), 0, "Should create LinkedWords from HTML")
|
||||
self.assertGreater(
|
||||
len(all_linked_words),
|
||||
0,
|
||||
"Should create LinkedWords from HTML")
|
||||
print(f"\n Created {len(all_linked_words)} LinkedWords from HTML")
|
||||
|
||||
# Step 2: Create EreaderLayoutManager (like the dreader app does)
|
||||
@@ -91,17 +94,19 @@ class TestHTMLLinksInEreader(unittest.TestCase):
|
||||
print(f" - '{elem['text']}' -> {elem['location']}")
|
||||
|
||||
# THIS IS THE KEY ASSERTION
|
||||
self.assertGreater(len(interactive_elements), 0,
|
||||
"Settings overlay should have interactive LinkText objects after rendering!")
|
||||
self.assertGreater(
|
||||
len(interactive_elements),
|
||||
0,
|
||||
"Settings overlay should have interactive LinkText objects after rendering!")
|
||||
|
||||
# Verify the expected links are present
|
||||
locations = {elem['location'] for elem in interactive_elements}
|
||||
self.assertIn("action:back_to_library", locations,
|
||||
"Should find 'Back to Library' link")
|
||||
"Should find 'Back to Library' link")
|
||||
self.assertIn("setting:font_decrease", locations,
|
||||
"Should find font decrease link")
|
||||
"Should find font decrease link")
|
||||
self.assertIn("setting:font_increase", locations,
|
||||
"Should find font increase link")
|
||||
"Should find font increase link")
|
||||
|
||||
def test_query_point_detects_links(self):
|
||||
"""Test that query_point can detect LinkText objects."""
|
||||
@@ -134,8 +139,9 @@ class TestHTMLLinksInEreader(unittest.TestCase):
|
||||
if found_link:
|
||||
break
|
||||
|
||||
self.assertTrue(found_link,
|
||||
"Should be able to detect link via query_point somewhere on the page")
|
||||
self.assertTrue(
|
||||
found_link,
|
||||
"Should be able to detect link via query_point somewhere on the page")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -5,14 +5,12 @@ This test focuses on verifying that the document layouter properly
|
||||
integrates word spacing constraints from the style system.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
from typing import List, Optional
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from pyWebLayout.layout.document_layouter import paragraph_layouter, table_layouter, DocumentLayouter
|
||||
from pyWebLayout.style.abstract_style import AbstractStyle
|
||||
from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext
|
||||
from pyWebLayout.abstract.block import Table, TableRow, TableCell
|
||||
from pyWebLayout.style.concrete_style import StyleResolver, RenderingContext
|
||||
from pyWebLayout.abstract.block import Table
|
||||
from pyWebLayout.concrete.table import TableStyle
|
||||
|
||||
|
||||
@@ -29,21 +27,21 @@ class TestDocumentLayouter:
|
||||
self.mock_page.draw = Mock()
|
||||
self.mock_page.can_fit_line = Mock(return_value=True)
|
||||
self.mock_page.add_child = Mock()
|
||||
|
||||
|
||||
# Create mock page style with all required numeric properties
|
||||
self.mock_page.style = Mock()
|
||||
self.mock_page.style.max_font_size = 72 # Reasonable maximum font size
|
||||
self.mock_page.style.line_spacing_multiplier = 1.2 # Standard line spacing
|
||||
|
||||
|
||||
# Create mock style resolver
|
||||
self.mock_style_resolver = Mock()
|
||||
self.mock_page.style_resolver = self.mock_style_resolver
|
||||
|
||||
|
||||
# Create mock paragraph
|
||||
self.mock_paragraph = Mock()
|
||||
self.mock_paragraph.line_height = 20
|
||||
self.mock_paragraph.style = AbstractStyle()
|
||||
|
||||
|
||||
# Create mock words
|
||||
self.mock_words = []
|
||||
for i in range(5):
|
||||
@@ -51,20 +49,22 @@ class TestDocumentLayouter:
|
||||
word.text = f"word{i}"
|
||||
self.mock_words.append(word)
|
||||
self.mock_paragraph.words = self.mock_words
|
||||
|
||||
|
||||
# Create mock concrete style with word spacing constraints
|
||||
self.mock_concrete_style = Mock()
|
||||
self.mock_concrete_style.word_spacing_min = 2.0
|
||||
self.mock_concrete_style.word_spacing_max = 8.0
|
||||
self.mock_concrete_style.text_align = "left"
|
||||
|
||||
|
||||
# Create mock font that returns proper numeric metrics (not Mock objects)
|
||||
mock_font = Mock()
|
||||
# CRITICAL: getmetrics() must return actual numeric values, not Mock objects
|
||||
# This prevents "TypeError: '>' not supported between instances of 'Mock' and 'Mock'"
|
||||
mock_font.getmetrics.return_value = (12, 4) # (ascent, descent) as actual integers
|
||||
# This prevents "TypeError: '>' not supported between instances of 'Mock'
|
||||
# and 'Mock'"
|
||||
# (ascent, descent) as actual integers
|
||||
mock_font.getmetrics.return_value = (12, 4)
|
||||
mock_font.font = mock_font # For accessing .font property
|
||||
|
||||
|
||||
# Create mock font object that can be used by create_font
|
||||
mock_font_instance = Mock()
|
||||
mock_font_instance.font = mock_font
|
||||
@@ -72,7 +72,7 @@ class TestDocumentLayouter:
|
||||
mock_font_instance.colour = (0, 0, 0)
|
||||
mock_font_instance.background = (255, 255, 255, 0)
|
||||
self.mock_concrete_style.create_font = Mock(return_value=mock_font_instance)
|
||||
|
||||
|
||||
# Update mock words to have proper style with font
|
||||
for word in self.mock_words:
|
||||
word.style = Mock()
|
||||
@@ -84,34 +84,39 @@ class TestDocumentLayouter:
|
||||
@patch('pyWebLayout.layout.document_layouter.StyleResolver')
|
||||
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
||||
@patch('pyWebLayout.layout.document_layouter.Line')
|
||||
def test_paragraph_layouter_basic_flow(self, mock_line_class, mock_style_registry_class, mock_style_resolver_class):
|
||||
def test_paragraph_layouter_basic_flow(
|
||||
self,
|
||||
mock_line_class,
|
||||
mock_style_registry_class,
|
||||
mock_style_resolver_class):
|
||||
"""Test basic paragraph layouter functionality."""
|
||||
# Setup mocks for StyleResolver and ConcreteStyleRegistry
|
||||
mock_style_resolver = Mock()
|
||||
mock_style_resolver_class.return_value = mock_style_resolver
|
||||
|
||||
|
||||
mock_style_registry = Mock()
|
||||
mock_style_registry_class.return_value = mock_style_registry
|
||||
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
||||
|
||||
|
||||
mock_line = Mock()
|
||||
mock_line_class.return_value = mock_line
|
||||
mock_line.add_word.return_value = (True, None) # All words fit successfully
|
||||
|
||||
|
||||
# Call function
|
||||
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
|
||||
|
||||
|
||||
# Verify results
|
||||
success, failed_word_index, remaining_pretext = result
|
||||
assert success is True
|
||||
assert failed_word_index is None
|
||||
assert remaining_pretext is None
|
||||
|
||||
|
||||
# Verify StyleResolver and ConcreteStyleRegistry were created correctly
|
||||
mock_style_resolver_class.assert_called_once()
|
||||
mock_style_registry_class.assert_called_once_with(mock_style_resolver)
|
||||
mock_style_registry.get_concrete_style.assert_called_once_with(self.mock_paragraph.style)
|
||||
|
||||
mock_style_registry.get_concrete_style.assert_called_once_with(
|
||||
self.mock_paragraph.style)
|
||||
|
||||
# Verify Line was created with correct spacing constraints
|
||||
expected_spacing = (2, 8) # From mock_concrete_style
|
||||
mock_line_class.assert_called_once()
|
||||
@@ -121,36 +126,37 @@ class TestDocumentLayouter:
|
||||
@patch('pyWebLayout.layout.document_layouter.StyleResolver')
|
||||
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
||||
@patch('pyWebLayout.layout.document_layouter.Line')
|
||||
def test_paragraph_layouter_word_spacing_constraints_extraction(self, mock_line_class, mock_style_registry_class, mock_style_resolver_class):
|
||||
def test_paragraph_layouter_word_spacing_constraints_extraction(
|
||||
self, mock_line_class, mock_style_registry_class, mock_style_resolver_class):
|
||||
"""Test that word spacing constraints are correctly extracted from style."""
|
||||
# Create concrete style with specific constraints
|
||||
concrete_style = Mock()
|
||||
concrete_style.word_spacing_min = 5.5
|
||||
concrete_style.word_spacing_max = 15.2
|
||||
concrete_style.text_align = "justify"
|
||||
|
||||
|
||||
# Create a mock font that concrete_style.create_font returns
|
||||
mock_font = Mock()
|
||||
mock_font.font = Mock()
|
||||
mock_font.font.getmetrics.return_value = (12, 4)
|
||||
mock_font.font_size = 16
|
||||
concrete_style.create_font = Mock(return_value=mock_font)
|
||||
|
||||
|
||||
# Setup StyleResolver and ConcreteStyleRegistry mocks
|
||||
mock_style_resolver = Mock()
|
||||
mock_style_resolver_class.return_value = mock_style_resolver
|
||||
|
||||
|
||||
mock_style_registry = Mock()
|
||||
mock_style_registry_class.return_value = mock_style_registry
|
||||
mock_style_registry.get_concrete_style.return_value = concrete_style
|
||||
|
||||
|
||||
mock_line = Mock()
|
||||
mock_line_class.return_value = mock_line
|
||||
mock_line.add_word.return_value = (True, None)
|
||||
|
||||
|
||||
# Call function
|
||||
paragraph_layouter(self.mock_paragraph, self.mock_page)
|
||||
|
||||
|
||||
# Verify spacing constraints were extracted correctly (converted to int)
|
||||
expected_spacing = (5, 15) # int() conversion of 5.5 and 15.2
|
||||
call_args = mock_line_class.call_args
|
||||
@@ -159,30 +165,34 @@ class TestDocumentLayouter:
|
||||
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
||||
@patch('pyWebLayout.layout.document_layouter.Line')
|
||||
@patch('pyWebLayout.layout.document_layouter.Text')
|
||||
def test_paragraph_layouter_line_overflow(self, mock_text_class, mock_line_class, mock_style_registry_class):
|
||||
def test_paragraph_layouter_line_overflow(
|
||||
self,
|
||||
mock_text_class,
|
||||
mock_line_class,
|
||||
mock_style_registry_class):
|
||||
"""Test handling of line overflow when words don't fit."""
|
||||
# Setup mocks
|
||||
mock_style_registry = Mock()
|
||||
mock_style_registry_class.return_value = mock_style_registry
|
||||
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
||||
|
||||
|
||||
# Create two mock lines with proper size attribute
|
||||
mock_line1 = Mock()
|
||||
mock_line1.size = (400, 20) # (width, height)
|
||||
mock_line2 = Mock()
|
||||
mock_line2.size = (400, 20) # (width, height)
|
||||
mock_line_class.side_effect = [mock_line1, mock_line2]
|
||||
|
||||
|
||||
# Mock Text.from_word to return mock text objects with numeric width
|
||||
mock_text = Mock()
|
||||
mock_text.width = 50 # Reasonable word width
|
||||
mock_text_class.from_word.return_value = mock_text
|
||||
|
||||
|
||||
# First line: first 2 words fit, third doesn't
|
||||
# Second line: remaining words fit
|
||||
mock_line1.add_word.side_effect = [
|
||||
(True, None), # word0 fits
|
||||
(True, None), # word1 fits
|
||||
(True, None), # word1 fits
|
||||
(False, None), # word2 doesn't fit
|
||||
]
|
||||
mock_line2.add_word.side_effect = [
|
||||
@@ -190,42 +200,43 @@ class TestDocumentLayouter:
|
||||
(True, None), # word3 fits
|
||||
(True, None), # word4 fits
|
||||
]
|
||||
|
||||
|
||||
# Call function
|
||||
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
|
||||
|
||||
|
||||
# Verify results
|
||||
success, failed_word_index, remaining_pretext = result
|
||||
assert success is True
|
||||
assert failed_word_index is None
|
||||
assert remaining_pretext is None
|
||||
|
||||
|
||||
# Verify two lines were created
|
||||
assert mock_line_class.call_count == 2
|
||||
assert self.mock_page.add_child.call_count == 2
|
||||
|
||||
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
||||
@patch('pyWebLayout.layout.document_layouter.Line')
|
||||
def test_paragraph_layouter_page_full(self, mock_line_class, mock_style_registry_class):
|
||||
def test_paragraph_layouter_page_full(
|
||||
self, mock_line_class, mock_style_registry_class):
|
||||
"""Test handling when page runs out of space."""
|
||||
# Setup mocks
|
||||
mock_style_registry = Mock()
|
||||
mock_style_registry_class.return_value = mock_style_registry
|
||||
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
||||
|
||||
|
||||
# Page can fit first line but not second
|
||||
self.mock_page.can_fit_line.side_effect = [True, False]
|
||||
|
||||
|
||||
mock_line = Mock()
|
||||
mock_line_class.return_value = mock_line
|
||||
mock_line.add_word.side_effect = [
|
||||
(True, None), # word0 fits
|
||||
(False, None), # word1 doesn't fit, need new line
|
||||
]
|
||||
|
||||
|
||||
# Call function
|
||||
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
|
||||
|
||||
|
||||
# Verify results indicate page is full
|
||||
success, failed_word_index, remaining_pretext = result
|
||||
assert success is False
|
||||
@@ -236,9 +247,9 @@ class TestDocumentLayouter:
|
||||
"""Test handling of empty paragraph."""
|
||||
empty_paragraph = Mock()
|
||||
empty_paragraph.words = []
|
||||
|
||||
|
||||
result = paragraph_layouter(empty_paragraph, self.mock_page)
|
||||
|
||||
|
||||
success, failed_word_index, remaining_pretext = result
|
||||
assert success is True
|
||||
assert failed_word_index is None
|
||||
@@ -247,7 +258,7 @@ class TestDocumentLayouter:
|
||||
def test_paragraph_layouter_invalid_start_word(self):
|
||||
"""Test handling of invalid start_word index."""
|
||||
result = paragraph_layouter(self.mock_paragraph, self.mock_page, start_word=10)
|
||||
|
||||
|
||||
success, failed_word_index, remaining_pretext = result
|
||||
assert success is True
|
||||
assert failed_word_index is None
|
||||
@@ -259,10 +270,10 @@ class TestDocumentLayouter:
|
||||
# Setup mock
|
||||
mock_style_registry = Mock()
|
||||
mock_style_registry_class.return_value = mock_style_registry
|
||||
|
||||
|
||||
# Create layouter
|
||||
layouter = DocumentLayouter(self.mock_page)
|
||||
|
||||
|
||||
# Verify initialization
|
||||
assert layouter.page == self.mock_page
|
||||
mock_style_registry_class.assert_called_once_with(self.mock_page.style_resolver)
|
||||
@@ -271,12 +282,13 @@ class TestDocumentLayouter:
|
||||
def test_document_layouter_layout_paragraph(self, mock_paragraph_layouter):
|
||||
"""Test DocumentLayouter.layout_paragraph method."""
|
||||
mock_paragraph_layouter.return_value = (True, None, None)
|
||||
|
||||
|
||||
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
||||
layouter = DocumentLayouter(self.mock_page)
|
||||
|
||||
result = layouter.layout_paragraph(self.mock_paragraph, start_word=2, pretext="test")
|
||||
|
||||
|
||||
result = layouter.layout_paragraph(
|
||||
self.mock_paragraph, start_word=2, pretext="test")
|
||||
|
||||
# Verify the function was called correctly
|
||||
mock_paragraph_layouter.assert_called_once_with(
|
||||
self.mock_paragraph, self.mock_page, 2, "test"
|
||||
@@ -286,46 +298,46 @@ class TestDocumentLayouter:
|
||||
def test_document_layouter_layout_document_success(self):
|
||||
"""Test DocumentLayouter.layout_document with successful layout."""
|
||||
from pyWebLayout.abstract import Paragraph
|
||||
|
||||
|
||||
# Create Mock paragraphs that pass isinstance checks
|
||||
paragraphs = [
|
||||
Mock(spec=Paragraph),
|
||||
Mock(spec=Paragraph),
|
||||
Mock(spec=Paragraph)
|
||||
]
|
||||
|
||||
|
||||
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
||||
layouter = DocumentLayouter(self.mock_page)
|
||||
|
||||
|
||||
# Mock the layout_paragraph method to return success
|
||||
layouter.layout_paragraph = Mock(return_value=(True, None, None))
|
||||
|
||||
|
||||
result = layouter.layout_document(paragraphs)
|
||||
|
||||
|
||||
assert result is True
|
||||
assert layouter.layout_paragraph.call_count == 3
|
||||
|
||||
def test_document_layouter_layout_document_failure(self):
|
||||
"""Test DocumentLayouter.layout_document with layout failure."""
|
||||
from pyWebLayout.abstract import Paragraph
|
||||
|
||||
|
||||
# Create Mock paragraphs that pass isinstance checks
|
||||
paragraphs = [
|
||||
Mock(spec=Paragraph),
|
||||
Mock(spec=Paragraph)
|
||||
]
|
||||
|
||||
|
||||
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
||||
layouter = DocumentLayouter(self.mock_page)
|
||||
|
||||
|
||||
# Mock the layout_paragraph method: first succeeds, second fails
|
||||
layouter.layout_paragraph = Mock(side_effect=[
|
||||
(True, None, None), # First paragraph succeeds
|
||||
(False, 3, None), # Second paragraph fails
|
||||
])
|
||||
|
||||
|
||||
result = layouter.layout_document(paragraphs)
|
||||
|
||||
|
||||
assert result is False
|
||||
assert layouter.layout_paragraph.call_count == 2
|
||||
|
||||
@@ -334,39 +346,40 @@ class TestDocumentLayouter:
|
||||
# Create real style objects
|
||||
context = RenderingContext(base_font_size=16)
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
abstract_style = AbstractStyle(
|
||||
word_spacing=4.0,
|
||||
word_spacing_min=2.0,
|
||||
word_spacing_max=10.0
|
||||
)
|
||||
|
||||
|
||||
concrete_style = resolver.resolve_style(abstract_style)
|
||||
|
||||
|
||||
# Verify constraints are resolved correctly
|
||||
assert concrete_style.word_spacing_min == 2.0
|
||||
assert concrete_style.word_spacing_max == 10.0
|
||||
|
||||
|
||||
# This demonstrates the integration works end-to-end
|
||||
|
||||
|
||||
class TestWordSpacingConstraintsInLayout:
|
||||
"""Specific tests for word spacing constraints in layout context."""
|
||||
|
||||
|
||||
def test_different_spacing_scenarios(self):
|
||||
"""Test various word spacing constraint scenarios."""
|
||||
context = RenderingContext(base_font_size=16)
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
test_cases = [
|
||||
# (word_spacing, word_spacing_min, word_spacing_max, expected_min, expected_max)
|
||||
(None, None, None, 2.0, 8.0), # Default case
|
||||
(5.0, None, None, 5.0, 10.0), # Only base specified
|
||||
(4.0, 2.0, 8.0, 2.0, 8.0), # All specified
|
||||
(3.0, 1.0, None, 1.0, 3.0), # Min specified, max = max(word_spacing, min*2) = max(3.0, 2.0) = 3.0
|
||||
(6.0, None, 12.0, 6.0, 12.0), # Max specified, min from base
|
||||
# Min specified, max = max(word_spacing, min*2) = max(3.0, 2.0) = 3.0
|
||||
(3.0, 1.0, None, 1.0, 3.0),
|
||||
(6.0, None, 12.0, 6.0, 12.0), # Max specified, min from base
|
||||
]
|
||||
|
||||
|
||||
for word_spacing, min_spacing, max_spacing, expected_min, expected_max in test_cases:
|
||||
style_kwargs = {}
|
||||
if word_spacing is not None:
|
||||
@@ -375,17 +388,17 @@ class TestWordSpacingConstraintsInLayout:
|
||||
style_kwargs['word_spacing_min'] = min_spacing
|
||||
if max_spacing is not None:
|
||||
style_kwargs['word_spacing_max'] = max_spacing
|
||||
|
||||
|
||||
abstract_style = AbstractStyle(**style_kwargs)
|
||||
concrete_style = resolver.resolve_style(abstract_style)
|
||||
|
||||
|
||||
assert concrete_style.word_spacing_min == expected_min, f"Failed for case: {style_kwargs}"
|
||||
assert concrete_style.word_spacing_max == expected_max, f"Failed for case: {style_kwargs}"
|
||||
|
||||
|
||||
class TestMultiPageLayout:
|
||||
"""Test cases for multi-page document layout scenarios."""
|
||||
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures for multi-page tests."""
|
||||
# Create multiple mock pages
|
||||
@@ -401,19 +414,19 @@ class TestMultiPageLayout:
|
||||
page.add_child = Mock()
|
||||
page.style_resolver = Mock()
|
||||
self.mock_pages.append(page)
|
||||
|
||||
|
||||
# Create a long paragraph that will span multiple pages
|
||||
self.long_paragraph = Mock()
|
||||
self.long_paragraph.line_height = 25
|
||||
self.long_paragraph.style = AbstractStyle()
|
||||
|
||||
|
||||
# Create many words to ensure page overflow
|
||||
self.long_paragraph.words = []
|
||||
for i in range(50): # 50 words should definitely overflow a page
|
||||
word = Mock()
|
||||
word.text = f"word_{i:02d}"
|
||||
self.long_paragraph.words.append(word)
|
||||
|
||||
|
||||
# Create mock concrete style
|
||||
self.mock_concrete_style = Mock()
|
||||
self.mock_concrete_style.word_spacing_min = 3.0
|
||||
@@ -421,7 +434,6 @@ class TestMultiPageLayout:
|
||||
self.mock_concrete_style.text_align = "justify"
|
||||
self.mock_concrete_style.create_font = Mock()
|
||||
|
||||
|
||||
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
||||
def test_document_layouter_multi_page_scenario(self, mock_style_registry_class):
|
||||
"""Test DocumentLayouter handling multiple pages with continuation."""
|
||||
@@ -429,7 +441,7 @@ class TestMultiPageLayout:
|
||||
mock_style_registry = Mock()
|
||||
mock_style_registry_class.return_value = mock_style_registry
|
||||
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
||||
|
||||
|
||||
# Create a multi-page document layouter
|
||||
class MultiPageDocumentLayouter(DocumentLayouter):
|
||||
def __init__(self, pages):
|
||||
@@ -437,7 +449,7 @@ class TestMultiPageLayout:
|
||||
self.current_page_index = 0
|
||||
self.page = pages[0]
|
||||
self.style_registry = Mock()
|
||||
|
||||
|
||||
def get_next_page(self):
|
||||
"""Get the next available page."""
|
||||
if self.current_page_index + 1 < len(self.pages):
|
||||
@@ -445,47 +457,47 @@ class TestMultiPageLayout:
|
||||
self.page = self.pages[self.current_page_index]
|
||||
return self.page
|
||||
return None
|
||||
|
||||
|
||||
def layout_document_with_pagination(self, paragraphs):
|
||||
"""Layout document with automatic pagination."""
|
||||
for paragraph in paragraphs:
|
||||
start_word = 0
|
||||
pretext = None
|
||||
|
||||
|
||||
while start_word < len(paragraph.words):
|
||||
complete, next_word, remaining_pretext = self.layout_paragraph(
|
||||
paragraph, start_word, pretext
|
||||
)
|
||||
|
||||
|
||||
if complete:
|
||||
# Paragraph finished
|
||||
break
|
||||
|
||||
|
||||
if next_word is None:
|
||||
# Error condition
|
||||
return False, f"Failed to layout paragraph at word {start_word}"
|
||||
|
||||
|
||||
# Try to get next page
|
||||
next_page = self.get_next_page()
|
||||
if not next_page:
|
||||
return False, f"Ran out of pages at word {next_word}"
|
||||
|
||||
|
||||
# Continue with remaining words on next page
|
||||
start_word = next_word
|
||||
pretext = remaining_pretext
|
||||
|
||||
|
||||
return True, "All paragraphs laid out successfully"
|
||||
|
||||
|
||||
# Create layouter with multiple pages
|
||||
layouter = MultiPageDocumentLayouter(self.mock_pages)
|
||||
|
||||
|
||||
# Mock the layout_paragraph method to simulate page filling
|
||||
original_layout_paragraph = layouter.layout_paragraph
|
||||
layouter.layout_paragraph
|
||||
call_count = [0]
|
||||
|
||||
|
||||
def mock_layout_paragraph(paragraph, start_word=0, pretext=None):
|
||||
call_count[0] += 1
|
||||
|
||||
|
||||
# Simulate different scenarios based on call count
|
||||
if call_count[0] == 1:
|
||||
# First page: can fit words 0-19, fails at word 20
|
||||
@@ -498,19 +510,19 @@ class TestMultiPageLayout:
|
||||
return (True, None, None)
|
||||
else:
|
||||
return (False, start_word, None)
|
||||
|
||||
|
||||
layouter.layout_paragraph = mock_layout_paragraph
|
||||
|
||||
|
||||
# Test multi-page layout
|
||||
success, message = layouter.layout_document_with_pagination([self.long_paragraph])
|
||||
|
||||
success, message = layouter.layout_document_with_pagination(
|
||||
[self.long_paragraph])
|
||||
|
||||
# Verify results
|
||||
assert success is True
|
||||
assert "successfully" in message
|
||||
assert call_count[0] == 3 # Should have made 3 layout attempts
|
||||
assert layouter.current_page_index == 2 # Should end on page 3 (index 2)
|
||||
|
||||
|
||||
def test_realistic_multi_page_scenario(self):
|
||||
"""Test a realistic scenario with actual content and page constraints."""
|
||||
# Create realistic paragraph with varied content
|
||||
@@ -522,7 +534,7 @@ class TestMultiPageLayout:
|
||||
word_spacing_max=8.0,
|
||||
text_align="justify"
|
||||
)
|
||||
|
||||
|
||||
# Create words of varying lengths (realistic text)
|
||||
words = [
|
||||
"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog.",
|
||||
@@ -534,13 +546,13 @@ class TestMultiPageLayout:
|
||||
"system", "to", "handle", "appropriately", "with", "the", "given",
|
||||
"constraints", "and", "spacing", "requirements."
|
||||
]
|
||||
|
||||
|
||||
realistic_paragraph.words = []
|
||||
for word_text in words:
|
||||
word = Mock()
|
||||
word.text = word_text
|
||||
realistic_paragraph.words.append(word)
|
||||
|
||||
|
||||
# Create page with realistic constraints
|
||||
realistic_page = Mock()
|
||||
realistic_page.border_size = 30
|
||||
@@ -550,33 +562,34 @@ class TestMultiPageLayout:
|
||||
realistic_page.draw = Mock()
|
||||
realistic_page.add_child = Mock()
|
||||
realistic_page.style_resolver = Mock()
|
||||
|
||||
|
||||
# Simulate page that can fit approximately 20 lines
|
||||
lines_fitted = [0]
|
||||
max_lines = 20
|
||||
|
||||
|
||||
def realistic_can_fit_line(line_height):
|
||||
lines_fitted[0] += 1
|
||||
return lines_fitted[0] <= max_lines
|
||||
|
||||
|
||||
realistic_page.can_fit_line = realistic_can_fit_line
|
||||
|
||||
|
||||
# Test with real style system
|
||||
context = RenderingContext(base_font_size=14)
|
||||
resolver = StyleResolver(context)
|
||||
concrete_style = resolver.resolve_style(realistic_paragraph.style)
|
||||
|
||||
|
||||
# Verify realistic constraints were calculated
|
||||
assert concrete_style.word_spacing == 4.0
|
||||
assert concrete_style.word_spacing_min == 2.0
|
||||
assert concrete_style.word_spacing_max == 8.0
|
||||
|
||||
|
||||
# This test demonstrates the integration without mocking everything
|
||||
# In a real scenario, this would interface with actual Line and Text objects
|
||||
print(f"✓ Realistic scenario test completed")
|
||||
print("✓ Realistic scenario test completed")
|
||||
print(f" - Words to layout: {len(realistic_paragraph.words)}")
|
||||
print(f" - Page width: {realistic_page.available_width}px")
|
||||
print(f" - Word spacing constraints: {concrete_style.word_spacing_min}-{concrete_style.word_spacing_max}px")
|
||||
print(
|
||||
f" - Word spacing constraints: {concrete_style.word_spacing_min}-{concrete_style.word_spacing_max}px")
|
||||
|
||||
|
||||
class TestTableLayouter:
|
||||
@@ -756,24 +769,24 @@ if __name__ == "__main__":
|
||||
# Run specific tests for debugging
|
||||
test = TestDocumentLayouter()
|
||||
test.setup_method()
|
||||
|
||||
|
||||
# Run a simple test
|
||||
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry') as mock_registry:
|
||||
with patch('pyWebLayout.layout.document_layouter.Line') as mock_line:
|
||||
mock_style_registry = Mock()
|
||||
mock_registry.return_value = mock_style_registry
|
||||
mock_style_registry.get_concrete_style.return_value = test.mock_concrete_style
|
||||
|
||||
|
||||
mock_line_instance = Mock()
|
||||
mock_line.return_value = mock_line_instance
|
||||
mock_line_instance.add_word.return_value = (True, None)
|
||||
|
||||
|
||||
result = paragraph_layouter(test.mock_paragraph, test.mock_page)
|
||||
print(f"Test result: {result}")
|
||||
|
||||
|
||||
# Run multi-page tests
|
||||
multi_test = TestMultiPageLayout()
|
||||
multi_test.setup_method()
|
||||
multi_test.test_realistic_multi_page_scenario()
|
||||
|
||||
|
||||
print("Document layouter tests completed!")
|
||||
|
||||
@@ -7,20 +7,15 @@ in multi-page layout scenarios.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from PIL import Image, ImageDraw
|
||||
import numpy as np
|
||||
from typing import List, Optional
|
||||
import os
|
||||
import logging
|
||||
|
||||
from pyWebLayout.layout.document_layouter import paragraph_layouter, DocumentLayouter
|
||||
from pyWebLayout.layout.document_layouter import paragraph_layouter
|
||||
from pyWebLayout.style.abstract_style import AbstractStyle
|
||||
from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext
|
||||
from pyWebLayout.style.fonts import Font
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.concrete.text import Line, Text
|
||||
from pyWebLayout.concrete.text import Line
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
|
||||
# Enable logging to see font loading messages
|
||||
@@ -34,47 +29,53 @@ def verify_bundled_font_available():
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# Navigate up to pyWebLayout root, then to assets/fonts
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
bundled_font_path = os.path.join(project_root, 'pyWebLayout', 'assets', 'fonts', 'DejaVuSans.ttf')
|
||||
|
||||
bundled_font_path = os.path.join(
|
||||
project_root,
|
||||
'pyWebLayout',
|
||||
'assets',
|
||||
'fonts',
|
||||
'DejaVuSans.ttf')
|
||||
|
||||
logger.info(f"Integration tests checking for bundled font at: {bundled_font_path}")
|
||||
|
||||
|
||||
if not os.path.exists(bundled_font_path):
|
||||
pytest.fail(
|
||||
f"INTEGRATION TEST FAILURE: Bundled font not found at {bundled_font_path}\n"
|
||||
f"Integration tests require the bundled font to ensure consistent behavior.\n"
|
||||
f"This likely means the font was not included in the package build."
|
||||
)
|
||||
|
||||
|
||||
logger.info(f"Bundled font found at: {bundled_font_path}")
|
||||
return bundled_font_path
|
||||
|
||||
|
||||
class MockWord(Word):
|
||||
"""A simple mock word that extends the real Word class."""
|
||||
|
||||
|
||||
def __init__(self, text, style=None):
|
||||
if style is None:
|
||||
# Integration tests MUST use the bundled font for consistency
|
||||
style = Font(font_size=16)
|
||||
# Verify the font loaded properly
|
||||
if style.font.path is None:
|
||||
logger.warning("Font loaded without explicit path - may be using PIL default")
|
||||
logger.warning(
|
||||
"Font loaded without explicit path - may be using PIL default")
|
||||
# Initialize the base Word with required parameters
|
||||
super().__init__(text, style)
|
||||
self._concrete_texts = []
|
||||
|
||||
|
||||
def add_concete(self, texts):
|
||||
"""Add concrete text representations."""
|
||||
if isinstance(texts, list):
|
||||
self._concrete_texts.extend(texts)
|
||||
else:
|
||||
self._concrete_texts.append(texts)
|
||||
|
||||
|
||||
def possible_hyphenation(self):
|
||||
"""Return possible hyphenation points."""
|
||||
if len(self.text) <= 6:
|
||||
return []
|
||||
|
||||
|
||||
# Simple hyphenation: split roughly in the middle
|
||||
mid = len(self.text) // 2
|
||||
return [(self.text[:mid] + "-", self.text[mid:])]
|
||||
@@ -82,7 +83,7 @@ class MockWord(Word):
|
||||
|
||||
class MockParagraph:
|
||||
"""A simple paragraph with words and styling."""
|
||||
|
||||
|
||||
def __init__(self, text_content, word_spacing_style=None):
|
||||
if word_spacing_style is None:
|
||||
word_spacing_style = AbstractStyle(
|
||||
@@ -90,10 +91,10 @@ class MockParagraph:
|
||||
word_spacing_min=2.0,
|
||||
word_spacing_max=8.0
|
||||
)
|
||||
|
||||
|
||||
self.style = word_spacing_style
|
||||
self.line_height = 25
|
||||
|
||||
|
||||
# Create words from text content
|
||||
self.words = []
|
||||
for word_text in text_content.split():
|
||||
@@ -103,39 +104,40 @@ class MockParagraph:
|
||||
|
||||
class TestDocumentLayouterIntegration:
|
||||
"""Integration tests using real components."""
|
||||
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Verify bundled font is available before running any tests."""
|
||||
verify_bundled_font_available()
|
||||
|
||||
|
||||
def test_single_page_layout_with_real_components(self):
|
||||
"""Test layout on a single page using real Line and Text objects."""
|
||||
# Create a real page that can fit content
|
||||
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
||||
page = Page(size=(500, 400), style=page_style)
|
||||
|
||||
|
||||
# Create a paragraph with realistic content
|
||||
paragraph = MockParagraph(
|
||||
"The quick brown fox jumps over the lazy dog and runs through the forest.",
|
||||
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=6.0)
|
||||
)
|
||||
|
||||
|
||||
# Layout the paragraph
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(paragraph, page)
|
||||
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
||||
paragraph, page)
|
||||
|
||||
# Verify successful layout
|
||||
assert success is True
|
||||
assert failed_word_index is None
|
||||
assert remaining_pretext is None
|
||||
|
||||
|
||||
# Verify lines were added to page
|
||||
assert len(page.children) > 0
|
||||
|
||||
|
||||
# Verify actual Line objects were created
|
||||
for child in page.children:
|
||||
assert isinstance(child, Line)
|
||||
|
||||
|
||||
print(f"✓ Single page test: {len(page.children)} lines created")
|
||||
|
||||
def test_multi_page_scenario_with_page_overflow(self):
|
||||
@@ -143,47 +145,52 @@ class TestDocumentLayouterIntegration:
|
||||
# Create a very small real page that will definitely overflow
|
||||
small_page_style = PageStyle(border_width=5, padding=(5, 5, 5, 5))
|
||||
small_page = Page(size=(150, 80), style=small_page_style)
|
||||
|
||||
|
||||
# Create a long paragraph that will definitely overflow
|
||||
long_text = " ".join([f"verylongword{i:02d}" for i in range(20)]) # 20 long words
|
||||
long_text = " ".join(
|
||||
[f"verylongword{i:02d}" for i in range(20)]) # 20 long words
|
||||
paragraph = MockParagraph(
|
||||
long_text,
|
||||
AbstractStyle(word_spacing=4.0, word_spacing_min=2.0, word_spacing_max=8.0)
|
||||
)
|
||||
|
||||
|
||||
# Layout the paragraph - should fail due to page overflow
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(paragraph, small_page)
|
||||
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
||||
paragraph, small_page)
|
||||
|
||||
# Either should fail due to overflow OR succeed with limited content
|
||||
if success:
|
||||
# If it succeeded, verify it fit some content
|
||||
assert len(small_page.children) > 0
|
||||
print(f"✓ Multi-page test: Content fit on small page, {len(small_page.children)} lines created")
|
||||
print(
|
||||
f"✓ Multi-page test: Content fit on small page, {len(small_page.children)} lines created")
|
||||
else:
|
||||
# If it failed, verify overflow handling
|
||||
assert failed_word_index is not None # Should indicate where it failed
|
||||
assert failed_word_index < len(paragraph.words) # Should be within word range
|
||||
print(f"✓ Multi-page test: Page overflow at word {failed_word_index}, {len(small_page.children)} lines fit")
|
||||
assert failed_word_index < len(
|
||||
paragraph.words) # Should be within word range
|
||||
print(
|
||||
f"✓ Multi-page test: Page overflow at word {failed_word_index}, {len(small_page.children)} lines fit")
|
||||
|
||||
def test_word_spacing_constraints_in_real_lines(self):
|
||||
"""Test that word spacing constraints are properly used in real Line objects."""
|
||||
# Create real page
|
||||
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
||||
page = Page(size=(400, 300), style=page_style)
|
||||
|
||||
|
||||
# Create paragraph with specific spacing constraints
|
||||
paragraph = MockParagraph(
|
||||
"Testing word spacing constraints with realistic content.",
|
||||
AbstractStyle(word_spacing=5.0, word_spacing_min=3.0, word_spacing_max=10.0)
|
||||
)
|
||||
|
||||
|
||||
# Layout paragraph
|
||||
success, _, _ = paragraph_layouter(paragraph, page)
|
||||
assert success is True
|
||||
|
||||
|
||||
# Verify that Line objects were created with correct spacing
|
||||
assert len(page.children) > 0
|
||||
|
||||
|
||||
for line in page.children:
|
||||
assert isinstance(line, Line)
|
||||
# Verify spacing constraints were applied
|
||||
@@ -191,35 +198,34 @@ class TestDocumentLayouterIntegration:
|
||||
min_spacing, max_spacing = line._spacing
|
||||
assert min_spacing == 3 # From our constraint
|
||||
assert max_spacing == 10 # From our constraint
|
||||
|
||||
print(f"✓ Word spacing test: {len(page.children)} lines with constraints (3, 10)")
|
||||
|
||||
print(
|
||||
f"✓ Word spacing test: {len(page.children)} lines with constraints (3, 10)")
|
||||
|
||||
def test_different_alignment_strategies_with_constraints(self):
|
||||
"""Test different text alignment strategies with word spacing constraints."""
|
||||
alignments_to_test = [
|
||||
("left", AbstractStyle(text_align="left", word_spacing_min=2.0, word_spacing_max=6.0)),
|
||||
("justify", AbstractStyle(text_align="justify", word_spacing_min=3.0, word_spacing_max=12.0)),
|
||||
("center", AbstractStyle(text_align="center", word_spacing_min=1.0, word_spacing_max=5.0))
|
||||
]
|
||||
|
||||
("left", AbstractStyle(
|
||||
text_align="left", word_spacing_min=2.0, word_spacing_max=6.0)), ("justify", AbstractStyle(
|
||||
text_align="justify", word_spacing_min=3.0, word_spacing_max=12.0)), ("center", AbstractStyle(
|
||||
text_align="center", word_spacing_min=1.0, word_spacing_max=5.0))]
|
||||
|
||||
for alignment_name, style in alignments_to_test:
|
||||
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
||||
page = Page(size=(350, 200), style=page_style)
|
||||
paragraph = MockParagraph(
|
||||
"This sentence will test different alignment strategies with word spacing.",
|
||||
style
|
||||
)
|
||||
|
||||
"This sentence will test different alignment strategies with word spacing.", style)
|
||||
|
||||
success, _, _ = paragraph_layouter(paragraph, page)
|
||||
assert success is True
|
||||
assert len(page.children) > 0
|
||||
|
||||
|
||||
# Verify alignment was applied to lines
|
||||
for line in page.children:
|
||||
assert isinstance(line, Line)
|
||||
# Check that the alignment handler was set correctly
|
||||
assert line._alignment_handler is not None
|
||||
|
||||
|
||||
print(f"✓ {alignment_name} alignment: {len(page.children)} lines created")
|
||||
|
||||
def test_realistic_document_with_multiple_pages(self):
|
||||
@@ -227,38 +233,42 @@ class TestDocumentLayouterIntegration:
|
||||
# Create multiple real pages
|
||||
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
||||
pages = [Page(size=(400, 300), style=page_style) for _ in range(3)]
|
||||
|
||||
|
||||
# Create a document with multiple paragraphs
|
||||
paragraphs = [
|
||||
MockParagraph(
|
||||
"This is the first paragraph of our document. It contains enough text to potentially span multiple lines and test the word spacing constraints properly.",
|
||||
"This is the first paragraph of our document. It contains enough text to "
|
||||
"potentially span multiple lines and test the word spacing constraints properly.",
|
||||
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=8.0)
|
||||
),
|
||||
MockParagraph(
|
||||
"Here is a second paragraph with different styling. This paragraph uses different word spacing constraints to test the flexibility of the system.",
|
||||
"Here is a second paragraph with different styling. This paragraph uses "
|
||||
"different word spacing constraints to test the flexibility of the system.",
|
||||
AbstractStyle(word_spacing=5.0, word_spacing_min=3.0, word_spacing_max=12.0)
|
||||
),
|
||||
MockParagraph(
|
||||
"The third and final paragraph completes our test document. It should demonstrate that the layouter can handle multiple paragraphs with varying content lengths and styling requirements.",
|
||||
"The third and final paragraph completes our test document. It should "
|
||||
"demonstrate that the layouter can handle multiple paragraphs with varying "
|
||||
"content lengths and styling requirements.",
|
||||
AbstractStyle(word_spacing=4.0, word_spacing_min=2.5, word_spacing_max=10.0)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# Layout paragraphs across pages
|
||||
current_page_index = 0
|
||||
|
||||
|
||||
for para_index, paragraph in enumerate(paragraphs):
|
||||
start_word = 0
|
||||
|
||||
|
||||
while start_word < len(paragraph.words):
|
||||
if current_page_index >= len(pages):
|
||||
break # Out of pages
|
||||
|
||||
|
||||
current_page = pages[current_page_index]
|
||||
success, failed_word_index, _ = paragraph_layouter(
|
||||
paragraph, current_page, start_word
|
||||
)
|
||||
|
||||
|
||||
if success:
|
||||
# Paragraph completed on this page
|
||||
break
|
||||
@@ -267,25 +277,25 @@ class TestDocumentLayouterIntegration:
|
||||
if failed_word_index is not None:
|
||||
start_word = failed_word_index
|
||||
current_page_index += 1
|
||||
|
||||
|
||||
# If we're out of pages, stop
|
||||
if current_page_index >= len(pages):
|
||||
break
|
||||
|
||||
|
||||
# Verify pages have content
|
||||
total_lines = sum(len(page.children) for page in pages)
|
||||
pages_used = sum(1 for page in pages if len(page.children) > 0)
|
||||
|
||||
|
||||
assert total_lines > 0
|
||||
assert pages_used > 1 # Should use multiple pages
|
||||
|
||||
|
||||
print(f"✓ Multi-document test: {total_lines} lines across {pages_used} pages")
|
||||
|
||||
def test_word_spacing_constraint_resolution_integration(self):
|
||||
"""Test the complete integration from AbstractStyle to Line spacing."""
|
||||
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
||||
page = Page(size=(400, 600), style=page_style)
|
||||
|
||||
_page = Page(size=(400, 600), style=page_style)
|
||||
|
||||
# Test different constraint scenarios
|
||||
test_cases = [
|
||||
{
|
||||
@@ -295,7 +305,7 @@ class TestDocumentLayouterIntegration:
|
||||
"expected_max": 12
|
||||
},
|
||||
{
|
||||
"name": "default_constraints",
|
||||
"name": "default_constraints",
|
||||
"style": AbstractStyle(word_spacing=6.0),
|
||||
"expected_min": 6, # Should use word_spacing as min
|
||||
"expected_max": 12 # Should use word_spacing * 2 as max
|
||||
@@ -307,7 +317,7 @@ class TestDocumentLayouterIntegration:
|
||||
"expected_max": 8 # Default based on font size (16 * 0.5)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
for case in test_cases:
|
||||
# Create fresh real page for each test
|
||||
test_page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
||||
@@ -316,18 +326,20 @@ class TestDocumentLayouterIntegration:
|
||||
"Testing constraint resolution with different scenarios.",
|
||||
case["style"]
|
||||
)
|
||||
|
||||
|
||||
success, _, _ = paragraph_layouter(paragraph, test_page)
|
||||
assert success is True
|
||||
assert len(test_page.children) > 0
|
||||
|
||||
|
||||
# Verify constraints were resolved correctly
|
||||
line = test_page.children[0]
|
||||
min_spacing, max_spacing = line._spacing
|
||||
|
||||
assert min_spacing == case["expected_min"], f"Min constraint failed for {case['name']}"
|
||||
assert max_spacing == case["expected_max"], f"Max constraint failed for {case['name']}"
|
||||
|
||||
|
||||
assert min_spacing == case["expected_min"], f"Min constraint failed for {
|
||||
case['name']}"
|
||||
assert max_spacing == case["expected_max"], f"Max constraint failed for {
|
||||
case['name']}"
|
||||
|
||||
print(f"✓ {case['name']}: constraints ({min_spacing}, {max_spacing})")
|
||||
|
||||
def test_hyphenation_with_word_spacing_constraints(self):
|
||||
@@ -335,15 +347,16 @@ class TestDocumentLayouterIntegration:
|
||||
# Create a narrow real page to force hyphenation
|
||||
narrow_page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
||||
narrow_page = Page(size=(200, 300), style=narrow_page_style)
|
||||
|
||||
|
||||
# Create paragraph with long words that will need hyphenation
|
||||
paragraph = MockParagraph(
|
||||
"supercalifragilisticexpialidocious antidisestablishmentarianism",
|
||||
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=8.0)
|
||||
)
|
||||
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(paragraph, narrow_page)
|
||||
|
||||
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
||||
paragraph, narrow_page)
|
||||
|
||||
# Should succeed with hyphenation or handle overflow gracefully
|
||||
if success:
|
||||
assert len(narrow_page.children) > 0
|
||||
@@ -357,10 +370,10 @@ class TestDocumentLayouterIntegration:
|
||||
if __name__ == "__main__":
|
||||
# Run integration tests
|
||||
test = TestDocumentLayouterIntegration()
|
||||
|
||||
|
||||
print("Running document layouter integration tests...")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
test.test_single_page_layout_with_real_components()
|
||||
test.test_multi_page_scenario_with_page_overflow()
|
||||
test.test_word_spacing_constraints_in_real_lines()
|
||||
@@ -368,6 +381,6 @@ if __name__ == "__main__":
|
||||
test.test_realistic_document_with_multiple_pages()
|
||||
test.test_word_spacing_constraint_resolution_integration()
|
||||
test.test_hyphenation_with_word_spacing_constraints()
|
||||
|
||||
|
||||
print("=" * 50)
|
||||
print("✅ All integration tests completed successfully!")
|
||||
|
||||
@@ -27,8 +27,12 @@ class FontRegistryTestMixin:
|
||||
obj = self.create_test_object()
|
||||
|
||||
# Create font twice with same properties
|
||||
font1 = obj.get_or_create_font(font_size=14, colour=(255, 0, 0), weight=FontWeight.BOLD)
|
||||
font2 = obj.get_or_create_font(font_size=14, colour=(255, 0, 0), weight=FontWeight.BOLD)
|
||||
font1 = obj.get_or_create_font(
|
||||
font_size=14, colour=(
|
||||
255, 0, 0), weight=FontWeight.BOLD)
|
||||
font2 = obj.get_or_create_font(
|
||||
font_size=14, colour=(
|
||||
255, 0, 0), weight=FontWeight.BOLD)
|
||||
|
||||
# Should return the same font object (cached)
|
||||
self.assertIs(font1, font2, "Fonts with identical properties should be cached")
|
||||
@@ -47,15 +51,21 @@ class FontRegistryTestMixin:
|
||||
font2 = obj.get_or_create_font(font_size=16, **base_params) # Different size
|
||||
|
||||
base_params2 = {'font_size': 18, 'weight': FontWeight.NORMAL}
|
||||
font3 = obj.get_or_create_font(colour=(255, 0, 0), **base_params2) # Different color
|
||||
font3 = obj.get_or_create_font(
|
||||
colour=(255, 0, 0), **base_params2) # Different color
|
||||
|
||||
base_params3 = {'font_size': 20, 'colour': (100, 100, 100)}
|
||||
font4 = obj.get_or_create_font(weight=FontWeight.BOLD, **base_params3) # Different weight
|
||||
font4 = obj.get_or_create_font(
|
||||
weight=FontWeight.BOLD,
|
||||
**base_params3) # Different weight
|
||||
|
||||
# All should be different objects
|
||||
self.assertIsNot(font1, font2, "Fonts with different sizes should be distinct")
|
||||
self.assertIsNot(font1, font3, "Fonts with different colors should be distinct")
|
||||
self.assertIsNot(font1, font4, "Fonts with different weights should be distinct")
|
||||
self.assertIsNot(
|
||||
font1,
|
||||
font4,
|
||||
"Fonts with different weights should be distinct")
|
||||
self.assertIsNot(font2, font3, "Fonts should be distinct")
|
||||
self.assertIsNot(font2, font4, "Fonts should be distinct")
|
||||
self.assertIsNot(font3, font4, "Fonts should be distinct")
|
||||
|
||||
@@ -53,7 +53,10 @@ class MetadataContainerTestMixin:
|
||||
|
||||
# Update value
|
||||
obj.set_metadata("key", "updated")
|
||||
self.assertEqual(obj.get_metadata("key"), "updated", "Metadata should be updateable")
|
||||
self.assertEqual(
|
||||
obj.get_metadata("key"),
|
||||
"updated",
|
||||
"Metadata should be updateable")
|
||||
|
||||
def test_metadata_isolation(self):
|
||||
"""Test that metadata is isolated between instances."""
|
||||
|
||||
@@ -11,35 +11,38 @@ from pyWebLayout.style import Alignment
|
||||
|
||||
class TestStyleObjects(unittest.TestCase):
|
||||
"""Test cases for pyWebLayout style objects."""
|
||||
|
||||
|
||||
def test_font_weight_enum(self):
|
||||
"""Test FontWeight enum values."""
|
||||
self.assertEqual(FontWeight.NORMAL.value, "normal")
|
||||
self.assertEqual(FontWeight.BOLD.value, "bold")
|
||||
|
||||
|
||||
# Test that all expected values exist
|
||||
weights = [FontWeight.NORMAL, FontWeight.BOLD]
|
||||
self.assertEqual(len(weights), 2)
|
||||
|
||||
|
||||
def test_font_style_enum(self):
|
||||
"""Test FontStyle enum values."""
|
||||
self.assertEqual(FontStyle.NORMAL.value, "normal")
|
||||
self.assertEqual(FontStyle.ITALIC.value, "italic")
|
||||
|
||||
|
||||
# Test that all expected values exist
|
||||
styles = [FontStyle.NORMAL, FontStyle.ITALIC]
|
||||
self.assertEqual(len(styles), 2)
|
||||
|
||||
|
||||
def test_text_decoration_enum(self):
|
||||
"""Test TextDecoration enum values."""
|
||||
self.assertEqual(TextDecoration.NONE.value, "none")
|
||||
self.assertEqual(TextDecoration.UNDERLINE.value, "underline")
|
||||
self.assertEqual(TextDecoration.STRIKETHROUGH.value, "strikethrough")
|
||||
|
||||
|
||||
# Test that all expected values exist
|
||||
decorations = [TextDecoration.NONE, TextDecoration.UNDERLINE, TextDecoration.STRIKETHROUGH]
|
||||
decorations = [
|
||||
TextDecoration.NONE,
|
||||
TextDecoration.UNDERLINE,
|
||||
TextDecoration.STRIKETHROUGH]
|
||||
self.assertEqual(len(decorations), 3)
|
||||
|
||||
|
||||
def test_alignment_enum(self):
|
||||
"""Test Alignment enum values."""
|
||||
self.assertEqual(Alignment.LEFT.value, "left")
|
||||
@@ -49,11 +52,11 @@ class TestStyleObjects(unittest.TestCase):
|
||||
self.assertEqual(Alignment.BOTTOM.value, "bottom")
|
||||
self.assertEqual(Alignment.JUSTIFY.value, "justify")
|
||||
self.assertEqual(Alignment.MIDDLE.value, "middle")
|
||||
|
||||
|
||||
def test_font_initialization_defaults(self):
|
||||
"""Test Font initialization with default values."""
|
||||
font = Font()
|
||||
|
||||
|
||||
self.assertIsNone(font._font_path)
|
||||
self.assertEqual(font.font_size, 16)
|
||||
self.assertEqual(font.colour, (0, 0, 0))
|
||||
@@ -63,7 +66,7 @@ class TestStyleObjects(unittest.TestCase):
|
||||
self.assertEqual(font.decoration, TextDecoration.NONE)
|
||||
self.assertEqual(font.background, (255, 255, 255, 0)) # Transparent
|
||||
self.assertEqual(font.language, "en_EN")
|
||||
|
||||
|
||||
def test_font_initialization_custom(self):
|
||||
"""Test Font initialization with custom values."""
|
||||
font = Font(
|
||||
@@ -76,7 +79,7 @@ class TestStyleObjects(unittest.TestCase):
|
||||
background=(255, 255, 0, 255),
|
||||
language="fr_FR"
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(font._font_path, "/path/to/font.ttf")
|
||||
self.assertEqual(font.font_size, 16)
|
||||
self.assertEqual(font.colour, (255, 0, 0))
|
||||
@@ -85,7 +88,7 @@ class TestStyleObjects(unittest.TestCase):
|
||||
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(font.background, (255, 255, 0, 255))
|
||||
self.assertEqual(font.language, "fr_FR")
|
||||
|
||||
|
||||
def test_font_with_methods(self):
|
||||
"""Test Font immutable modification methods."""
|
||||
original_font = Font(
|
||||
@@ -95,34 +98,36 @@ class TestStyleObjects(unittest.TestCase):
|
||||
style=FontStyle.NORMAL,
|
||||
decoration=TextDecoration.NONE
|
||||
)
|
||||
|
||||
|
||||
# Test with_size
|
||||
size_font = original_font.with_size(16)
|
||||
self.assertEqual(size_font.font_size, 16)
|
||||
self.assertEqual(original_font.font_size, 12) # Original unchanged
|
||||
self.assertEqual(size_font.colour, (0, 0, 0)) # Other properties preserved
|
||||
|
||||
|
||||
# Test with_colour
|
||||
color_font = original_font.with_colour((255, 0, 0))
|
||||
self.assertEqual(color_font.colour, (255, 0, 0))
|
||||
self.assertEqual(original_font.colour, (0, 0, 0)) # Original unchanged
|
||||
self.assertEqual(color_font.font_size, 12) # Other properties preserved
|
||||
|
||||
|
||||
# Test with_weight
|
||||
weight_font = original_font.with_weight(FontWeight.BOLD)
|
||||
self.assertEqual(weight_font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(original_font.weight, FontWeight.NORMAL) # Original unchanged
|
||||
|
||||
|
||||
# Test with_style
|
||||
style_font = original_font.with_style(FontStyle.ITALIC)
|
||||
self.assertEqual(style_font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(original_font.style, FontStyle.NORMAL) # Original unchanged
|
||||
|
||||
|
||||
# Test with_decoration
|
||||
decoration_font = original_font.with_decoration(TextDecoration.UNDERLINE)
|
||||
self.assertEqual(decoration_font.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(original_font.decoration, TextDecoration.NONE) # Original unchanged
|
||||
|
||||
self.assertEqual(
|
||||
original_font.decoration,
|
||||
TextDecoration.NONE) # Original unchanged
|
||||
|
||||
def test_font_property_access(self):
|
||||
"""Test Font property access methods."""
|
||||
font = Font(
|
||||
@@ -132,7 +137,7 @@ class TestStyleObjects(unittest.TestCase):
|
||||
style=FontStyle.ITALIC,
|
||||
decoration=TextDecoration.STRIKETHROUGH
|
||||
)
|
||||
|
||||
|
||||
# Test all property getters
|
||||
self.assertEqual(font.font_size, 20)
|
||||
self.assertEqual(font.colour, (128, 128, 128))
|
||||
@@ -140,41 +145,41 @@ class TestStyleObjects(unittest.TestCase):
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
|
||||
# Test that font object is accessible
|
||||
self.assertIsNotNone(font.font)
|
||||
|
||||
|
||||
def test_font_immutability(self):
|
||||
"""Test that Font objects behave immutably."""
|
||||
font1 = Font(font_size=12, colour=(0, 0, 0))
|
||||
font2 = font1.with_size(16)
|
||||
font3 = font2.with_colour((255, 0, 0))
|
||||
|
||||
|
||||
# Each should be different objects
|
||||
self.assertIsNot(font1, font2)
|
||||
self.assertIsNot(font2, font3)
|
||||
self.assertIsNot(font1, font3)
|
||||
|
||||
|
||||
# Original properties should be unchanged
|
||||
self.assertEqual(font1.font_size, 12)
|
||||
self.assertEqual(font1.colour, (0, 0, 0))
|
||||
|
||||
|
||||
self.assertEqual(font2.font_size, 16)
|
||||
self.assertEqual(font2.colour, (0, 0, 0))
|
||||
|
||||
|
||||
self.assertEqual(font3.font_size, 16)
|
||||
self.assertEqual(font3.colour, (255, 0, 0))
|
||||
|
||||
|
||||
def test_background_handling(self):
|
||||
"""Test background color handling."""
|
||||
# Test default transparent background
|
||||
font1 = Font()
|
||||
self.assertEqual(font1.background, (255, 255, 255, 0))
|
||||
|
||||
|
||||
# Test explicit background
|
||||
font2 = Font(background=(255, 0, 0, 128))
|
||||
self.assertEqual(font2.background, (255, 0, 0, 128))
|
||||
|
||||
|
||||
# Test None background becomes transparent
|
||||
font3 = Font(background=None)
|
||||
self.assertEqual(font3.background, (255, 255, 255, 0))
|
||||
|
||||
@@ -8,12 +8,12 @@ based on user preferences.
|
||||
|
||||
import pytest
|
||||
from pyWebLayout.style.abstract_style import (
|
||||
AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize, TextAlign
|
||||
AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize
|
||||
)
|
||||
from pyWebLayout.style.concrete_style import (
|
||||
ConcreteStyle, ConcreteStyleRegistry, RenderingContext, StyleResolver
|
||||
ConcreteStyleRegistry, RenderingContext, StyleResolver
|
||||
)
|
||||
from pyWebLayout.style.fonts import FontWeight, FontStyle, TextDecoration
|
||||
from pyWebLayout.style.fonts import FontWeight
|
||||
|
||||
|
||||
def test_abstract_style_is_hashable():
|
||||
@@ -25,18 +25,18 @@ def test_abstract_style_is_hashable():
|
||||
font_weight=FontWeight.BOLD,
|
||||
color="red"
|
||||
)
|
||||
|
||||
|
||||
style2 = AbstractStyle(
|
||||
font_family=FontFamily.SERIF,
|
||||
font_size=16,
|
||||
font_weight=FontWeight.BOLD,
|
||||
color="red"
|
||||
)
|
||||
|
||||
|
||||
# They should be equal and have the same hash
|
||||
assert style1 == style2
|
||||
assert hash(style1) == hash(style2)
|
||||
|
||||
|
||||
# They should work as dictionary keys
|
||||
style_dict = {style1: "first", style2: "second"}
|
||||
assert len(style_dict) == 1 # Should be deduplicated
|
||||
@@ -46,15 +46,15 @@ def test_abstract_style_is_hashable():
|
||||
def test_abstract_style_registry_deduplication():
|
||||
"""Test that the registry prevents duplicate styles."""
|
||||
registry = AbstractStyleRegistry()
|
||||
|
||||
|
||||
# Create the same style twice
|
||||
style1 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD)
|
||||
style2 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD)
|
||||
|
||||
|
||||
# Register both - should get same ID
|
||||
id1, _ = registry.get_or_create_style(style1)
|
||||
id2, _ = registry.get_or_create_style(style2)
|
||||
|
||||
|
||||
assert id1 == id2 # Same style should get same ID
|
||||
assert registry.get_style_count() == 2 # Only default + our style
|
||||
|
||||
@@ -62,21 +62,21 @@ def test_abstract_style_registry_deduplication():
|
||||
def test_style_inheritance():
|
||||
"""Test that style inheritance works properly."""
|
||||
registry = AbstractStyleRegistry()
|
||||
|
||||
|
||||
# Create base style
|
||||
base_style = AbstractStyle(font_size=16, color="black")
|
||||
base_id, _ = registry.get_or_create_style(base_style)
|
||||
|
||||
|
||||
# Create derived style
|
||||
derived_id, derived_style = registry.create_derived_style(
|
||||
base_id,
|
||||
base_id,
|
||||
font_weight=FontWeight.BOLD,
|
||||
color="red"
|
||||
)
|
||||
|
||||
|
||||
# Resolve effective style
|
||||
effective = registry.resolve_effective_style(derived_id)
|
||||
|
||||
|
||||
assert effective.font_size == 16 # Inherited from base
|
||||
assert effective.font_weight == FontWeight.BOLD # Overridden
|
||||
assert effective.color == "red" # Overridden
|
||||
@@ -90,16 +90,17 @@ def test_style_resolver_user_preferences():
|
||||
font_scale_factor=1.5, # Additional scaling
|
||||
large_text=True # Accessibility preference
|
||||
)
|
||||
|
||||
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
# Create abstract style with medium size
|
||||
abstract_style = AbstractStyle(font_size=FontSize.MEDIUM)
|
||||
|
||||
|
||||
# Resolve to concrete style
|
||||
concrete_style = resolver.resolve_style(abstract_style)
|
||||
|
||||
# Font size should be: 20 (base) * 1.0 (medium) * 1.5 (scale) * 1.2 (large_text) = 36
|
||||
|
||||
# Font size should be: 20 (base) * 1.0 (medium) * 1.5 (scale) * 1.2
|
||||
# (large_text) = 36
|
||||
expected_size = int(20 * 1.0 * 1.5 * 1.2)
|
||||
assert concrete_style.font_size == expected_size
|
||||
|
||||
@@ -108,17 +109,17 @@ def test_style_resolver_color_resolution():
|
||||
"""Test color name resolution."""
|
||||
context = RenderingContext()
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
# Test named colors
|
||||
red_style = AbstractStyle(color="red")
|
||||
concrete_red = resolver.resolve_style(red_style)
|
||||
assert concrete_red.color == (255, 0, 0)
|
||||
|
||||
|
||||
# Test hex colors
|
||||
hex_style = AbstractStyle(color="#ff0000")
|
||||
concrete_hex = resolver.resolve_style(hex_style)
|
||||
assert concrete_hex.color == (255, 0, 0)
|
||||
|
||||
|
||||
# Test RGB tuple (should pass through)
|
||||
rgb_style = AbstractStyle(color=(128, 64, 192))
|
||||
concrete_rgb = resolver.resolve_style(rgb_style)
|
||||
@@ -129,17 +130,17 @@ def test_concrete_style_caching():
|
||||
"""Test that concrete styles are cached efficiently."""
|
||||
context = RenderingContext()
|
||||
registry = ConcreteStyleRegistry(StyleResolver(context))
|
||||
|
||||
|
||||
# Create abstract style
|
||||
abstract_style = AbstractStyle(font_size=16, color="blue")
|
||||
|
||||
|
||||
# Get font twice - should be cached
|
||||
font1 = registry.get_font(abstract_style)
|
||||
font2 = registry.get_font(abstract_style)
|
||||
|
||||
|
||||
# Should be the same object (cached)
|
||||
assert font1 is font2
|
||||
|
||||
|
||||
# Check cache stats
|
||||
stats = registry.get_cache_stats()
|
||||
assert stats["concrete_styles"] == 1
|
||||
@@ -151,17 +152,17 @@ def test_global_font_scaling():
|
||||
# Create two contexts with different scaling
|
||||
context_normal = RenderingContext(font_scale_factor=1.0)
|
||||
context_large = RenderingContext(font_scale_factor=2.0)
|
||||
|
||||
|
||||
resolver_normal = StyleResolver(context_normal)
|
||||
resolver_large = StyleResolver(context_large)
|
||||
|
||||
|
||||
# Same abstract style
|
||||
abstract_style = AbstractStyle(font_size=16)
|
||||
|
||||
|
||||
# Resolve with different contexts
|
||||
concrete_normal = resolver_normal.resolve_style(abstract_style)
|
||||
concrete_large = resolver_large.resolve_style(abstract_style)
|
||||
|
||||
|
||||
# Large should be 2x the size
|
||||
assert concrete_large.font_size == concrete_normal.font_size * 2
|
||||
|
||||
@@ -169,7 +170,7 @@ def test_global_font_scaling():
|
||||
def test_memory_efficiency():
|
||||
"""Test that the new system is more memory efficient."""
|
||||
registry = AbstractStyleRegistry()
|
||||
|
||||
|
||||
# Create many "different" styles that are actually the same
|
||||
styles = []
|
||||
for i in range(100):
|
||||
@@ -181,26 +182,26 @@ def test_memory_efficiency():
|
||||
)
|
||||
style_id, _ = registry.get_or_create_style(style)
|
||||
styles.append(style_id)
|
||||
|
||||
|
||||
# All should reference the same style
|
||||
assert len(set(styles)) == 1 # All IDs are the same
|
||||
assert registry.get_style_count() == 2 # Only default + our style
|
||||
|
||||
|
||||
# This demonstrates that we don't create duplicate styles
|
||||
|
||||
|
||||
def test_word_style_reference_concept():
|
||||
"""Demonstrate how words would reference styles instead of storing fonts."""
|
||||
registry = AbstractStyleRegistry()
|
||||
|
||||
|
||||
# Create paragraph style
|
||||
para_style = AbstractStyle(font_size=16, color="black")
|
||||
para_id, _ = registry.get_or_create_style(para_style)
|
||||
|
||||
|
||||
# Create bold word style
|
||||
bold_style = AbstractStyle(font_size=16, color="black", font_weight=FontWeight.BOLD)
|
||||
bold_id, _ = registry.get_or_create_style(bold_style)
|
||||
|
||||
|
||||
# Simulate words storing style IDs instead of full Font objects
|
||||
words_data = [
|
||||
{"text": "This", "style_id": para_id},
|
||||
@@ -208,19 +209,19 @@ def test_word_style_reference_concept():
|
||||
{"text": "bold", "style_id": bold_id},
|
||||
{"text": "text", "style_id": para_id},
|
||||
]
|
||||
|
||||
|
||||
# To get the actual font for rendering, we resolve through registry
|
||||
context = RenderingContext()
|
||||
concrete_registry = ConcreteStyleRegistry(StyleResolver(context))
|
||||
|
||||
|
||||
for word_data in words_data:
|
||||
abstract_style = registry.get_style_by_id(word_data["style_id"])
|
||||
font = concrete_registry.get_font(abstract_style)
|
||||
|
||||
|
||||
# Now we have the actual Font object for rendering
|
||||
assert font is not None
|
||||
assert hasattr(font, 'font_size')
|
||||
|
||||
|
||||
# Bold word should have bold weight
|
||||
if word_data["text"] == "bold":
|
||||
assert font.weight == FontWeight.BOLD
|
||||
|
||||
@@ -5,14 +5,13 @@ This test shows how to use the new min/max word spacing constraints
|
||||
in the style system.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pyWebLayout.style.abstract_style import AbstractStyle, AbstractStyleRegistry
|
||||
from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext
|
||||
from pyWebLayout.style.concrete_style import StyleResolver, RenderingContext
|
||||
|
||||
|
||||
class TestWordSpacingConstraints:
|
||||
"""Test cases for word spacing constraints feature."""
|
||||
|
||||
|
||||
def test_abstract_style_with_word_spacing_constraints(self):
|
||||
"""Test that AbstractStyle accepts word spacing constraint fields."""
|
||||
style = AbstractStyle(
|
||||
@@ -20,116 +19,116 @@ class TestWordSpacingConstraints:
|
||||
word_spacing_min=2.0,
|
||||
word_spacing_max=10.0
|
||||
)
|
||||
|
||||
|
||||
assert style.word_spacing == 5.0
|
||||
assert style.word_spacing_min == 2.0
|
||||
assert style.word_spacing_max == 10.0
|
||||
|
||||
|
||||
def test_concrete_style_resolution_with_constraints(self):
|
||||
"""Test that word spacing constraints are resolved correctly."""
|
||||
# Create rendering context
|
||||
context = RenderingContext(base_font_size=16)
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
# Create abstract style with constraints
|
||||
abstract_style = AbstractStyle(
|
||||
word_spacing=5.0,
|
||||
word_spacing_min=2.0,
|
||||
word_spacing_max=12.0
|
||||
)
|
||||
|
||||
|
||||
# Resolve to concrete style
|
||||
concrete_style = resolver.resolve_style(abstract_style)
|
||||
|
||||
|
||||
# Check that constraints are preserved
|
||||
assert concrete_style.word_spacing == 5.0
|
||||
assert concrete_style.word_spacing_min == 2.0
|
||||
assert concrete_style.word_spacing_max == 12.0
|
||||
|
||||
|
||||
def test_default_constraint_logic(self):
|
||||
"""Test default constraint logic when not specified."""
|
||||
context = RenderingContext(base_font_size=16)
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
# Style with only base word spacing
|
||||
abstract_style = AbstractStyle(word_spacing=6.0)
|
||||
concrete_style = resolver.resolve_style(abstract_style)
|
||||
|
||||
|
||||
# Should apply default logic: min = base, max = base * 2
|
||||
assert concrete_style.word_spacing == 6.0
|
||||
assert concrete_style.word_spacing_min == 6.0
|
||||
assert concrete_style.word_spacing_max == 12.0
|
||||
|
||||
|
||||
def test_no_word_spacing_defaults(self):
|
||||
"""Test defaults when no word spacing is specified."""
|
||||
context = RenderingContext(base_font_size=16)
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
# Style with no word spacing specified
|
||||
abstract_style = AbstractStyle()
|
||||
concrete_style = resolver.resolve_style(abstract_style)
|
||||
|
||||
|
||||
# Should apply font-based defaults
|
||||
assert concrete_style.word_spacing == 0.0
|
||||
assert concrete_style.word_spacing_min == 2.0 # Minimum default
|
||||
assert concrete_style.word_spacing_max == 8.0 # 50% of font size (16 * 0.5)
|
||||
|
||||
|
||||
def test_partial_constraints(self):
|
||||
"""Test behavior when only min or max is specified."""
|
||||
context = RenderingContext(base_font_size=16)
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
# Only min specified
|
||||
abstract_style_min = AbstractStyle(
|
||||
word_spacing=4.0,
|
||||
word_spacing_min=3.0
|
||||
)
|
||||
concrete_style_min = resolver.resolve_style(abstract_style_min)
|
||||
|
||||
|
||||
assert concrete_style_min.word_spacing_min == 3.0
|
||||
assert concrete_style_min.word_spacing_max == 6.0 # 3.0 * 2
|
||||
|
||||
|
||||
# Only max specified
|
||||
abstract_style_max = AbstractStyle(
|
||||
word_spacing=4.0,
|
||||
word_spacing_max=8.0
|
||||
)
|
||||
concrete_style_max = resolver.resolve_style(abstract_style_max)
|
||||
|
||||
|
||||
assert concrete_style_max.word_spacing_min == 4.0 # max(word_spacing, 2.0)
|
||||
assert concrete_style_max.word_spacing_max == 8.0
|
||||
|
||||
|
||||
def test_style_registry_with_constraints(self):
|
||||
"""Test that style registry handles word spacing constraints."""
|
||||
registry = AbstractStyleRegistry()
|
||||
|
||||
|
||||
# Create style with constraints
|
||||
style_id, style = registry.get_or_create_style(
|
||||
word_spacing=5.0,
|
||||
word_spacing_min=3.0,
|
||||
word_spacing_max=10.0
|
||||
)
|
||||
|
||||
|
||||
# Verify the style was created correctly
|
||||
retrieved_style = registry.get_style_by_id(style_id)
|
||||
assert retrieved_style.word_spacing == 5.0
|
||||
assert retrieved_style.word_spacing_min == 3.0
|
||||
assert retrieved_style.word_spacing_max == 10.0
|
||||
|
||||
|
||||
def test_em_units_in_constraints(self):
|
||||
"""Test that em units work in word spacing constraints."""
|
||||
context = RenderingContext(base_font_size=16)
|
||||
resolver = StyleResolver(context)
|
||||
|
||||
|
||||
# Use em units
|
||||
abstract_style = AbstractStyle(
|
||||
word_spacing="0.25em",
|
||||
word_spacing_min="0.1em",
|
||||
word_spacing_max="0.5em"
|
||||
)
|
||||
|
||||
|
||||
concrete_style = resolver.resolve_style(abstract_style)
|
||||
|
||||
|
||||
# Should convert em to pixels based on font size (16px)
|
||||
assert concrete_style.word_spacing == 4.0 # 0.25 * 16
|
||||
assert concrete_style.word_spacing_min == 1.6 # 0.1 * 16
|
||||
@@ -146,5 +145,5 @@ if __name__ == "__main__":
|
||||
test.test_partial_constraints()
|
||||
test.test_style_registry_with_constraints()
|
||||
test.test_em_units_in_constraints()
|
||||
|
||||
|
||||
print("All word spacing constraint tests passed!")
|
||||
|
||||
@@ -6,9 +6,8 @@ import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.core.callback_registry import CallbackRegistry
|
||||
from pyWebLayout.core.base import Interactable
|
||||
from pyWebLayout.concrete.functional import LinkText, ButtonText, FormFieldText
|
||||
from pyWebLayout.abstract.functional import Link, Button, FormField, LinkType, FormFieldType
|
||||
from pyWebLayout.concrete.functional import LinkText, ButtonText
|
||||
from pyWebLayout.abstract.functional import Link, Button, LinkType
|
||||
from pyWebLayout.style import Font
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import unittest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from PIL import Image as PILImage
|
||||
import numpy as np
|
||||
|
||||
from pyWebLayout.abstract.interactive_image import InteractiveImage
|
||||
|
||||
@@ -123,7 +122,6 @@ class TestInteractiveImage(unittest.TestCase):
|
||||
|
||||
def test_create_and_add_to(self):
|
||||
"""Test the convenience factory method"""
|
||||
callback_result = []
|
||||
|
||||
def callback(point):
|
||||
return "added!"
|
||||
@@ -171,8 +169,18 @@ class TestInteractiveImage(unittest.TestCase):
|
||||
def callback2(point):
|
||||
return "image2"
|
||||
|
||||
img1 = InteractiveImage(source=str(self.test_image_path), width=50, height=50, callback=callback1)
|
||||
img2 = InteractiveImage(source=str(self.test_image_path), width=50, height=50, callback=callback2)
|
||||
img1 = InteractiveImage(
|
||||
source=str(
|
||||
self.test_image_path),
|
||||
width=50,
|
||||
height=50,
|
||||
callback=callback1)
|
||||
img2 = InteractiveImage(
|
||||
source=str(
|
||||
self.test_image_path),
|
||||
width=50,
|
||||
height=50,
|
||||
callback=callback2)
|
||||
|
||||
# Set different bounds
|
||||
img1.set_rendered_bounds(origin=(0, 0), size=(50, 50))
|
||||
|
||||
@@ -20,18 +20,23 @@ from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
class TestFontUtilities(unittest.TestCase):
|
||||
"""Test cases for font utility functions."""
|
||||
|
||||
|
||||
def test_get_bundled_font_path_finds_font(self):
|
||||
"""Test that get_bundled_font_path finds the bundled font."""
|
||||
font_path = get_bundled_font_path()
|
||||
self.assertIsNotNone(font_path, "Bundled font path should not be None")
|
||||
self.assertTrue(os.path.exists(font_path), f"Font file should exist at {font_path}")
|
||||
self.assertTrue(font_path.endswith("DejaVuSans.ttf"), "Font path should end with DejaVuSans.ttf")
|
||||
|
||||
self.assertTrue(
|
||||
os.path.exists(font_path),
|
||||
f"Font file should exist at {font_path}")
|
||||
self.assertTrue(
|
||||
font_path.endswith("DejaVuSans.ttf"),
|
||||
"Font path should end with DejaVuSans.ttf")
|
||||
|
||||
def test_verify_bundled_font_available(self):
|
||||
"""Test that the bundled font can be verified and loaded."""
|
||||
self.assertTrue(verify_bundled_font_available(), "Bundled font should be available and loadable")
|
||||
|
||||
self.assertTrue(verify_bundled_font_available(),
|
||||
"Bundled font should be available and loadable")
|
||||
|
||||
def test_create_test_font_with_defaults(self):
|
||||
"""Test creating a test font with default parameters."""
|
||||
font = create_test_font()
|
||||
@@ -41,7 +46,7 @@ class TestFontUtilities(unittest.TestCase):
|
||||
self.assertEqual(font.weight, FontWeight.NORMAL)
|
||||
self.assertEqual(font.style, FontStyle.NORMAL)
|
||||
self.assertEqual(font.decoration, TextDecoration.NONE)
|
||||
|
||||
|
||||
def test_create_test_font_with_custom_parameters(self):
|
||||
"""Test creating a test font with custom parameters."""
|
||||
font = create_test_font(
|
||||
@@ -57,50 +62,52 @@ class TestFontUtilities(unittest.TestCase):
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||
|
||||
|
||||
def test_create_default_test_font(self):
|
||||
"""Test creating a default test font."""
|
||||
font = create_default_test_font()
|
||||
self.assertIsInstance(font, Font)
|
||||
self.assertEqual(font.font_size, 16)
|
||||
self.assertEqual(font.colour, (0, 0, 0))
|
||||
|
||||
|
||||
def test_ensure_consistent_font_in_tests_succeeds(self):
|
||||
"""Test that ensure_consistent_font_in_tests runs without error when font is available."""
|
||||
# This should not raise any exceptions if the font is properly available
|
||||
try:
|
||||
ensure_consistent_font_in_tests()
|
||||
except RuntimeError:
|
||||
self.fail("ensure_consistent_font_in_tests() raised RuntimeError when font should be available")
|
||||
|
||||
self.fail(
|
||||
"ensure_consistent_font_in_tests() raised RuntimeError when font should be available")
|
||||
|
||||
def test_bundled_font_loads_with_pil(self):
|
||||
"""Test that the bundled font can be loaded directly with PIL."""
|
||||
font_path = get_bundled_font_path()
|
||||
self.assertIsNotNone(font_path)
|
||||
|
||||
|
||||
# Test loading with different sizes
|
||||
for size in [12, 16, 24, 48]:
|
||||
with self.subTest(size=size):
|
||||
pil_font = ImageFont.truetype(font_path, size)
|
||||
self.assertIsNotNone(pil_font)
|
||||
|
||||
|
||||
def test_font_metrics_consistency(self):
|
||||
"""Test that font metrics are consistent between different Font objects using the same parameters."""
|
||||
font1 = create_test_font(font_size=16)
|
||||
font2 = create_test_font(font_size=16)
|
||||
|
||||
|
||||
# Both fonts should have the same size
|
||||
self.assertEqual(font1.font_size, font2.font_size)
|
||||
|
||||
|
||||
# Test that text measurements are consistent
|
||||
# This is a basic check - in real usage, text measurement consistency is what matters most
|
||||
# This is a basic check - in real usage, text measurement consistency is
|
||||
# what matters most
|
||||
self.assertEqual(font1.font_size, font2.font_size)
|
||||
|
||||
|
||||
def test_different_sizes_create_different_fonts(self):
|
||||
"""Test that different font sizes create fonts with different metrics."""
|
||||
small_font = create_test_font(font_size=12)
|
||||
large_font = create_test_font(font_size=24)
|
||||
|
||||
|
||||
self.assertNotEqual(small_font.font_size, large_font.font_size)
|
||||
self.assertEqual(small_font.font_size, 12)
|
||||
self.assertEqual(large_font.font_size, 24)
|
||||
@@ -108,24 +115,28 @@ class TestFontUtilities(unittest.TestCase):
|
||||
|
||||
class TestFontPathResolution(unittest.TestCase):
|
||||
"""Test cases for font path resolution from different locations."""
|
||||
|
||||
|
||||
def test_font_path_is_absolute(self):
|
||||
"""Test that the returned font path is absolute."""
|
||||
font_path = get_bundled_font_path()
|
||||
if font_path:
|
||||
self.assertTrue(os.path.isabs(font_path), "Font path should be absolute")
|
||||
|
||||
|
||||
def test_font_path_points_to_file(self):
|
||||
"""Test that the font path points to a file, not a directory."""
|
||||
font_path = get_bundled_font_path()
|
||||
if font_path:
|
||||
self.assertTrue(os.path.isfile(font_path), "Font path should point to a file")
|
||||
|
||||
self.assertTrue(
|
||||
os.path.isfile(font_path),
|
||||
"Font path should point to a file")
|
||||
|
||||
def test_font_file_has_correct_extension(self):
|
||||
"""Test that the font file has the expected .ttf extension."""
|
||||
font_path = get_bundled_font_path()
|
||||
if font_path:
|
||||
self.assertTrue(font_path.lower().endswith('.ttf'), "Font file should have .ttf extension")
|
||||
self.assertTrue(
|
||||
font_path.lower().endswith('.ttf'),
|
||||
"Font file should have .ttf extension")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+26
-22
@@ -6,7 +6,6 @@ preventing inconsistencies that can arise from different system fonts.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
from PIL import ImageFont
|
||||
|
||||
@@ -16,25 +15,30 @@ from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
|
||||
def get_bundled_font_path() -> Optional[str]:
|
||||
"""
|
||||
Get the path to the bundled DejaVuSans.ttf font.
|
||||
|
||||
|
||||
This function works from test directories by finding the font relative to the
|
||||
test file locations.
|
||||
|
||||
|
||||
Returns:
|
||||
str: Path to the bundled font file, or None if not found
|
||||
"""
|
||||
# Get the directory containing this test utility file
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
# Navigate up to the project root (tests/utils -> tests -> root)
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
|
||||
|
||||
# Path to the bundled font
|
||||
bundled_font_path = os.path.join(project_root, 'pyWebLayout', 'assets', 'fonts', 'DejaVuSans.ttf')
|
||||
|
||||
bundled_font_path = os.path.join(
|
||||
project_root,
|
||||
'pyWebLayout',
|
||||
'assets',
|
||||
'fonts',
|
||||
'DejaVuSans.ttf')
|
||||
|
||||
if os.path.exists(bundled_font_path):
|
||||
return bundled_font_path
|
||||
|
||||
|
||||
# Alternative: try to find it relative to the pyWebLayout module
|
||||
try:
|
||||
import pyWebLayout
|
||||
@@ -44,24 +48,24 @@ def get_bundled_font_path() -> Optional[str]:
|
||||
return alt_font_path
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def verify_bundled_font_available() -> bool:
|
||||
"""
|
||||
Verify that the bundled font is available and can be loaded.
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if the bundled font is available and loadable
|
||||
"""
|
||||
font_path = get_bundled_font_path()
|
||||
if not font_path:
|
||||
return False
|
||||
|
||||
|
||||
try:
|
||||
# Try to load the font with PIL to verify it's valid
|
||||
test_font = ImageFont.truetype(font_path, 16)
|
||||
ImageFont.truetype(font_path, 16)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -77,10 +81,10 @@ def create_test_font(font_size: int = 16,
|
||||
min_hyphenation_width: Optional[int] = None) -> Font:
|
||||
"""
|
||||
Create a Font object that uses the bundled font for consistent testing.
|
||||
|
||||
|
||||
This function ensures all tests use the same font file, preventing
|
||||
cross-system inconsistencies in text measurements and layout.
|
||||
|
||||
|
||||
Args:
|
||||
font_size: Size of the font in points
|
||||
colour: RGB color tuple for the text
|
||||
@@ -90,10 +94,10 @@ def create_test_font(font_size: int = 16,
|
||||
background: RGBA background color for the text
|
||||
language: Language code for hyphenation and text processing
|
||||
min_hyphenation_width: Minimum width in pixels for hyphenation
|
||||
|
||||
|
||||
Returns:
|
||||
Font: A Font object guaranteed to use the bundled font
|
||||
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the bundled font cannot be found or loaded
|
||||
"""
|
||||
@@ -103,13 +107,13 @@ def create_test_font(font_size: int = 16,
|
||||
"Bundled font (DejaVuSans.ttf) not found. "
|
||||
"Ensure the font exists in pyWebLayout/assets/fonts/"
|
||||
)
|
||||
|
||||
|
||||
if not verify_bundled_font_available():
|
||||
raise RuntimeError(
|
||||
f"Bundled font at {font_path} cannot be loaded. "
|
||||
"Font file may be corrupted or invalid."
|
||||
)
|
||||
|
||||
|
||||
return Font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
@@ -126,9 +130,9 @@ def create_test_font(font_size: int = 16,
|
||||
def create_default_test_font() -> Font:
|
||||
"""
|
||||
Create a default Font object for testing with the bundled font.
|
||||
|
||||
|
||||
This is equivalent to Font() but guarantees the bundled font is used.
|
||||
|
||||
|
||||
Returns:
|
||||
Font: A default Font object using the bundled font
|
||||
"""
|
||||
@@ -138,10 +142,10 @@ def create_default_test_font() -> Font:
|
||||
def ensure_consistent_font_in_tests():
|
||||
"""
|
||||
Ensure that tests are using consistent fonts by checking availability.
|
||||
|
||||
|
||||
This function can be called in test setup to verify the font environment
|
||||
is properly configured.
|
||||
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the bundled font is not available
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user