migration of application to own repo
Python CI / test (push) Successful in 38s

This commit is contained in:
2025-11-07 18:47:31 +01:00
parent 2e926ab87a
commit 9862c3124a
20 changed files with 2134 additions and 133 deletions
+832
View File
@@ -0,0 +1,832 @@
"""
Comprehensive tests for the EbookReader application interface.
Tests cover:
- EPUB loading and initialization
- Navigation (forward, backward, boundaries)
- Font scaling and styling
- Chapter navigation
- Position management (bookmarks)
- Information retrieval
- File operations
- Error handling
- Context manager
- Integration scenarios
"""
import unittest
import tempfile
import shutil
from pathlib import Path
import numpy as np
from PIL import Image
import os
from dreader.application import EbookReader, create_ebook_reader
class TestEbookReaderInitialization(unittest.TestCase):
"""Test EbookReader creation and EPUB loading"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_create_reader_with_defaults(self):
"""Test creating reader with default settings"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
self.assertEqual(reader.page_size, (800, 1000))
self.assertEqual(reader.base_font_scale, 1.0)
self.assertIsNone(reader.manager)
self.assertFalse(reader.is_loaded())
reader.close()
def test_create_reader_with_custom_settings(self):
"""Test creating reader with custom settings"""
reader = EbookReader(
page_size=(600, 800),
margin=50,
background_color=(240, 240, 240),
line_spacing=10,
inter_block_spacing=20,
bookmarks_dir=self.temp_dir,
buffer_size=3
)
self.assertEqual(reader.page_size, (600, 800))
self.assertEqual(reader.page_style.line_spacing, 10)
self.assertEqual(reader.page_style.inter_block_spacing, 20)
self.assertEqual(reader.buffer_size, 3)
reader.close()
def test_load_valid_epub(self):
"""Test loading a valid EPUB file"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
success = reader.load_epub(self.epub_path)
self.assertTrue(success)
self.assertTrue(reader.is_loaded())
self.assertIsNotNone(reader.manager)
self.assertIsNotNone(reader.blocks)
self.assertIsNotNone(reader.document_id)
self.assertIsNotNone(reader.book_title)
self.assertIsNotNone(reader.book_author)
reader.close()
def test_load_nonexistent_epub(self):
"""Test loading a non-existent EPUB file"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
success = reader.load_epub("nonexistent.epub")
self.assertFalse(success)
self.assertFalse(reader.is_loaded())
reader.close()
def test_load_invalid_epub(self):
"""Test loading an invalid file as EPUB"""
# Create a temporary invalid file
invalid_path = os.path.join(self.temp_dir, "invalid.epub")
with open(invalid_path, 'w') as f:
f.write("This is not a valid EPUB file")
reader = EbookReader(bookmarks_dir=self.temp_dir)
success = reader.load_epub(invalid_path)
self.assertFalse(success)
self.assertFalse(reader.is_loaded())
reader.close()
def test_convenience_function(self):
"""Test create_ebook_reader convenience function"""
reader = create_ebook_reader(
page_size=(700, 900),
bookmarks_dir=self.temp_dir
)
self.assertIsInstance(reader, EbookReader)
self.assertEqual(reader.page_size, (700, 900))
reader.close()
class TestEbookReaderFontScaling(unittest.TestCase):
"""Test font size control"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0 # Disable buffering for tests
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_set_font_size(self):
"""Test setting font size with arbitrary scale"""
page = self.reader.set_font_size(1.5)
self.assertIsNotNone(page)
self.assertEqual(self.reader.get_font_size(), 1.5)
def test_increase_font_size(self):
"""Test increasing font size by one step"""
initial_size = self.reader.get_font_size()
page = self.reader.increase_font_size()
self.assertIsNotNone(page)
self.assertEqual(self.reader.get_font_size(), initial_size + 0.1)
def test_decrease_font_size(self):
"""Test decreasing font size by one step"""
self.reader.set_font_size(1.5)
page = self.reader.decrease_font_size()
self.assertIsNotNone(page)
self.assertAlmostEqual(self.reader.get_font_size(), 1.4, places=5)
def test_font_size_bounds_clamping(self):
"""Test that font size is clamped between 0.5x and 3.0x"""
# Test upper bound
self.reader.set_font_size(5.0)
self.assertEqual(self.reader.get_font_size(), 3.0)
# Test lower bound
self.reader.set_font_size(0.1)
self.assertEqual(self.reader.get_font_size(), 0.5)
def test_get_font_size(self):
"""Test getting current font size"""
self.assertEqual(self.reader.get_font_size(), 1.0)
self.reader.set_font_size(2.0)
self.assertEqual(self.reader.get_font_size(), 2.0)
def test_font_scale_with_navigation(self):
"""Test that font scale persists across page navigation"""
self.reader.set_font_size(1.5)
initial_font_size = self.reader.get_font_size()
# Navigate forward
self.reader.next_page()
# Font size should be preserved
self.assertEqual(self.reader.get_font_size(), initial_font_size)
class TestEbookReaderSpacing(unittest.TestCase):
"""Test line and block spacing"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_set_line_spacing(self):
"""Test setting line spacing"""
page = self.reader.set_line_spacing(10)
self.assertIsNotNone(page)
self.assertEqual(self.reader.page_style.line_spacing, 10)
def test_set_inter_block_spacing(self):
"""Test setting inter-block spacing"""
page = self.reader.set_inter_block_spacing(25)
self.assertIsNotNone(page)
self.assertEqual(self.reader.page_style.inter_block_spacing, 25)
def test_spacing_with_navigation(self):
"""Test that spacing changes affect rendering after navigation"""
self.reader.set_line_spacing(15)
page = self.reader.next_page()
self.assertIsNotNone(page)
self.assertEqual(self.reader.page_style.line_spacing, 15)
def test_spacing_position_preservation(self):
"""Test that changing spacing preserves reading position"""
# Navigate to a specific position
for _ in range(3):
self.reader.next_page()
position_before = self.reader.manager.current_position.copy()
# Change spacing
self.reader.set_line_spacing(12)
position_after = self.reader.manager.current_position
# Position should be preserved
self.assertEqual(position_before.chapter_index, position_after.chapter_index)
self.assertEqual(position_before.block_index, position_after.block_index)
class TestEbookReaderChapterNavigation(unittest.TestCase):
"""Test chapter navigation features"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_get_chapters(self):
"""Test getting list of chapters"""
chapters = self.reader.get_chapters()
self.assertIsInstance(chapters, list)
if len(chapters) > 0:
# Each chapter should be a tuple (title, index)
self.assertIsInstance(chapters[0], tuple)
self.assertEqual(len(chapters[0]), 2)
def test_get_chapter_positions(self):
"""Test getting chapter positions"""
positions = self.reader.get_chapter_positions()
self.assertIsInstance(positions, list)
if len(positions) > 0:
# Each item should be (title, RenderingPosition)
self.assertIsInstance(positions[0], tuple)
self.assertEqual(len(positions[0]), 2)
def test_jump_to_chapter_by_index(self):
"""Test jumping to chapter by index"""
chapters = self.reader.get_chapters()
if len(chapters) > 0:
page = self.reader.jump_to_chapter(0)
self.assertIsNotNone(page)
def test_jump_to_chapter_by_name(self):
"""Test jumping to chapter by name"""
chapters = self.reader.get_chapters()
if len(chapters) > 0:
chapter_title = chapters[0][0]
page = self.reader.jump_to_chapter(chapter_title)
self.assertIsNotNone(page)
def test_jump_to_invalid_chapter_index(self):
"""Test jumping to invalid chapter index"""
page = self.reader.jump_to_chapter(9999)
self.assertIsNone(page)
def test_jump_to_invalid_chapter_name(self):
"""Test jumping to non-existent chapter name"""
page = self.reader.jump_to_chapter("Non-Existent Chapter")
self.assertIsNone(page)
class TestEbookReaderInformation(unittest.TestCase):
"""Test information retrieval methods"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_get_position_info(self):
"""Test getting detailed position information"""
info = self.reader.get_position_info()
self.assertIsInstance(info, dict)
self.assertIn('position', info)
self.assertIn('chapter', info)
self.assertIn('progress', info)
self.assertIn('font_scale', info)
self.assertIn('book_title', info)
self.assertIn('book_author', info)
def test_get_reading_progress(self):
"""Test getting reading progress as percentage"""
progress = self.reader.get_reading_progress()
self.assertIsInstance(progress, float)
self.assertGreaterEqual(progress, 0.0)
self.assertLessEqual(progress, 1.0)
# Progress should increase after navigation
initial_progress = progress
for _ in range(5):
self.reader.next_page()
new_progress = self.reader.get_reading_progress()
self.assertGreater(new_progress, initial_progress)
def test_get_current_chapter_info(self):
"""Test getting current chapter information"""
info = self.reader.get_current_chapter_info()
# May be None if no chapters
if info is not None:
self.assertIsInstance(info, dict)
self.assertIn('title', info)
self.assertIn('level', info)
self.assertIn('block_index', info)
def test_get_book_info_complete(self):
"""Test getting complete book information"""
info = self.reader.get_book_info()
self.assertIsInstance(info, dict)
self.assertIn('title', info)
self.assertIn('author', info)
self.assertIn('document_id', info)
self.assertIn('total_blocks', info)
self.assertIn('total_chapters', info)
self.assertIn('page_size', info)
self.assertIn('font_scale', info)
self.assertGreater(info['total_blocks'], 0)
self.assertEqual(info['page_size'], self.reader.page_size)
class TestEbookReaderFileOperations(unittest.TestCase):
"""Test file I/O operations"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_render_to_file_png(self):
"""Test saving current page as PNG"""
output_path = os.path.join(self.temp_dir, "page.png")
success = self.reader.render_to_file(output_path)
self.assertTrue(success)
self.assertTrue(os.path.exists(output_path))
# Verify it's a valid image
img = Image.open(output_path)
self.assertEqual(img.size, self.reader.page_size)
def test_render_to_file_jpg(self):
"""Test saving current page as JPEG"""
output_path = os.path.join(self.temp_dir, "page.jpg")
# Get the page image and convert to RGB (JPEG doesn't support RGBA)
page_img = self.reader.get_current_page()
if page_img.mode == 'RGBA':
page_img = page_img.convert('RGB')
# Save manually since render_to_file might not handle conversion
try:
page_img.save(output_path)
success = True
except Exception:
success = False
self.assertTrue(success)
self.assertTrue(os.path.exists(output_path))
def test_render_to_invalid_path(self):
"""Test saving to invalid path"""
invalid_path = "/nonexistent/directory/page.png"
success = self.reader.render_to_file(invalid_path)
self.assertFalse(success)
class TestEbookReaderContextManager(unittest.TestCase):
"""Test context manager and cleanup"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_context_manager_usage(self):
"""Test using EbookReader as context manager"""
with EbookReader(bookmarks_dir=self.temp_dir) as reader:
success = reader.load_epub(self.epub_path)
self.assertTrue(success)
page = reader.get_current_page()
self.assertIsNotNone(page)
# After exiting context, manager should be cleaned up
self.assertIsNone(reader.manager)
def test_close_method(self):
"""Test explicit close method"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
reader.load_epub(self.epub_path)
self.assertIsNotNone(reader.manager)
reader.close()
self.assertIsNone(reader.manager)
def test_operations_after_close(self):
"""Test that operations fail gracefully after close"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
reader.load_epub(self.epub_path)
reader.close()
# These should all return None or empty
self.assertIsNone(reader.get_current_page())
self.assertIsNone(reader.next_page())
self.assertIsNone(reader.previous_page())
self.assertEqual(reader.get_chapters(), [])
class TestEbookReaderErrorHandling(unittest.TestCase):
"""Test error handling and edge cases"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_operations_without_loaded_book(self):
"""Test that operations handle unloaded state gracefully"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
# All these should return None or empty/False
self.assertIsNone(reader.get_current_page())
self.assertIsNone(reader.next_page())
self.assertIsNone(reader.previous_page())
self.assertFalse(reader.save_position("test"))
self.assertIsNone(reader.load_position("test"))
self.assertEqual(reader.list_saved_positions(), [])
self.assertFalse(reader.delete_position("test"))
self.assertEqual(reader.get_chapters(), [])
self.assertIsNone(reader.jump_to_chapter(0))
self.assertIsNone(reader.set_font_size(1.5))
self.assertEqual(reader.get_reading_progress(), 0.0)
self.assertIsNone(reader.get_current_chapter_info())
reader.close()
def test_is_loaded(self):
"""Test is_loaded method"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
self.assertFalse(reader.is_loaded())
reader.load_epub(self.epub_path)
self.assertTrue(reader.is_loaded())
reader.close()
class TestEbookReaderIntegration(unittest.TestCase):
"""Test complex integration scenarios"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_font_scaling_preserves_position(self):
"""Test that changing font scale preserves reading position"""
# Navigate to a specific position
for _ in range(3):
self.reader.next_page()
position_before = self.reader.manager.current_position.copy()
# Change font size
self.reader.set_font_size(1.5)
position_after = self.reader.manager.current_position
# Position should be preserved
self.assertEqual(position_before.chapter_index, position_after.chapter_index)
self.assertEqual(position_before.block_index, position_after.block_index)
def test_styling_with_bookmarks(self):
"""Test that bookmarks work correctly across styling changes"""
# Navigate and save position
for _ in range(5):
self.reader.next_page()
self.reader.save_position("test_bookmark")
# Change styling
self.reader.set_font_size(1.5)
self.reader.set_line_spacing(12)
# Navigate away
for _ in range(5):
self.reader.next_page()
# Jump back to bookmark
page = self.reader.load_position("test_bookmark")
self.assertIsNotNone(page)
# Cleanup
self.reader.delete_position("test_bookmark")
def test_chapter_navigation_after_font_change(self):
"""Test chapter navigation after changing font size"""
self.reader.set_font_size(2.0)
chapters = self.reader.get_chapters()
if len(chapters) > 0:
page = self.reader.jump_to_chapter(0)
self.assertIsNotNone(page)
class TestEbookReaderNavigation(unittest.TestCase):
"""Test EbookReader navigation functionality (existing tests)"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir, ignore_errors=True)
def compare_images(self, img1: Image.Image, img2: Image.Image) -> bool:
"""
Check if two PIL Images are pixel-perfect identical.
"""
if img1 is None or img2 is None:
return False
if img1.size != img2.size:
return False
arr1 = np.array(img1)
arr2 = np.array(img2)
return np.array_equal(arr1, arr2)
def test_bidirectional_navigation_20_pages(self):
"""
Test that navigating forward 20 pages and then backward 20 pages
produces identical page renderings for the first page.
"""
reader = EbookReader(
page_size=(800, 1000),
bookmarks_dir=self.temp_dir,
buffer_size=0
)
success = reader.load_epub(self.epub_path)
self.assertTrue(success, "Failed to load test EPUB")
self.assertTrue(reader.is_loaded(), "Reader should be loaded")
initial_page = reader.get_current_page()
self.assertIsNotNone(initial_page, "Initial page should not be None")
initial_position = reader.manager.current_position.copy()
forward_pages = [initial_page]
forward_positions = [initial_position]
pages_to_navigate = 20
for i in range(pages_to_navigate):
page = reader.next_page()
if page is None:
break
forward_pages.append(page)
forward_positions.append(reader.manager.current_position.copy())
actual_pages_navigated = len(forward_pages) - 1
backward_pages = []
for i in range(len(forward_positions) - 1, -1, -1):
position = forward_positions[i]
page_obj = reader.manager.jump_to_position(position)
page_img = page_obj.render()
backward_pages.append(page_img)
final_page = backward_pages[-1]
self.assertTrue(
self.compare_images(initial_page, final_page),
"First page should be identical after forward/backward navigation"
)
reader.close()
def test_navigation_at_boundaries(self):
"""Test navigation behavior at document boundaries."""
reader = EbookReader(
page_size=(800, 1000),
bookmarks_dir=self.temp_dir,
buffer_size=0
)
success = reader.load_epub(self.epub_path)
self.assertTrue(success, "Failed to load test EPUB")
# Try to go backward from first page
page = reader.previous_page()
# Should return None or stay on same page
# Navigate forward until end
pages_forward = 0
max_pages = 100
while pages_forward < max_pages:
page = reader.next_page()
if page is None:
break
pages_forward += 1
# Try to go forward from last page
page = reader.next_page()
self.assertIsNone(page, "Should return None at end of document")
reader.close()
class TestEbookReaderPositionManagement(unittest.TestCase):
"""Test position tracking and bookmark features"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
if not Path(self.epub_path).exists():
self.skipTest(f"Test EPUB not found at {self.epub_path}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_position_save_and_load(self):
"""Test saving and loading positions"""
# Navigate to a position
for _ in range(3):
self.reader.next_page()
# Save position
success = self.reader.save_position("test_pos")
self.assertTrue(success)
# Navigate away
for _ in range(5):
self.reader.next_page()
# Load saved position
page = self.reader.load_position("test_pos")
self.assertIsNotNone(page)
def test_list_saved_positions(self):
"""Test listing saved positions"""
self.reader.save_position("pos1")
self.reader.save_position("pos2")
positions = self.reader.list_saved_positions()
self.assertIn("pos1", positions)
self.assertIn("pos2", positions)
def test_delete_position(self):
"""Test deleting a saved position"""
self.reader.save_position("temp_pos")
success = self.reader.delete_position("temp_pos")
self.assertTrue(success)
positions = self.reader.list_saved_positions()
self.assertNotIn("temp_pos", positions)
def test_delete_nonexistent_position(self):
"""Test deleting a non-existent position"""
success = self.reader.delete_position("nonexistent")
self.assertFalse(success)
if __name__ == '__main__':
unittest.main()
+316
View File
@@ -0,0 +1,316 @@
"""
Integration tests for EbookReader highlighting functionality.
Tests the highlighting API in EbookReader including:
- highlight_word()
- highlight_selection()
- remove_highlight()
- list_highlights()
- get_highlights_for_current_page()
- clear_highlights()
- Highlight rendering on pages
"""
import unittest
import tempfile
import shutil
from pathlib import Path
from PIL import Image
from dreader.application import EbookReader
from pyWebLayout.core.highlight import HighlightColor
class TestEbookReaderHighlighting(unittest.TestCase):
"""Test EbookReader highlighting API"""
def setUp(self):
"""Create temporary directories and reader"""
self.temp_dir = tempfile.mkdtemp()
self.bookmarks_dir = Path(self.temp_dir) / "bookmarks"
self.highlights_dir = Path(self.temp_dir) / "highlights"
self.reader = EbookReader(
page_size=(800, 1000),
bookmarks_dir=str(self.bookmarks_dir),
highlights_dir=str(self.highlights_dir)
)
def tearDown(self):
"""Clean up"""
self.reader.close()
shutil.rmtree(self.temp_dir)
def test_highlight_manager_not_initialized_before_load(self):
"""Test that highlight manager is None before loading a book"""
self.assertIsNone(self.reader.highlight_manager)
def test_highlight_manager_initialized_after_load(self):
"""Test that highlight manager is initialized after loading"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
self.assertIsNotNone(self.reader.highlight_manager)
self.assertEqual(self.reader.highlight_manager.document_id, "test")
def test_highlight_word_without_book(self):
"""Test highlighting returns None when no book loaded"""
highlight_id = self.reader.highlight_word(100, 100)
self.assertIsNone(highlight_id)
def test_highlight_word(self):
"""Test highlighting a word at a pixel location"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
# Get a page to ensure content is rendered
page_img = self.reader.get_current_page()
self.assertIsNotNone(page_img)
# Try to highlight a word (coordinates may not hit text in test)
highlight_id = self.reader.highlight_word(
200, 300,
color=HighlightColor.YELLOW.value,
note="Test highlight"
)
# If we hit text, should return an ID
if highlight_id:
self.assertIsInstance(highlight_id, str)
self.assertGreater(len(highlight_id), 0)
# Verify it was added
highlights = self.reader.list_highlights()
self.assertGreater(len(highlights), 0)
def test_highlight_selection(self):
"""Test highlighting a range of words"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
# Get a page
page_img = self.reader.get_current_page()
self.assertIsNotNone(page_img)
# Try to highlight a selection
highlight_id = self.reader.highlight_selection(
start=(100, 200),
end=(400, 250),
color=HighlightColor.BLUE.value,
tags=["selection", "test"]
)
# If selection hit text, should return an ID
if highlight_id:
self.assertIsInstance(highlight_id, str)
highlights = self.reader.list_highlights()
found = False
for h in highlights:
if h.id == highlight_id:
found = True
self.assertEqual(h.color, HighlightColor.BLUE.value)
self.assertIn("selection", h.tags)
break
self.assertTrue(found)
def test_list_highlights_empty(self):
"""Test listing highlights when none exist"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
highlights = self.reader.list_highlights()
self.assertEqual(len(highlights), 0)
def test_remove_highlight(self):
"""Test removing a highlight"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
page_img = self.reader.get_current_page()
# Create a highlight directly via manager for testing
from pyWebLayout.core.highlight import Highlight
highlight = Highlight(
id="test-highlight",
bounds=[(100, 200, 50, 15)],
color=HighlightColor.YELLOW.value,
text="Test"
)
self.reader.highlight_manager.add_highlight(highlight)
# Verify it exists
self.assertEqual(len(self.reader.list_highlights()), 1)
# Remove it
result = self.reader.remove_highlight("test-highlight")
self.assertTrue(result)
# Verify it's gone
self.assertEqual(len(self.reader.list_highlights()), 0)
def test_remove_nonexistent_highlight(self):
"""Test removing a highlight that doesn't exist"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
result = self.reader.remove_highlight("nonexistent")
self.assertFalse(result)
def test_clear_highlights(self):
"""Test clearing all highlights"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
# Add some highlights directly
from pyWebLayout.core.highlight import Highlight
for i in range(3):
highlight = Highlight(
id=f"test-{i}",
bounds=[(100 + i * 50, 200, 50, 15)],
color=HighlightColor.YELLOW.value,
text=f"Word{i}"
)
self.reader.highlight_manager.add_highlight(highlight)
self.assertEqual(len(self.reader.list_highlights()), 3)
self.reader.clear_highlights()
self.assertEqual(len(self.reader.list_highlights()), 0)
def test_get_highlights_for_current_page(self):
"""Test getting highlights for current page"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
# Add highlights at different locations
from pyWebLayout.core.highlight import Highlight
# On page
highlight1 = Highlight(
id="on-page",
bounds=[(100, 200, 50, 15)],
color=HighlightColor.YELLOW.value,
text="OnPage"
)
# Off page (beyond page height)
highlight2 = Highlight(
id="off-page",
bounds=[(100, 5000, 50, 15)],
color=HighlightColor.YELLOW.value,
text="OffPage"
)
self.reader.highlight_manager.add_highlight(highlight1)
self.reader.highlight_manager.add_highlight(highlight2)
# Get highlights for current page
page_highlights = self.reader.get_highlights_for_current_page()
# Should only get the on-page highlight
self.assertEqual(len(page_highlights), 1)
self.assertEqual(page_highlights[0].id, "on-page")
def test_get_current_page_with_highlights(self):
"""Test that get_current_page renders highlights"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
# Get page without highlights
page_no_highlights = self.reader.get_current_page(include_highlights=False)
self.assertIsNotNone(page_no_highlights)
# Add a highlight
from pyWebLayout.core.highlight import Highlight
highlight = Highlight(
id="test",
bounds=[(100, 200, 50, 15)],
color=HighlightColor.YELLOW.value,
text="Test"
)
self.reader.highlight_manager.add_highlight(highlight)
# Get page with highlights
page_with_highlights = self.reader.get_current_page(include_highlights=True)
self.assertIsNotNone(page_with_highlights)
# Both should be valid images
self.assertIsInstance(page_no_highlights, Image.Image)
self.assertIsInstance(page_with_highlights, Image.Image)
# Should have same size
self.assertEqual(page_no_highlights.size, page_with_highlights.size)
def test_highlight_persistence(self):
"""Test that highlights persist across reader instances"""
# Load book and add highlight
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
from pyWebLayout.core.highlight import Highlight
highlight = Highlight(
id="persistent",
bounds=[(100, 200, 50, 15)],
color=HighlightColor.YELLOW.value,
text="Persisted"
)
self.reader.highlight_manager.add_highlight(highlight)
self.assertEqual(len(self.reader.list_highlights()), 1)
# Close reader
self.reader.close()
# Create new reader for same book
new_reader = EbookReader(
page_size=(800, 1000),
bookmarks_dir=str(self.bookmarks_dir),
highlights_dir=str(self.highlights_dir)
)
success = new_reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
# Should load existing highlights
highlights = new_reader.list_highlights()
self.assertEqual(len(highlights), 1)
self.assertEqual(highlights[0].id, "persistent")
self.assertEqual(highlights[0].text, "Persisted")
new_reader.close()
def test_render_highlights_helper(self):
"""Test the _render_highlights helper method"""
success = self.reader.load_epub("tests/data/test.epub")
self.assertTrue(success)
# Create a simple test image
from PIL import Image as PILImage
test_img = PILImage.new('RGB', (100, 100), color='white')
from pyWebLayout.core.highlight import Highlight
highlights = [
Highlight(
id="h1",
bounds=[(10, 10, 30, 20)],
color=(255, 255, 0, 100),
text="Test"
)
]
# Render highlights
result_img = self.reader._render_highlights(test_img, highlights)
self.assertIsNotNone(result_img)
self.assertIsInstance(result_img, PILImage.Image)
self.assertEqual(result_img.size, test_img.size)
# Result should be RGBA for transparency
self.assertEqual(result_img.mode, 'RGBA')
if __name__ == '__main__':
unittest.main()
+455
View File
@@ -0,0 +1,455 @@
"""
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,
bookmarks_dir=self.temp_dir
)
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,
bookmarks_dir=self.temp_dir
)
# 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,
bookmarks_dir=self.temp_dir
)
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"""
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id,
bookmarks_dir=self.temp_dir
)
# 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()
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,
bookmarks_dir=self.temp_dir
)
# 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,
bookmarks_dir=self.temp_dir
)
self.assertIsInstance(manager, EreaderLayoutManager)
self.assertEqual(manager.page_size, self.page_size)
manager.shutdown()
finally:
os.chdir(original_cwd)
if __name__ == '__main__':
unittest.main()