Large clean up
Python CI / test (push) Failing after 5m5s

This commit is contained in:
2025-06-28 21:10:30 +02:00
parent d0153c6397
commit 56a6ec19e8
60 changed files with 2016 additions and 12198 deletions
+830
View File
@@ -0,0 +1,830 @@
"""
Unit tests for EPUB reader functionality.
Tests the EPUB parsing and conversion to pyWebLayout abstract elements,
using ebooklib to generate test EPUB files.
"""
import unittest
import tempfile
import os
import shutil
from datetime import datetime
# Import ebooklib for creating test EPUB files
try:
from ebooklib import epub
EBOOKLIB_AVAILABLE = True
except ImportError:
EBOOKLIB_AVAILABLE = False
from pyWebLayout.io.readers.epub_reader import read_epub, EPUBReader
from pyWebLayout.abstract.document import Book
from pyWebLayout.abstract.block import (
Paragraph, Heading, HeadingLevel, Quote, CodeBlock,
HList, ListStyle, Table, HorizontalRule, Image
)
from pyWebLayout.style import FontWeight, FontStyle, TextDecoration
@unittest.skipUnless(EBOOKLIB_AVAILABLE, "ebooklib not available")
class TestEPUBReader(unittest.TestCase):
"""Test cases for EPUB reader functionality."""
def setUp(self):
"""Set up test environment."""
self.test_dir = tempfile.mkdtemp()
self.epub_files = []
def tearDown(self):
"""Clean up test environment."""
# Clean up test EPUB files
for epub_file in self.epub_files:
try:
os.remove(epub_file)
except OSError:
pass
# Clean up test directory
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir, ignore_errors=True)
def create_simple_epub(self, title="Test Book", author="Test Author"):
"""Create a simple EPUB file for testing."""
book = epub.EpubBook()
# Set metadata
book.set_identifier('test-id-123')
book.set_title(title)
book.set_language('en')
book.add_author(author)
# Create a simple chapter
chapter1 = epub.EpubHtml(
title='Chapter 1',
file_name='chapter1.xhtml',
lang='en'
)
chapter1.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Chapter 1</title></head>
<body>
<h1>Chapter One</h1>
<p>This is the first paragraph of the first chapter.</p>
<p>This is a <strong>second paragraph</strong> with <em>some formatting</em>.</p>
</body>
</html>
'''
# Add chapter to book
book.add_item(chapter1)
# Define table of contents
book.toc = (epub.Link("chapter1.xhtml", "Chapter 1", "ch1"),)
# Add navigation files
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
# Define spine
book.spine = ['nav', chapter1]
# Create temporary file
epub_path = os.path.join(self.test_dir, f'test_simple_{len(self.epub_files)}.epub')
epub.write_epub(epub_path, book, {})
self.epub_files.append(epub_path)
return epub_path
def create_complex_epub(self):
"""Create a more complex EPUB file with multiple chapters and content types."""
book = epub.EpubBook()
# Set metadata
book.set_identifier('complex-test-id-456')
book.set_title('Complex Test Book')
book.set_language('en')
book.add_author('Test Author')
book.add_metadata('DC', 'description', 'A test book with complex content')
book.add_metadata('DC', 'subject', 'Testing')
book.add_metadata('DC', 'date', '2024-01-01')
book.add_metadata('DC', 'publisher', 'Test Publisher')
# Chapter 1: Basic content
chapter1 = epub.EpubHtml(
title='Introduction',
file_name='chapter1.xhtml',
lang='en'
)
chapter1.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Introduction</title></head>
<body>
<h1>Introduction</h1>
<p>Welcome to this <strong>complex test book</strong>.</p>
<p>This chapter contains basic content to test paragraph parsing.</p>
</body>
</html>
'''
# Chapter 2: Styled content
chapter2 = epub.EpubHtml(
title='Styled Content',
file_name='chapter2.xhtml',
lang='en'
)
chapter2.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Styled Content</title></head>
<body>
<h1>Styled Content</h1>
<p>This chapter contains various <strong>bold text</strong>, <em>italic text</em>,
and <span style="color: red; font-weight: bold;">colored text</span>.</p>
<h2>Subsection</h2>
<p>Text with <u>underline</u> and <s>strikethrough</s>.</p>
<h3>More Formatting</h3>
<p>Nested formatting: <strong>bold with <em>italic inside</em></strong>.</p>
</body>
</html>
'''
# Chapter 3: Lists and quotes
chapter3 = epub.EpubHtml(
title='Lists and Quotes',
file_name='chapter3.xhtml',
lang='en'
)
chapter3.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Lists and Quotes</title></head>
<body>
<h1>Lists and Quotes</h1>
<h2>Unordered List</h2>
<ul>
<li>First item</li>
<li><strong>Bold item</strong></li>
<li>Item with <em>italic text</em></li>
</ul>
<h2>Ordered List</h2>
<ol>
<li>First numbered item</li>
<li>Second numbered item</li>
<li>Third numbered item</li>
</ol>
<h2>Quote</h2>
<blockquote>
<p>This is a <span style="font-style: italic;">quoted paragraph</span>
with some styling.</p>
</blockquote>
</body>
</html>
'''
# Chapter 4: Tables and code
chapter4 = epub.EpubHtml(
title='Tables and Code',
file_name='chapter4.xhtml',
lang='en'
)
chapter4.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Tables and Code</title></head>
<body>
<h1>Tables and Code</h1>
<h2>Simple Table</h2>
<table>
<thead>
<tr>
<th><strong>Header 1</strong></th>
<th><em>Header 2</em></th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell 1</td>
<td>Cell 2 with <span style="color: blue;">blue text</span></td>
</tr>
<tr>
<td><strong>Bold cell</strong></td>
<td>Normal cell</td>
</tr>
</tbody>
</table>
<h2>Code Block</h2>
<pre><code>function test() {
console.log("Hello, world!");
return true;
}</code></pre>
<h2>Inline Code</h2>
<p>Use the <code>print()</code> function to output text.</p>
</body>
</html>
'''
# Add chapters to book
book.add_item(chapter1)
book.add_item(chapter2)
book.add_item(chapter3)
book.add_item(chapter4)
# Define table of contents
book.toc = (
epub.Link("chapter1.xhtml", "Introduction", "intro"),
epub.Link("chapter2.xhtml", "Styled Content", "styled"),
epub.Link("chapter3.xhtml", "Lists and Quotes", "lists"),
epub.Link("chapter4.xhtml", "Tables and Code", "tables")
)
# Add navigation files
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
# Define spine
book.spine = ['nav', chapter1, chapter2, chapter3, chapter4]
# Create temporary file
epub_path = os.path.join(self.test_dir, f'test_complex_{len(self.epub_files)}.epub')
epub.write_epub(epub_path, book, {})
self.epub_files.append(epub_path)
return epub_path
def create_epub_with_nested_content(self):
"""Create an EPUB with nested content structures."""
book = epub.EpubBook()
# Set metadata
book.set_identifier('nested-test-id-789')
book.set_title('Nested Content Test')
book.set_language('en')
book.add_author('Test Author')
# Chapter with nested content
chapter = epub.EpubHtml(
title='Nested Content',
file_name='nested.xhtml',
lang='en'
)
chapter.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Nested Content</title></head>
<body>
<h1>Nested Content Examples</h1>
<div>
<h2>Section in Div</h2>
<p>Paragraph inside div.</p>
<section>
<h3>Subsection</h3>
<article>
<h4>Article Header</h4>
<p>Article content with <strong>nested <em>formatting</em></strong>.</p>
<aside>
<p>Sidebar content in aside element.</p>
<ul>
<li>Nested list item</li>
<li>Another <strong>bold</strong> item</li>
</ul>
</aside>
</article>
</section>
</div>
<footer>
<p>Footer content with <span style="font-size: 12px; color: gray;">small gray text</span>.</p>
</footer>
</body>
</html>
'''
# Add chapter to book
book.add_item(chapter)
# Define table of contents
book.toc = (epub.Link("nested.xhtml", "Nested Content", "nested"),)
# Add navigation files
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
# Define spine
book.spine = ['nav', chapter]
# Create temporary file
epub_path = os.path.join(self.test_dir, f'test_nested_{len(self.epub_files)}.epub')
epub.write_epub(epub_path, book, {})
self.epub_files.append(epub_path)
return epub_path
def test_simple_epub_reading(self):
"""Test reading a simple EPUB file."""
epub_path = self.create_simple_epub()
# Read the EPUB
book = read_epub(epub_path)
# Verify it's a Book object
self.assertIsInstance(book, Book)
# Check metadata
self.assertEqual(book.title, "Test Book")
# Check chapters
chapters = list(book.chapters)
self.assertEqual(len(chapters), 1)
# Check chapter content
chapter = chapters[0]
blocks = list(chapter.blocks)
self.assertGreater(len(blocks), 0)
# Should have a heading and paragraphs
has_heading = any(isinstance(block, Heading) for block in blocks)
has_paragraph = any(isinstance(block, Paragraph) for block in blocks)
self.assertTrue(has_heading, "Should contain at least one heading")
self.assertTrue(has_paragraph, "Should contain at least one paragraph")
def test_complex_epub_reading(self):
"""Test reading a complex EPUB file with multiple chapters."""
epub_path = self.create_complex_epub()
# Read the EPUB
book = read_epub(epub_path)
# Verify it's a Book object
self.assertIsInstance(book, Book)
# Check metadata
self.assertEqual(book.title, "Complex Test Book")
# Check chapters
chapters = list(book.chapters)
self.assertEqual(len(chapters), 4)
# Test each chapter has content
for i, chapter in enumerate(chapters):
blocks = list(chapter.blocks)
self.assertGreater(len(blocks), 0, f"Chapter {i+1} should have blocks")
# Each chapter should start with a heading
first_block = blocks[0]
self.assertIsInstance(first_block, Heading, f"Chapter {i+1} should start with heading")
def test_epub_styled_content(self):
"""Test that styled content in EPUB is properly parsed."""
epub_path = self.create_complex_epub()
book = read_epub(epub_path)
chapters = list(book.chapters)
# Check styled content in chapter 2 (index 1)
if len(chapters) > 1:
chapter2_blocks = list(chapters[1].blocks)
# Find paragraphs with styled text
styled_words_found = False
for block in chapter2_blocks:
if isinstance(block, Paragraph):
words = list(block.words())
for _, word in words:
if (word.style.weight == FontWeight.BOLD or
word.style.style == FontStyle.ITALIC or
word.style.colour != (0, 0, 0)): # Non-black color
styled_words_found = True
break
if styled_words_found:
break
self.assertTrue(styled_words_found, "Should find styled words in chapter 2")
def test_epub_lists(self):
"""Test that lists in EPUB are properly parsed."""
epub_path = self.create_complex_epub()
book = read_epub(epub_path)
chapters = list(book.chapters)
# Check lists in chapter 3 (index 2)
if len(chapters) > 2:
chapter3_blocks = list(chapters[2].blocks)
# Find list blocks
unordered_list_found = False
ordered_list_found = False
quote_found = False
for block in chapter3_blocks:
if isinstance(block, HList):
if block.style == ListStyle.UNORDERED:
unordered_list_found = True
# Check list items
items = list(block.items())
self.assertGreater(len(items), 0, "Unordered list should have items")
elif block.style == ListStyle.ORDERED:
ordered_list_found = True
# Check list items
items = list(block.items())
self.assertGreater(len(items), 0, "Ordered list should have items")
elif isinstance(block, Quote):
quote_found = True
self.assertTrue(unordered_list_found, "Should find unordered list in chapter 3")
self.assertTrue(ordered_list_found, "Should find ordered list in chapter 3")
self.assertTrue(quote_found, "Should find quote in chapter 3")
def test_epub_tables(self):
"""Test that tables in EPUB are properly parsed."""
epub_path = self.create_complex_epub()
book = read_epub(epub_path)
chapters = list(book.chapters)
# Check tables in chapter 4 (index 3)
if len(chapters) > 3:
chapter4_blocks = list(chapters[3].blocks)
# Find table blocks
table_found = False
code_block_found = False
for block in chapter4_blocks:
if isinstance(block, Table):
table_found = True
# Check table has rows
rows = list(block.all_rows())
self.assertGreater(len(rows), 0, "Table should have rows")
elif isinstance(block, CodeBlock):
code_block_found = True
# Check code block has lines
lines = list(block.lines())
self.assertGreater(len(lines), 0, "Code block should have lines")
self.assertTrue(table_found, "Should find table in chapter 4")
self.assertTrue(code_block_found, "Should find code block in chapter 4")
def test_epub_nested_content(self):
"""Test that nested content structures are properly parsed."""
epub_path = self.create_epub_with_nested_content()
book = read_epub(epub_path)
chapters = list(book.chapters)
self.assertEqual(len(chapters), 1)
chapter_blocks = list(chapters[0].blocks)
self.assertGreater(len(chapter_blocks), 0)
# Should have multiple headings (h1, h2, h3, h4)
headings = [block for block in chapter_blocks if isinstance(block, Heading)]
self.assertGreater(len(headings), 2, "Should have multiple headings from nested content")
# Should have paragraphs and lists from nested content
paragraphs = [block for block in chapter_blocks if isinstance(block, Paragraph)]
lists = [block for block in chapter_blocks if isinstance(block, HList)]
self.assertGreater(len(paragraphs), 0, "Should have paragraphs from nested content")
self.assertGreater(len(lists), 0, "Should have lists from nested content")
def test_epub_metadata_extraction(self):
"""Test that EPUB metadata is properly extracted."""
epub_path = self.create_complex_epub()
book = read_epub(epub_path)
# Check basic metadata
self.assertEqual(book.title, "Complex Test Book")
# Check that metadata was set (implementation may vary)
# This tests that the metadata parsing doesn't crash
self.assertIsNotNone(book.title)
def test_epub_reader_class_direct(self):
"""Test EPUBReader class directly."""
epub_path = self.create_simple_epub()
reader = EPUBReader(epub_path)
book = reader.read()
self.assertIsInstance(book, Book)
self.assertEqual(book.title, "Test Book")
def test_invalid_epub_handling(self):
"""Test handling of invalid EPUB files."""
# Create a non-EPUB file
invalid_path = os.path.join(self.test_dir, 'invalid.epub')
with open(invalid_path, 'w') as f:
f.write("This is not an EPUB file")
# Should raise an exception or handle gracefully
with self.assertRaises(Exception):
read_epub(invalid_path)
def test_nonexistent_epub_handling(self):
"""Test handling of nonexistent EPUB files."""
nonexistent_path = os.path.join(self.test_dir, 'nonexistent.epub')
# Should raise an exception
with self.assertRaises(Exception):
read_epub(nonexistent_path)
def test_epub_with_custom_metadata(self):
"""Test EPUB with various metadata fields."""
book = epub.EpubBook()
# Set comprehensive metadata
book.set_identifier('custom-metadata-test')
book.set_title('Custom Metadata Test')
book.set_language('en')
book.add_author('Primary Author')
book.add_author('Secondary Author')
book.add_metadata('DC', 'description', 'A comprehensive test of metadata extraction')
book.add_metadata('DC', 'subject', 'Testing')
book.add_metadata('DC', 'subject', 'EPUB')
book.add_metadata('DC', 'date', '2024-06-07')
book.add_metadata('DC', 'publisher', 'Test Publishing House')
book.add_metadata('DC', 'rights', 'Public Domain')
# Simple chapter
chapter = epub.EpubHtml(
title='Metadata Test',
file_name='metadata.xhtml',
lang='en'
)
chapter.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Metadata Test</title></head>
<body>
<h1>Metadata Test Chapter</h1>
<p>This chapter tests metadata extraction.</p>
</body>
</html>
'''
book.add_item(chapter)
book.toc = (epub.Link("metadata.xhtml", "Metadata Test", "meta"),)
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
book.spine = ['nav', chapter]
# Write and test
epub_path = os.path.join(self.test_dir, f'test_metadata_{len(self.epub_files)}.epub')
epub.write_epub(epub_path, book, {})
self.epub_files.append(epub_path)
# Read and verify
parsed_book = read_epub(epub_path)
self.assertEqual(parsed_book.title, "Custom Metadata Test")
# Verify chapters were created
chapters = list(parsed_book.chapters)
self.assertEqual(len(chapters), 1)
class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
"""Test cases that specifically verify EPUB reader uses html_extraction properly."""
def setUp(self):
"""Set up test environment."""
self.test_dir = tempfile.mkdtemp()
self.epub_files = []
def tearDown(self):
"""Clean up test environment."""
for epub_file in self.epub_files:
try:
os.remove(epub_file)
except OSError:
pass
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir, ignore_errors=True)
@unittest.skipUnless(EBOOKLIB_AVAILABLE, "ebooklib not available")
def test_html_extraction_integration(self):
"""Test that EPUB reader properly uses html_extraction functionality."""
# Create an EPUB that exercises various HTML extraction features
book = epub.EpubBook()
book.set_identifier('html-extraction-test')
book.set_title('HTML Extraction Test')
book.set_language('en')
book.add_author('Test Author')
# Chapter that exercises html_extraction features
chapter = epub.EpubHtml(
title='HTML Features',
file_name='html_features.xhtml',
lang='en'
)
chapter.content = '''
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>HTML Features</title></head>
<body>
<h1>HTML Extraction Test</h1>
<!-- Test paragraph with inline formatting -->
<p>This paragraph has <strong>bold</strong>, <em>italic</em>,
<u>underlined</u>, and <span style="color: #ff0000; font-weight: bold;">styled</span> text.</p>
<!-- Test headings -->
<h2>Second Level Heading</h2>
<h3>Third Level Heading</h3>
<!-- Test lists with styled content -->
<ul>
<li>Plain list item</li>
<li><strong>Bold list item</strong></li>
<li>List item with <em>italic text</em></li>
</ul>
<!-- Test table with styled cells -->
<table>
<tr>
<th style="font-weight: bold;">Header</th>
<th>Value</th>
</tr>
<tr>
<td><span style="color: blue;">Blue text</span></td>
<td>Normal text</td>
</tr>
</table>
<!-- Test blockquote -->
<blockquote>
<p>This is a quoted paragraph with <strong>bold text</strong>.</p>
</blockquote>
<!-- Test code block -->
<pre><code>def test_function():
return "Hello, World!"</code></pre>
<!-- Test nested formatting -->
<p>Nested formatting: <strong>bold with <em>italic nested</em> inside</strong>.</p>
<!-- Test color variations -->
<p>
<span style="color: red;">Red text</span>,
<span style="color: #00ff00;">Green hex</span>,
<span style="color: blue; text-decoration: underline;">Blue underlined</span>.
</p>
</body>
</html>
'''
book.add_item(chapter)
book.toc = (epub.Link("html_features.xhtml", "HTML Features", "html"),)
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
book.spine = ['nav', chapter]
# Write EPUB
epub_path = os.path.join(self.test_dir, 'html_extraction_test.epub')
epub.write_epub(epub_path, book, {})
self.epub_files.append(epub_path)
# Read and analyze
parsed_book = read_epub(epub_path)
chapters = list(parsed_book.chapters)
self.assertEqual(len(chapters), 1)
blocks = list(chapters[0].blocks)
self.assertGreater(len(blocks), 5) # Should have multiple blocks
# Test that we get the expected block types
block_types = [type(block).__name__ for block in blocks]
self.assertIn('Heading', block_types, "Should have heading blocks")
self.assertIn('Paragraph', block_types, "Should have paragraph blocks")
self.assertIn('HList', block_types, "Should have list blocks")
self.assertIn('Table', block_types, "Should have table blocks")
self.assertIn('Quote', block_types, "Should have quote blocks")
self.assertIn('CodeBlock', block_types, "Should have code blocks")
# Test styled content was preserved
styled_content_found = False
for block in blocks:
if isinstance(block, Paragraph):
words = list(block.words())
for _, word in words:
if (word.style.weight == FontWeight.BOLD or
word.style.style == FontStyle.ITALIC or
word.style.decoration == TextDecoration.UNDERLINE or
word.style.colour != (0, 0, 0)):
styled_content_found = True
break
if styled_content_found:
break
self.assertTrue(styled_content_found, "Should find styled content in parsed blocks")
# Test specific color parsing
red_text_found = False
green_text_found = False
blue_text_found = False
for block in blocks:
if isinstance(block, (Paragraph, Table)):
if isinstance(block, Paragraph):
words = list(block.words())
for _, word in words:
if word.style.colour == (255, 0, 0): # Red
red_text_found = True
elif word.style.colour == (0, 255, 0): # Green
green_text_found = True
elif word.style.colour == (0, 0, 255): # Blue
blue_text_found = True
# At least one color should be found (depending on implementation)
color_found = red_text_found or green_text_found or blue_text_found
self.assertTrue(color_found, "Should find at least one colored text")
def test_epub_with_image(self):
"""Test that images in EPUB are properly parsed."""
book = epub.EpubBook()
book.set_identifier('image-test-id')
book.set_title('Image Test Book')
book.set_language('en')
book.add_author('Test Author')
# Create minimal JPEG data for testing
img_data = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342\xff\xc0\x00\x11\x08\x00d\x00d\x01\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00\xaa\xff\xd9'
# Create an EpubImage item
image_item = epub.EpubImage()
image_item.id = 'test_img'
image_item.file_name = 'images/test_image.jpg'
image_item.media_type = 'image/jpeg'
image_item.content = img_data
# Add image to book
book.add_item(image_item)
# Create a chapter that references the image
chapter = epub.EpubHtml(
title='Image Chapter',
file_name='image_chapter.xhtml',
lang='en'
)
chapter.content = '''<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Image Chapter</title></head>
<body>
<h1>Chapter with Image</h1>
<p>This chapter contains an image:</p>
<img src="images/test_image.jpg" alt="Test image" width="300" height="200" />
<p>Text after the image.</p>
</body>
</html>'''
book.add_item(chapter)
book.toc = (epub.Link("image_chapter.xhtml", "Image Chapter", "img_ch"),)
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
book.spine = ['nav', chapter]
# Write EPUB
epub_path = os.path.join(self.test_dir, f'test_image_{len(self.epub_files)}.epub')
epub.write_epub(epub_path, book, {})
self.epub_files.append(epub_path)
# Read and analyze
parsed_book = read_epub(epub_path)
chapters = list(parsed_book.chapters)
self.assertEqual(len(chapters), 1)
blocks = list(chapters[0].blocks)
self.assertGreater(len(blocks), 0)
# Find blocks by type
heading_blocks = [block for block in blocks if isinstance(block, Heading)]
paragraph_blocks = [block for block in blocks if isinstance(block, Paragraph)]
image_blocks = [block for block in blocks if isinstance(block, Image)]
# Verify we have the expected blocks
self.assertEqual(len(heading_blocks), 1, "Should find exactly one heading block")
self.assertGreaterEqual(len(paragraph_blocks), 2, "Should find at least two paragraph blocks")
self.assertEqual(len(image_blocks), 1, "Should find exactly one image block")
# Verify image properties
image_block = image_blocks[0]
self.assertEqual(image_block.alt_text, "Test image")
self.assertEqual(image_block.width, 300)
self.assertEqual(image_block.height, 200)
self.assertIn("test_image.jpg", image_block.source)
if __name__ == '__main__':
unittest.main()
+564
View File
@@ -0,0 +1,564 @@
"""
Unit tests for HTML extraction functionality.
Tests the HTML parsing and conversion to pyWebLayout abstract elements,
including styled content within paragraphs and block-level elements.
"""
import unittest
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel, Quote, CodeBlock, HList, ListStyle, Table
from pyWebLayout.abstract.document import Document
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
class TestHTMLParagraph(unittest.TestCase):
"""Test cases for basic paragraph parsing."""
def test_simple(self):
text = "<p>This is a paragraph.</p>"
paragraphs = parse_html_string(text)
self.assertEqual(len(paragraphs), 1)
self.assertEqual(len(paragraphs[0]), 4)
for w1, t1 in zip(paragraphs[0].words(), "This is a paragraph.".split(" ")):
self.assertEqual(w1[1].text, t1)
def test_multiple(self):
text = "<p>This is a paragraph.</p><p>This is another paragraph.</p>"
paragraphs = parse_html_string(text)
self.assertEqual(len(paragraphs), 2)
self.assertEqual(len(paragraphs[0]), 4)
self.assertEqual(len(paragraphs[1]), 4)
for w1, t1 in zip(paragraphs[0].words(), "This is a paragraph.".split(" ")):
self.assertEqual(w1[1].text, t1)
for w1, t1 in zip(paragraphs[1].words(), "This is another paragraph.".split(" ")):
self.assertEqual(w1[1].text, t1)
class TestHTMLStyledParagraphs(unittest.TestCase):
"""Test cases for paragraphs with inline styling."""
def test_bold_text(self):
"""Test paragraphs with bold text using <strong> and <b> tags."""
text = "<p>This is <strong>bold text</strong> in a paragraph.</p>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
words = list(blocks[0].words())
self.assertEqual(len(words), 7) # "This is bold text in a paragraph."
# Check that 'bold' and 'text' words have bold font weight
bold_word = words[2][1] # 'bold'
text_word = words[3][1] # 'text'
self.assertEqual(bold_word.text, "bold")
self.assertEqual(bold_word.style.weight, FontWeight.BOLD)
self.assertEqual(text_word.text, "text")
self.assertEqual(text_word.style.weight, FontWeight.BOLD)
# Check that other words are not bold
normal_word = words[0][1] # 'This'
self.assertEqual(normal_word.text, "This")
self.assertNotEqual(normal_word.style.weight, FontWeight.BOLD)
def test_italic_text(self):
"""Test paragraphs with italic text using <em> and <i> tags."""
text = "<p>This is <em>italic text</em> in a paragraph.</p>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
words = list(blocks[0].words())
# Check that 'italic' and 'text' words have italic font style
italic_word = words[2][1] # 'italic'
text_word = words[3][1] # 'text'
self.assertEqual(italic_word.text, "italic")
self.assertEqual(italic_word.style.style, FontStyle.ITALIC)
self.assertEqual(text_word.text, "text")
self.assertEqual(text_word.style.style, FontStyle.ITALIC)
def test_underlined_text(self):
"""Test paragraphs with underlined text using <u> tag."""
text = "<p>This is <u>underlined text</u> here.</p>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
words = list(blocks[0].words())
underlined_word = words[2][1] # 'underlined'
self.assertEqual(underlined_word.style.decoration, TextDecoration.UNDERLINE)
def test_strikethrough_text(self):
"""Test paragraphs with strikethrough text using <s> and <del> tags."""
text = "<p>This is <s>strikethrough text</s> here.</p>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
words = list(blocks[0].words())
strike_word = words[2][1] # 'strikethrough'
self.assertEqual(strike_word.style.decoration, TextDecoration.STRIKETHROUGH)
def test_span_with_inline_styles(self):
"""Test paragraphs with span elements containing inline CSS styles."""
text = '<p>This text is normal, but <span style="color: red; font-weight: bold;">this part is red and bold</span>.</p>'
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
words = list(blocks[0].words())
# Find the styled words
styled_words = []
for _, word in words:
if word.text in ["this", "part", "is", "red", "and", "bold"]:
if word.style.weight == FontWeight.BOLD:
styled_words.append(word)
self.assertGreater(len(styled_words), 0, "Should have bold words in styled span")
# Check that at least one word has the red color
red_words = [w for w in styled_words if w.style.colour == (255, 0, 0)]
self.assertGreater(len(red_words), 0, "Should have red colored words")
def test_mixed_formatting(self):
"""Test paragraphs with multiple formatting elements combined."""
text = "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style=\"color: blue;\">blue</span>, and <mark>highlighted</mark> text all together.</p>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
words = list(blocks[0].words())
# Check for bold word
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
self.assertGreater(len(bold_words), 0, "Should have bold words")
# Check for italic word
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
self.assertGreater(len(italic_words), 0, "Should have italic words")
# Check for blue colored word
blue_words = [w for _, w in words if w.style.colour == (0, 0, 255)]
self.assertGreater(len(blue_words), 0, "Should have blue colored words")
def test_nested_formatting(self):
"""Test nested formatting elements."""
text = "<p>This has <strong>bold with <em>italic inside</em></strong> formatting.</p>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
words = list(blocks[0].words())
# Find words that should be both bold and italic
bold_italic_words = [w for _, w in words
if w.style.weight == FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
self.assertGreater(len(bold_italic_words), 0, "Should have words that are both bold and italic")
def test_color_variations(self):
"""Test different color formats in CSS."""
text = '<p><span style="color: #ff0000;">Hex red</span> and <span style="color: green;">Named green</span>.</p>'
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
words = list(blocks[0].words())
# Check for hex red color
hex_red_words = [w for _, w in words if w.style.colour == (255, 0, 0)]
self.assertGreater(len(hex_red_words), 0, "Should have hex red colored words")
# Check for named green color
green_words = [w for _, w in words if w.style.colour == (0, 255, 0)]
self.assertGreater(len(green_words), 0, "Should have green colored words")
class TestHTMLBlockElements(unittest.TestCase):
"""Test cases for block-level HTML elements."""
def test_body_element(self):
"""Test parsing of body element containing other elements."""
text = "<body><p>Paragraph one.</p><p>Paragraph two.</p></body>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 2)
self.assertIsInstance(blocks[0], Paragraph)
self.assertIsInstance(blocks[1], Paragraph)
def test_div_container(self):
"""Test div elements as generic containers."""
text = "<div><p>First paragraph.</p><p>Second paragraph.</p></div>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 2)
self.assertIsInstance(blocks[0], Paragraph)
self.assertIsInstance(blocks[1], Paragraph)
def test_headings(self):
"""Test all heading levels h1-h6."""
text = "<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 6)
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
for i, block in enumerate(blocks):
self.assertIsInstance(block, Heading)
self.assertEqual(block.level, expected_levels[i])
words = list(block.words())
self.assertEqual(len(words), 2) # "Heading" and number
self.assertEqual(words[0][1].text, "Heading")
def test_blockquote(self):
"""Test blockquote elements."""
text = "<blockquote><p>This is a quoted paragraph.</p></blockquote>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Quote)
# Check that the quote contains a paragraph
quote_blocks = list(blocks[0].blocks())
self.assertEqual(len(quote_blocks), 1)
self.assertIsInstance(quote_blocks[0], Paragraph)
def test_preformatted_code(self):
"""Test preformatted code blocks."""
text = "<pre><code>function hello() {\n console.log('Hello');\n}</code></pre>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], CodeBlock)
lines = list(blocks[0].lines())
self.assertGreater(len(lines), 0)
def test_unordered_list(self):
"""Test unordered lists."""
text = "<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], HList)
self.assertEqual(blocks[0].style, ListStyle.UNORDERED)
items = list(blocks[0].items())
self.assertEqual(len(items), 3)
def test_ordered_list(self):
"""Test ordered lists."""
text = "<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], HList)
self.assertEqual(blocks[0].style, ListStyle.ORDERED)
def test_list_with_styled_content(self):
"""Test lists containing styled content."""
text = "<ul><li>Normal item</li><li><strong>Bold item</strong></li><li>Item with <em>italic</em> text</li></ul>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], HList)
items = list(blocks[0].items())
self.assertEqual(len(items), 3)
# Check second item has bold text
second_item_blocks = list(items[1].blocks())
if second_item_blocks:
words = list(second_item_blocks[0].words())
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
self.assertGreater(len(bold_words), 0)
def test_table_basic(self):
"""Test basic table structure."""
text = """
<table>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</table>
"""
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Table)
def test_semantic_elements(self):
"""Test semantic HTML5 elements treated as containers."""
text = "<section><article><p>Article content</p></article></section>"
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
def test_nested_block_elements(self):
"""Test nested block elements."""
text = """
<div>
<h2>Section Title</h2>
<p>Some introductory text.</p>
<blockquote>
<p>A quoted paragraph.</p>
</blockquote>
</div>
"""
blocks = parse_html_string(text)
self.assertGreater(len(blocks), 2)
# Should have at least a heading, paragraph, and quote
has_heading = any(isinstance(b, Heading) for b in blocks)
has_paragraph = any(isinstance(b, Paragraph) for b in blocks)
has_quote = any(isinstance(b, Quote) for b in blocks)
self.assertTrue(has_heading, "Should contain a heading")
self.assertTrue(has_paragraph, "Should contain a paragraph")
self.assertTrue(has_quote, "Should contain a quote")
def test_empty_elements(self):
"""Test handling of empty elements."""
text = "<p></p><div></div><span></span>"
blocks = parse_html_string(text)
# Empty elements may not create blocks, which is acceptable behavior
self.assertGreaterEqual(len(blocks), 0)
# Test that empty paragraph with some content does create a block
text_with_content = "<p> </p>" # Contains whitespace
blocks_with_content = parse_html_string(text_with_content)
# This should create at least one block since there's whitespace content
self.assertGreaterEqual(len(blocks_with_content), 0)
class TestHTMLComplexStructures(unittest.TestCase):
"""Test cases for complex HTML structures combining multiple features."""
def test_article_with_mixed_content(self):
"""Test a realistic article structure with mixed content."""
text = """
<article>
<h1>Article Title</h1>
<p>This is the <strong>introduction</strong> paragraph with <em>some emphasis</em>.</p>
<blockquote>
<p>This is a <span style="color: blue;">quoted section</span> with styling.</p>
</blockquote>
<ul>
<li>First <strong>important</strong> point</li>
<li>Second point with <code>inline code</code></li>
</ul>
</article>
"""
blocks = parse_html_string(text)
self.assertGreater(len(blocks), 3)
# Verify we have the expected block types
block_types = [type(b).__name__ for b in blocks]
self.assertIn('Heading', block_types)
self.assertIn('Paragraph', block_types)
self.assertIn('Quote', block_types)
self.assertIn('HList', block_types)
def test_styled_table_content(self):
"""Test table with styled cell content."""
text = """
<table>
<thead>
<tr>
<th><strong>Product</strong></th>
<th><em>Price</em></th>
</tr>
</thead>
<tbody>
<tr>
<td>Item with <span style="color: red;">red text</span></td>
<td><strong>$19.99</strong></td>
</tr>
</tbody>
</table>
"""
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Table)
class TestHTMLFontRegistryIntegration(unittest.TestCase):
"""Test cases for font registry integration with HTML extraction."""
def setUp(self):
"""Set up test fixtures."""
self.doc = Document("Test Document", "en-US")
self.base_font = Font(font_size=16, colour=(0, 0, 0))
def test_font_registry_creates_fonts(self):
"""Test that HTML parsing with document context creates fonts in registry."""
html_content = """
<div>
<p>This is <strong>bold text</strong> and <em>italic text</em>.</p>
<h1>Main Header</h1>
</div>
"""
# Initially empty style registry
initial_style_count = self.doc.get_style_registry().get_style_count()
# Parse HTML with document context
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
# Should have created styles for different formatting
final_style_count = self.doc.get_style_registry().get_style_count()
self.assertGreater(final_style_count, initial_style_count,
"Should have created styles in registry")
# Should have created blocks
self.assertGreater(len(blocks), 0, "Should have created blocks")
def test_font_registry_reuses_fonts(self):
"""Test that parsing same content reuses existing styles."""
html_content = """
<div>
<p>This is <strong>bold text</strong> and <em>italic text</em>.</p>
<h1>Main Header</h1>
</div>
"""
# First parse
blocks1 = parse_html_string(html_content, self.base_font, document=self.doc)
first_parse_style_count = self.doc.get_style_registry().get_style_count()
# Second parse with same content
blocks2 = parse_html_string(html_content, self.base_font, document=self.doc)
second_parse_style_count = self.doc.get_style_registry().get_style_count()
# Style count should not increase on second parse
self.assertEqual(first_parse_style_count, second_parse_style_count,
"Should reuse existing styles instead of creating new ones")
# Both parses should create same number of blocks
self.assertEqual(len(blocks1), len(blocks2),
"Should create same structure on both parses")
def test_font_registry_different_styles_create_different_fonts(self):
"""Test that different styles create different style objects."""
# Create styles with different properties
style_id1, style1 = self.doc.get_or_create_style(
font_size=14, color=(255, 0, 0), font_weight=FontWeight.BOLD
)
style_id2, style2 = self.doc.get_or_create_style(
font_size=16, color=(255, 0, 0), font_weight=FontWeight.BOLD
)
style_id3, style3 = self.doc.get_or_create_style(
font_size=14, color=(0, 255, 0), font_weight=FontWeight.BOLD
)
# Should be different style IDs
self.assertNotEqual(style_id1, style_id2, "Different sizes should create different styles")
self.assertNotEqual(style_id1, style_id3, "Different colors should create different styles")
self.assertNotEqual(style_id2, style_id3, "All styles should be different")
# Should have multiple styles in registry
self.assertGreaterEqual(self.doc.get_style_registry().get_style_count(), 3)
def test_font_registry_integration_with_html_styles(self):
"""Test that HTML parsing uses style registry for styled content."""
html_content = """
<p>Normal text with <strong>bold</strong> and <em>italic</em> and
<span style="color: red;">red text</span>.</p>
"""
# Parse content
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
# Extract all words from the paragraph
paragraph = blocks[0]
words = list(paragraph.words())
# Find words with different styles
normal_words = [w for _, w in words if w.style.weight == FontWeight.NORMAL
and w.style.style == FontStyle.NORMAL]
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
red_words = [w for _, w in words if w.style.colour == (255, 0, 0)]
# Should have words with different styles
self.assertGreater(len(normal_words), 0, "Should have normal words")
self.assertGreater(len(bold_words), 0, "Should have bold words")
self.assertGreater(len(italic_words), 0, "Should have italic words")
self.assertGreater(len(red_words), 0, "Should have red words")
# Style registry should contain multiple styles for different formatting
self.assertGreater(self.doc.get_style_registry().get_style_count(), 1,
"Should have multiple styles for different formatting")
def test_font_registry_without_document_context(self):
"""Test that parsing without document context works (fallback behavior)."""
html_content = "<p>This is <strong>bold text</strong>.</p>"
# Get initial style count (should include default style)
initial_style_count = self.doc.get_style_registry().get_style_count()
# Parse without document context
blocks = parse_html_string(html_content, self.base_font)
# Should still create blocks successfully
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
# Should not affect document's style registry
final_style_count = self.doc.get_style_registry().get_style_count()
self.assertEqual(final_style_count, initial_style_count,
"Document style registry should remain unchanged")
def test_complex_html_font_reuse(self):
"""Test style reuse with complex HTML containing repeated styles."""
html_content = """
<div>
<h1>First Header</h1>
<p>Paragraph with <strong>bold</strong> text.</p>
<h1>Second Header</h1>
<p>Another paragraph with <strong>bold</strong> text.</p>
</div>
"""
# Parse content
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
style_count_after_parse = self.doc.get_style_registry().get_style_count()
# Parse same content again
blocks2 = parse_html_string(html_content, self.base_font, document=self.doc)
style_count_after_second_parse = self.doc.get_style_registry().get_style_count()
# Style count should not increase on second parse
self.assertEqual(style_count_after_parse, style_count_after_second_parse,
"Styles should be reused for repeated formatting")
# Both should create same structure
self.assertEqual(len(blocks), len(blocks2))
def test_font_registry_with_nested_styles(self):
"""Test style registry with nested HTML styles."""
html_content = """
<p>Text with <strong>bold and <em>bold italic</em> nested</strong> styles.</p>
"""
# Parse content
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
# Should create styles for different style combinations
paragraph = blocks[0]
words = list(paragraph.words())
# Find words that are both bold and italic
bold_italic_words = [w for _, w in words
if w.style.weight == FontWeight.BOLD
and w.style.style == FontStyle.ITALIC]
self.assertGreater(len(bold_italic_words), 0,
"Should have words with combined bold+italic style")
# Should have multiple styles in registry for different combinations
self.assertGreater(self.doc.get_style_registry().get_style_count(), 1,
"Should create separate styles for style combinations")
if __name__ == '__main__':
unittest.main()
+493
View File
@@ -0,0 +1,493 @@
"""
Unit tests for individual HTML extraction functions.
Tests the specific handler functions and utility functions in html_extraction module,
reusing test patterns from test_html_extraction.py that are known to pass.
"""
import unittest
from bs4 import BeautifulSoup, Tag
from pyWebLayout.io.readers.html_extraction import (
create_base_context,
apply_element_styling,
parse_inline_styles,
apply_element_font_styles,
extract_text_content,
paragraph_handler,
div_handler,
heading_handler,
blockquote_handler,
preformatted_handler,
unordered_list_handler,
ordered_list_handler,
list_item_handler,
table_handler,
table_row_handler,
table_cell_handler,
table_header_cell_handler,
horizontal_rule_handler,
image_handler,
StyleContext,
)
from pyWebLayout.abstract.block import (
Paragraph,
Heading,
HeadingLevel,
Quote,
CodeBlock,
HList,
ListItem,
ListStyle,
Table,
TableRow,
TableCell,
HorizontalRule,
Image,
)
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
class TestUtilityFunctions(unittest.TestCase):
"""Test cases for utility functions."""
def test_create_base_context(self):
"""Test creation of base style context."""
context = create_base_context()
self.assertIsInstance(context, StyleContext)
self.assertIsInstance(context.font, Font)
self.assertIsNone(context.background)
self.assertEqual(context.css_classes, set())
self.assertEqual(context.css_styles, {})
self.assertEqual(context.element_attributes, {})
self.assertEqual(context.parent_elements, [])
def test_parse_inline_styles_from_existing_tests(self):
"""Test parsing CSS inline styles - adapted from test_span_with_inline_styles."""
# From: '<span style="color: red; font-weight: bold;">this part is red and bold</span>'
style_text = "color: red; font-weight: bold;"
styles = parse_inline_styles(style_text)
expected = {
"color": "red",
"font-weight": "bold"
}
self.assertEqual(styles, expected)
def test_parse_inline_styles_color_variations(self):
"""Test parsing different color formats - adapted from test_color_variations."""
# Test hex color parsing
hex_style = "color: #ff0000;"
styles = parse_inline_styles(hex_style)
self.assertEqual(styles.get("color"), "#ff0000")
# Test named color parsing
named_style = "color: green;"
styles = parse_inline_styles(named_style)
self.assertEqual(styles.get("color"), "green")
def test_apply_element_font_styles_bold_elements(self):
"""Test font style application for bold elements - adapted from test_bold_text."""
base_font = Font()
# Test <strong> tag - from "<strong>bold text</strong>"
font = apply_element_font_styles(base_font, "strong", {})
self.assertEqual(font.weight, FontWeight.BOLD)
# Test <b> tag
font = apply_element_font_styles(base_font, "b", {})
self.assertEqual(font.weight, FontWeight.BOLD)
def test_apply_element_font_styles_italic_elements(self):
"""Test font style application for italic elements - adapted from test_italic_text."""
base_font = Font()
# Test <em> tag - from "<em>italic text</em>"
font = apply_element_font_styles(base_font, "em", {})
self.assertEqual(font.style, FontStyle.ITALIC)
# Test <i> tag
font = apply_element_font_styles(base_font, "i", {})
self.assertEqual(font.style, FontStyle.ITALIC)
def test_apply_element_font_styles_decoration_elements(self):
"""Test font decoration - adapted from test_underlined_text and test_strikethrough_text."""
base_font = Font()
# Test <u> tag - from "<u>underlined text</u>"
font = apply_element_font_styles(base_font, "u", {})
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
# Test <s> tag - from "<s>strikethrough text</s>"
font = apply_element_font_styles(base_font, "s", {})
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
# Test <del> tag
font = apply_element_font_styles(base_font, "del", {})
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
def test_apply_element_font_styles_headings(self):
"""Test heading font styles - adapted from test_headings."""
base_font = Font()
# Test heading sizes and weights - from test_headings which tests h1-h6
headings = [("h1", 24), ("h2", 20), ("h3", 18), ("h4", 16), ("h5", 14), ("h6", 12)]
for tag, expected_size in headings:
font = apply_element_font_styles(base_font, tag, {})
self.assertEqual(font.font_size, expected_size, f"Size mismatch for {tag}")
self.assertEqual(font.weight, FontWeight.BOLD, f"Weight should be bold for {tag}")
def test_apply_element_font_styles_color_parsing(self):
"""Test color parsing - adapted from test_color_variations."""
base_font = Font()
# Test named colors - from '<span style="color: green;">Named green</span>'
css_styles = {"color": "green"}
font = apply_element_font_styles(base_font, "span", css_styles)
self.assertEqual(font.colour, (0, 255, 0))
# Test hex colors - from '<span style="color: #ff0000;">Hex red</span>'
css_styles = {"color": "#ff0000"}
font = apply_element_font_styles(base_font, "span", css_styles)
self.assertEqual(font.colour, (255, 0, 0))
def test_apply_element_styling_with_classes_and_styles(self):
"""Test complete element styling - adapted from test_span_with_inline_styles."""
# From: '<span style="color: red; font-weight: bold;">this part is red and bold</span>'
soup = BeautifulSoup('<span class="highlight" style="color: red; font-weight: bold;">text</span>', 'html.parser')
element = soup.find('span')
base_context = create_base_context()
styled_context = apply_element_styling(base_context, element)
# Check CSS classes
self.assertIn("highlight", styled_context.css_classes)
# Check CSS styles
self.assertEqual(styled_context.css_styles.get("color"), "red")
self.assertEqual(styled_context.css_styles.get("font-weight"), "bold")
# Check font styling
self.assertEqual(styled_context.font.colour, (255, 0, 0))
self.assertEqual(styled_context.font.weight, FontWeight.BOLD)
class TestExtractTextContent(unittest.TestCase):
"""Test cases for text content extraction."""
def setUp(self):
"""Set up test fixtures."""
self.base_context = create_base_context()
def test_extract_simple_text(self):
"""Test extracting simple text - adapted from test_simple."""
# From: "<p>This is a paragraph.</p>"
soup = BeautifulSoup('<p>This is a paragraph.</p>', 'html.parser')
element = soup.find('p')
words = extract_text_content(element, self.base_context)
# Should match the expected word count from original test
self.assertEqual(len(words), 4) # "This", "is", "a", "paragraph."
self.assertIsInstance(words[0], Word)
self.assertEqual(words[0].text, "This")
def test_extract_styled_text_bold(self):
"""Test extracting bold styled text - adapted from test_bold_text."""
# From: "<p>This is <strong>bold text</strong> in a paragraph.</p>"
soup = BeautifulSoup('<span>This is <strong>bold text</strong> in a paragraph.</span>', 'html.parser')
element = soup.find('span')
words = extract_text_content(element, self.base_context)
# Find the bold words
bold_words = [w for w in words if w.style.weight == FontWeight.BOLD]
self.assertGreater(len(bold_words), 0, "Should have bold words")
# Check specific words are bold (from original test expectations)
bold_word_texts = [w.text for w in bold_words]
self.assertIn("bold", bold_word_texts)
self.assertIn("text", bold_word_texts)
def test_extract_nested_formatting(self):
"""Test nested formatting - adapted from test_nested_formatting."""
# From: "<p>This has <strong>bold with <em>italic inside</em></strong> formatting.</p>"
soup = BeautifulSoup('<span>This has <strong>bold with <em>italic inside</em></strong> formatting.</span>', 'html.parser')
element = soup.find('span')
words = extract_text_content(element, self.base_context)
# Find words that should be both bold and italic
bold_italic_words = [w for w in words
if w.style.weight == FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
self.assertGreater(len(bold_italic_words), 0, "Should have words that are both bold and italic")
class TestHandlerFunctions(unittest.TestCase):
"""Test cases for HTML element handler functions using known working patterns."""
def setUp(self):
"""Set up test fixtures."""
self.base_context = create_base_context()
def test_paragraph_handler_simple(self):
"""Test paragraph handler - adapted from test_simple."""
# From: "<p>This is a paragraph.</p>"
soup = BeautifulSoup('<p>This is a paragraph.</p>', 'html.parser')
element = soup.find('p')
result = paragraph_handler(element, self.base_context)
self.assertIsInstance(result, Paragraph)
# Should match original test expectations
self.assertEqual(len(result), 4) # 4 words
words = list(result.words())
expected_texts = ["This", "is", "a", "paragraph."]
for i, expected_text in enumerate(expected_texts):
self.assertEqual(words[i][1].text, expected_text)
def test_heading_handler_all_levels(self):
"""Test heading handler - adapted from test_headings."""
# From: "<h1>Heading 1</h1><h2>Heading 2</h2>..."
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
for i, expected_level in enumerate(expected_levels, 1):
tag = f"h{i}"
soup = BeautifulSoup(f'<{tag}>Heading {i}</{tag}>', 'html.parser')
element = soup.find(tag)
result = heading_handler(element, self.base_context)
self.assertIsInstance(result, Heading)
self.assertEqual(result.level, expected_level)
# Should match original test word expectations
words = list(result.words())
self.assertEqual(len(words), 2) # "Heading" and number
self.assertEqual(words[0][1].text, "Heading")
def test_blockquote_handler(self):
"""Test blockquote handler - adapted from test_blockquote."""
# From: "<blockquote><p>This is a quoted paragraph.</p></blockquote>"
soup = BeautifulSoup('<blockquote><p>This is a quoted paragraph.</p></blockquote>', 'html.parser')
element = soup.find('blockquote')
result = blockquote_handler(element, self.base_context)
self.assertIsInstance(result, Quote)
# Check that the quote contains a paragraph (from original test)
quote_blocks = list(result.blocks())
self.assertEqual(len(quote_blocks), 1)
self.assertIsInstance(quote_blocks[0], Paragraph)
def test_preformatted_handler(self):
"""Test preformatted handler - adapted from test_preformatted_code."""
# From: "<pre><code>function hello() {\n console.log('Hello');\n}</code></pre>"
soup = BeautifulSoup('<pre><code>function hello() {\n console.log(\'Hello\');\n}</code></pre>', 'html.parser')
element = soup.find('pre')
result = preformatted_handler(element, self.base_context)
self.assertIsInstance(result, CodeBlock)
# Should have lines (from original test expectation)
lines = list(result.lines())
self.assertGreater(len(lines), 0)
def test_unordered_list_handler(self):
"""Test unordered list handler - adapted from test_unordered_list."""
# From: "<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>"
soup = BeautifulSoup('<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>', 'html.parser')
element = soup.find('ul')
result = unordered_list_handler(element, self.base_context)
self.assertIsInstance(result, HList)
self.assertEqual(result.style, ListStyle.UNORDERED)
# Should match original test expectations
items = list(result.items())
self.assertEqual(len(items), 3)
def test_ordered_list_handler(self):
"""Test ordered list handler - adapted from test_ordered_list."""
# From: "<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>"
soup = BeautifulSoup('<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>', 'html.parser')
element = soup.find('ol')
result = ordered_list_handler(element, self.base_context)
self.assertIsInstance(result, HList)
self.assertEqual(result.style, ListStyle.ORDERED)
# Should match original test expectations
items = list(result.items())
self.assertEqual(len(items), 3) # "First item", "Second item", "Third item"
def test_list_item_handler(self):
"""Test list item handler."""
soup = BeautifulSoup('<li>List item content</li>', 'html.parser')
element = soup.find('li')
result = list_item_handler(element, self.base_context)
self.assertIsInstance(result, ListItem)
blocks = list(result.blocks())
self.assertGreater(len(blocks), 0)
def test_table_handler(self):
"""Test table handler - adapted from test_table_basic."""
# From test_table_basic structure
soup = BeautifulSoup('''
<table>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</table>
''', 'html.parser')
element = soup.find('table')
result = table_handler(element, self.base_context)
self.assertIsInstance(result, Table)
def test_table_row_handler(self):
"""Test table row handler."""
soup = BeautifulSoup('<tr><td>Cell 1</td><td>Cell 2</td></tr>', 'html.parser')
element = soup.find('tr')
result = table_row_handler(element, self.base_context)
self.assertIsInstance(result, TableRow)
def test_table_cell_handler(self):
"""Test table cell handler."""
soup = BeautifulSoup('<td>Cell content</td>', 'html.parser')
element = soup.find('td')
# Apply styling to get attributes
styled_context = apply_element_styling(self.base_context, element)
result = table_cell_handler(element, styled_context)
self.assertIsInstance(result, TableCell)
self.assertEqual(result.is_header, False)
def test_table_header_cell_handler(self):
"""Test table header cell handler."""
soup = BeautifulSoup('<th>Header content</th>', 'html.parser')
element = soup.find('th')
# Apply styling to get attributes
styled_context = apply_element_styling(self.base_context, element)
result = table_header_cell_handler(element, styled_context)
self.assertIsInstance(result, TableCell)
self.assertEqual(result.is_header, True)
def test_horizontal_rule_handler(self):
"""Test horizontal rule handler."""
soup = BeautifulSoup('<hr>', 'html.parser')
element = soup.find('hr')
result = horizontal_rule_handler(element, self.base_context)
self.assertIsInstance(result, HorizontalRule)
def test_image_handler(self):
"""Test image handler."""
soup = BeautifulSoup('<img src="test.jpg" alt="Test image" width="100" height="50">', 'html.parser')
element = soup.find('img')
# Need to apply styling first to get attributes
styled_context = apply_element_styling(self.base_context, element)
result = image_handler(element, styled_context)
self.assertIsInstance(result, Image)
self.assertEqual(result.source, "test.jpg")
self.assertEqual(result.alt_text, "Test image")
self.assertEqual(result.width, 100)
self.assertEqual(result.height, 50)
def test_div_handler_container(self):
"""Test div handler - adapted from test_div_container."""
# From: "<div><p>First paragraph.</p><p>Second paragraph.</p></div>"
soup = BeautifulSoup('<div><p>First paragraph.</p><p>Second paragraph.</p></div>', 'html.parser')
element = soup.find('div')
result = div_handler(element, self.base_context)
self.assertIsInstance(result, list)
# Should match original test expectations
self.assertEqual(len(result), 2)
self.assertIsInstance(result[0], Paragraph)
self.assertIsInstance(result[1], Paragraph)
class TestStyledContentHandling(unittest.TestCase):
"""Test styled content handling using patterns from existing tests."""
def setUp(self):
"""Set up test fixtures."""
self.base_context = create_base_context()
def test_paragraph_with_bold_content(self):
"""Test paragraph with bold content - adapted from test_bold_text."""
# From: "<p>This is <strong>bold text</strong> in a paragraph.</p>"
soup = BeautifulSoup('<p>This is <strong>bold text</strong> in a paragraph.</p>', 'html.parser')
element = soup.find('p')
result = paragraph_handler(element, self.base_context)
self.assertIsInstance(result, Paragraph)
words = list(result.words())
self.assertEqual(len(words), 7) # From original test expectation
# Check that 'bold' and 'text' words have bold font weight (from original test)
bold_word = words[2][1] # 'bold'
text_word = words[3][1] # 'text'
self.assertEqual(bold_word.text, "bold")
self.assertEqual(bold_word.style.weight, FontWeight.BOLD)
self.assertEqual(text_word.text, "text")
self.assertEqual(text_word.style.weight, FontWeight.BOLD)
# Check that other words are not bold (from original test)
normal_word = words[0][1] # 'This'
self.assertEqual(normal_word.text, "This")
self.assertNotEqual(normal_word.style.weight, FontWeight.BOLD)
def test_paragraph_with_mixed_formatting(self):
"""Test mixed formatting - adapted from test_mixed_formatting."""
# From: "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style=\"color: blue;\">blue</span>..."
soup = BeautifulSoup('<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style="color: blue;">blue</span> text.</p>', 'html.parser')
element = soup.find('p')
result = paragraph_handler(element, self.base_context)
self.assertIsInstance(result, Paragraph)
words = list(result.words())
# Check for bold word (from original test pattern)
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
self.assertGreater(len(bold_words), 0, "Should have bold words")
# Check for italic word (from original test pattern)
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
self.assertGreater(len(italic_words), 0, "Should have italic words")
# Check for blue colored word (from original test pattern)
blue_words = [w for _, w in words if w.style.colour == (0, 0, 255)]
self.assertGreater(len(blue_words), 0, "Should have blue colored words")
if __name__ == '__main__':
unittest.main()
+118
View File
@@ -0,0 +1,118 @@
"""
Test module for loading HTML files using the html_extraction module.
This test verifies that HTML files can be loaded from disk and processed
using the html_extraction.parse_html_string function.
"""
import os
import unittest
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.abstract.block import Block
from pyWebLayout.style import Font
class TestHTMLFileLoader(unittest.TestCase):
"""Test class for HTML file loading functionality."""
def test_load_html_file(self):
"""Test loading and parsing an HTML file from disk."""
# Path to the test HTML file
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
# Verify the test file exists
self.assertTrue(os.path.exists(html_file_path), f"Test HTML file not found: {html_file_path}")
# Read the HTML file
with open(html_file_path, 'r', encoding='utf-8') as file:
html_content = file.read()
# Verify we got some content
self.assertGreater(len(html_content), 0, "HTML file should not be empty")
# Parse the HTML content using the html_extraction module
try:
blocks = parse_html_string(html_content)
except Exception as e:
self.fail(f"Failed to parse HTML file: {e}")
# Verify we got some blocks
self.assertIsInstance(blocks, list, "parse_html_string should return a list")
self.assertGreater(len(blocks), 0, "Should extract at least one block from the HTML file")
# Verify all returned items are Block instances
for i, block in enumerate(blocks):
self.assertIsInstance(block, Block, f"Item {i} should be a Block instance, got {type(block)}")
print(f"Successfully loaded and parsed HTML file with {len(blocks)} blocks")
def test_load_html_file_with_custom_font(self):
"""Test loading HTML file with a custom base font."""
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
# Skip if file doesn't exist
if not os.path.exists(html_file_path):
self.skipTest(f"Test HTML file not found: {html_file_path}")
# Create a custom font
custom_font = Font(font_size=14, colour=(100, 100, 100))
# Read and parse with custom font
with open(html_file_path, 'r', encoding='utf-8') as file:
html_content = file.read()
blocks = parse_html_string(html_content, base_font=custom_font)
# Verify we got blocks
self.assertGreater(len(blocks), 0, "Should extract blocks with custom font")
print(f"Successfully parsed HTML file with custom font, got {len(blocks)} blocks")
def test_load_html_file_content_types(self):
"""Test that the loaded HTML file contains expected content types."""
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
# Skip if file doesn't exist
if not os.path.exists(html_file_path):
self.skipTest(f"Test HTML file not found: {html_file_path}")
with open(html_file_path, 'r', encoding='utf-8') as file:
html_content = file.read()
blocks = parse_html_string(html_content)
# Check that we have different types of blocks
block_type_names = [type(block).__name__ for block in blocks]
unique_types = set(block_type_names)
# A Wikipedia page should contain multiple types of content
self.assertGreater(len(unique_types), 1, "Should have multiple types of blocks in Wikipedia page")
print(f"Found block types: {sorted(unique_types)}")
def test_html_file_size_handling(self):
"""Test that large HTML files can be handled gracefully."""
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
# Skip if file doesn't exist
if not os.path.exists(html_file_path):
self.skipTest(f"Test HTML file not found: {html_file_path}")
# Get file size
file_size = os.path.getsize(html_file_path)
print(f"HTML file size: {file_size} bytes")
# Read and parse
with open(html_file_path, 'r', encoding='utf-8') as file:
html_content = file.read()
# This should not raise an exception even for large files
blocks = parse_html_string(html_content)
# Basic verification
self.assertIsInstance(blocks, list)
print(f"Successfully processed {file_size} byte file into {len(blocks)} blocks")
if __name__ == '__main__':
unittest.main()