""" 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 pyWebLayout.layout.ereader_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()