diff --git a/tests/layout/test_ereader_manager.py b/tests/layout/test_ereader_manager.py new file mode 100644 index 0000000..13ef8f9 --- /dev/null +++ b/tests/layout/test_ereader_manager.py @@ -0,0 +1,784 @@ +""" +Tests for the ereader manager components. + +This module tests: +- BookmarkManager: Bookmark and position persistence +- EreaderLayoutManager: High-level ereader interface +- create_ereader_manager: Convenience function +""" + +import pytest +import json +import tempfile +from pathlib import Path +from unittest.mock import Mock, MagicMock, patch + +from pyWebLayout.layout.ereader_manager import ( + BookmarkManager, + EreaderLayoutManager, + create_ereader_manager +) +from pyWebLayout.layout.ereader_layout import RenderingPosition +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 + + +# ============================================================================ +# Fixtures +# ============================================================================ + +@pytest.fixture +def temp_bookmarks_dir(tmp_path): + """Create a temporary directory for bookmarks.""" + bookmarks_dir = tmp_path / "bookmarks" + bookmarks_dir.mkdir() + return str(bookmarks_dir) + + +@pytest.fixture +def sample_font(): + """Create a standard font for testing.""" + return Font(font_size=12, colour=(0, 0, 0)) + + +@pytest.fixture +def sample_blocks(sample_font): + """Create sample document blocks.""" + blocks = [] + + # Heading + h1 = Heading(HeadingLevel.H1, sample_font) + h1.add_word(Word("Chapter", sample_font)) + h1.add_word(Word("One", sample_font)) + blocks.append(h1) + + # Paragraphs + for i in range(5): + p = Paragraph(sample_font) + p.add_word(Word(f"Paragraph", sample_font)) + p.add_word(Word(f"{i}", sample_font)) + blocks.append(p) + + return blocks + + +@pytest.fixture +def sample_position(): + """Create a sample rendering position.""" + return RenderingPosition( + chapter_index=1, + block_index=5, + word_index=10 + ) + + +# ============================================================================ +# BookmarkManager Tests +# ============================================================================ + +class TestBookmarkManager: + """Tests for the BookmarkManager class.""" + + def test_initialization(self, temp_bookmarks_dir): + """Test BookmarkManager initialization.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + assert manager.document_id == "test_doc" + assert manager.bookmarks_dir == Path(temp_bookmarks_dir) + assert manager.bookmarks_file.exists() or True # May not exist yet + assert isinstance(manager._bookmarks, dict) + + def test_initialization_creates_directory(self, tmp_path): + """Test that initialization creates bookmarks directory if needed.""" + bookmarks_dir = str(tmp_path / "new_bookmarks") + + manager = BookmarkManager("test_doc", bookmarks_dir) + + assert Path(bookmarks_dir).exists() + assert Path(bookmarks_dir).is_dir() + + def test_add_bookmark(self, temp_bookmarks_dir, sample_position): + """Test adding a bookmark.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + manager.add_bookmark("Chapter 1", sample_position) + + # Verify bookmark was added + bookmark = manager.get_bookmark("Chapter 1") + assert bookmark is not None + assert bookmark == sample_position + + def test_add_multiple_bookmarks(self, temp_bookmarks_dir): + """Test adding multiple bookmarks.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + pos1 = RenderingPosition(chapter_index=1, block_index=5) + pos2 = RenderingPosition(chapter_index=2, block_index=10) + pos3 = RenderingPosition(chapter_index=3, block_index=15) + + manager.add_bookmark("Bookmark 1", pos1) + manager.add_bookmark("Bookmark 2", pos2) + manager.add_bookmark("Bookmark 3", pos3) + + assert len(manager._bookmarks) == 3 + assert manager.get_bookmark("Bookmark 1") == pos1 + assert manager.get_bookmark("Bookmark 2") == pos2 + assert manager.get_bookmark("Bookmark 3") == pos3 + + def test_remove_bookmark(self, temp_bookmarks_dir, sample_position): + """Test removing a bookmark.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + manager.add_bookmark("Test", sample_position) + assert manager.get_bookmark("Test") is not None + + result = manager.remove_bookmark("Test") + + assert result is True + assert manager.get_bookmark("Test") is None + + def test_remove_nonexistent_bookmark(self, temp_bookmarks_dir): + """Test removing a bookmark that doesn't exist.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + result = manager.remove_bookmark("Nonexistent") + + assert result is False + + def test_get_bookmark(self, temp_bookmarks_dir, sample_position): + """Test getting a bookmark.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + manager.add_bookmark("Test", sample_position) + + retrieved = manager.get_bookmark("Test") + + assert retrieved == sample_position + + def test_get_nonexistent_bookmark(self, temp_bookmarks_dir): + """Test getting a bookmark that doesn't exist.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + result = manager.get_bookmark("Nonexistent") + + assert result is None + + def test_list_bookmarks(self, temp_bookmarks_dir): + """Test listing all bookmarks.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + pos1 = RenderingPosition(chapter_index=1, block_index=5) + pos2 = RenderingPosition(chapter_index=2, block_index=10) + + manager.add_bookmark("First", pos1) + manager.add_bookmark("Second", pos2) + + bookmarks = manager.list_bookmarks() + + assert len(bookmarks) == 2 + assert ("First", pos1) in bookmarks + assert ("Second", pos2) in bookmarks + + def test_list_bookmarks_empty(self, temp_bookmarks_dir): + """Test listing bookmarks when none exist.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + bookmarks = manager.list_bookmarks() + + assert bookmarks == [] + + def test_save_reading_position(self, temp_bookmarks_dir, sample_position): + """Test saving current reading position.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + manager.save_reading_position(sample_position) + + # Verify file was created + assert manager.position_file.exists() + + # Verify content + with open(manager.position_file, 'r') as f: + data = json.load(f) + + assert data['chapter_index'] == sample_position.chapter_index + assert data['block_index'] == sample_position.block_index + + def test_load_reading_position(self, temp_bookmarks_dir, sample_position): + """Test loading saved reading position.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + manager.save_reading_position(sample_position) + + loaded = manager.load_reading_position() + + assert loaded is not None + assert loaded == sample_position + + def test_load_reading_position_nonexistent(self, temp_bookmarks_dir): + """Test loading reading position when file doesn't exist.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + loaded = manager.load_reading_position() + + assert loaded is None + + def test_bookmark_persistence(self, temp_bookmarks_dir, sample_position): + """Test that bookmarks persist across manager instances.""" + # Create first manager and add bookmark + manager1 = BookmarkManager("test_doc", temp_bookmarks_dir) + manager1.add_bookmark("Persistent", sample_position) + + # Create second manager and verify bookmark exists + manager2 = BookmarkManager("test_doc", temp_bookmarks_dir) + loaded = manager2.get_bookmark("Persistent") + + assert loaded is not None + assert loaded == sample_position + + def test_position_persistence(self, temp_bookmarks_dir, sample_position): + """Test that reading position persists across manager instances.""" + # Save with first manager + manager1 = BookmarkManager("test_doc", temp_bookmarks_dir) + manager1.save_reading_position(sample_position) + + # Load with second manager + manager2 = BookmarkManager("test_doc", temp_bookmarks_dir) + loaded = manager2.load_reading_position() + + assert loaded == sample_position + + def test_corrupt_bookmarks_file(self, temp_bookmarks_dir): + """Test handling of corrupt bookmarks file.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + # Write corrupt JSON + with open(manager.bookmarks_file, 'w') as f: + f.write("{ invalid json }}") + + # Should handle gracefully and load empty bookmarks + manager2 = BookmarkManager("test_doc", temp_bookmarks_dir) + assert manager2._bookmarks == {} + + def test_corrupt_position_file(self, temp_bookmarks_dir): + """Test handling of corrupt position file.""" + manager = BookmarkManager("test_doc", temp_bookmarks_dir) + + # Write corrupt JSON + with open(manager.position_file, 'w') as f: + f.write("{ invalid json }}") + + # Should handle gracefully + loaded = manager.load_reading_position() + assert loaded is None + + +# ============================================================================ +# EreaderLayoutManager Tests +# ============================================================================ + +class TestEreaderLayoutManager: + """Tests for the EreaderLayoutManager class.""" + + def test_initialization(self, sample_blocks, temp_bookmarks_dir): + """Test EreaderLayoutManager initialization.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + document_id="test_doc", + bookmarks_dir=temp_bookmarks_dir + ) + + assert manager.blocks == sample_blocks + assert manager.page_size == (800, 600) + assert manager.document_id == "test_doc" + assert manager.font_scale == 1.0 + assert isinstance(manager.current_position, RenderingPosition) + + def test_initialization_with_custom_page_style(self, sample_blocks, temp_bookmarks_dir): + """Test initialization with custom page style.""" + custom_style = PageStyle() + + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + page_style=custom_style, + bookmarks_dir=temp_bookmarks_dir + ) + + assert manager.page_style == custom_style + + def test_initialization_loads_saved_position(self, sample_blocks, temp_bookmarks_dir): + """Test that initialization loads saved reading position.""" + # Save a position first + bookmark_mgr = BookmarkManager("test_doc", temp_bookmarks_dir) + saved_pos = RenderingPosition(chapter_index=2, block_index=10) + bookmark_mgr.save_reading_position(saved_pos) + + # Create manager - should load saved position + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + document_id="test_doc", + bookmarks_dir=temp_bookmarks_dir + ) + + assert manager.current_position == saved_pos + + def test_get_current_page(self, sample_blocks, temp_bookmarks_dir): + """Test getting the current page.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + page = manager.get_current_page() + + from pyWebLayout.concrete.page import Page + assert isinstance(page, Page) + + def test_next_page(self, sample_blocks, temp_bookmarks_dir): + """Test advancing to next page.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + initial_pos = manager.current_position.copy() + + next_page = manager.next_page() + + # Position should have advanced + assert manager.current_position != initial_pos or next_page is None + + def test_next_page_at_end(self, sample_blocks, temp_bookmarks_dir): + """Test next_page when at end of document.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + # Move to end + manager.current_position = RenderingPosition( + block_index=len(sample_blocks) + 100 + ) + + result = manager.next_page() + + # Should return None at end + assert result is None + + def test_previous_page(self, sample_blocks, temp_bookmarks_dir): + """Test going to previous page.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + # Move forward first + manager.current_position = RenderingPosition(block_index=3) + + prev_page = manager.previous_page() + + # Should return a page or None if at beginning + from pyWebLayout.concrete.page import Page + assert prev_page is None or isinstance(prev_page, Page) + + def test_previous_page_at_beginning(self, sample_blocks, temp_bookmarks_dir): + """Test previous_page when at beginning of document.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + # At beginning + manager.current_position = RenderingPosition() + + result = manager.previous_page() + + assert result is None + + def test_jump_to_position(self, sample_blocks, temp_bookmarks_dir): + """Test jumping to a specific position.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + target_pos = RenderingPosition(chapter_index=1, block_index=3) + + page = manager.jump_to_position(target_pos) + + assert manager.current_position == target_pos + from pyWebLayout.concrete.page import Page + assert isinstance(page, Page) + + def test_jump_to_chapter(self, sample_blocks, temp_bookmarks_dir): + """Test jumping to a chapter by title.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + page = manager.jump_to_chapter("Chapter One") + + # May return page or None if chapter not found + from pyWebLayout.concrete.page import Page + assert page is None or isinstance(page, Page) + + def test_jump_to_chapter_not_found(self, sample_blocks, temp_bookmarks_dir): + """Test jumping to non-existent chapter.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + result = manager.jump_to_chapter("Nonexistent Chapter") + + assert result is None + + def test_jump_to_chapter_index(self, sample_blocks, temp_bookmarks_dir): + """Test jumping to chapter by index.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + page = manager.jump_to_chapter_index(0) + + from pyWebLayout.concrete.page import Page + assert page is None or isinstance(page, Page) + + def test_jump_to_chapter_index_invalid(self, sample_blocks, temp_bookmarks_dir): + """Test jumping to invalid chapter index.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + result = manager.jump_to_chapter_index(999) + + assert result is None + + def test_set_font_scale(self, sample_blocks, temp_bookmarks_dir): + """Test changing font scale.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + page = manager.set_font_scale(1.5) + + assert manager.font_scale == 1.5 + from pyWebLayout.concrete.page import Page + assert isinstance(page, Page) + + def test_set_font_scale_same(self, sample_blocks, temp_bookmarks_dir): + """Test setting font scale to same value.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + page = manager.set_font_scale(1.0) + + assert manager.font_scale == 1.0 + + def test_get_font_scale(self, sample_blocks, temp_bookmarks_dir): + """Test getting current font scale.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + scale = manager.get_font_scale() + + assert scale == 1.0 + + def test_get_table_of_contents(self, sample_blocks, temp_bookmarks_dir): + """Test getting table of contents.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + toc = manager.get_table_of_contents() + + assert isinstance(toc, list) + + def test_get_current_chapter(self, sample_blocks, temp_bookmarks_dir): + """Test getting current chapter info.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + chapter = manager.get_current_chapter() + + # May be None or ChapterInfo + assert chapter is None or hasattr(chapter, 'title') + + def test_add_bookmark(self, sample_blocks, temp_bookmarks_dir): + """Test adding a bookmark at current position.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + result = manager.add_bookmark("Test Bookmark") + + assert result is True + # Verify bookmark was added + bookmark = manager.bookmark_manager.get_bookmark("Test Bookmark") + assert bookmark is not None + + def test_remove_bookmark(self, sample_blocks, temp_bookmarks_dir): + """Test removing a bookmark.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + manager.add_bookmark("Test") + result = manager.remove_bookmark("Test") + + assert result is True + + def test_jump_to_bookmark(self, sample_blocks, temp_bookmarks_dir): + """Test jumping to a bookmark.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + # Add bookmark at specific position + manager.current_position = RenderingPosition(block_index=3) + manager.add_bookmark("Test Position") + + # Move away + manager.current_position = RenderingPosition(block_index=0) + + # Jump to bookmark + page = manager.jump_to_bookmark("Test Position") + + from pyWebLayout.concrete.page import Page + assert isinstance(page, Page) + assert manager.current_position.block_index == 3 + + def test_jump_to_nonexistent_bookmark(self, sample_blocks, temp_bookmarks_dir): + """Test jumping to non-existent bookmark.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + result = manager.jump_to_bookmark("Nonexistent") + + assert result is None + + def test_list_bookmarks(self, sample_blocks, temp_bookmarks_dir): + """Test listing all bookmarks.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + manager.add_bookmark("Bookmark 1") + manager.add_bookmark("Bookmark 2") + + bookmarks = manager.list_bookmarks() + + assert len(bookmarks) == 2 + + def test_get_reading_progress(self, sample_blocks, temp_bookmarks_dir): + """Test getting reading progress percentage.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + progress = manager.get_reading_progress() + + assert 0.0 <= progress <= 1.0 + + def test_get_reading_progress_at_end(self, sample_blocks, temp_bookmarks_dir): + """Test reading progress at end of document.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + manager.current_position = RenderingPosition( + block_index=len(sample_blocks) - 1 + ) + + progress = manager.get_reading_progress() + + assert progress == 1.0 + + def test_get_reading_progress_empty_document(self, temp_bookmarks_dir): + """Test reading progress with empty document.""" + manager = EreaderLayoutManager( + [], + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + progress = manager.get_reading_progress() + + assert progress == 0.0 + + def test_get_position_info(self, sample_blocks, temp_bookmarks_dir): + """Test getting detailed position information.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + info = manager.get_position_info() + + assert isinstance(info, dict) + assert 'position' in info + assert 'chapter' in info + assert 'progress' in info + assert 'font_scale' in info + assert 'page_size' in info + + def test_get_cache_stats(self, sample_blocks, temp_bookmarks_dir): + """Test getting cache statistics.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + stats = manager.get_cache_stats() + + assert isinstance(stats, dict) + + def test_position_changed_callback(self, sample_blocks, temp_bookmarks_dir): + """Test position changed callback.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + callback_called = [] + + def callback(position): + callback_called.append(position) + + manager.set_position_changed_callback(callback) + manager.jump_to_position(RenderingPosition(block_index=3)) + + assert len(callback_called) > 0 + + def test_chapter_changed_callback(self, sample_blocks, temp_bookmarks_dir): + """Test chapter changed callback.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + bookmarks_dir=temp_bookmarks_dir + ) + + callback_called = [] + + def callback(chapter): + callback_called.append(chapter) + + manager.set_chapter_changed_callback(callback) + manager.jump_to_position(RenderingPosition(block_index=3)) + + assert len(callback_called) > 0 + + def test_shutdown(self, sample_blocks, temp_bookmarks_dir): + """Test shutdown saves position.""" + manager = EreaderLayoutManager( + sample_blocks, + page_size=(800, 600), + document_id="test_doc", + bookmarks_dir=temp_bookmarks_dir + ) + + test_pos = RenderingPosition(block_index=5) + manager.current_position = test_pos + + manager.shutdown() + + # Verify position was saved + bookmark_mgr = BookmarkManager("test_doc", temp_bookmarks_dir) + loaded = bookmark_mgr.load_reading_position() + assert loaded == test_pos + + +# ============================================================================ +# Convenience Function Tests +# ============================================================================ + +class TestCreateEreaderManager: + """Tests for the create_ereader_manager convenience function.""" + + def test_create_with_defaults(self, sample_blocks): + """Test creating manager with default parameters.""" + manager = create_ereader_manager( + sample_blocks, + page_size=(800, 600) + ) + + assert isinstance(manager, EreaderLayoutManager) + assert manager.blocks == sample_blocks + assert manager.page_size == (800, 600) + assert manager.document_id == "default" + + def test_create_with_custom_document_id(self, sample_blocks): + """Test creating manager with custom document ID.""" + manager = create_ereader_manager( + sample_blocks, + page_size=(800, 600), + document_id="custom_doc" + ) + + assert manager.document_id == "custom_doc" + + def test_create_with_kwargs(self, sample_blocks, tmp_path): + """Test creating manager with additional kwargs.""" + bookmarks_dir = str(tmp_path / "custom_bookmarks") + + manager = create_ereader_manager( + sample_blocks, + page_size=(800, 600), + document_id="test", + buffer_size=10, + bookmarks_dir=bookmarks_dir + ) + + assert isinstance(manager, EreaderLayoutManager) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])