""" Unit tests for Image block rendering in the ereader layout system. Tests cover: - Image block layout on pages - Navigation with images (next/previous page) - Images at different positions (start, middle, end) - Cover page detection and handling - Multi-page scenarios with images """ import unittest import tempfile import shutil from pathlib import Path from pyWebLayout.layout.ereader_manager import EreaderLayoutManager from pyWebLayout.layout.ereader_layout import BidirectionalLayouter, RenderingPosition from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel, Image from pyWebLayout.abstract.inline import Word from pyWebLayout.concrete.page import Page from pyWebLayout.style import Font from pyWebLayout.style.page_style import PageStyle class TestImageBlockLayout(unittest.TestCase): """Test basic Image block layout functionality.""" def setUp(self): """Set up test fixtures.""" self.base_font = Font(font_size=14) self.page_size = (400, 600) self.page_style = PageStyle(padding=(20, 20, 20, 20)) def test_layout_image_block_on_page(self): """Test that Image blocks can be laid out on pages.""" # Create a simple document with an image blocks = [ Image(source="test.jpg", alt_text="Test Image", width=200, height=300) ] layouter = BidirectionalLayouter(blocks, self.page_style) position = RenderingPosition() # Render page with image page, next_pos = layouter.render_page_forward(position, font_scale=1.0) # Should successfully render the page self.assertIsNotNone(page) self.assertIsInstance(page, Page) # Position should advance past the image block self.assertEqual(next_pos.block_index, 1) def test_image_block_advances_position(self): """Test that rendering an image block correctly advances the position.""" blocks = [ Image(source="img1.jpg", alt_text="Image 1"), Paragraph(self.base_font) ] # Add some words to the paragraph blocks[1].add_word(Word("Text after image", self.base_font)) layouter = BidirectionalLayouter(blocks, self.page_style) position = RenderingPosition(block_index=0) # Render page starting at image page, next_pos = layouter.render_page_forward(position, font_scale=1.0) # Position should either: # 1. Move to next block if image was successfully laid out, OR # 2. Stay at same position if image couldn't fit/render # In either case, the layouter should handle it gracefully self.assertIsNotNone(page) self.assertGreaterEqual(next_pos.block_index, 0) # If the image is at start and can't render, it may skip to next block anyway # The important thing is the system doesn't crash class TestImageNavigationScenarios(unittest.TestCase): """Test navigation scenarios with images in different positions.""" def setUp(self): """Set up test fixtures.""" self.base_font = Font(font_size=14) self.page_size = (400, 600) self.page_style = PageStyle(padding=(20, 20, 20, 20)) self.temp_dir = tempfile.mkdtemp() def tearDown(self): """Clean up temporary files.""" shutil.rmtree(self.temp_dir, ignore_errors=True) def _create_paragraph(self, text: str) -> Paragraph: """Helper to create a paragraph with text.""" para = Paragraph(self.base_font) para.add_word(Word(text, self.base_font)) return para def test_next_page_with_image_on_second_page(self): """Test navigating to next page when an image is on the second page.""" # Document structure: paragraph → image → paragraph blocks = [ self._create_paragraph("First paragraph on page 1."), Image(source="middle.jpg", alt_text="Middle Image"), self._create_paragraph("Third paragraph after image.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_image_nav", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Start at beginning initial_pos = manager.current_position.block_index self.assertEqual(initial_pos, 0) # Navigate to next page next_page = manager.next_page() self.assertIsNotNone(next_page) # Position should have advanced self.assertGreater(manager.current_position.block_index, initial_pos) def test_previous_page_with_image_on_previous_page(self): """Test navigating back when previous page contains an image.""" blocks = [ self._create_paragraph("First paragraph."), Image(source="image1.jpg", alt_text="Image 1"), self._create_paragraph("Third paragraph."), self._create_paragraph("Fourth paragraph.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_prev_image", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Navigate forward to get past the image manager.next_page() manager.next_page() current_block = manager.current_position.block_index self.assertGreater(current_block, 0) # Navigate backward prev_page = manager.previous_page() self.assertIsNotNone(prev_page) # Should have moved to an earlier position self.assertLess(manager.current_position.block_index, current_block) def test_multiple_images_in_sequence(self): """Test document with multiple consecutive images.""" blocks = [ self._create_paragraph("Introduction text."), Image(source="img1.jpg", alt_text="Image 1"), Image(source="img2.jpg", alt_text="Image 2"), Image(source="img3.jpg", alt_text="Image 3"), self._create_paragraph("Text after images.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_multi_images", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Navigate through pages pages_rendered = 0 max_pages = 10 # Safety limit while pages_rendered < max_pages: current_block = manager.current_position.block_index # Try to go to next page next_page = manager.next_page() if next_page is None: # Reached end break pages_rendered += 1 # Position should advance self.assertGreaterEqual( manager.current_position.block_index, current_block, f"Position should advance or stay same, page {pages_rendered}" ) # Should have rendered at least 2 pages self.assertGreaterEqual(pages_rendered, 1) def test_image_at_document_start(self): """Test document starting with an image (not as cover).""" blocks = [ Image(source="start.jpg", alt_text="Start Image"), self._create_paragraph("Text after image.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_image_start", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # First image should be detected as cover self.assertTrue(manager.has_cover()) self.assertTrue(manager.is_on_cover()) # Navigate past cover manager.next_page() # Should now be at the text self.assertFalse(manager.is_on_cover()) # Should have skipped the image block (cover) self.assertEqual(manager.current_position.block_index, 1) def test_image_at_document_end(self): """Test document ending with an image.""" blocks = [ self._create_paragraph("First paragraph."), self._create_paragraph("Second paragraph."), Image(source="end.jpg", alt_text="End Image") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_image_end", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Navigate to end page_count = 0 max_pages = 10 while page_count < max_pages: next_page = manager.next_page() if next_page is None: break page_count += 1 # Should have successfully navigated through document including final image self.assertGreater(page_count, 0) def test_alternating_text_and_images(self): """Test document with alternating text and images.""" blocks = [ self._create_paragraph("Paragraph 1"), Image(source="img1.jpg", alt_text="Image 1"), self._create_paragraph("Paragraph 2"), Image(source="img2.jpg", alt_text="Image 2"), self._create_paragraph("Paragraph 3"), Image(source="img3.jpg", alt_text="Image 3"), self._create_paragraph("Paragraph 4") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_alternating", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Track blocks visited blocks_visited = set() max_pages = 15 for _ in range(max_pages): blocks_visited.add(manager.current_position.block_index) next_page = manager.next_page() if next_page is None: break # Should have visited multiple different blocks self.assertGreater(len(blocks_visited), 1) class TestCoverPageWithImages(unittest.TestCase): """Test cover page detection and handling with images.""" def setUp(self): """Set up test fixtures.""" self.base_font = Font(font_size=14) self.page_size = (400, 600) self.page_style = PageStyle(padding=(20, 20, 20, 20)) self.temp_dir = tempfile.mkdtemp() def tearDown(self): """Clean up temporary files.""" shutil.rmtree(self.temp_dir, ignore_errors=True) def _create_paragraph(self, text: str) -> Paragraph: """Helper to create a paragraph with text.""" para = Paragraph(self.base_font) para.add_word(Word(text, self.base_font)) return para def test_cover_page_detected_from_first_image(self): """Test that first image is detected as cover.""" blocks = [ Image(source="cover.jpg", alt_text="Cover"), self._create_paragraph("Chapter text.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_cover_detection", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Should detect cover self.assertTrue(manager.has_cover()) self.assertTrue(manager.is_on_cover()) def test_no_cover_when_first_block_is_text(self): """Test that cover is not detected when first block is text.""" blocks = [ self._create_paragraph("First paragraph."), Image(source="image.jpg", alt_text="Not a cover"), self._create_paragraph("Second paragraph.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_no_cover", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Should NOT detect cover self.assertFalse(manager.has_cover()) self.assertFalse(manager.is_on_cover()) def test_navigation_from_cover_skips_image_block(self): """Test that next_page from cover skips the cover image block.""" blocks = [ Image(source="cover.jpg", alt_text="Cover"), self._create_paragraph("First content paragraph."), self._create_paragraph("Second content paragraph.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_cover_skip", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Start on cover self.assertTrue(manager.is_on_cover()) self.assertEqual(manager.current_position.block_index, 0) # Navigate past cover manager.next_page() # Should skip cover image block (index 0) and go to first content (index 1) self.assertFalse(manager.is_on_cover()) self.assertEqual(manager.current_position.block_index, 1) def test_previous_page_returns_to_cover(self): """Test that previous_page from first content returns to cover.""" blocks = [ Image(source="cover.jpg", alt_text="Cover"), self._create_paragraph("Content text.") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_back_to_cover", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Navigate past cover manager.next_page() self.assertFalse(manager.is_on_cover()) # Go back manager.previous_page() # Should be back on cover self.assertTrue(manager.is_on_cover()) def test_jump_to_cover_from_middle(self): """Test jumping to cover from middle of document.""" blocks = [ Image(source="cover.jpg", alt_text="Cover"), self._create_paragraph("Paragraph 1"), self._create_paragraph("Paragraph 2"), self._create_paragraph("Paragraph 3") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_jump_cover", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Navigate to middle manager.next_page() manager.next_page() self.assertFalse(manager.is_on_cover()) # Jump to cover cover_page = manager.jump_to_cover() self.assertIsNotNone(cover_page) self.assertTrue(manager.is_on_cover()) class TestImageBlockPositionTracking(unittest.TestCase): """Test position tracking with Image blocks.""" def setUp(self): """Set up test fixtures.""" self.base_font = Font(font_size=14) self.page_size = (400, 600) self.page_style = PageStyle(padding=(20, 20, 20, 20)) self.temp_dir = tempfile.mkdtemp() def tearDown(self): """Clean up temporary files.""" shutil.rmtree(self.temp_dir, ignore_errors=True) def _create_paragraph(self, text: str) -> Paragraph: """Helper to create a paragraph with text.""" para = Paragraph(self.base_font) para.add_word(Word(text, self.base_font)) return para def test_position_info_includes_image_blocks(self): """Test that position info correctly handles image blocks.""" blocks = [ self._create_paragraph("Text 1"), Image(source="img.jpg", alt_text="Image"), self._create_paragraph("Text 2") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_pos_info", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Get initial position info pos_info = manager.get_position_info() self.assertIn('position', pos_info) self.assertIn('block_index', pos_info['position']) self.assertEqual(pos_info['position']['block_index'], 0) def test_bookmark_image_position(self): """Test bookmarking at an image position.""" blocks = [ self._create_paragraph("Before image"), Image(source="bookmarked.jpg", alt_text="Bookmarked Image"), self._create_paragraph("After image") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_bookmark_image", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # Navigate to image position manager.next_page() # Add bookmark bookmark_name = "image_location" success = manager.add_bookmark(bookmark_name) self.assertTrue(success) # Navigate away manager.next_page() # Jump back to bookmark page = manager.jump_to_bookmark(bookmark_name) self.assertIsNotNone(page) # Should be at or near the image position # (exact position depends on how much fits on page) self.assertGreater(manager.current_position.block_index, 0) def test_reading_progress_with_images(self): """Test reading progress calculation with images in document.""" blocks = [ self._create_paragraph("Text 1"), Image(source="img1.jpg", alt_text="Image 1"), self._create_paragraph("Text 2"), Image(source="img2.jpg", alt_text="Image 2"), self._create_paragraph("Text 3") ] manager = EreaderLayoutManager( blocks=blocks, page_size=self.page_size, document_id="test_progress", page_style=self.page_style, bookmarks_dir=self.temp_dir ) # At start progress_start = manager.get_reading_progress() self.assertEqual(progress_start, 0.0) # Navigate through document for _ in range(5): if manager.next_page() is None: break # Progress should have increased progress_end = manager.get_reading_progress() self.assertGreater(progress_end, progress_start) if __name__ == '__main__': unittest.main()