This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 you’ll notice is that responses don’t 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()
|
||||
|
||||
+2
-1
@@ -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,
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user