library and toc navigation
Python CI / test (push) Successful in 4m30s

This commit is contained in:
2025-11-08 12:20:23 +01:00
parent 60426432a0
commit 284a6e3393
13 changed files with 2365 additions and 219 deletions
+167
View File
@@ -0,0 +1,167 @@
"""
Unit tests for library interaction and tap detection.
These tests demonstrate the issue with interactive images in the library
and verify that tap detection works correctly.
"""
import unittest
from pathlib import Path
from dreader import LibraryManager
class TestLibraryInteraction(unittest.TestCase):
"""Test library browsing and tap interaction"""
def setUp(self):
"""Set up test library"""
self.library_path = Path(__file__).parent / 'data' / 'library-epub'
self.library = LibraryManager(
library_path=str(self.library_path),
page_size=(800, 1200)
)
def tearDown(self):
"""Clean up"""
self.library.cleanup()
def test_library_scan(self):
"""Test that library scanning finds books"""
books = self.library.scan_library()
# Should find at least one book
self.assertGreater(len(books), 0, "Library should contain at least one book")
# Each book should have required fields
for book in books:
self.assertIn('path', book)
self.assertIn('title', book)
self.assertIn('filename', book)
def test_library_table_creation(self):
"""Test that library table can be created"""
books = self.library.scan_library()
table = self.library.create_library_table()
# Table should exist
self.assertIsNotNone(table)
# Table should have body rows matching book count
body_rows = list(table.body_rows())
self.assertEqual(len(body_rows), len(books))
def test_library_rendering(self):
"""Test that library can be rendered to image"""
self.library.scan_library()
self.library.create_library_table()
# Render library
image = self.library.render_library()
# Image should be created with correct size
self.assertIsNotNone(image)
self.assertEqual(image.size, self.library.page_size)
def test_tap_detection_first_book(self):
"""Test that tapping on first book row selects it
The entire row is interactive, so tapping anywhere in the row
(not just on the cover image) will select the book.
"""
books = self.library.scan_library()
self.library.create_library_table()
self.library.render_library()
# Tap anywhere in the first book's row
# Based on layout: padding 30px, caption ~40px, first row starts at ~70px
selected_path = self.library.handle_library_tap(x=100, y=100)
# Should select the first book
self.assertIsNotNone(selected_path, "Tap should select a book")
self.assertEqual(selected_path, books[0]['path'], "Should select first book")
def test_tap_detection_second_book(self):
"""Test that tapping on second book selects it"""
books = self.library.scan_library()
if len(books) < 2:
self.skipTest("Need at least 2 books for this test")
self.library.create_library_table()
self.library.render_library()
# Tap in the region of the second book
# Row height is ~180px, so second book is at ~70 + 180 = 250px
selected_path = self.library.handle_library_tap(x=400, y=250)
# Should select the second book
self.assertIsNotNone(selected_path, "Tap should select a book")
self.assertEqual(selected_path, books[1]['path'], "Should select second book")
def test_tap_outside_table(self):
"""Test that tapping outside table returns None"""
self.library.scan_library()
self.library.create_library_table()
self.library.render_library()
# Tap outside the table area (far right)
selected_path = self.library.handle_library_tap(x=1000, y=100)
# Should not select anything
self.assertIsNone(selected_path, "Tap outside table should not select anything")
def test_tap_above_table(self):
"""Test that tapping in caption area returns None"""
self.library.scan_library()
self.library.create_library_table()
self.library.render_library()
# Tap in caption area (above first row)
selected_path = self.library.handle_library_tap(x=400, y=40)
# Should not select anything
self.assertIsNone(selected_path, "Tap in caption should not select anything")
def test_tap_below_last_book(self):
"""Test that tapping below all books returns None"""
books = self.library.scan_library()
self.library.create_library_table()
self.library.render_library()
# Tap way below the last book
# With 5 books and row height 180px: ~70 + (5 * 180) = 970px
selected_path = self.library.handle_library_tap(x=400, y=1100)
# Should not select anything
self.assertIsNone(selected_path, "Tap below last book should not select anything")
def test_multiple_taps(self):
"""Test that multiple taps work correctly"""
books = self.library.scan_library()
if len(books) < 3:
self.skipTest("Need at least 3 books for this test")
self.library.create_library_table()
self.library.render_library()
# Tap first book (row 0: y=60-180)
path1 = self.library.handle_library_tap(x=100, y=100)
self.assertEqual(path1, books[0]['path'])
# Tap second book (row 1: y=181-301)
path2 = self.library.handle_library_tap(x=400, y=250)
self.assertEqual(path2, books[1]['path'])
# Tap third book (row 2: y=302-422)
path3 = self.library.handle_library_tap(x=400, y=360)
self.assertEqual(path3, books[2]['path'])
# All should be different
self.assertNotEqual(path1, path2)
self.assertNotEqual(path2, path3)
self.assertNotEqual(path1, path3)
if __name__ == '__main__':
unittest.main()
+308
View File
@@ -0,0 +1,308 @@
"""
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 manager is properly initialized"""
self.assertIsNotNone(self.reader.overlay_manager)
self.assertEqual(self.reader.overlay_manager.page_size, (800, 1200))
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
self.assertEqual(response.action, ActionType.OVERLAY_OPENED)
self.assertEqual(response.data['overlay_type'], 'toc')
self.assertTrue(self.reader.is_overlay_open())
def test_swipe_up_from_middle_does_not_open_toc(self):
"""Test that swipe up from middle of screen does NOT open TOC"""
# Create swipe up event from middle of screen (y=600, which is < 80% of 1200)
event = TouchEvent(
gesture=GestureType.SWIPE_UP,
x=400,
y=600
)
# Handle gesture
response = self.reader.handle_touch(event)
# Should not open overlay
self.assertEqual(response.action, ActionType.NONE)
self.assertFalse(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: chapter 1 link is at screen Y=282, X=200-300
tap_x = 250 # Within the link text bounds
tap_y = 282 # Chapter 1 "Metamorphosis"
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)
# Render HTML to image using overlay manager
overlay_manager = self.reader.overlay_manager
image = overlay_manager.render_html_to_image(html)
# Should produce valid image
self.assertIsNotNone(image)
self.assertEqual(image.size, (800, 1200))
if __name__ == '__main__':
unittest.main()