@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user