first code commit

This commit is contained in:
2025-05-27 11:58:19 +02:00
commit f7ad69f9ec
55 changed files with 10682 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
# PyWebLayout Testing Strategy
This document outlines the comprehensive unit testing strategy for the pyWebLayout project.
## Testing Philosophy
The testing strategy follows these principles:
- **Separation of Concerns**: Each component is tested independently
- **Comprehensive Coverage**: All public APIs and critical functionality are tested
- **Integration Testing**: End-to-end workflows are validated
- **Regression Prevention**: Tests prevent breaking changes
- **Documentation**: Tests serve as living documentation of expected behavior
## Test Organization
### Current Test Files (Implemented)
#### ✅ `test_html_style.py`
Tests the `HTMLStyleManager` class for CSS parsing and style management.
**Coverage:**
- Style initialization and defaults
- Style stack operations (push/pop)
- CSS property parsing (font-size, font-weight, colors, etc.)
- Color parsing (named, hex, rgb, rgba)
- Tag-specific default styles
- Inline style parsing
- Font object creation
- Style combination (tag + inline styles)
#### ✅ `test_html_text.py`
Tests the `HTMLTextProcessor` class for text buffering and word creation.
**Coverage:**
- Text buffer management
- HTML entity reference handling
- Character reference processing (decimal/hex)
- Word creation with styling
- Paragraph management
- Text flushing operations
- Buffer state operations
#### ✅ `test_html_content.py`
Integration tests for the `HTMLContentReader` class covering complete HTML parsing.
**Coverage:**
- Simple paragraph parsing
- Heading levels (h1-h6)
- Styled text (bold, italic)
- Lists (ul, ol, dl)
- Tables with headers and cells
- Blockquotes with nested content
- Code blocks with language detection
- HTML entities
- Nested element structures
- Complex document parsing
#### ✅ `test_abstract_blocks.py`
Tests for the core abstract block element classes.
**Coverage:**
- Paragraph word management
- Heading levels and properties
- Quote nesting capabilities
- Code block line management
- List creation and item handling
- Table structure (rows, cells, sections)
- Image properties and scaling
- Simple elements (hr, br)
#### ✅ `test_runner.py`
Test runner script for executing all tests with summary reporting.
---
## Additional Tests Needed
### 🔄 High Priority (Should Implement Next)
#### `test_abstract_inline.py`
Tests for inline elements and text formatting.
**Needed Coverage:**
- Word creation and properties
- Word hyphenation functionality
- FormattedSpan management
- Word chaining (previous/next relationships)
- Font style application
- Language-specific hyphenation
#### `test_abstract_document.py`
Tests for document structure and metadata.
**Needed Coverage:**
- Document creation and initialization
- Metadata management (title, author, language, etc.)
- Block addition and management
- Anchor creation and resolution
- Resource management
- Table of contents generation
- Chapter and book structures
#### `test_abstract_functional.py`
Tests for functional elements (links, buttons, forms).
**Needed Coverage:**
- Link creation and type detection
- Link execution for different types
- Button functionality and state
- Form field management
- Form validation and submission
- Parameter handling
#### `test_style_system.py`
Tests for the style system (fonts, colors, alignment).
**Needed Coverage:**
- Font creation and properties
- Color representation and manipulation
- Font weight, style, decoration enums
- Alignment enums and behavior
- Style inheritance and cascading
### 🔧 Medium Priority
#### `test_html_elements.py`
Unit tests for the HTML element handlers.
**Needed Coverage:**
- BlockElementHandler individual methods
- ListElementHandler state management
- TableElementHandler complex scenarios
- InlineElementHandler link processing
- Handler coordination and delegation
- Error handling in handlers
#### `test_html_metadata.py`
Tests for HTML metadata extraction.
**Needed Coverage:**
- Meta tag parsing
- Open Graph extraction
- JSON-LD structured data
- Title and description extraction
- Language detection
- Character encoding handling
#### `test_html_resources.py`
Tests for HTML resource extraction.
**Needed Coverage:**
- CSS stylesheet extraction
- JavaScript resource identification
- Image source collection
- Media element detection
- External resource resolution
- Base URL handling
#### `test_io_base.py`
Tests for the base reader architecture.
**Needed Coverage:**
- BaseReader interface compliance
- MetadataReader abstract methods
- ContentReader abstract methods
- ResourceReader abstract methods
- CompositeReader coordination
### 🔍 Lower Priority
#### `test_concrete_elements.py`
Tests for concrete rendering implementations.
**Needed Coverage:**
- Box model calculations
- Text rendering specifics
- Image rendering and scaling
- Page layout management
- Functional element rendering
#### `test_typesetting.py`
Tests for the typesetting system.
**Needed Coverage:**
- Flow algorithms
- Pagination logic
- Document pagination
- Line breaking
- Hyphenation integration
#### `test_epub_reader.py`
Tests for EPUB reading functionality.
**Needed Coverage:**
- EPUB file structure parsing
- Manifest processing
- Chapter extraction
- Metadata reading
- Navigation document parsing
#### `test_integration.py`
End-to-end integration tests.
**Needed Coverage:**
- Complete HTML-to-document workflows
- EPUB-to-document workflows
- Style application across parsers
- Resource resolution chains
- Error handling scenarios
## Testing Infrastructure
### Test Dependencies
```python
# Required for testing
unittest # Built-in Python testing framework
unittest.mock # For mocking and test doubles
```
### Test Data
- Create `tests/data/` directory with sample files:
- `sample.html` - Well-formed HTML document
- `complex.html` - Complex nested HTML
- `malformed.html` - Edge cases and error conditions
- `sample.epub` - Sample EPUB file
- `test_images/` - Sample images for testing
### Continuous Integration
- Tests should run on Python 3.6+
- All tests must pass before merging
- Aim for >90% code coverage
- Performance regression testing for parsing speed
## Running Tests
### Run All Tests
```bash
python tests/test_runner.py
```
### Run Specific Test Module
```bash
python tests/test_runner.py html_style
python -m unittest tests.test_html_style
```
### Run Individual Test
```bash
python -m unittest tests.test_html_style.TestHTMLStyleManager.test_color_parsing
```
### Run with Coverage
```bash
pip install coverage
coverage run -m unittest discover tests/
coverage report -m
coverage html # Generate HTML report
```
## Test Quality Guidelines
### Test Naming
- Test files: `test_<module_name>.py`
- Test classes: `Test<ClassName>`
- Test methods: `test_<specific_functionality>`
### Test Structure
1. **Arrange**: Set up test data and mocks
2. **Act**: Execute the functionality being tested
3. **Assert**: Verify the expected behavior
### Mock Usage
- Mock external dependencies (file I/O, network)
- Mock complex objects when testing units in isolation
- Prefer real objects for integration tests
### Edge Cases
- Empty inputs
- Invalid inputs
- Boundary conditions
- Error scenarios
- Performance edge cases
## Success Metrics
- **Coverage**: >90% line coverage across all modules
- **Performance**: No test takes longer than 1 second
- **Reliability**: Tests pass consistently across environments
- **Maintainability**: Tests are easy to understand and modify
- **Documentation**: Tests clearly show expected behavior
## Implementation Priority
1. **Week 1**: Complete high-priority abstract tests
2. **Week 2**: Implement HTML processing component tests
3. **Week 3**: Add integration and end-to-end tests
4. **Week 4**: Performance and edge case testing
This testing strategy ensures comprehensive coverage of the pyWebLayout library while maintaining good separation of concerns and providing clear documentation of expected behavior.
+6
View File
@@ -0,0 +1,6 @@
"""
Test suite for pyWebLayout.
This package contains comprehensive unit tests for all components of the pyWebLayout library,
organized by module and functionality.
"""
+275
View File
@@ -0,0 +1,275 @@
"""
Unit tests for abstract block elements.
Tests the core abstract block classes that form the foundation of the document model.
"""
import unittest
from pyWebLayout.abstract.block import (
Block, BlockType, Parapgraph, Heading, HeadingLevel, Quote, CodeBlock,
HList, ListStyle, ListItem, Table, TableRow, TableCell,
HorizontalRule, LineBreak, Image
)
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
class TestBlockElements(unittest.TestCase):
"""Test cases for basic block elements."""
def test_paragraph_creation(self):
"""Test creating and using paragraphs."""
paragraph = Parapgraph()
self.assertEqual(paragraph.block_type, BlockType.PARAGRAPH)
self.assertEqual(paragraph.word_count, 0)
self.assertIsNone(paragraph.parent)
# Add words
font = Font()
word1 = Word("Hello", font)
word2 = Word("World", font)
paragraph.add_word(word1)
paragraph.add_word(word2)
self.assertEqual(paragraph.word_count, 2)
# Test word iteration
words = list(paragraph.words())
self.assertEqual(len(words), 2)
self.assertEqual(words[0][1].text, "Hello")
self.assertEqual(words[1][1].text, "World")
def test_heading_levels(self):
"""Test heading creation with different levels."""
h1 = Heading(HeadingLevel.H1)
h3 = Heading(HeadingLevel.H3)
h6 = Heading(HeadingLevel.H6)
self.assertEqual(h1.level, HeadingLevel.H1)
self.assertEqual(h3.level, HeadingLevel.H3)
self.assertEqual(h6.level, HeadingLevel.H6)
self.assertEqual(h1.block_type, BlockType.HEADING)
# Test level modification
h1.level = HeadingLevel.H2
self.assertEqual(h1.level, HeadingLevel.H2)
def test_quote_nesting(self):
"""Test blockquote with nested content."""
quote = Quote()
# Add nested paragraphs
p1 = Parapgraph()
p2 = Parapgraph()
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)
if __name__ == '__main__':
unittest.main()
+354
View File
@@ -0,0 +1,354 @@
"""
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 (
Parapgraph, Heading, HeadingLevel, HList, ListStyle,
Table, Quote, CodeBlock, HorizontalRule, 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], Parapgraph)
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], Parapgraph)
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], Parapgraph)
self.assertIsInstance(quote_blocks[1], Parapgraph)
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], Parapgraph)
self.assertIsInstance(self.document.blocks[1], HorizontalRule)
self.assertIsInstance(self.document.blocks[2], Parapgraph)
def test_html_entities(self):
"""Test handling HTML entities."""
html = '<p>Less than: &lt; Greater than: &gt; Ampersand: &amp;</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('Parapgraph', 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', 'Parapgraph', 'HList', 'Quote', 'Table'}
self.assertTrue(expected_types.issubset(block_types))
if __name__ == '__main__':
unittest.main()
+182
View File
@@ -0,0 +1,182 @@
"""
Unit tests for HTML style management.
Tests the HTMLStyleManager class for CSS parsing, style stacks, and font creation.
"""
import unittest
from pyWebLayout.io.readers.html_style import HTMLStyleManager
from pyWebLayout.style import FontStyle, FontWeight, TextDecoration
class TestHTMLStyleManager(unittest.TestCase):
"""Test cases for HTMLStyleManager."""
def setUp(self):
"""Set up test fixtures."""
self.style_manager = HTMLStyleManager()
def test_initialization(self):
"""Test proper initialization of style manager."""
style = self.style_manager.get_current_style()
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')
def test_style_stack_operations(self):
"""Test push and pop operations on style stack."""
# Initial state
initial_style = self.style_manager.get_current_style()
# 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)
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_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)
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.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_size, 16)
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))
if __name__ == '__main__':
unittest.main()
+247
View File
@@ -0,0 +1,247 @@
"""
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 Parapgraph
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=Parapgraph)
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()
+84
View File
@@ -0,0 +1,84 @@
"""
Test runner for pyWebLayout.
This script runs all unit tests and provides a summary of results.
"""
import unittest
import sys
import os
# Add the project root to the Python path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def run_all_tests():
"""Run all unit tests and return the result."""
# Discover and run all tests
loader = unittest.TestLoader()
start_dir = os.path.dirname(os.path.abspath(__file__))
suite = loader.discover(start_dir, pattern='test_*.py')
# Run tests with detailed output
runner = unittest.TextTestRunner(
verbosity=2,
stream=sys.stdout,
descriptions=True,
failfast=False
)
result = runner.run(suite)
# Print summary
print("\n" + "="*70)
print("TEST SUMMARY")
print("="*70)
print(f"Tests run: {result.testsRun}")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print(f"Skipped: {len(result.skipped) if hasattr(result, 'skipped') else 0}")
if result.failures:
print(f"\nFAILURES ({len(result.failures)}):")
for test, traceback in result.failures:
print(f"- {test}")
if result.errors:
print(f"\nERRORS ({len(result.errors)}):")
for test, traceback in result.errors:
print(f"- {test}")
success = len(result.failures) == 0 and len(result.errors) == 0
print(f"\nResult: {'PASSED' if success else 'FAILED'}")
print("="*70)
return success
def run_specific_test(test_module):
"""Run a specific test module."""
loader = unittest.TestLoader()
suite = loader.loadTestsFromName(test_module)
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
return len(result.failures) == 0 and len(result.errors) == 0
if __name__ == '__main__':
if len(sys.argv) > 1:
# Run specific test
test_name = sys.argv[1]
if not test_name.startswith('test_'):
test_name = f'test_{test_name}'
if not test_name.endswith('.py'):
test_name = f'{test_name}.py'
module_name = test_name[:-3] # Remove .py extension
success = run_specific_test(module_name)
else:
# Run all tests
success = run_all_tests()
sys.exit(0 if success else 1)