@@ -1,354 +0,0 @@
|
||||
"""
|
||||
Unit tests for HTML content reading.
|
||||
|
||||
Tests the HTMLContentReader class for parsing complete HTML documents.
|
||||
This is more of an integration test covering the entire parsing pipeline.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pyWebLayout.io.readers.html_content import HTMLContentReader
|
||||
from pyWebLayout.abstract.document import Document
|
||||
from pyWebLayout.abstract.block import (
|
||||
Paragraph, Heading, HeadingLevel, HList, ListStyle,
|
||||
Table, Quote, CodeBlock, HorizontalRule
|
||||
)
|
||||
from pyWebLayout.abstract.inline import LineBreak
|
||||
|
||||
class TestHTMLContentReader(unittest.TestCase):
|
||||
"""Test cases for HTMLContentReader."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.reader = HTMLContentReader()
|
||||
self.document = Document()
|
||||
|
||||
def test_simple_paragraph(self):
|
||||
"""Test parsing a simple paragraph."""
|
||||
html = '<p>Hello world!</p>'
|
||||
|
||||
result = self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
self.assertIsInstance(self.document.blocks[0], Paragraph)
|
||||
|
||||
paragraph = self.document.blocks[0]
|
||||
words = list(paragraph.words())
|
||||
self.assertEqual(len(words), 2)
|
||||
self.assertEqual(words[0][1].text, "Hello")
|
||||
self.assertEqual(words[1][1].text, "world!")
|
||||
|
||||
def test_headings(self):
|
||||
"""Test parsing different heading levels."""
|
||||
html = '''
|
||||
<h1>Heading 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
<h6>Heading 6</h6>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
# Should have 4 heading blocks
|
||||
headings = [block for block in self.document.blocks if isinstance(block, Heading)]
|
||||
self.assertEqual(len(headings), 4)
|
||||
|
||||
# Check heading levels
|
||||
self.assertEqual(headings[0].level, HeadingLevel.H1)
|
||||
self.assertEqual(headings[1].level, HeadingLevel.H2)
|
||||
self.assertEqual(headings[2].level, HeadingLevel.H3)
|
||||
self.assertEqual(headings[3].level, HeadingLevel.H6)
|
||||
|
||||
# Check text content
|
||||
h1_words = list(headings[0].words())
|
||||
self.assertEqual(len(h1_words), 2)
|
||||
self.assertEqual(h1_words[0][1].text, "Heading")
|
||||
self.assertEqual(h1_words[1][1].text, "1")
|
||||
|
||||
def test_styled_text(self):
|
||||
"""Test parsing text with inline styling."""
|
||||
html = '<p>This is <b>bold</b> and <i>italic</i> text.</p>'
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
paragraph = self.document.blocks[0]
|
||||
words = list(paragraph.words())
|
||||
|
||||
# Should have words: "This", "is", "bold", "and", "italic", "text."
|
||||
self.assertEqual(len(words), 6)
|
||||
|
||||
# The styling information is embedded in the Font objects
|
||||
# We can't easily test the exact styling without more complex setup
|
||||
# but we can verify the words are created correctly
|
||||
word_texts = [word[1].text for word in words]
|
||||
self.assertEqual(word_texts, ["This", "is", "bold", "and", "italic", "text."])
|
||||
|
||||
def test_unordered_list(self):
|
||||
"""Test parsing unordered lists."""
|
||||
html = '''
|
||||
<ul>
|
||||
<li>First item</li>
|
||||
<li>Second item</li>
|
||||
<li>Third item</li>
|
||||
</ul>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
self.assertIsInstance(self.document.blocks[0], HList)
|
||||
|
||||
list_block = self.document.blocks[0]
|
||||
self.assertEqual(list_block.style, ListStyle.UNORDERED)
|
||||
|
||||
items = list(list_block.items())
|
||||
self.assertEqual(len(items), 3)
|
||||
|
||||
# Check first item content
|
||||
first_item_blocks = list(items[0].blocks())
|
||||
self.assertEqual(len(first_item_blocks), 1)
|
||||
self.assertIsInstance(first_item_blocks[0], Paragraph)
|
||||
|
||||
def test_ordered_list(self):
|
||||
"""Test parsing ordered lists."""
|
||||
html = '''
|
||||
<ol>
|
||||
<li>First step</li>
|
||||
<li>Second step</li>
|
||||
</ol>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
list_block = self.document.blocks[0]
|
||||
self.assertEqual(list_block.style, ListStyle.ORDERED)
|
||||
|
||||
items = list(list_block.items())
|
||||
self.assertEqual(len(items), 2)
|
||||
|
||||
def test_definition_list(self):
|
||||
"""Test parsing definition lists."""
|
||||
html = '''
|
||||
<dl>
|
||||
<dt>Term 1</dt>
|
||||
<dd>Definition 1</dd>
|
||||
<dt>Term 2</dt>
|
||||
<dd>Definition 2</dd>
|
||||
</dl>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
list_block = self.document.blocks[0]
|
||||
self.assertEqual(list_block.style, ListStyle.DEFINITION)
|
||||
|
||||
items = list(list_block.items())
|
||||
self.assertEqual(len(items), 2) # Two dt/dd pairs
|
||||
|
||||
def test_table(self):
|
||||
"""Test parsing simple tables."""
|
||||
html = '''
|
||||
<table>
|
||||
<tr>
|
||||
<th>Header 1</th>
|
||||
<th>Header 2</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell 1</td>
|
||||
<td>Cell 2</td>
|
||||
</tr>
|
||||
</table>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
self.assertIsInstance(self.document.blocks[0], Table)
|
||||
|
||||
table = self.document.blocks[0]
|
||||
|
||||
# Check body rows
|
||||
body_rows = list(table.body_rows())
|
||||
self.assertEqual(len(body_rows), 2) # Header row + data row
|
||||
|
||||
# Check first row (header)
|
||||
first_row_cells = list(body_rows[0].cells())
|
||||
self.assertEqual(len(first_row_cells), 2)
|
||||
self.assertTrue(first_row_cells[0].is_header)
|
||||
self.assertTrue(first_row_cells[1].is_header)
|
||||
|
||||
# Check second row (data)
|
||||
second_row_cells = list(body_rows[1].cells())
|
||||
self.assertEqual(len(second_row_cells), 2)
|
||||
self.assertFalse(second_row_cells[0].is_header)
|
||||
self.assertFalse(second_row_cells[1].is_header)
|
||||
|
||||
def test_blockquote(self):
|
||||
"""Test parsing blockquotes."""
|
||||
html = '''
|
||||
<blockquote>
|
||||
<p>This is a quoted paragraph.</p>
|
||||
<p>Another quoted paragraph.</p>
|
||||
</blockquote>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
self.assertIsInstance(self.document.blocks[0], Quote)
|
||||
|
||||
quote = self.document.blocks[0]
|
||||
quote_blocks = list(quote.blocks())
|
||||
self.assertEqual(len(quote_blocks), 2)
|
||||
self.assertIsInstance(quote_blocks[0], Paragraph)
|
||||
self.assertIsInstance(quote_blocks[1], Paragraph)
|
||||
|
||||
def test_code_block(self):
|
||||
"""Test parsing code blocks."""
|
||||
html = '''
|
||||
<pre><code class="language-python">
|
||||
def hello():
|
||||
print("Hello, world!")
|
||||
</code></pre>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 1)
|
||||
self.assertIsInstance(self.document.blocks[0], CodeBlock)
|
||||
|
||||
code_block = self.document.blocks[0]
|
||||
self.assertEqual(code_block.language, "python")
|
||||
|
||||
def test_horizontal_rule(self):
|
||||
"""Test parsing horizontal rules."""
|
||||
html = '<p>Before</p><hr><p>After</p>'
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
self.assertEqual(len(self.document.blocks), 3)
|
||||
self.assertIsInstance(self.document.blocks[0], Paragraph)
|
||||
self.assertIsInstance(self.document.blocks[1], HorizontalRule)
|
||||
self.assertIsInstance(self.document.blocks[2], Paragraph)
|
||||
|
||||
def test_html_entities(self):
|
||||
"""Test handling HTML entities."""
|
||||
html = '<p>Less than: < Greater than: > Ampersand: &</p>'
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
paragraph = self.document.blocks[0]
|
||||
words = list(paragraph.words())
|
||||
|
||||
# Find the entity words
|
||||
word_texts = [word[1].text for word in words]
|
||||
self.assertIn('<', word_texts)
|
||||
self.assertIn('>', word_texts)
|
||||
self.assertIn('&', word_texts)
|
||||
|
||||
def test_nested_elements(self):
|
||||
"""Test parsing nested HTML elements."""
|
||||
html = '''
|
||||
<div>
|
||||
<h2>Section Title</h2>
|
||||
<p>Section content with <strong>important</strong> text.</p>
|
||||
<ul>
|
||||
<li>List item 1</li>
|
||||
<li>List item 2</li>
|
||||
</ul>
|
||||
</div>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
# Should have multiple blocks
|
||||
self.assertGreater(len(self.document.blocks), 1)
|
||||
|
||||
# Check that we have different types of blocks
|
||||
block_types = [type(block).__name__ for block in self.document.blocks]
|
||||
self.assertIn('Paragraph', block_types) # From div
|
||||
self.assertIn('Heading', block_types)
|
||||
self.assertIn('HList', block_types)
|
||||
|
||||
def test_empty_elements(self):
|
||||
"""Test handling empty HTML elements."""
|
||||
html = '<p></p><div></div><ul></ul>'
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
# Empty elements should still create blocks
|
||||
self.assertEqual(len(self.document.blocks), 3)
|
||||
|
||||
def test_whitespace_handling(self):
|
||||
"""Test proper whitespace handling."""
|
||||
html = '''
|
||||
<p> Word1 Word2
|
||||
Word3 </p>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
paragraph = self.document.blocks[0]
|
||||
words = list(paragraph.words())
|
||||
|
||||
# Should normalize whitespace and create separate words
|
||||
word_texts = [word[1].text for word in words]
|
||||
self.assertEqual(word_texts, ["Word1", "Word2", "Word3"])
|
||||
|
||||
def test_base_url_setting(self):
|
||||
"""Test setting base URL for link resolution."""
|
||||
base_url = "https://example.com/path/"
|
||||
self.reader.set_base_url(base_url)
|
||||
|
||||
# The base URL should be passed to the inline handler
|
||||
self.assertEqual(self.reader.inline_handler.base_url, base_url)
|
||||
|
||||
def test_complex_document(self):
|
||||
"""Test parsing a complex HTML document."""
|
||||
html = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Document</title>
|
||||
<style>body { font-family: Arial; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Main Title</h1>
|
||||
<p>Introduction paragraph with <em>emphasis</em>.</p>
|
||||
|
||||
<h2>Section 1</h2>
|
||||
<p>Content with <a href="link.html">a link</a>.</p>
|
||||
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2 with <strong>bold text</strong></li>
|
||||
</ul>
|
||||
|
||||
<h2>Section 2</h2>
|
||||
<blockquote>
|
||||
<p>A quoted paragraph.</p>
|
||||
</blockquote>
|
||||
|
||||
<table>
|
||||
<tr><th>Col1</th><th>Col2</th></tr>
|
||||
<tr><td>A</td><td>B</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
self.reader.extract_content(html, self.document)
|
||||
|
||||
# Should have parsed multiple blocks
|
||||
self.assertGreater(len(self.document.blocks), 5)
|
||||
|
||||
# Should have different types of content
|
||||
block_types = set(type(block).__name__ for block in self.document.blocks)
|
||||
expected_types = {'Heading', 'Paragraph', 'HList', 'Quote', 'Table'}
|
||||
self.assertTrue(expected_types.issubset(block_types))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+156
-156
@@ -1,181 +1,181 @@
|
||||
"""
|
||||
Unit tests for HTML style management.
|
||||
Unit tests for pyWebLayout style objects.
|
||||
|
||||
Tests the HTMLStyleManager class for CSS parsing, style stacks, and font creation.
|
||||
Tests the Font class and style enums for proper functionality and immutability.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pyWebLayout.io.readers.html_style import HTMLStyleManager
|
||||
from pyWebLayout.style import FontStyle, FontWeight, TextDecoration
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration, Alignment
|
||||
|
||||
|
||||
class TestHTMLStyleManager(unittest.TestCase):
|
||||
"""Test cases for HTMLStyleManager."""
|
||||
class TestStyleObjects(unittest.TestCase):
|
||||
"""Test cases for pyWebLayout style objects."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.style_manager = HTMLStyleManager()
|
||||
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_initialization(self):
|
||||
"""Test proper initialization of style manager."""
|
||||
style = self.style_manager.get_current_style()
|
||||
def test_font_style_enum(self):
|
||||
"""Test FontStyle enum values."""
|
||||
self.assertEqual(FontStyle.NORMAL.value, "normal")
|
||||
self.assertEqual(FontStyle.ITALIC.value, "italic")
|
||||
|
||||
self.assertEqual(style['font_size'], 12)
|
||||
self.assertEqual(style['font_weight'], FontWeight.NORMAL)
|
||||
self.assertEqual(style['font_style'], FontStyle.NORMAL)
|
||||
self.assertEqual(style['decoration'], TextDecoration.NONE)
|
||||
self.assertEqual(style['color'], (0, 0, 0))
|
||||
self.assertIsNone(style['background'])
|
||||
self.assertEqual(style['language'], 'en_US')
|
||||
# Test that all expected values exist
|
||||
styles = [FontStyle.NORMAL, FontStyle.ITALIC]
|
||||
self.assertEqual(len(styles), 2)
|
||||
|
||||
def test_style_stack_operations(self):
|
||||
"""Test push and pop operations on style stack."""
|
||||
# Initial state
|
||||
initial_style = self.style_manager.get_current_style()
|
||||
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")
|
||||
|
||||
# Push a new style
|
||||
new_style = {'font_size': 16, 'font_weight': FontWeight.BOLD}
|
||||
self.style_manager.push_style(new_style)
|
||||
|
||||
current_style = self.style_manager.get_current_style()
|
||||
self.assertEqual(current_style['font_size'], 16)
|
||||
self.assertEqual(current_style['font_weight'], FontWeight.BOLD)
|
||||
self.assertEqual(current_style['color'], (0, 0, 0)) # Unchanged
|
||||
|
||||
# Pop the style
|
||||
self.style_manager.pop_style()
|
||||
restored_style = self.style_manager.get_current_style()
|
||||
self.assertEqual(restored_style, initial_style)
|
||||
# Test that all expected values exist
|
||||
decorations = [TextDecoration.NONE, TextDecoration.UNDERLINE, TextDecoration.STRIKETHROUGH]
|
||||
self.assertEqual(len(decorations), 3)
|
||||
|
||||
def test_tag_styles(self):
|
||||
"""Test default styles for HTML tags."""
|
||||
h1_style = self.style_manager.get_tag_style('h1')
|
||||
self.assertEqual(h1_style['font_size'], 24)
|
||||
self.assertEqual(h1_style['font_weight'], FontWeight.BOLD)
|
||||
|
||||
h6_style = self.style_manager.get_tag_style('h6')
|
||||
self.assertEqual(h6_style['font_size'], 12)
|
||||
self.assertEqual(h6_style['font_weight'], FontWeight.BOLD)
|
||||
|
||||
em_style = self.style_manager.get_tag_style('em')
|
||||
self.assertEqual(em_style['font_style'], FontStyle.ITALIC)
|
||||
|
||||
unknown_style = self.style_manager.get_tag_style('unknown')
|
||||
self.assertEqual(unknown_style, {})
|
||||
def test_alignment_enum(self):
|
||||
"""Test Alignment enum values."""
|
||||
self.assertEqual(Alignment.LEFT.value, 1)
|
||||
self.assertEqual(Alignment.CENTER.value, 2)
|
||||
self.assertEqual(Alignment.RIGHT.value, 3)
|
||||
self.assertEqual(Alignment.TOP.value, 4)
|
||||
self.assertEqual(Alignment.BOTTOM.value, 5)
|
||||
self.assertEqual(Alignment.JUSTIFY.value, 6)
|
||||
|
||||
def test_inline_style_parsing(self):
|
||||
"""Test parsing of inline CSS styles."""
|
||||
# Test font-size
|
||||
style = self.style_manager.parse_inline_style('font-size: 18px')
|
||||
self.assertEqual(style['font_size'], 18)
|
||||
def test_font_initialization_defaults(self):
|
||||
"""Test Font initialization with default values."""
|
||||
font = Font()
|
||||
|
||||
style = self.style_manager.parse_inline_style('font-size: 14pt')
|
||||
self.assertEqual(style['font_size'], 14)
|
||||
|
||||
# Test font-weight
|
||||
style = self.style_manager.parse_inline_style('font-weight: bold')
|
||||
self.assertEqual(style['font_weight'], FontWeight.BOLD)
|
||||
|
||||
# Test font-style
|
||||
style = self.style_manager.parse_inline_style('font-style: italic')
|
||||
self.assertEqual(style['font_style'], FontStyle.ITALIC)
|
||||
|
||||
# Test text-decoration
|
||||
style = self.style_manager.parse_inline_style('text-decoration: underline')
|
||||
self.assertEqual(style['decoration'], TextDecoration.UNDERLINE)
|
||||
|
||||
# Test multiple properties
|
||||
style = self.style_manager.parse_inline_style(
|
||||
'font-size: 20px; font-weight: bold; color: red'
|
||||
self.assertIsNone(font._font_path)
|
||||
self.assertEqual(font.font_size, 12)
|
||||
self.assertEqual(font.colour, (0, 0, 0))
|
||||
self.assertEqual(font.color, (0, 0, 0)) # Alias
|
||||
self.assertEqual(font.weight, FontWeight.NORMAL)
|
||||
self.assertEqual(font.style, FontStyle.NORMAL)
|
||||
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(
|
||||
font_path="/path/to/font.ttf",
|
||||
font_size=16,
|
||||
colour=(255, 0, 0),
|
||||
weight=FontWeight.BOLD,
|
||||
style=FontStyle.ITALIC,
|
||||
decoration=TextDecoration.UNDERLINE,
|
||||
background=(255, 255, 0, 255),
|
||||
langauge="fr_FR"
|
||||
)
|
||||
self.assertEqual(style['font_size'], 20)
|
||||
self.assertEqual(style['font_weight'], FontWeight.BOLD)
|
||||
self.assertEqual(style['color'], (255, 0, 0))
|
||||
|
||||
def test_color_parsing(self):
|
||||
"""Test CSS color parsing."""
|
||||
# Named colors
|
||||
self.assertEqual(self.style_manager.parse_color('red'), (255, 0, 0))
|
||||
self.assertEqual(self.style_manager.parse_color('blue'), (0, 0, 255))
|
||||
self.assertEqual(self.style_manager.parse_color('white'), (255, 255, 255))
|
||||
self.assertEqual(self.style_manager.parse_color('gray'), (128, 128, 128))
|
||||
self.assertEqual(self.style_manager.parse_color('grey'), (128, 128, 128))
|
||||
|
||||
# Hex colors
|
||||
self.assertEqual(self.style_manager.parse_color('#ff0000'), (255, 0, 0))
|
||||
self.assertEqual(self.style_manager.parse_color('#00ff00'), (0, 255, 0))
|
||||
self.assertEqual(self.style_manager.parse_color('#f00'), (255, 0, 0))
|
||||
self.assertEqual(self.style_manager.parse_color('#0f0'), (0, 255, 0))
|
||||
|
||||
# RGB colors
|
||||
self.assertEqual(self.style_manager.parse_color('rgb(255, 0, 0)'), (255, 0, 0))
|
||||
self.assertEqual(self.style_manager.parse_color('rgb(128, 128, 128)'), (128, 128, 128))
|
||||
self.assertEqual(self.style_manager.parse_color('rgb( 255 , 255 , 255 )'), (255, 255, 255))
|
||||
|
||||
# RGBA colors (alpha ignored)
|
||||
self.assertEqual(self.style_manager.parse_color('rgba(255, 0, 0, 0.5)'), (255, 0, 0))
|
||||
|
||||
# Invalid colors
|
||||
self.assertIsNone(self.style_manager.parse_color('invalid'))
|
||||
self.assertIsNone(self.style_manager.parse_color('#gg0000'))
|
||||
self.assertIsNone(self.style_manager.parse_color('rgb(300, 0, 0)')) # Invalid values return None
|
||||
|
||||
def test_color_clamping(self):
|
||||
"""Test that RGB values outside valid range return None."""
|
||||
# Values outside 0-255 range should return None
|
||||
color = self.style_manager.parse_color('rgb(300, -10, 128)')
|
||||
self.assertIsNone(color) # Invalid values return None
|
||||
|
||||
def test_apply_style_to_element(self):
|
||||
"""Test combining tag styles with inline styles."""
|
||||
# Test h1 with inline style
|
||||
attrs = {'style': 'color: blue; font-size: 30px'}
|
||||
combined = self.style_manager.apply_style_to_element('h1', attrs)
|
||||
|
||||
# Should have h1 defaults plus inline overrides
|
||||
self.assertEqual(combined['font_size'], 30) # Overridden
|
||||
self.assertEqual(combined['font_weight'], FontWeight.BOLD) # From h1
|
||||
self.assertEqual(combined['color'], (0, 0, 255)) # Inline
|
||||
|
||||
# Test without inline styles
|
||||
combined = self.style_manager.apply_style_to_element('strong', {})
|
||||
self.assertEqual(combined['font_weight'], FontWeight.BOLD)
|
||||
|
||||
def test_reset(self):
|
||||
"""Test resetting the style manager."""
|
||||
# Change the state
|
||||
self.style_manager.push_style({'font_size': 20})
|
||||
self.style_manager.push_style({'color': (255, 0, 0)})
|
||||
|
||||
# Reset
|
||||
self.style_manager.reset()
|
||||
|
||||
# Should be back to initial state
|
||||
style = self.style_manager.get_current_style()
|
||||
self.assertEqual(style['font_size'], 12)
|
||||
self.assertEqual(style['color'], (0, 0, 0))
|
||||
self.assertEqual(len(self.style_manager._style_stack), 0)
|
||||
|
||||
def test_font_creation(self):
|
||||
"""Test Font object creation from current style."""
|
||||
# Set some specific styles
|
||||
self.style_manager.push_style({
|
||||
'font_size': 16,
|
||||
'font_weight': FontWeight.BOLD,
|
||||
'font_style': FontStyle.ITALIC,
|
||||
'decoration': TextDecoration.UNDERLINE,
|
||||
'color': (255, 0, 0),
|
||||
'background': (255, 255, 0, 255)
|
||||
})
|
||||
|
||||
font = self.style_manager.create_font()
|
||||
|
||||
self.assertEqual(font._font_path, "/path/to/font.ttf")
|
||||
self.assertEqual(font.font_size, 16)
|
||||
self.assertEqual(font.colour, (255, 0, 0))
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||
self.assertEqual(font.colour, (255, 0, 0))
|
||||
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(
|
||||
font_size=12,
|
||||
colour=(0, 0, 0),
|
||||
weight=FontWeight.NORMAL,
|
||||
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
|
||||
|
||||
def test_font_property_access(self):
|
||||
"""Test Font property access methods."""
|
||||
font = Font(
|
||||
font_size=20,
|
||||
colour=(128, 128, 128),
|
||||
weight=FontWeight.BOLD,
|
||||
style=FontStyle.ITALIC,
|
||||
decoration=TextDecoration.STRIKETHROUGH
|
||||
)
|
||||
|
||||
# Test all property getters
|
||||
self.assertEqual(font.font_size, 20)
|
||||
self.assertEqual(font.colour, (128, 128, 128))
|
||||
self.assertEqual(font.color, (128, 128, 128)) # Alias
|
||||
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))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
"""
|
||||
Unit tests for HTML text processing.
|
||||
|
||||
Tests the HTMLTextProcessor class for text buffering, entity handling, and word creation.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from pyWebLayout.io.readers.html_text import HTMLTextProcessor
|
||||
from pyWebLayout.io.readers.html_style import HTMLStyleManager
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
|
||||
|
||||
class TestHTMLTextProcessor(unittest.TestCase):
|
||||
"""Test cases for HTMLTextProcessor."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.style_manager = HTMLStyleManager()
|
||||
self.text_processor = HTMLTextProcessor(self.style_manager)
|
||||
|
||||
# Create a mock paragraph
|
||||
self.mock_paragraph = Mock(spec=Paragraph)
|
||||
self.mock_paragraph.add_word = Mock()
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test proper initialization of text processor."""
|
||||
self.assertEqual(self.text_processor._text_buffer, "")
|
||||
self.assertIsNone(self.text_processor._current_paragraph)
|
||||
self.assertEqual(self.text_processor._style_manager, self.style_manager)
|
||||
|
||||
def test_add_text(self):
|
||||
"""Test adding text to buffer."""
|
||||
self.text_processor.add_text("Hello")
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), "Hello")
|
||||
|
||||
self.text_processor.add_text(" World")
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), "Hello World")
|
||||
|
||||
def test_entity_references(self):
|
||||
"""Test HTML entity reference handling."""
|
||||
test_cases = [
|
||||
('lt', '<'),
|
||||
('gt', '>'),
|
||||
('amp', '&'),
|
||||
('quot', '"'),
|
||||
('apos', "'"),
|
||||
('nbsp', ' '),
|
||||
('copy', '©'),
|
||||
('reg', '®'),
|
||||
('trade', '™'),
|
||||
('mdash', '—'),
|
||||
('ndash', '–'),
|
||||
('hellip', '…'),
|
||||
('euro', '€'),
|
||||
('unknown', '&unknown;') # Unknown entities should be preserved
|
||||
]
|
||||
|
||||
for entity, expected in test_cases:
|
||||
with self.subTest(entity=entity):
|
||||
self.text_processor.clear_buffer()
|
||||
self.text_processor.add_entity_reference(entity)
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), expected)
|
||||
|
||||
def test_character_references(self):
|
||||
"""Test character reference handling."""
|
||||
# Decimal character references
|
||||
self.text_processor.clear_buffer()
|
||||
self.text_processor.add_character_reference('65') # 'A'
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), 'A')
|
||||
|
||||
# Hexadecimal character references
|
||||
self.text_processor.clear_buffer()
|
||||
self.text_processor.add_character_reference('x41') # 'A'
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), 'A')
|
||||
|
||||
# Unicode character
|
||||
self.text_processor.clear_buffer()
|
||||
self.text_processor.add_character_reference('8364') # Euro symbol
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), '€')
|
||||
|
||||
# Invalid character reference
|
||||
self.text_processor.clear_buffer()
|
||||
self.text_processor.add_character_reference('invalid')
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), '&#invalid;')
|
||||
|
||||
# Out of range character
|
||||
self.text_processor.clear_buffer()
|
||||
self.text_processor.add_character_reference('99999999999')
|
||||
self.assertTrue(self.text_processor.get_buffer_content().startswith('&#'))
|
||||
|
||||
def test_buffer_operations(self):
|
||||
"""Test buffer state operations."""
|
||||
# Test has_pending_text
|
||||
self.assertFalse(self.text_processor.has_pending_text())
|
||||
|
||||
self.text_processor.add_text("Some text")
|
||||
self.assertTrue(self.text_processor.has_pending_text())
|
||||
|
||||
# Test clear_buffer
|
||||
self.text_processor.clear_buffer()
|
||||
self.assertFalse(self.text_processor.has_pending_text())
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), "")
|
||||
|
||||
# Test with whitespace only
|
||||
self.text_processor.add_text(" \n\t ")
|
||||
self.assertFalse(self.text_processor.has_pending_text()) # Should ignore whitespace
|
||||
|
||||
def test_paragraph_management(self):
|
||||
"""Test current paragraph setting."""
|
||||
# Initially no paragraph
|
||||
self.assertIsNone(self.text_processor._current_paragraph)
|
||||
|
||||
# Set paragraph
|
||||
self.text_processor.set_current_paragraph(self.mock_paragraph)
|
||||
self.assertEqual(self.text_processor._current_paragraph, self.mock_paragraph)
|
||||
|
||||
# Clear paragraph
|
||||
self.text_processor.set_current_paragraph(None)
|
||||
self.assertIsNone(self.text_processor._current_paragraph)
|
||||
|
||||
def test_flush_text_with_paragraph(self):
|
||||
"""Test flushing text when paragraph is set."""
|
||||
self.text_processor.set_current_paragraph(self.mock_paragraph)
|
||||
self.text_processor.add_text("Hello world test")
|
||||
|
||||
# Mock the style manager to return a specific font
|
||||
mock_font = Mock()
|
||||
self.style_manager.create_font = Mock(return_value=mock_font)
|
||||
|
||||
result = self.text_processor.flush_text()
|
||||
|
||||
# Should return True (text was flushed)
|
||||
self.assertTrue(result)
|
||||
|
||||
# Should have created words
|
||||
self.assertEqual(self.mock_paragraph.add_word.call_count, 3) # "Hello", "world", "test"
|
||||
|
||||
# Verify the words were created with correct text
|
||||
calls = self.mock_paragraph.add_word.call_args_list
|
||||
word_texts = [call[0][0].text for call in calls]
|
||||
self.assertEqual(word_texts, ["Hello", "world", "test"])
|
||||
|
||||
# Buffer should be empty after flush
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), "")
|
||||
|
||||
def test_flush_text_without_paragraph(self):
|
||||
"""Test flushing text when no paragraph is set."""
|
||||
self.text_processor.add_text("Hello world")
|
||||
|
||||
result = self.text_processor.flush_text()
|
||||
|
||||
# Should return False (no paragraph to flush to)
|
||||
self.assertFalse(result)
|
||||
|
||||
# Buffer should be cleared anyway
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), "")
|
||||
|
||||
def test_flush_empty_buffer(self):
|
||||
"""Test flushing when buffer is empty."""
|
||||
self.text_processor.set_current_paragraph(self.mock_paragraph)
|
||||
|
||||
result = self.text_processor.flush_text()
|
||||
|
||||
# Should return False (nothing to flush)
|
||||
self.assertFalse(result)
|
||||
|
||||
# No words should be added
|
||||
self.mock_paragraph.add_word.assert_not_called()
|
||||
|
||||
def test_flush_whitespace_only(self):
|
||||
"""Test flushing when buffer contains only whitespace."""
|
||||
self.text_processor.set_current_paragraph(self.mock_paragraph)
|
||||
self.text_processor.add_text(" \n\t ")
|
||||
|
||||
result = self.text_processor.flush_text()
|
||||
|
||||
# Should return False (no meaningful content)
|
||||
self.assertFalse(result)
|
||||
|
||||
# No words should be added
|
||||
self.mock_paragraph.add_word.assert_not_called()
|
||||
|
||||
def test_word_creation_with_styling(self):
|
||||
"""Test that words are created with proper styling."""
|
||||
self.text_processor.set_current_paragraph(self.mock_paragraph)
|
||||
self.text_processor.add_text("styled text")
|
||||
|
||||
# Set up style manager to return specific font
|
||||
mock_font = Mock()
|
||||
mock_font.font_size = 16
|
||||
mock_font.weight = "bold"
|
||||
self.style_manager.create_font = Mock(return_value=mock_font)
|
||||
|
||||
self.text_processor.flush_text()
|
||||
|
||||
# Verify font was created
|
||||
self.style_manager.create_font.assert_called()
|
||||
|
||||
# Verify words were created with the font
|
||||
calls = self.mock_paragraph.add_word.call_args_list
|
||||
for call in calls:
|
||||
word = call[0][0]
|
||||
self.assertEqual(word.style, mock_font)
|
||||
|
||||
def test_reset(self):
|
||||
"""Test resetting the text processor."""
|
||||
# Set up some state
|
||||
self.text_processor.set_current_paragraph(self.mock_paragraph)
|
||||
self.text_processor.add_text("Some text")
|
||||
|
||||
# Reset
|
||||
self.text_processor.reset()
|
||||
|
||||
# Should be back to initial state
|
||||
self.assertEqual(self.text_processor._text_buffer, "")
|
||||
self.assertIsNone(self.text_processor._current_paragraph)
|
||||
|
||||
def test_complex_text_processing(self):
|
||||
"""Test processing text with mixed content."""
|
||||
self.text_processor.set_current_paragraph(self.mock_paragraph)
|
||||
|
||||
# Mock font creation
|
||||
mock_font = Mock()
|
||||
self.style_manager.create_font = Mock(return_value=mock_font)
|
||||
|
||||
# Add mixed content
|
||||
self.text_processor.add_text("Hello ")
|
||||
self.text_processor.add_entity_reference('amp')
|
||||
self.text_processor.add_text(" world")
|
||||
self.text_processor.add_character_reference('33') # '!'
|
||||
|
||||
# Should have "Hello & world!"
|
||||
expected_content = "Hello & world!"
|
||||
self.assertEqual(self.text_processor.get_buffer_content(), expected_content)
|
||||
|
||||
# Flush and verify words
|
||||
self.text_processor.flush_text()
|
||||
|
||||
calls = self.mock_paragraph.add_word.call_args_list
|
||||
word_texts = [call[0][0].text for call in calls]
|
||||
self.assertEqual(word_texts, ["Hello", "&", "world!"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user