big update with ok rendering
Python CI / test (push) Failing after 3m55s

This commit is contained in:
2025-08-27 22:22:54 +02:00
parent 36281be77a
commit 65ab46556f
54 changed files with 6157 additions and 438 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ import numpy as np
from unittest.mock import Mock
from pyWebLayout.concrete.text import Line, Text, LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Alignment
from pyWebLayout.style import Font
from pyWebLayout.abstract import Word
from PIL import Image, ImageFont, ImageDraw
+1 -1
View File
@@ -9,7 +9,7 @@ from PIL import Image
from unittest.mock import Mock, patch
from pyWebLayout.concrete.box import Box
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Alignment
class TestBox(unittest.TestCase):
+1 -1
View File
@@ -16,7 +16,7 @@ from pyWebLayout.abstract.functional import (
Link, Button, Form, FormField, LinkType, FormFieldType
)
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Alignment
class TestLinkText(unittest.TestCase):
+1 -1
View File
@@ -12,7 +12,7 @@ from unittest.mock import Mock, patch, MagicMock
from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.abstract.block import Image as AbstractImage
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Alignment
class TestRenderableImage(unittest.TestCase):
+3 -3
View File
@@ -12,7 +12,7 @@ from unittest.mock import Mock, patch, MagicMock
from pyWebLayout.concrete.text import Text, Line
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Alignment
class TestText(unittest.TestCase):
def setUp(self):
@@ -247,14 +247,14 @@ class TestLine(unittest.TestCase):
# Create a word to add
for i in range(100):
word = Word(text="AAAAAAAA", style=self.style)
word = Word(text="AAAAAAA", style=self.style)
# This test may need adjustment based on the actual implementation
success, overflow_part = line.add_word(word)
# If successful, the word should be added
if overflow_part:
self.assertEqual(overflow_part.text , "AA")
self.assertEqual(overflow_part.text , "A")
return
self.assertFalse(True)
+217 -156
View File
@@ -1,5 +1,5 @@
"""
Test the new Page implementation to verify it meets the requirements:
Unit tests for the new Page implementation to verify it meets the requirements:
1. Accepts a PageStyle that defines borders, line spacing and inter-block spacing
2. Makes an image canvas
3. Provides a method for accepting child objects
@@ -7,8 +7,7 @@ Test the new Page implementation to verify it meets the requirements:
5. Has a method that calls render on all children
6. Has a method to query a point and determine which child it belongs to
"""
import pytest
import unittest
import numpy as np
from PIL import Image, ImageDraw
from pyWebLayout.concrete.page import Page
@@ -28,162 +27,224 @@ class SimpleTestRenderable(Renderable, Queriable):
def render(self):
"""Render returns None - drawing is done via the page's draw object"""
return None
class TestPageImplementation(unittest.TestCase):
"""Test cases for the Page class implementation"""
def setUp(self):
"""Set up test fixtures"""
self.basic_style = PageStyle(
border_width=2,
border_color=(255, 0, 0),
line_spacing=8,
inter_block_spacing=20,
padding=(15, 15, 15, 15),
background_color=(240, 240, 240)
)
self.page_size = (800, 600)
def test_page_creation_with_style(self):
"""Test creating a page with a PageStyle"""
page = Page(size=self.page_size, style=self.basic_style)
self.assertEqual(page.size, self.page_size)
self.assertEqual(page.style, self.basic_style)
self.assertEqual(page.border_size, 2)
def test_page_creation_without_style(self):
"""Test creating a page without a PageStyle (should use defaults)"""
page = Page(size=self.page_size)
self.assertEqual(page.size, self.page_size)
self.assertIsNotNone(page.style)
def test_page_canvas_and_content_sizes(self):
"""Test that page correctly calculates canvas and content sizes"""
style = PageStyle(
border_width=5,
padding=(10, 20, 30, 40) # top, right, bottom, left
)
page = Page(size=self.page_size, style=style)
# Canvas size should be page size minus borders
expected_canvas_size = (790, 590) # 800-10, 600-10 (border on both sides)
self.assertEqual(page.canvas_size, expected_canvas_size)
# Content size should be canvas minus padding
expected_content_size = (730, 550) # 790-60, 590-40 (padding left+right, top+bottom)
self.assertEqual(page.content_size, expected_content_size)
def test_page_add_remove_children(self):
"""Test adding and removing children from the page"""
page = Page(size=self.page_size)
# Initially no children
self.assertEqual(len(page.children), 0)
# Add children
child1 = SimpleTestRenderable("Child 1")
child2 = SimpleTestRenderable("Child 2")
page.add_child(child1)
self.assertEqual(len(page.children), 1)
self.assertIn(child1, page.children)
page.add_child(child2)
self.assertEqual(len(page.children), 2)
self.assertIn(child2, page.children)
# Test method chaining
child3 = SimpleTestRenderable("Child 3")
result = page.add_child(child3)
self.assertIs(result, page) # Should return self for chaining
self.assertEqual(len(page.children), 3)
self.assertIn(child3, page.children)
# Remove childce youll notice is that responses dont stream character-by-character like other providers. Instead, Claude Code processes your full request before sending back the complete response.
removed = page.remove_child(child2)
self.assertTrue(removed)
self.assertEqual(len(page.children), 2)
self.assertNotIn(child2, page.children)
# Try to remove non-existent child
removed = page.remove_child(child2)
self.assertFalse(removed)
# Clear all children
page.clear_children()
self.assertEqual(len(page.children), 0)
def test_page_render(self):
"""Test that page renders and creates a canvas"""
style = PageStyle(
border_width=2,
border_color=(255, 0, 0),
background_color=(255, 255, 255)
)
page = Page(size=(200, 150), style=style)
# Add a child
child = SimpleTestRenderable("Test child")
page.add_child(child)
# Render the page
image = page.render()
# Check that we got an image
self.assertIsInstance(image, Image.Image)
self.assertEqual(image.size, (200, 150))
self.assertEqual(image.mode, 'RGBA')
# Check that draw object is available
self.assertIsNotNone(page.draw)
def test_page_query_point(self):
"""Test querying points to find children"""
page = Page(size=(400, 300))
# Add children with known positions and sizes
child1 = SimpleTestRenderable("Child 1", (100, 50))
child2 = SimpleTestRenderable("Child 2", (80, 40))
page.add_child(child1).add_child(child2)
def test_page_creation_with_style():
"""Test creating a page with a PageStyle"""
style = PageStyle(
border_width=2,
border_color=(255, 0, 0),
line_spacing=8,
inter_block_spacing=20,
padding=(15, 15, 15, 15),
background_color=(240, 240, 240)
)
# Query points
# Point within first child
found_child = page.query_point((90, 30))
self.assertEqual(found_child, child1)
# Point within second child
found_child = page.query_point((30, 30))
self.assertEqual(found_child, child2)
# Point outside any child
found_child = page.query_point((300, 250))
self.assertIsNone(found_child)
page = Page(size=(800, 600), style=style)
def test_page_in_object(self):
"""Test that page correctly implements in_object"""
page = Page(size=(400, 300))
# Points within page bounds
self.assertTrue(page.in_object((0, 0)))
self.assertTrue(page.in_object((200, 150)))
self.assertTrue(page.in_object((399, 299)))
# Points outside page bounds
self.assertFalse(page.in_object((-1, 0)))
self.assertFalse(page.in_object((0, -1)))
self.assertFalse(page.in_object((400, 299)))
self.assertFalse(page.in_object((399, 300)))
assert page.size == (800, 600)
assert page.style == style
assert page.border_size == 2
def test_page_canvas_and_content_sizes():
"""Test that page correctly calculates canvas and content sizes"""
style = PageStyle(
border_width=5,
padding=(10, 20, 30, 40) # top, right, bottom, left
)
page = Page(size=(800, 600), style=style)
# Canvas size should be page size minus borders
assert page.canvas_size == (790, 590) # 800-10, 600-10 (border on both sides)
# Content size should be canvas minus padding
assert page.content_size == (730, 550) # 790-60, 590-40 (padding left+right, top+bottom)
def test_page_add_remove_children():
"""Test adding and removing children from the page"""
page = Page(size=(800, 600))
# Initially no children
assert len(page.children) == 0
# Add children
child1 = SimpleTestRenderable("Child 1")
child2 = SimpleTestRenderable("Child 2")
page.add_child(child1)
assert len(page.children) == 1
page.add_child(child2)
assert len(page.children) == 2
# Test method chaining
child3 = SimpleTestRenderable("Child 3")
result = page.add_child(child3)
assert result is page # Should return self for chaining
assert len(page.children) == 3
# Remove child
removed = page.remove_child(child2)
assert removed is True
assert len(page.children) == 2
assert child2 not in page.children
# Try to remove non-existent child
removed = page.remove_child(child2)
assert removed is False
# Clear all children
page.clear_children()
assert len(page.children) == 0
def test_page_render():
"""Test that page renders and creates a canvas"""
style = PageStyle(
border_width=2,
border_color=(255, 0, 0),
background_color=(255, 255, 255)
)
page = Page(size=(200, 150), style=style)
# Add a child
child = SimpleTestRenderable("Test child")
page.add_child(child)
# Render the page
image = page.render()
# Check that we got an image
assert isinstance(image, Image.Image)
assert image.size == (200, 150)
assert image.mode == 'RGBA'
# Check that draw object is available
assert page.draw is not None
def test_page_query_point():
"""Test querying points to find children"""
page = Page(size=(400, 300))
# Add children with known positions and sizes
child1 = SimpleTestRenderable("Child 1", (100, 50))
child2 = SimpleTestRenderable("Child 2", (80, 40))
page.add_child(child1).add_child(child2)
# Query points
# Point within first child
found_child = page.query_point((90, 30))
assert found_child == child1
# Point within second child
found_child = page.query_point((30, 30))
assert found_child == child2
# Point outside any child
found_child = page.query_point((300, 250))
assert found_child is None
def test_page_in_object():
"""Test that page correctly implements in_object"""
page = Page(size=(400, 300))
# Points within page bounds
assert page.in_object((0, 0)) is True
assert page.in_object((200, 150)) is True
assert page.in_object((399, 299)) is True
# Points outside page bounds
assert page.in_object((-1, 0)) is False
assert page.in_object((0, -1)) is False
assert page.in_object((400, 299)) is False
assert page.in_object((399, 300)) is False
def test_page_with_borders():
"""Test page rendering with borders"""
style = PageStyle(
border_width=3,
border_color=(128, 128, 128),
background_color=(255, 255, 255)
)
page = Page(size=(100, 100), style=style)
image = page.render()
# Check that image was created
assert isinstance(image, Image.Image)
assert image.size == (100, 100)
# The border should be drawn but we can't easily test pixel values
# Just verify the image exists and has the right properties
def test_page_with_borders(self):
"""Test page rendering with borders"""
style = PageStyle(
border_width=3,
border_color=(128, 128, 128),
background_color=(255, 255, 255)
)
page = Page(size=(100, 100), style=style)
image = page.render()
# Check that image was created
self.assertIsInstance(image, Image.Image)
self.assertEqual(image.size, (100, 100))
# The border should be drawn but we can't easily test pixel values
# Just verify the image exists and has the right properties
def test_page_border_size_property(self):
"""Test that border_size property returns correct value"""
# Test with border
style_with_border = PageStyle(border_width=5)
page_with_border = Page(size=self.page_size, style=style_with_border)
self.assertEqual(page_with_border.border_size, 5)
# Test without border
style_no_border = PageStyle(border_width=0)
page_no_border = Page(size=self.page_size, style=style_no_border)
self.assertEqual(page_no_border.border_size, 0)
def test_page_style_properties(self):
"""Test that page correctly exposes style properties"""
page = Page(size=self.page_size, style=self.basic_style)
# Test that style properties are accessible
self.assertEqual(page.style.border_width, 2)
self.assertEqual(page.style.border_color, (255, 0, 0))
self.assertEqual(page.style.line_spacing, 8)
self.assertEqual(page.style.inter_block_spacing, 20)
self.assertEqual(page.style.padding, (15, 15, 15, 15))
self.assertEqual(page.style.background_color, (240, 240, 240))
def test_page_children_list_operations(self):
"""Test that children list behaves correctly"""
page = Page(size=self.page_size)
# Test that children is initially empty list
self.assertIsInstance(page.children, list)
self.assertEqual(len(page.children), 0)
# Test adding multiple children
children = [
SimpleTestRenderable(f"Child {i}")
for i in range(5)
]
for child in children:
page.add_child(child)
self.assertEqual(len(page.children), 5)
# Test that children are in the correct order
for i, child in enumerate(page.children):
self.assertEqual(child._text, f"Child {i}")
if __name__ == '__main__':
unittest.main()
View File
@@ -7,7 +7,8 @@ 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 (
from pyWebLayout.io.rea
ders.html_extraction import (
create_base_context,
apply_element_styling,
parse_inline_styles,
+9
View File
@@ -0,0 +1,9 @@
"""
Tests for the layout module.
This package contains tests for the layout system including:
- Document layouter tests
- Ereader layout system tests
- Page buffer tests
- Position tracking tests
"""
+456
View File
@@ -0,0 +1,456 @@
"""
Comprehensive tests for the ereader layout system.
Tests the complete ereader functionality including position tracking,
font scaling, chapter navigation, and page buffering.
"""
import unittest
import tempfile
import shutil
from pathlib import Path
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.ereader_layout import RenderingPosition, ChapterNavigator, FontScaler, BidirectionalLayouter
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager, BookmarkManager, create_ereader_manager
class TestRenderingPosition(unittest.TestCase):
"""Test the RenderingPosition class"""
def test_position_creation(self):
"""Test creating a rendering position"""
pos = RenderingPosition(
chapter_index=1,
block_index=5,
word_index=10,
table_row=2,
table_col=3
)
self.assertEqual(pos.chapter_index, 1)
self.assertEqual(pos.block_index, 5)
self.assertEqual(pos.word_index, 10)
self.assertEqual(pos.table_row, 2)
self.assertEqual(pos.table_col, 3)
def test_position_serialization(self):
"""Test position serialization and deserialization"""
pos = RenderingPosition(
chapter_index=1,
block_index=5,
word_index=10,
remaining_pretext="test"
)
# Serialize to dict
pos_dict = pos.to_dict()
self.assertIsInstance(pos_dict, dict)
self.assertEqual(pos_dict['chapter_index'], 1)
self.assertEqual(pos_dict['remaining_pretext'], "test")
# Deserialize from dict
pos2 = RenderingPosition.from_dict(pos_dict)
self.assertEqual(pos, pos2)
def test_position_copy(self):
"""Test position copying"""
pos = RenderingPosition(chapter_index=1, block_index=5)
pos_copy = pos.copy()
self.assertEqual(pos, pos_copy)
self.assertIsNot(pos, pos_copy) # Different objects
# Modify copy
pos_copy.word_index = 10
self.assertNotEqual(pos, pos_copy)
def test_position_equality_and_hashing(self):
"""Test position equality and hashing"""
pos1 = RenderingPosition(chapter_index=1, block_index=5)
pos2 = RenderingPosition(chapter_index=1, block_index=5)
pos3 = RenderingPosition(chapter_index=1, block_index=6)
self.assertEqual(pos1, pos2)
self.assertNotEqual(pos1, pos3)
# Test hashing (for use as dict keys)
pos_dict = {pos1: "test"}
self.assertEqual(pos_dict[pos2], "test") # Should work due to equality
class TestChapterNavigator(unittest.TestCase):
"""Test the ChapterNavigator class"""
def setUp(self):
"""Set up test data"""
self.font = Font()
# Create test blocks with headings
self.blocks = [
Paragraph(self.font), # Block 0
Heading(HeadingLevel.H1, self.font), # Block 1 - Chapter 1
Paragraph(self.font), # Block 2
Heading(HeadingLevel.H2, self.font), # Block 3 - Subsection
Paragraph(self.font), # Block 4
Heading(HeadingLevel.H1, self.font), # Block 5 - Chapter 2
Paragraph(self.font), # Block 6
]
# Add text to headings
self.blocks[1].add_word(Word("Chapter", self.font))
self.blocks[1].add_word(Word("One", self.font))
self.blocks[3].add_word(Word("Subsection", self.font))
self.blocks[3].add_word(Word("A", self.font))
self.blocks[5].add_word(Word("Chapter", self.font))
self.blocks[5].add_word(Word("Two", self.font))
def test_chapter_detection(self):
"""Test that chapters are detected correctly"""
navigator = ChapterNavigator(self.blocks)
self.assertEqual(len(navigator.chapters), 3) # 2 H1s + 1 H2
# Check chapter titles
titles = [chapter.title for chapter in navigator.chapters]
self.assertIn("Chapter One", titles)
self.assertIn("Subsection A", titles)
self.assertIn("Chapter Two", titles)
def test_table_of_contents(self):
"""Test table of contents generation"""
navigator = ChapterNavigator(self.blocks)
toc = navigator.get_table_of_contents()
self.assertEqual(len(toc), 3)
# Check first entry
title, level, position = toc[0]
self.assertEqual(title, "Chapter One")
self.assertEqual(level, HeadingLevel.H1)
self.assertIsInstance(position, RenderingPosition)
def test_chapter_position_lookup(self):
"""Test looking up chapter positions"""
navigator = ChapterNavigator(self.blocks)
pos = navigator.get_chapter_position("Chapter One")
self.assertIsNotNone(pos)
self.assertEqual(pos.chapter_index, 0)
pos = navigator.get_chapter_position("Nonexistent Chapter")
self.assertIsNone(pos)
def test_current_chapter_detection(self):
"""Test detecting current chapter from position"""
navigator = ChapterNavigator(self.blocks)
# Position in first chapter
pos = RenderingPosition(chapter_index=0, block_index=2)
chapter = navigator.get_current_chapter(pos)
self.assertIsNotNone(chapter)
self.assertEqual(chapter.title, "Chapter One")
class TestFontScaler(unittest.TestCase):
"""Test the FontScaler class"""
def test_font_scaling(self):
"""Test font scaling functionality"""
original_font = Font(font_size=12)
# Test no scaling
scaled_font = FontScaler.scale_font(original_font, 1.0)
self.assertEqual(scaled_font.font_size, 12)
# Test 2x scaling
scaled_font = FontScaler.scale_font(original_font, 2.0)
self.assertEqual(scaled_font.font_size, 24)
# Test 0.5x scaling
scaled_font = FontScaler.scale_font(original_font, 0.5)
self.assertEqual(scaled_font.font_size, 6)
# Test minimum size constraint
scaled_font = FontScaler.scale_font(original_font, 0.01)
self.assertGreaterEqual(scaled_font.font_size, 1)
def test_word_spacing_scaling(self):
"""Test word spacing scaling"""
original_spacing = (5, 15)
# Test no scaling
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 1.0)
self.assertEqual(scaled_spacing, (5, 15))
# Test 2x scaling
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 2.0)
self.assertEqual(scaled_spacing, (10, 30))
# Test minimum constraints
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 0.1)
self.assertGreaterEqual(scaled_spacing[0], 1)
self.assertGreaterEqual(scaled_spacing[1], 2)
class TestBookmarkManager(unittest.TestCase):
"""Test the BookmarkManager class"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.document_id = "test_document"
self.bookmark_manager = BookmarkManager(self.document_id, self.temp_dir)
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir)
def test_bookmark_operations(self):
"""Test bookmark add/remove/get operations"""
pos = RenderingPosition(chapter_index=1, block_index=5)
# Add bookmark
self.bookmark_manager.add_bookmark("test_bookmark", pos)
# Get bookmark
retrieved_pos = self.bookmark_manager.get_bookmark("test_bookmark")
self.assertEqual(retrieved_pos, pos)
# List bookmarks
bookmarks = self.bookmark_manager.list_bookmarks()
self.assertEqual(len(bookmarks), 1)
self.assertEqual(bookmarks[0][0], "test_bookmark")
self.assertEqual(bookmarks[0][1], pos)
# Remove bookmark
success = self.bookmark_manager.remove_bookmark("test_bookmark")
self.assertTrue(success)
# Verify removal
retrieved_pos = self.bookmark_manager.get_bookmark("test_bookmark")
self.assertIsNone(retrieved_pos)
def test_reading_position_persistence(self):
"""Test saving and loading reading position"""
pos = RenderingPosition(chapter_index=2, block_index=10, word_index=5)
# Save position
self.bookmark_manager.save_reading_position(pos)
# Create new manager instance (simulates app restart)
new_manager = BookmarkManager(self.document_id, self.temp_dir)
# Load position
loaded_pos = new_manager.load_reading_position()
self.assertEqual(loaded_pos, pos)
def test_bookmark_persistence(self):
"""Test that bookmarks persist across manager instances"""
pos = RenderingPosition(chapter_index=1, block_index=5)
# Add bookmark
self.bookmark_manager.add_bookmark("persistent_bookmark", pos)
# Create new manager instance
new_manager = BookmarkManager(self.document_id, self.temp_dir)
# Verify bookmark exists
retrieved_pos = new_manager.get_bookmark("persistent_bookmark")
self.assertEqual(retrieved_pos, pos)
class TestEreaderLayoutManager(unittest.TestCase):
"""Test the complete EreaderLayoutManager"""
def setUp(self):
"""Set up test data"""
self.temp_dir = tempfile.mkdtemp()
self.font = Font()
# Create test document with multiple paragraphs and headings
self.blocks = []
# Add a heading
heading = Heading(HeadingLevel.H1, self.font)
heading.add_word(Word("Test", self.font))
heading.add_word(Word("Chapter", self.font))
self.blocks.append(heading)
# Add several paragraphs with multiple words
for i in range(3):
paragraph = Paragraph(self.font)
for j in range(20): # 20 words per paragraph
paragraph.add_word(Word(f"Word{i}_{j}", self.font))
self.blocks.append(paragraph)
self.page_size = (400, 600)
self.document_id = "test_document"
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir)
def test_manager_initialization(self):
"""Test ereader manager initialization"""
# Change to temp directory for bookmarks
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id
)
self.assertEqual(manager.page_size, self.page_size)
self.assertEqual(manager.document_id, self.document_id)
self.assertEqual(manager.font_scale, 1.0)
self.assertIsInstance(manager.current_position, RenderingPosition)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_font_scaling(self):
"""Test font scaling functionality"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id
)
# Test initial scale
self.assertEqual(manager.get_font_scale(), 1.0)
# Test scaling
page = manager.set_font_scale(1.5)
self.assertEqual(manager.get_font_scale(), 1.5)
self.assertIsNotNone(page)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_table_of_contents(self):
"""Test table of contents functionality"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id
)
toc = manager.get_table_of_contents()
self.assertGreater(len(toc), 0)
# Check first entry
title, level, position = toc[0]
self.assertEqual(title, "Test Chapter")
self.assertEqual(level, HeadingLevel.H1)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_bookmark_functionality(self):
"""Test bookmark functionality"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id
)
# Add bookmark
success = manager.add_bookmark("test_bookmark")
self.assertTrue(success)
# List bookmarks
bookmarks = manager.list_bookmarks()
self.assertEqual(len(bookmarks), 1)
self.assertEqual(bookmarks[0][0], "test_bookmark")
# Jump to bookmark (should work even though it's the same position)
page = manager.jump_to_bookmark("test_bookmark")
self.assertIsNotNone(page)
# Remove bookmark
success = manager.remove_bookmark("test_bookmark")
self.assertTrue(success)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_progress_tracking(self):
"""Test reading progress tracking"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id
)
# Initial progress should be 0
progress = manager.get_reading_progress()
self.assertGreaterEqual(progress, 0.0)
self.assertLessEqual(progress, 1.0)
# Get position info
info = manager.get_position_info()
self.assertIn('position', info)
self.assertIn('progress', info)
self.assertIn('font_scale', info)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_convenience_function(self):
"""Test the convenience function"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = create_ereader_manager(
self.blocks,
self.page_size,
self.document_id
)
self.assertIsInstance(manager, EreaderLayoutManager)
self.assertEqual(manager.page_size, self.page_size)
manager.shutdown()
finally:
os.chdir(original_cwd)
if __name__ == '__main__':
unittest.main()
+578
View File
@@ -0,0 +1,578 @@
"""
Unit tests for the recursive position system.
Tests the hierarchical position tracking that can reference any nested content structure.
"""
import unittest
import tempfile
import shutil
import json
from pathlib import Path
from pyWebLayout.layout.recursive_position import (
ContentType, LocationNode, RecursivePosition, PositionBuilder, PositionStorage,
create_word_position, create_image_position, create_table_cell_position, create_list_item_position
)
class TestLocationNode(unittest.TestCase):
"""Test cases for LocationNode"""
def test_node_creation(self):
"""Test basic node creation"""
node = LocationNode(ContentType.WORD, 5, 3, {"text": "hello"})
self.assertEqual(node.content_type, ContentType.WORD)
self.assertEqual(node.index, 5)
self.assertEqual(node.offset, 3)
self.assertEqual(node.metadata["text"], "hello")
def test_node_serialization(self):
"""Test node serialization to/from dict"""
node = LocationNode(ContentType.TABLE_CELL, 2, 0, {"colspan": 2})
# Serialize
data = node.to_dict()
expected = {
'content_type': 'table_cell',
'index': 2,
'offset': 0,
'metadata': {'colspan': 2}
}
self.assertEqual(data, expected)
# Deserialize
restored = LocationNode.from_dict(data)
self.assertEqual(restored.content_type, ContentType.TABLE_CELL)
self.assertEqual(restored.index, 2)
self.assertEqual(restored.offset, 0)
self.assertEqual(restored.metadata, {'colspan': 2})
def test_node_string_representation(self):
"""Test string representation of nodes"""
node1 = LocationNode(ContentType.PARAGRAPH, 3)
self.assertEqual(str(node1), "paragraph[3]")
node2 = LocationNode(ContentType.WORD, 5, 2)
self.assertEqual(str(node2), "word[5]+2")
class TestRecursivePosition(unittest.TestCase):
"""Test cases for RecursivePosition"""
def test_position_creation(self):
"""Test basic position creation"""
pos = RecursivePosition()
# Should have document root by default
self.assertEqual(len(pos.path), 1)
self.assertEqual(pos.path[0].content_type, ContentType.DOCUMENT)
def test_position_building(self):
"""Test building complex positions"""
pos = RecursivePosition()
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
pos.add_node(LocationNode(ContentType.BLOCK, 5))
pos.add_node(LocationNode(ContentType.PARAGRAPH, 0))
pos.add_node(LocationNode(ContentType.WORD, 12, 3))
self.assertEqual(len(pos.path), 5) # Including document root
self.assertEqual(pos.path[1].content_type, ContentType.CHAPTER)
self.assertEqual(pos.path[1].index, 2)
self.assertEqual(pos.path[-1].content_type, ContentType.WORD)
self.assertEqual(pos.path[-1].index, 12)
self.assertEqual(pos.path[-1].offset, 3)
def test_position_copy(self):
"""Test position copying"""
original = RecursivePosition()
original.add_node(LocationNode(ContentType.CHAPTER, 1))
original.add_node(LocationNode(ContentType.WORD, 5, 2, {"text": "test"}))
original.rendering_metadata = {"font_scale": 1.5}
copy = original.copy()
# Should be equal but not the same object
self.assertEqual(original, copy)
self.assertIsNot(original, copy)
self.assertIsNot(original.path, copy.path)
self.assertIsNot(original.rendering_metadata, copy.rendering_metadata)
# Modifying copy shouldn't affect original
copy.add_node(LocationNode(ContentType.IMAGE, 0))
self.assertNotEqual(len(original.path), len(copy.path))
def test_node_queries(self):
"""Test querying nodes by type"""
pos = RecursivePosition()
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
pos.add_node(LocationNode(ContentType.BLOCK, 5))
pos.add_node(LocationNode(ContentType.TABLE, 0))
pos.add_node(LocationNode(ContentType.TABLE_ROW, 1))
pos.add_node(LocationNode(ContentType.TABLE_CELL, 2))
# Get single node
chapter_node = pos.get_node(ContentType.CHAPTER)
self.assertIsNotNone(chapter_node)
self.assertEqual(chapter_node.index, 2)
# Get non-existent node
word_node = pos.get_node(ContentType.WORD)
self.assertIsNone(word_node)
# Get multiple nodes (if there were multiple)
table_nodes = pos.get_nodes(ContentType.TABLE_ROW)
self.assertEqual(len(table_nodes), 1)
self.assertEqual(table_nodes[0].index, 1)
def test_position_hierarchy_operations(self):
"""Test ancestor/descendant relationships"""
# Create ancestor position: document -> chapter[1] -> block[2]
ancestor = RecursivePosition()
ancestor.add_node(LocationNode(ContentType.CHAPTER, 1))
ancestor.add_node(LocationNode(ContentType.BLOCK, 2))
# Create descendant position: document -> chapter[1] -> block[2] -> paragraph -> word[5]
descendant = ancestor.copy()
descendant.add_node(LocationNode(ContentType.PARAGRAPH, 0))
descendant.add_node(LocationNode(ContentType.WORD, 5))
# Create unrelated position: document -> chapter[2] -> block[1]
unrelated = RecursivePosition()
unrelated.add_node(LocationNode(ContentType.CHAPTER, 2))
unrelated.add_node(LocationNode(ContentType.BLOCK, 1))
# Test relationships
self.assertTrue(ancestor.is_ancestor_of(descendant))
self.assertTrue(descendant.is_descendant_of(ancestor))
self.assertFalse(ancestor.is_ancestor_of(unrelated))
self.assertFalse(unrelated.is_descendant_of(ancestor))
# Test common ancestor
common = ancestor.get_common_ancestor(descendant)
self.assertEqual(len(common.path), 3) # document + chapter + block
common_unrelated = ancestor.get_common_ancestor(unrelated)
self.assertEqual(len(common_unrelated.path), 1) # Only document root
def test_position_truncation(self):
"""Test truncating position to specific content type"""
pos = RecursivePosition()
pos.add_node(LocationNode(ContentType.CHAPTER, 1))
pos.add_node(LocationNode(ContentType.BLOCK, 2))
pos.add_node(LocationNode(ContentType.PARAGRAPH, 0))
pos.add_node(LocationNode(ContentType.WORD, 5))
# Truncate to block level
truncated = pos.copy().truncate_to_type(ContentType.BLOCK)
self.assertEqual(len(truncated.path), 3) # document + chapter + block
self.assertEqual(truncated.path[-1].content_type, ContentType.BLOCK)
def test_position_serialization(self):
"""Test position serialization to/from dict and JSON"""
pos = RecursivePosition()
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
pos.add_node(LocationNode(ContentType.WORD, 5, 3, {"text": "hello"}))
pos.rendering_metadata = {"font_scale": 1.5, "page_size": [800, 600]}
# Test dict serialization
data = pos.to_dict()
restored = RecursivePosition.from_dict(data)
self.assertEqual(pos, restored)
# Test JSON serialization
json_str = pos.to_json()
restored_json = RecursivePosition.from_json(json_str)
self.assertEqual(pos, restored_json)
def test_position_equality_and_hashing(self):
"""Test position equality and hashing"""
pos1 = RecursivePosition()
pos1.add_node(LocationNode(ContentType.CHAPTER, 1))
pos1.add_node(LocationNode(ContentType.WORD, 5))
pos2 = RecursivePosition()
pos2.add_node(LocationNode(ContentType.CHAPTER, 1))
pos2.add_node(LocationNode(ContentType.WORD, 5))
pos3 = RecursivePosition()
pos3.add_node(LocationNode(ContentType.CHAPTER, 1))
pos3.add_node(LocationNode(ContentType.WORD, 6)) # Different word
# Test equality
self.assertEqual(pos1, pos2)
self.assertNotEqual(pos1, pos3)
# Test hashing (should be able to use as dict keys)
position_dict = {pos1: "value1", pos3: "value2"}
self.assertEqual(position_dict[pos2], "value1") # pos2 should hash same as pos1
def test_string_representation(self):
"""Test human-readable string representation"""
pos = RecursivePosition()
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
pos.add_node(LocationNode(ContentType.BLOCK, 5))
pos.add_node(LocationNode(ContentType.WORD, 12, 3))
expected = "document[0] -> chapter[2] -> block[5] -> word[12]+3"
self.assertEqual(str(pos), expected)
class TestPositionBuilder(unittest.TestCase):
"""Test cases for PositionBuilder"""
def test_fluent_building(self):
"""Test fluent interface for building positions"""
pos = (PositionBuilder()
.chapter(2)
.block(5)
.paragraph()
.word(12, offset=3)
.with_rendering_metadata(font_scale=1.5, page_size=[800, 600])
.build())
# Check path structure
self.assertEqual(len(pos.path), 5) # document + chapter + block + paragraph + word
self.assertEqual(pos.path[1].content_type, ContentType.CHAPTER)
self.assertEqual(pos.path[1].index, 2)
self.assertEqual(pos.path[-1].content_type, ContentType.WORD)
self.assertEqual(pos.path[-1].index, 12)
self.assertEqual(pos.path[-1].offset, 3)
# Check rendering metadata
self.assertEqual(pos.rendering_metadata["font_scale"], 1.5)
self.assertEqual(pos.rendering_metadata["page_size"], [800, 600])
def test_table_building(self):
"""Test building table cell positions"""
pos = (PositionBuilder()
.chapter(1)
.block(3)
.table()
.table_row(2)
.table_cell(1)
.word(0)
.build())
# Verify table structure
table_node = pos.get_node(ContentType.TABLE)
row_node = pos.get_node(ContentType.TABLE_ROW)
cell_node = pos.get_node(ContentType.TABLE_CELL)
self.assertIsNotNone(table_node)
self.assertIsNotNone(row_node)
self.assertIsNotNone(cell_node)
self.assertEqual(row_node.index, 2)
self.assertEqual(cell_node.index, 1)
def test_list_building(self):
"""Test building list item positions"""
pos = (PositionBuilder()
.chapter(0)
.block(2)
.list()
.list_item(3)
.word(1)
.build())
# Verify list structure
list_node = pos.get_node(ContentType.LIST)
item_node = pos.get_node(ContentType.LIST_ITEM)
self.assertIsNotNone(list_node)
self.assertIsNotNone(item_node)
self.assertEqual(item_node.index, 3)
def test_image_building(self):
"""Test building image positions"""
pos = (PositionBuilder()
.chapter(1)
.block(4)
.image(0, alt_text="Test image", width=300, height=200)
.build())
image_node = pos.get_node(ContentType.IMAGE)
self.assertIsNotNone(image_node)
self.assertEqual(image_node.metadata["alt_text"], "Test image")
self.assertEqual(image_node.metadata["width"], 300)
class TestPositionStorage(unittest.TestCase):
"""Test cases for PositionStorage"""
def setUp(self):
"""Set up temporary directory for testing"""
self.temp_dir = tempfile.mkdtemp()
self.storage_json = PositionStorage(self.temp_dir, use_shelf=False)
self.storage_shelf = PositionStorage(self.temp_dir, use_shelf=True)
def tearDown(self):
"""Clean up temporary directory"""
shutil.rmtree(self.temp_dir)
def test_json_storage(self):
"""Test JSON-based position storage"""
# Create test position
pos = (PositionBuilder()
.chapter(2)
.block(5)
.word(12, offset=3)
.with_rendering_metadata(font_scale=1.5)
.build())
# Save position
self.storage_json.save_position("test_doc", "bookmark1", pos)
# Load position
loaded = self.storage_json.load_position("test_doc", "bookmark1")
self.assertIsNotNone(loaded)
self.assertEqual(pos, loaded)
# List positions
positions = self.storage_json.list_positions("test_doc")
self.assertIn("bookmark1", positions)
# Delete position
success = self.storage_json.delete_position("test_doc", "bookmark1")
self.assertTrue(success)
# Verify deletion
loaded_after_delete = self.storage_json.load_position("test_doc", "bookmark1")
self.assertIsNone(loaded_after_delete)
def test_shelf_storage(self):
"""Test shelf-based position storage"""
# Create test position
pos = (PositionBuilder()
.chapter(1)
.block(3)
.table()
.table_row(2)
.table_cell(1)
.build())
# Save position
self.storage_shelf.save_position("test_doc", "table_pos", pos)
# Load position
loaded = self.storage_shelf.load_position("test_doc", "table_pos")
self.assertIsNotNone(loaded)
self.assertEqual(pos, loaded)
# List positions
positions = self.storage_shelf.list_positions("test_doc")
self.assertIn("table_pos", positions)
# Delete position
success = self.storage_shelf.delete_position("test_doc", "table_pos")
self.assertTrue(success)
def test_multiple_positions(self):
"""Test storing multiple positions for same document"""
pos1 = create_word_position(0, 1, 5)
pos2 = create_image_position(1, 2)
pos3 = create_table_cell_position(2, 3, 1, 2, 0)
# Save multiple positions
self.storage_json.save_position("multi_doc", "pos1", pos1)
self.storage_json.save_position("multi_doc", "pos2", pos2)
self.storage_json.save_position("multi_doc", "pos3", pos3)
# List all positions
positions = self.storage_json.list_positions("multi_doc")
self.assertEqual(len(positions), 3)
self.assertIn("pos1", positions)
self.assertIn("pos2", positions)
self.assertIn("pos3", positions)
# Load and verify each position
loaded1 = self.storage_json.load_position("multi_doc", "pos1")
loaded2 = self.storage_json.load_position("multi_doc", "pos2")
loaded3 = self.storage_json.load_position("multi_doc", "pos3")
self.assertEqual(pos1, loaded1)
self.assertEqual(pos2, loaded2)
self.assertEqual(pos3, loaded3)
class TestConvenienceFunctions(unittest.TestCase):
"""Test cases for convenience functions"""
def test_create_word_position(self):
"""Test word position creation"""
pos = create_word_position(2, 5, 12, 3)
chapter_node = pos.get_node(ContentType.CHAPTER)
block_node = pos.get_node(ContentType.BLOCK)
word_node = pos.get_node(ContentType.WORD)
self.assertEqual(chapter_node.index, 2)
self.assertEqual(block_node.index, 5)
self.assertEqual(word_node.index, 12)
self.assertEqual(word_node.offset, 3)
def test_create_image_position(self):
"""Test image position creation"""
pos = create_image_position(1, 3, 0)
chapter_node = pos.get_node(ContentType.CHAPTER)
block_node = pos.get_node(ContentType.BLOCK)
image_node = pos.get_node(ContentType.IMAGE)
self.assertEqual(chapter_node.index, 1)
self.assertEqual(block_node.index, 3)
self.assertEqual(image_node.index, 0)
def test_create_table_cell_position(self):
"""Test table cell position creation"""
pos = create_table_cell_position(0, 2, 1, 3, 5)
chapter_node = pos.get_node(ContentType.CHAPTER)
block_node = pos.get_node(ContentType.BLOCK)
table_node = pos.get_node(ContentType.TABLE)
row_node = pos.get_node(ContentType.TABLE_ROW)
cell_node = pos.get_node(ContentType.TABLE_CELL)
word_node = pos.get_node(ContentType.WORD)
self.assertEqual(chapter_node.index, 0)
self.assertEqual(block_node.index, 2)
self.assertEqual(row_node.index, 1)
self.assertEqual(cell_node.index, 3)
self.assertEqual(word_node.index, 5)
def test_create_list_item_position(self):
"""Test list item position creation"""
pos = create_list_item_position(1, 4, 2, 7)
chapter_node = pos.get_node(ContentType.CHAPTER)
block_node = pos.get_node(ContentType.BLOCK)
list_node = pos.get_node(ContentType.LIST)
item_node = pos.get_node(ContentType.LIST_ITEM)
word_node = pos.get_node(ContentType.WORD)
self.assertEqual(chapter_node.index, 1)
self.assertEqual(block_node.index, 4)
self.assertEqual(item_node.index, 2)
self.assertEqual(word_node.index, 7)
class TestRealWorldScenarios(unittest.TestCase):
"""Test cases for real-world usage scenarios"""
def test_ereader_bookmark_scenario(self):
"""Test typical ereader bookmark usage"""
# User is reading chapter 3, paragraph 2, word 15, character 5
reading_pos = (PositionBuilder()
.chapter(3)
.block(8) # Block 8 in chapter 3
.paragraph()
.word(15, offset=5)
.with_rendering_metadata(
font_scale=1.2,
page_size=[600, 800],
theme="dark"
)
.build())
# Save as bookmark
storage = PositionStorage(use_shelf=False)
storage.save_position("my_novel", "chapter3_climax", reading_pos)
# Later, load bookmark
loaded_pos = storage.load_position("my_novel", "chapter3_climax")
self.assertEqual(reading_pos, loaded_pos)
# Verify we can extract the reading context
chapter_node = loaded_pos.get_node(ContentType.CHAPTER)
word_node = loaded_pos.get_node(ContentType.WORD)
self.assertEqual(chapter_node.index, 3)
self.assertEqual(word_node.index, 15)
self.assertEqual(word_node.offset, 5)
self.assertEqual(loaded_pos.rendering_metadata["font_scale"], 1.2)
def test_table_navigation_scenario(self):
"""Test navigating within a complex table"""
# User is in a table: chapter 2, table block 5, row 3, cell 2, word 1
table_pos = (PositionBuilder()
.chapter(2)
.block(5)
.table(0, table_type="data", columns=4, rows=10)
.table_row(3, row_type="data")
.table_cell(2, cell_type="data", colspan=1)
.word(1)
.build())
# Navigate to next cell (same row, next column)
next_cell_pos = table_pos.copy()
cell_node = next_cell_pos.get_node(ContentType.TABLE_CELL)
cell_node.index = 3 # Move to next column
word_node = next_cell_pos.get_node(ContentType.WORD)
word_node.index = 0 # Reset to first word in new cell
# Verify positions are different but related
self.assertNotEqual(table_pos, next_cell_pos)
# They should share common ancestor up to table row level
common = table_pos.get_common_ancestor(next_cell_pos)
row_node = common.get_node(ContentType.TABLE_ROW)
self.assertIsNotNone(row_node)
self.assertEqual(row_node.index, 3)
def test_multi_level_list_scenario(self):
"""Test navigating nested lists"""
# Position in nested list: chapter 1, list block 3, item 2, sub-list, sub-item 1, word 3
nested_pos = (PositionBuilder()
.chapter(1)
.block(3)
.list(0, list_type="ordered")
.list_item(2)
.list(1, list_type="unordered") # Nested list
.list_item(1)
.word(3)
.build())
# Verify we can distinguish between the two list levels
list_nodes = nested_pos.get_nodes(ContentType.LIST)
self.assertEqual(len(list_nodes), 2)
self.assertEqual(list_nodes[0].index, 0) # Outer list
self.assertEqual(list_nodes[1].index, 1) # Inner list
# Verify list item hierarchy
item_nodes = nested_pos.get_nodes(ContentType.LIST_ITEM)
self.assertEqual(len(item_nodes), 2)
self.assertEqual(item_nodes[0].index, 2) # Outer item
self.assertEqual(item_nodes[1].index, 1) # Inner item
def test_position_comparison_and_sorting(self):
"""Test comparing positions for sorting/ordering"""
# Create positions at different locations
pos1 = create_word_position(1, 2, 5) # Chapter 1, block 2, word 5
pos2 = create_word_position(1, 2, 10) # Chapter 1, block 2, word 10
pos3 = create_word_position(1, 3, 1) # Chapter 1, block 3, word 1
pos4 = create_word_position(2, 1, 1) # Chapter 2, block 1, word 1
positions = [pos4, pos2, pos1, pos3] # Unsorted
# For proper sorting, we'd need to implement comparison operators
# For now, we can test that positions are distinguishable
unique_positions = set(positions)
self.assertEqual(len(unique_positions), 4)
# Test that we can find common ancestors
common_12 = pos1.get_common_ancestor(pos2)
common_13 = pos1.get_common_ancestor(pos3)
common_14 = pos1.get_common_ancestor(pos4)
# pos1 and pos2 share paragraph-level ancestor (same chapter, block, paragraph)
self.assertEqual(len(common_12.path), 4) # document + chapter + block + paragraph
# pos1 and pos3 share chapter-level ancestor (same chapter, different blocks)
self.assertEqual(len(common_13.path), 2) # document + chapter
# pos1 and pos4 share only document-level ancestor (different chapters)
self.assertEqual(len(common_14.path), 1) # document only
if __name__ == '__main__':
unittest.main()
+2 -1
View File
@@ -5,7 +5,8 @@ Tests the Font class and style enums for proper functionality and immutability.
"""
import unittest
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration, Alignment
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
from pyWebLayout.style import Alignment
class TestStyleObjects(unittest.TestCase):