first code commit
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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()
|
||||
@@ -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: < 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('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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user