""" Unit tests for TOC overlay functionality. Tests the complete workflow of: 1. Opening TOC overlay with swipe up gesture 2. Selecting a chapter from the TOC 3. Closing overlay by tapping outside or swiping down """ import unittest from pathlib import Path from dreader import ( EbookReader, TouchEvent, GestureType, ActionType, OverlayState ) class TestTOCOverlay(unittest.TestCase): """Test TOC overlay opening, interaction, and closing""" def setUp(self): """Set up test reader with a book""" self.reader = EbookReader(page_size=(800, 1200)) # Load a test EPUB test_epub = Path(__file__).parent / 'data' / 'library-epub' / 'alice.epub' if not test_epub.exists(): # Try to find any EPUB in test data epub_dir = Path(__file__).parent / 'data' / 'library-epub' epubs = list(epub_dir.glob('*.epub')) if epubs: test_epub = epubs[0] else: self.skipTest("No test EPUB files available") success = self.reader.load_epub(str(test_epub)) self.assertTrue(success, "Failed to load test EPUB") def tearDown(self): """Clean up""" self.reader.close() def test_overlay_manager_initialization(self): """Test that overlay sub-applications are properly initialized""" # Check that overlay sub-applications exist self.assertIsNotNone(self.reader._overlay_subapps) self.assertIn(OverlayState.TOC, self.reader._overlay_subapps) self.assertIn(OverlayState.SETTINGS, self.reader._overlay_subapps) self.assertIn(OverlayState.NAVIGATION, self.reader._overlay_subapps) # Initially no overlay should be active self.assertFalse(self.reader.is_overlay_open()) self.assertEqual(self.reader.get_overlay_state(), OverlayState.NONE) def test_open_toc_overlay_directly(self): """Test opening TOC overlay using direct API call""" # Initially no overlay self.assertFalse(self.reader.is_overlay_open()) # Open TOC overlay overlay_image = self.reader.open_toc_overlay() # Should return an image self.assertIsNotNone(overlay_image) self.assertEqual(overlay_image.size, (800, 1200)) # Overlay should be open self.assertTrue(self.reader.is_overlay_open()) self.assertEqual(self.reader.get_overlay_state(), OverlayState.TOC) def test_close_toc_overlay_directly(self): """Test closing TOC overlay using direct API call""" # Open overlay first self.reader.open_toc_overlay() self.assertTrue(self.reader.is_overlay_open()) # Close overlay page_image = self.reader.close_overlay() # Should return base page self.assertIsNotNone(page_image) # Overlay should be closed self.assertFalse(self.reader.is_overlay_open()) self.assertEqual(self.reader.get_overlay_state(), OverlayState.NONE) def test_swipe_up_from_bottom_opens_toc(self): """Test that swipe up from bottom of screen opens TOC overlay""" # Create swipe up event from bottom of screen (y=1100, which is > 80% of 1200) event = TouchEvent( gesture=GestureType.SWIPE_UP, x=400, y=1100 ) # Handle gesture response = self.reader.handle_touch(event) # Should open overlay (navigation or toc, depending on implementation) self.assertEqual(response.action, ActionType.OVERLAY_OPENED) self.assertIn(response.data['overlay_type'], ['toc', 'navigation']) self.assertTrue(self.reader.is_overlay_open()) def test_swipe_up_from_middle_opens_navigation(self): """Test that swipe up from anywhere opens navigation overlay""" # Create swipe up event from middle of screen event = TouchEvent( gesture=GestureType.SWIPE_UP, x=400, y=600 ) # Handle gesture response = self.reader.handle_touch(event) # Should open navigation overlay from anywhere self.assertEqual(response.action, ActionType.OVERLAY_OPENED) self.assertIn(response.data['overlay_type'], ['toc', 'navigation']) self.assertTrue(self.reader.is_overlay_open()) def test_swipe_down_closes_overlay(self): """Test that swipe down closes the overlay""" # Open overlay first self.reader.open_toc_overlay() self.assertTrue(self.reader.is_overlay_open()) # Create swipe down event event = TouchEvent( gesture=GestureType.SWIPE_DOWN, x=400, y=300 ) # Handle gesture response = self.reader.handle_touch(event) # Should close overlay self.assertEqual(response.action, ActionType.OVERLAY_CLOSED) self.assertFalse(self.reader.is_overlay_open()) def test_tap_outside_overlay_closes_it(self): """Test that tapping outside the overlay panel closes it""" # Open overlay first self.reader.open_toc_overlay() self.assertTrue(self.reader.is_overlay_open()) # Tap in the far left (outside the centered panel) # Panel is 60% wide centered, so left edge is at 20% event = TouchEvent( gesture=GestureType.TAP, x=50, # Well outside panel y=600 ) # Handle gesture response = self.reader.handle_touch(event) # Should close overlay self.assertEqual(response.action, ActionType.OVERLAY_CLOSED) self.assertFalse(self.reader.is_overlay_open()) def test_tap_on_chapter_selects_and_closes(self): """Test that tapping on a chapter navigates to it and closes overlay""" # Open overlay first self.reader.open_toc_overlay() chapters = self.reader.get_chapters() if len(chapters) < 2: self.skipTest("Need at least 2 chapters for this test") # Calculate tap position for second chapter (index 1 - "Metamorphosis") # Based on actual measurements from pyWebLayout query_point: # Overlay bounds: (38, 138, 122, 16) -> X=[38,160], Y=[138,154] # With panel offset (160, 180): Screen X=[198,320], Y=[318,334] tap_x = 250 # Within the link text bounds tap_y = 335 # Chapter 1 "Metamorphosis" at overlay Y=155 (138+16=154, screen 180+155=335) event = TouchEvent( gesture=GestureType.TAP, x=tap_x, y=tap_y ) # Handle gesture response = self.reader.handle_touch(event) # Should select chapter self.assertEqual(response.action, ActionType.CHAPTER_SELECTED) self.assertIn('chapter_index', response.data) # Overlay should be closed self.assertFalse(self.reader.is_overlay_open()) def test_multiple_overlay_operations(self): """Test opening and closing overlay multiple times""" # Open and close 3 times for i in range(3): # Open self.reader.open_toc_overlay() self.assertTrue(self.reader.is_overlay_open()) # Close self.reader.close_overlay() self.assertFalse(self.reader.is_overlay_open()) def test_overlay_with_page_navigation(self): """Test that overlay works correctly after navigating pages""" # Navigate to page 2 self.reader.next_page() # Open overlay overlay_image = self.reader.open_toc_overlay() self.assertIsNotNone(overlay_image) self.assertTrue(self.reader.is_overlay_open()) # Close overlay self.reader.close_overlay() self.assertFalse(self.reader.is_overlay_open()) def test_toc_overlay_contains_all_chapters(self): """Test that TOC overlay includes all book chapters""" chapters = self.reader.get_chapters() # Open overlay (this generates the HTML with chapters) overlay_image = self.reader.open_toc_overlay() self.assertIsNotNone(overlay_image) # Verify overlay manager has correct chapter count # This is implicit in the rendering - if it renders without error, # all chapters were included self.assertTrue(self.reader.is_overlay_open()) def test_overlay_state_persistence_ready(self): """Test that overlay state can be tracked (for future state persistence)""" # This test verifies the state tracking is ready for StateManager integration # Start with no overlay self.assertEqual(self.reader.current_overlay_state, OverlayState.NONE) # Open TOC self.reader.open_toc_overlay() self.assertEqual(self.reader.current_overlay_state, OverlayState.TOC) # Close overlay self.reader.close_overlay() self.assertEqual(self.reader.current_overlay_state, OverlayState.NONE) class TestOverlayRendering(unittest.TestCase): """Test overlay rendering and compositing""" def setUp(self): """Set up test reader""" self.reader = EbookReader(page_size=(800, 1200)) test_epub = Path(__file__).parent / 'data' / 'library-epub' / 'alice.epub' if not test_epub.exists(): epub_dir = Path(__file__).parent / 'data' / 'library-epub' epubs = list(epub_dir.glob('*.epub')) if epubs: test_epub = epubs[0] else: self.skipTest("No test EPUB files available") self.reader.load_epub(str(test_epub)) def tearDown(self): """Clean up""" self.reader.close() def test_overlay_image_size(self): """Test that overlay image matches page size""" overlay_image = self.reader.open_toc_overlay() self.assertEqual(overlay_image.size, (800, 1200)) def test_overlay_compositing(self): """Test that overlay is properly composited on base page""" # Get base page base_page = self.reader.get_current_page() # Open overlay (creates composited image) overlay_image = self.reader.open_toc_overlay() # Composited image should be different from base page self.assertIsNotNone(overlay_image) # Images should have same size but different content self.assertEqual(base_page.size, overlay_image.size) def test_overlay_html_to_image_conversion(self): """Test that HTML overlay is correctly converted to image""" from dreader.html_generator import generate_toc_overlay # Get chapters chapters = self.reader.get_chapters() chapter_data = [{"index": idx, "title": title} for title, idx in chapters] # Generate HTML html = generate_toc_overlay(chapter_data) self.assertIsNotNone(html) self.assertIn("Table of Contents", html) # Open the TOC overlay which internally renders HTML to image overlay_image = self.reader.open_toc_overlay() # Should produce valid image self.assertIsNotNone(overlay_image) self.assertEqual(overlay_image.size, (800, 1200)) class TestTOCPagination(unittest.TestCase): """Test TOC overlay pagination functionality""" def setUp(self): """Set up test reader with a book""" self.reader = EbookReader(page_size=(800, 1200)) # Load a test EPUB test_epub = Path(__file__).parent / 'data' / 'library-epub' / 'alice.epub' if not test_epub.exists(): epub_dir = Path(__file__).parent / 'data' / 'library-epub' epubs = list(epub_dir.glob('*.epub')) if epubs: test_epub = epubs[0] else: self.skipTest("No test EPUB files available") success = self.reader.load_epub(str(test_epub)) self.assertTrue(success, "Failed to load test EPUB") def tearDown(self): """Clean up""" self.reader.close() def test_pagination_with_many_chapters(self): """Test pagination when there are more chapters than fit on one page""" from dreader.html_generator import generate_toc_overlay # Create test data with many chapters chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(25)] # Generate HTML for page 1 (chapters 0-9) html_page1 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10) self.assertIn("1. Chapter 1", html_page1) self.assertIn("10. Chapter 10", html_page1) self.assertNotIn("11. Chapter 11", html_page1) self.assertIn("Page 1 of 3", html_page1) # Generate HTML for page 2 (chapters 10-19) html_page2 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=1, toc_items_per_page=10) self.assertNotIn("10. Chapter 10", html_page2) self.assertIn("11. Chapter 11", html_page2) self.assertIn("20. Chapter 20", html_page2) self.assertIn("Page 2 of 3", html_page2) # Generate HTML for page 3 (chapters 20-24) html_page3 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=2, toc_items_per_page=10) self.assertNotIn("20. Chapter 20", html_page3) self.assertIn("21. Chapter 21", html_page3) self.assertIn("25. Chapter 25", html_page3) self.assertIn("Page 3 of 3", html_page3) def test_pagination_buttons_disabled_at_boundaries(self): """Test that pagination buttons are disabled at first and last pages""" from dreader.html_generator import generate_toc_overlay chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(25)] # Page 1: prev button should be disabled html_page1 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10) self.assertIn("page:prev", html_page1) self.assertIn("page:next", html_page1) # Check that prev button has disabled styling self.assertIn("opacity: 0.3; pointer-events: none;", html_page1) # Last page: next button should be disabled html_page3 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=2, toc_items_per_page=10) self.assertIn("page:prev", html_page3) self.assertIn("page:next", html_page3) def test_no_pagination_for_small_list(self): """Test that pagination is not shown when all chapters fit on one page""" from dreader.html_generator import generate_toc_overlay chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(5)] html = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10) self.assertNotIn("page:prev", html) self.assertNotIn("page:next", html) self.assertNotIn("Page", html.split("chapters")[1]) # No "Page X of Y" after "N chapters" def test_navigation_overlay_pagination(self): """Test pagination in the modern navigation overlay""" from dreader.html_generator import generate_navigation_overlay chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(25)] bookmarks = [{"name": f"Bookmark {i+1}", "position": f"Page {i}"} for i in range(15)] # Generate navigation overlay with pagination html = generate_navigation_overlay( chapters=chapters, bookmarks=bookmarks, active_tab="contents", page_size=(800, 1200), toc_page=1, toc_items_per_page=10, bookmarks_page=0 ) # Should show chapters 11-20 on page 2 self.assertIn("11. Chapter 11", html) self.assertIn("20. Chapter 20", html) self.assertNotIn("10. Chapter 10", html) self.assertNotIn("21. Chapter 21", html) def test_bookmarks_pagination(self): """Test pagination works for bookmarks tab too""" from dreader.html_generator import generate_navigation_overlay chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(5)] bookmarks = [{"name": f"Bookmark {i+1}", "position": f"Page {i}"} for i in range(25)] # Generate navigation overlay with bookmarks on page 2 html = generate_navigation_overlay( chapters=chapters, bookmarks=bookmarks, active_tab="bookmarks", page_size=(800, 1200), toc_page=0, toc_items_per_page=10, bookmarks_page=1 ) # Should show bookmarks 11-20 on page 2 self.assertIn("Bookmark 11", html) self.assertIn("Bookmark 20", html) self.assertNotIn("Bookmark 10", html) self.assertNotIn("Bookmark 21", html) def test_pagination_handles_empty_list(self): """Test pagination handles empty chapter list gracefully""" from dreader.html_generator import generate_toc_overlay chapters = [] html = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10) self.assertIn("0 chapters", html) self.assertNotIn("page:prev", html) self.assertNotIn("page:next", html) if __name__ == '__main__': unittest.main()