improved library screen, fixed issues with image rendering and navigation
This commit is contained in:
parent
8f6205dcfe
commit
dd392d2e15
@ -45,7 +45,8 @@ class LibraryManager:
|
|||||||
self,
|
self,
|
||||||
library_path: str,
|
library_path: str,
|
||||||
cache_dir: Optional[str] = None,
|
cache_dir: Optional[str] = None,
|
||||||
page_size: Tuple[int, int] = (800, 1200)
|
page_size: Tuple[int, int] = (800, 1200),
|
||||||
|
books_per_page: int = 10
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize library manager.
|
Initialize library manager.
|
||||||
@ -54,9 +55,11 @@ class LibraryManager:
|
|||||||
library_path: Path to directory containing EPUB files
|
library_path: Path to directory containing EPUB files
|
||||||
cache_dir: Optional cache directory for covers. If None, uses default.
|
cache_dir: Optional cache directory for covers. If None, uses default.
|
||||||
page_size: Page size for library view rendering
|
page_size: Page size for library view rendering
|
||||||
|
books_per_page: Number of books to display per page (must be even for 2-column layout)
|
||||||
"""
|
"""
|
||||||
self.library_path = Path(library_path)
|
self.library_path = Path(library_path)
|
||||||
self.page_size = page_size
|
self.page_size = page_size
|
||||||
|
self.books_per_page = books_per_page if books_per_page % 2 == 0 else books_per_page + 1
|
||||||
|
|
||||||
# Set cache directory
|
# Set cache directory
|
||||||
if cache_dir:
|
if cache_dir:
|
||||||
@ -75,6 +78,7 @@ class LibraryManager:
|
|||||||
self.temp_cover_files: List[str] = [] # Track temp files for cleanup
|
self.temp_cover_files: List[str] = [] # Track temp files for cleanup
|
||||||
self.row_bounds: List[Tuple[int, int, int, int]] = [] # Bounding boxes for rows (x, y, w, h)
|
self.row_bounds: List[Tuple[int, int, int, int]] = [] # Bounding boxes for rows (x, y, w, h)
|
||||||
self.table_renderer: Optional[TableRenderer] = None # Store renderer for bounds info
|
self.table_renderer: Optional[TableRenderer] = None # Store renderer for bounds info
|
||||||
|
self.current_page: int = 0 # Current page index for pagination
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_default_cache_dir() -> Path:
|
def _get_default_cache_dir() -> Path:
|
||||||
@ -149,12 +153,13 @@ class LibraryManager:
|
|||||||
print(f"Error caching cover for {book['title']}: {e}")
|
print(f"Error caching cover for {book['title']}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create_library_table(self, books: Optional[List[Dict]] = None) -> Table:
|
def create_library_table(self, books: Optional[List[Dict]] = None, page: Optional[int] = None) -> Table:
|
||||||
"""
|
"""
|
||||||
Create interactive library table with book covers and info.
|
Create interactive library table with book covers and info in 2-column grid.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
books: List of books to display. If None, uses self.books
|
books: List of books to display. If None, uses self.books
|
||||||
|
page: Page number to display (0-indexed). If None, uses self.current_page
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Table object ready for rendering
|
Table object ready for rendering
|
||||||
@ -162,27 +167,73 @@ class LibraryManager:
|
|||||||
if books is None:
|
if books is None:
|
||||||
books = self.books
|
books = self.books
|
||||||
|
|
||||||
|
if page is None:
|
||||||
|
page = self.current_page
|
||||||
|
|
||||||
if not books:
|
if not books:
|
||||||
print("No books to display in library")
|
print("No books to display in library")
|
||||||
books = []
|
books = []
|
||||||
|
|
||||||
print(f"Creating library table with {len(books)} books...")
|
# Calculate pagination
|
||||||
|
total_pages = (len(books) + self.books_per_page - 1) // self.books_per_page
|
||||||
|
start_idx = page * self.books_per_page
|
||||||
|
end_idx = min(start_idx + self.books_per_page, len(books))
|
||||||
|
page_books = books[start_idx:end_idx]
|
||||||
|
|
||||||
# Create table
|
print(f"Creating library table with {len(page_books)} books (page {page + 1}/{total_pages})...")
|
||||||
table = Table(caption="My Library", style=Font(font_size=18, weight="bold"))
|
|
||||||
|
|
||||||
# Add books as rows
|
# Create table with caption showing page info
|
||||||
for i, book in enumerate(books):
|
caption_text = f"My Library (Page {page + 1}/{total_pages})" if total_pages > 1 else "My Library"
|
||||||
row = table.create_row("body")
|
table = Table(caption=caption_text, style=Font(font_size=18, weight="bold"))
|
||||||
|
|
||||||
# Cover cell with interactive image
|
# Add books in 2-column grid (each pair of books gets 2 rows: covers then details)
|
||||||
|
for i in range(0, len(page_books), 2):
|
||||||
|
# Row 1: Covers for this pair
|
||||||
|
cover_row = table.create_row("body")
|
||||||
|
|
||||||
|
# Add first book's cover (left column)
|
||||||
|
self._add_book_cover(cover_row, page_books[i])
|
||||||
|
|
||||||
|
# Add second book's cover (right column) if it exists
|
||||||
|
if i + 1 < len(page_books):
|
||||||
|
self._add_book_cover(cover_row, page_books[i + 1])
|
||||||
|
else:
|
||||||
|
# Add empty cell if odd number of books
|
||||||
|
cover_row.create_cell()
|
||||||
|
|
||||||
|
# Row 2: Details for this pair
|
||||||
|
details_row = table.create_row("body")
|
||||||
|
|
||||||
|
# Add first book's details (left column)
|
||||||
|
self._add_book_details(details_row, page_books[i])
|
||||||
|
|
||||||
|
# Add second book's details (right column) if it exists
|
||||||
|
if i + 1 < len(page_books):
|
||||||
|
self._add_book_details(details_row, page_books[i + 1])
|
||||||
|
else:
|
||||||
|
# Add empty cell if odd number of books
|
||||||
|
details_row.create_cell()
|
||||||
|
|
||||||
|
self.library_table = table
|
||||||
|
return table
|
||||||
|
|
||||||
|
def _add_book_cover(self, row, book: Dict):
|
||||||
|
"""
|
||||||
|
Add a book cover to a table row.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row: Table row to add cover to
|
||||||
|
book: Book dictionary with metadata
|
||||||
|
"""
|
||||||
cover_cell = row.create_cell()
|
cover_cell = row.create_cell()
|
||||||
|
|
||||||
cover_path = book.get('cover_path')
|
cover_path = book.get('cover_path')
|
||||||
book_path = book['path']
|
book_path = book['path']
|
||||||
|
|
||||||
# Create callback that returns book path
|
# Create callback that returns book path
|
||||||
callback = lambda point, path=book_path: path
|
callback = lambda point, path=book_path: path
|
||||||
|
|
||||||
|
# Add cover image
|
||||||
if cover_path and Path(cover_path).exists():
|
if cover_path and Path(cover_path).exists():
|
||||||
# Use cached cover with callback
|
# Use cached cover with callback
|
||||||
img = InteractiveImage.create_and_add_to(
|
img = InteractiveImage.create_and_add_to(
|
||||||
@ -216,29 +267,33 @@ class LibraryManager:
|
|||||||
# No cover available
|
# No cover available
|
||||||
self._add_no_cover_text(cover_cell)
|
self._add_no_cover_text(cover_cell)
|
||||||
|
|
||||||
# Book info cell
|
def _add_book_details(self, row, book: Dict):
|
||||||
info_cell = row.create_cell()
|
"""
|
||||||
|
Add book details (title, author, filename) to a table row.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row: Table row to add details to
|
||||||
|
book: Book dictionary with metadata
|
||||||
|
"""
|
||||||
|
details_cell = row.create_cell()
|
||||||
|
|
||||||
# Title paragraph
|
# Title paragraph
|
||||||
title_para = info_cell.create_paragraph()
|
title_para = details_cell.create_paragraph()
|
||||||
for word in book['title'].split():
|
for word in book['title'].split():
|
||||||
title_para.add_word(Word(word, Font(font_size=14, weight="bold")))
|
title_para.add_word(Word(word, Font(font_size=14, weight="bold")))
|
||||||
|
|
||||||
# Author paragraph
|
# Author paragraph
|
||||||
author_para = info_cell.create_paragraph()
|
author_para = details_cell.create_paragraph()
|
||||||
for word in book.get('author', 'Unknown').split():
|
for word in book.get('author', 'Unknown').split():
|
||||||
author_para.add_word(Word(word, Font(font_size=12)))
|
author_para.add_word(Word(word, Font(font_size=12)))
|
||||||
|
|
||||||
# Filename paragraph (small, gray)
|
# Filename paragraph (small, gray)
|
||||||
filename_para = info_cell.create_paragraph()
|
filename_para = details_cell.create_paragraph()
|
||||||
filename_para.add_word(Word(
|
filename_para.add_word(Word(
|
||||||
Path(book['path']).name,
|
Path(book['path']).name,
|
||||||
Font(font_size=10, colour=(150, 150, 150))
|
Font(font_size=10, colour=(150, 150, 150))
|
||||||
))
|
))
|
||||||
|
|
||||||
self.library_table = table
|
|
||||||
return table
|
|
||||||
|
|
||||||
def _add_no_cover_text(self, cell):
|
def _add_no_cover_text(self, cell):
|
||||||
"""Add placeholder text when no cover is available"""
|
"""Add placeholder text when no cover is available"""
|
||||||
para = cell.create_paragraph()
|
para = cell.create_paragraph()
|
||||||
@ -306,10 +361,11 @@ class LibraryManager:
|
|||||||
|
|
||||||
def handle_library_tap(self, x: int, y: int) -> Optional[str]:
|
def handle_library_tap(self, x: int, y: int) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Handle tap event on library view.
|
Handle tap event on library view with 2-column grid.
|
||||||
|
|
||||||
Checks if the tap is within any row's bounds and returns the corresponding
|
The layout has alternating rows: cover rows and detail rows.
|
||||||
book path. This makes the entire row interactive, not just the cover image.
|
Each pair of rows (cover + detail) represents one pair of books (2 books).
|
||||||
|
Tapping on either the cover row or detail row selects the corresponding book.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
x: X coordinate of tap
|
x: X coordinate of tap
|
||||||
@ -323,6 +379,11 @@ class LibraryManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Get paginated books for current page
|
||||||
|
start_idx = self.current_page * self.books_per_page
|
||||||
|
end_idx = min(start_idx + self.books_per_page, len(self.books))
|
||||||
|
page_books = self.books[start_idx:end_idx]
|
||||||
|
|
||||||
# Build a mapping of row sections in order
|
# Build a mapping of row sections in order
|
||||||
all_rows = list(self.library_table.all_rows())
|
all_rows = list(self.library_table.all_rows())
|
||||||
|
|
||||||
@ -345,10 +406,42 @@ class LibraryManager:
|
|||||||
# Find which body row this is (0-indexed)
|
# Find which body row this is (0-indexed)
|
||||||
body_row_index = sum(1 for s, _ in all_rows[:row_idx] if s == "body")
|
body_row_index = sum(1 for s, _ in all_rows[:row_idx] if s == "body")
|
||||||
|
|
||||||
# Return the corresponding book
|
# Each pair of books uses 2 rows (cover row + detail row)
|
||||||
if body_row_index < len(self.books):
|
# Determine which book pair this row belongs to
|
||||||
book_path = self.books[body_row_index]['path']
|
book_pair_index = body_row_index // 2 # Which pair of books (0, 1, 2, ...)
|
||||||
print(f"Book selected (row {body_row_index}): {book_path}")
|
is_cover_row = body_row_index % 2 == 0 # Even rows are covers, odd are details
|
||||||
|
|
||||||
|
# Check cell renderers in this row
|
||||||
|
if hasattr(row_renderer, '_cell_renderers') and len(row_renderer._cell_renderers) >= 1:
|
||||||
|
# Check left cell (first book in pair)
|
||||||
|
left_cell = row_renderer._cell_renderers[0]
|
||||||
|
left_x, left_y = left_cell._origin
|
||||||
|
left_w, left_h = left_cell._size
|
||||||
|
|
||||||
|
if (left_x <= x <= left_x + left_w and
|
||||||
|
left_y <= y <= left_y + left_h):
|
||||||
|
# Left column (first book in pair)
|
||||||
|
book_index = book_pair_index * 2
|
||||||
|
if book_index < len(page_books):
|
||||||
|
book_path = page_books[book_index]['path']
|
||||||
|
row_type = "cover" if is_cover_row else "detail"
|
||||||
|
print(f"Book selected (pair {book_pair_index}, left {row_type}): {book_path}")
|
||||||
|
return book_path
|
||||||
|
|
||||||
|
# Check right cell (second book in pair) if it exists
|
||||||
|
if len(row_renderer._cell_renderers) >= 2:
|
||||||
|
right_cell = row_renderer._cell_renderers[1]
|
||||||
|
right_x, right_y = right_cell._origin
|
||||||
|
right_w, right_h = right_cell._size
|
||||||
|
|
||||||
|
if (right_x <= x <= right_x + right_w and
|
||||||
|
right_y <= y <= right_y + right_h):
|
||||||
|
# Right column (second book in pair)
|
||||||
|
book_index = book_pair_index * 2 + 1
|
||||||
|
if book_index < len(page_books):
|
||||||
|
book_path = page_books[book_index]['path']
|
||||||
|
row_type = "cover" if is_cover_row else "detail"
|
||||||
|
print(f"Book selected (pair {book_pair_index}, right {row_type}): {book_path}")
|
||||||
return book_path
|
return book_path
|
||||||
|
|
||||||
print(f"No book tapped at ({x}, {y})")
|
print(f"No book tapped at ({x}, {y})")
|
||||||
@ -374,6 +467,56 @@ class LibraryManager:
|
|||||||
return self.books[index]
|
return self.books[index]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def next_page(self) -> bool:
|
||||||
|
"""
|
||||||
|
Navigate to next page of library.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if page changed, False if already on last page
|
||||||
|
"""
|
||||||
|
total_pages = (len(self.books) + self.books_per_page - 1) // self.books_per_page
|
||||||
|
if self.current_page < total_pages - 1:
|
||||||
|
self.current_page += 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def previous_page(self) -> bool:
|
||||||
|
"""
|
||||||
|
Navigate to previous page of library.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if page changed, False if already on first page
|
||||||
|
"""
|
||||||
|
if self.current_page > 0:
|
||||||
|
self.current_page -= 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_page(self, page: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set current page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Page number (0-indexed)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if page changed, False if invalid page
|
||||||
|
"""
|
||||||
|
total_pages = (len(self.books) + self.books_per_page - 1) // self.books_per_page
|
||||||
|
if 0 <= page < total_pages:
|
||||||
|
self.current_page = page
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_total_pages(self) -> int:
|
||||||
|
"""
|
||||||
|
Get total number of pages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Total number of pages
|
||||||
|
"""
|
||||||
|
return (len(self.books) + self.books_per_page - 1) // self.books_per_page
|
||||||
|
|
||||||
def get_library_state(self) -> LibraryState:
|
def get_library_state(self) -> LibraryState:
|
||||||
"""
|
"""
|
||||||
Get current library state for persistence.
|
Get current library state for persistence.
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class DocumentManager:
|
|||||||
|
|
||||||
# Extract metadata
|
# Extract metadata
|
||||||
self.title = book.get_title() or "Unknown Title"
|
self.title = book.get_title() or "Unknown Title"
|
||||||
self.author = book.get_metadata('AUTHOR') or "Unknown Author"
|
self.author = book.get_author() or "Unknown Author"
|
||||||
|
|
||||||
# Create document ID from filename
|
# Create document ID from filename
|
||||||
self.document_id = Path(epub_path).stem
|
self.document_id = Path(epub_path).stem
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test the debug overlay visualization feature.
|
|
||||||
This creates an overlay with debug bounding boxes and saves it as an image.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Enable debug mode
|
|
||||||
os.environ['DREADER_DEBUG_OVERLAY'] = '1'
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from dreader.application import EbookReader
|
|
||||||
from dreader.overlays.settings import SettingsOverlay
|
|
||||||
from dreader.overlays.navigation import NavigationOverlay
|
|
||||||
|
|
||||||
def test_settings_overlay_debug():
|
|
||||||
"""Create settings overlay with debug visualization."""
|
|
||||||
print("="*70)
|
|
||||||
print("Creating Settings Overlay with Debug Bounding Boxes")
|
|
||||||
print("="*70)
|
|
||||||
|
|
||||||
# Create reader
|
|
||||||
reader = EbookReader(page_size=(800, 1200))
|
|
||||||
|
|
||||||
# Load a test book
|
|
||||||
test_book = Path(__file__).parent / "tests" / "data" / "library-epub" / "pg11-images-3.epub"
|
|
||||||
if not test_book.exists():
|
|
||||||
print(f"Test book not found: {test_book}")
|
|
||||||
return
|
|
||||||
|
|
||||||
reader.load_epub(str(test_book))
|
|
||||||
|
|
||||||
# Create settings overlay
|
|
||||||
settings_overlay = SettingsOverlay(reader)
|
|
||||||
base_page = reader.get_current_page()
|
|
||||||
|
|
||||||
# Open overlay (with debug mode, this will draw bounding boxes)
|
|
||||||
overlay_image = settings_overlay.open(
|
|
||||||
base_page,
|
|
||||||
font_scale=1.0,
|
|
||||||
line_spacing=5,
|
|
||||||
inter_block_spacing=15,
|
|
||||||
word_spacing=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save the result
|
|
||||||
output_path = Path(__file__).parent / "settings_overlay_debug.png"
|
|
||||||
overlay_image.save(output_path)
|
|
||||||
print(f"\nSettings overlay with debug boxes saved to: {output_path}")
|
|
||||||
print("Red boxes show clickable areas")
|
|
||||||
|
|
||||||
reader.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_navigation_overlay_debug():
|
|
||||||
"""Create navigation overlay with debug visualization."""
|
|
||||||
print("\n" + "="*70)
|
|
||||||
print("Creating Navigation Overlay with Debug Bounding Boxes")
|
|
||||||
print("="*70)
|
|
||||||
|
|
||||||
# Create reader
|
|
||||||
reader = EbookReader(page_size=(800, 1200))
|
|
||||||
|
|
||||||
# Load a test book
|
|
||||||
test_book = Path(__file__).parent / "tests" / "data" / "library-epub" / "pg11-images-3.epub"
|
|
||||||
if not test_book.exists():
|
|
||||||
print(f"Test book not found: {test_book}")
|
|
||||||
return
|
|
||||||
|
|
||||||
reader.load_epub(str(test_book))
|
|
||||||
|
|
||||||
# Create navigation overlay
|
|
||||||
nav_overlay = NavigationOverlay(reader)
|
|
||||||
base_page = reader.get_current_page()
|
|
||||||
|
|
||||||
# Get chapters
|
|
||||||
chapters = reader.get_chapters()
|
|
||||||
|
|
||||||
# Open overlay (with debug mode, this will draw bounding boxes)
|
|
||||||
overlay_image = nav_overlay.open(
|
|
||||||
base_page,
|
|
||||||
chapters=chapters,
|
|
||||||
bookmarks=[{"name": "Test Bookmark"}],
|
|
||||||
active_tab="contents"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save the result
|
|
||||||
output_path = Path(__file__).parent / "navigation_overlay_debug.png"
|
|
||||||
overlay_image.save(output_path)
|
|
||||||
print(f"\nNavigation overlay with debug boxes saved to: {output_path}")
|
|
||||||
print("Red boxes show clickable areas")
|
|
||||||
|
|
||||||
reader.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_settings_overlay_debug()
|
|
||||||
test_navigation_overlay_debug()
|
|
||||||
print("\n" + "="*70)
|
|
||||||
print("Debug visualizations created!")
|
|
||||||
print("Check the PNG files to see clickable areas marked with red boxes.")
|
|
||||||
print("="*70)
|
|
||||||
@ -1,211 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Quick integration test for the main application controller.
|
|
||||||
|
|
||||||
This test verifies that all components integrate correctly without
|
|
||||||
requiring a GUI. It uses a mock HAL to simulate display/input.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import asyncio
|
|
||||||
from pathlib import Path
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from dreader.main import DReaderApplication, AppConfig
|
|
||||||
from dreader.hal import DisplayHAL
|
|
||||||
from dreader.gesture import TouchEvent, GestureType
|
|
||||||
|
|
||||||
|
|
||||||
class MockDisplayHAL(DisplayHAL):
|
|
||||||
"""Mock HAL for headless testing."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.images_displayed = []
|
|
||||||
self.events = []
|
|
||||||
|
|
||||||
async def show_image(self, image: Image.Image):
|
|
||||||
"""Record displayed images."""
|
|
||||||
self.images_displayed.append(image)
|
|
||||||
print(f" [HAL] Displayed image: {image.size}")
|
|
||||||
|
|
||||||
async def get_touch_event(self):
|
|
||||||
"""Return queued events."""
|
|
||||||
if self.events:
|
|
||||||
return self.events.pop(0)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def set_brightness(self, level: int):
|
|
||||||
"""Mock brightness control."""
|
|
||||||
print(f" [HAL] Brightness set to {level}")
|
|
||||||
|
|
||||||
def queue_event(self, event: TouchEvent):
|
|
||||||
"""Add event to queue for testing."""
|
|
||||||
self.events.append(event)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_integration():
|
|
||||||
"""Test the complete integration."""
|
|
||||||
print("=" * 70)
|
|
||||||
print("DReader Main Application Integration Test")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
# Find test library
|
|
||||||
library_path = Path(__file__).parent / "tests" / "data" / "library-epub"
|
|
||||||
|
|
||||||
if not library_path.exists():
|
|
||||||
print(f"\nError: Test library not found at {library_path}")
|
|
||||||
print("Please ensure test EPUB files exist in tests/data/library-epub/")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"\nLibrary path: {library_path}")
|
|
||||||
|
|
||||||
# Create mock HAL
|
|
||||||
hal = MockDisplayHAL()
|
|
||||||
|
|
||||||
# Create config
|
|
||||||
config = AppConfig(
|
|
||||||
display_hal=hal,
|
|
||||||
library_path=str(library_path),
|
|
||||||
page_size=(800, 1200),
|
|
||||||
auto_save_interval=999 # Don't auto-save during test
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create application
|
|
||||||
app = DReaderApplication(config)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Test 1: Start application (should show library)
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 1: Starting application")
|
|
||||||
print("-" * 70)
|
|
||||||
await app.start()
|
|
||||||
print(f"✓ Application started")
|
|
||||||
print(f" Current mode: {app.get_current_mode()}")
|
|
||||||
print(f" Images displayed: {len(hal.images_displayed)}")
|
|
||||||
|
|
||||||
assert len(hal.images_displayed) > 0, "No image displayed after start"
|
|
||||||
assert app.get_current_mode().value == "library", "Should start in library mode"
|
|
||||||
print("✓ Test 1 passed")
|
|
||||||
|
|
||||||
# Test 2: Simulate tap on first book
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 2: Selecting a book from library")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
# Tap in approximate location of first book
|
|
||||||
tap_event = TouchEvent(GestureType.TAP, 400, 150)
|
|
||||||
print(f" Simulating tap at (400, 150)")
|
|
||||||
|
|
||||||
images_before = len(hal.images_displayed)
|
|
||||||
await app.handle_touch(tap_event)
|
|
||||||
|
|
||||||
print(f" Current mode: {app.get_current_mode()}")
|
|
||||||
print(f" New images displayed: {len(hal.images_displayed) - images_before}")
|
|
||||||
|
|
||||||
# Should have transitioned to reading mode
|
|
||||||
if app.get_current_mode().value == "reading":
|
|
||||||
print("✓ Successfully entered reading mode")
|
|
||||||
print("✓ Test 2 passed")
|
|
||||||
else:
|
|
||||||
print("⚠ Tap may not have hit a book (this is OK for the test)")
|
|
||||||
print(" Manually entering reading mode for further tests...")
|
|
||||||
|
|
||||||
# Get first book from library and enter reading mode manually
|
|
||||||
if app.library:
|
|
||||||
books = app.library.scan_library()
|
|
||||||
if books:
|
|
||||||
await app._enter_reading_mode(books[0]['path'])
|
|
||||||
print(f"✓ Loaded book: {books[0]['title']}")
|
|
||||||
|
|
||||||
# Test 3: Page navigation
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 3: Page navigation")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
if app.get_current_mode().value == "reading":
|
|
||||||
images_before = len(hal.images_displayed)
|
|
||||||
|
|
||||||
# Next page (swipe left)
|
|
||||||
swipe_event = TouchEvent(GestureType.SWIPE_LEFT, 600, 600)
|
|
||||||
print(" Simulating swipe left (next page)")
|
|
||||||
await app.handle_touch(swipe_event)
|
|
||||||
|
|
||||||
print(f" New images displayed: {len(hal.images_displayed) - images_before}")
|
|
||||||
print("✓ Test 3 passed")
|
|
||||||
else:
|
|
||||||
print("⊘ Skipping (not in reading mode)")
|
|
||||||
|
|
||||||
# Test 4: Font size change (pinch gesture)
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 4: Font size adjustment")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
if app.get_current_mode().value == "reading":
|
|
||||||
images_before = len(hal.images_displayed)
|
|
||||||
|
|
||||||
# Increase font size
|
|
||||||
pinch_event = TouchEvent(GestureType.PINCH_OUT, 400, 600)
|
|
||||||
print(" Simulating pinch out (increase font)")
|
|
||||||
await app.handle_touch(pinch_event)
|
|
||||||
|
|
||||||
print(f" New images displayed: {len(hal.images_displayed) - images_before}")
|
|
||||||
print("✓ Test 4 passed")
|
|
||||||
else:
|
|
||||||
print("⊘ Skipping (not in reading mode)")
|
|
||||||
|
|
||||||
# Test 5: State persistence check
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 5: State persistence")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
state = app.state
|
|
||||||
print(f" Current mode: {state.mode}")
|
|
||||||
print(f" Current book: {state.current_book}")
|
|
||||||
print(f" Font scale: {state.settings.font_scale}")
|
|
||||||
|
|
||||||
# Save state
|
|
||||||
success = app.state_manager.save_state(force=True)
|
|
||||||
if success:
|
|
||||||
print("✓ State saved successfully")
|
|
||||||
print("✓ Test 5 passed")
|
|
||||||
else:
|
|
||||||
print("✗ State save failed")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Test 6: Graceful shutdown
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 6: Graceful shutdown")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
await app.shutdown()
|
|
||||||
print("✓ Application shut down cleanly")
|
|
||||||
print("✓ Test 6 passed")
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
print("✓ ALL TESTS PASSED")
|
|
||||||
print("=" * 70)
|
|
||||||
print(f"\nTotal images displayed: {len(hal.images_displayed)}")
|
|
||||||
print(f"Final mode: {app.get_current_mode()}")
|
|
||||||
print("\nIntegration test successful!")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n✗ TEST FAILED: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point."""
|
|
||||||
success = asyncio.run(test_integration())
|
|
||||||
sys.exit(0 if success else 1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test page navigation (forward and backward).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from dreader.application import EbookReader
|
|
||||||
|
|
||||||
def test_navigation():
|
|
||||||
"""Test forward and backward page navigation."""
|
|
||||||
|
|
||||||
# Find a test book
|
|
||||||
epub_path = Path("tests/data/library-epub/pg11-images-3.epub")
|
|
||||||
|
|
||||||
if not epub_path.exists():
|
|
||||||
print(f"Error: Test EPUB not found at {epub_path}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("=" * 70)
|
|
||||||
print("Page Navigation Test")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
# Create reader
|
|
||||||
reader = EbookReader(page_size=(800, 1200))
|
|
||||||
|
|
||||||
# Load book
|
|
||||||
print(f"\nLoading: {epub_path}")
|
|
||||||
if not reader.load_epub(str(epub_path)):
|
|
||||||
print("Failed to load EPUB")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✓ Loaded: {reader.book_title}")
|
|
||||||
|
|
||||||
# Get starting position
|
|
||||||
start_pos = reader.get_position_info()
|
|
||||||
print(f"\nStarting position: {start_pos}")
|
|
||||||
|
|
||||||
# Test forward navigation
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 1: Forward navigation (next_page)")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
for i in range(3):
|
|
||||||
page = reader.next_page()
|
|
||||||
if page:
|
|
||||||
pos = reader.get_position_info()
|
|
||||||
print(f" Page {i+1}: position={pos['position']}, progress={reader.get_reading_progress()*100:.1f}%")
|
|
||||||
else:
|
|
||||||
print(f" Page {i+1}: Failed to advance")
|
|
||||||
return False
|
|
||||||
|
|
||||||
forward_pos = reader.get_position_info()
|
|
||||||
print(f"\nPosition after 3 forward: {forward_pos}")
|
|
||||||
|
|
||||||
# Test backward navigation
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 2: Backward navigation (previous_page)")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
for i in range(3):
|
|
||||||
page = reader.previous_page()
|
|
||||||
if page:
|
|
||||||
pos = reader.get_position_info()
|
|
||||||
print(f" Back {i+1}: position={pos['position']}, progress={reader.get_reading_progress()*100:.1f}%")
|
|
||||||
else:
|
|
||||||
print(f" Back {i+1}: Failed to go back (might be at start)")
|
|
||||||
|
|
||||||
final_pos = reader.get_position_info()
|
|
||||||
print(f"\nPosition after 3 backward: {final_pos}")
|
|
||||||
|
|
||||||
# Verify we're back at start
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 3: Verify position")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
if final_pos['position'] == start_pos['position']:
|
|
||||||
print("✓ Successfully returned to starting position")
|
|
||||||
print("✓ Backward navigation working correctly")
|
|
||||||
result = True
|
|
||||||
else:
|
|
||||||
print(f"✗ Position mismatch!")
|
|
||||||
print(f" Expected: {start_pos['position']}")
|
|
||||||
print(f" Got: {final_pos['position']}")
|
|
||||||
result = False
|
|
||||||
|
|
||||||
# Test going back from start (should return None or current page)
|
|
||||||
print("\n" + "-" * 70)
|
|
||||||
print("Test 4: Try going back from first page")
|
|
||||||
print("-" * 70)
|
|
||||||
|
|
||||||
page = reader.previous_page()
|
|
||||||
if page:
|
|
||||||
pos = reader.get_position_info()
|
|
||||||
print(f" Still at position: {pos['position']}")
|
|
||||||
if pos['position'] == 0:
|
|
||||||
print("✓ Correctly stayed at first page")
|
|
||||||
else:
|
|
||||||
print("⚠ Position changed but shouldn't have")
|
|
||||||
else:
|
|
||||||
print(" previous_page() returned None (at start)")
|
|
||||||
print("✓ Correctly indicated at start of book")
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
reader.close()
|
|
||||||
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
if result:
|
|
||||||
print("✓ ALL TESTS PASSED - Page navigation working correctly")
|
|
||||||
else:
|
|
||||||
print("✗ TESTS FAILED")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
success = test_navigation()
|
|
||||||
sys.exit(0 if success else 1)
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test swipe detection in Pygame HAL.
|
|
||||||
This will show you how to perform swipes and what gestures are detected.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from dreader.hal_pygame import PygameDisplayHAL
|
|
||||||
from dreader.gesture import GestureType
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
|
|
||||||
async def test_swipes():
|
|
||||||
"""Test swipe detection with visual feedback."""
|
|
||||||
|
|
||||||
print("=" * 70)
|
|
||||||
print("Swipe Detection Test")
|
|
||||||
print("=" * 70)
|
|
||||||
print("\nInstructions:")
|
|
||||||
print(" - Click and drag to create swipes")
|
|
||||||
print(" - Drag at least 30 pixels for swipe detection")
|
|
||||||
print(" - Short movements are detected as taps")
|
|
||||||
print(" - Press Q or ESC to quit")
|
|
||||||
print("\nSwipe directions:")
|
|
||||||
print(" - Drag LEFT → Next page (SWIPE_LEFT)")
|
|
||||||
print(" - Drag RIGHT → Previous page (SWIPE_RIGHT)")
|
|
||||||
print(" - Drag UP → Scroll up (SWIPE_UP)")
|
|
||||||
print(" - Drag DOWN → Scroll down (SWIPE_DOWN)")
|
|
||||||
print("\nOR use keyboard shortcuts:")
|
|
||||||
print(" - Left/Right Arrow or Space/PageUp/PageDown")
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
|
|
||||||
# Create HAL
|
|
||||||
hal = PygameDisplayHAL(width=800, height=600, title="Swipe Detection Test")
|
|
||||||
|
|
||||||
await hal.initialize()
|
|
||||||
|
|
||||||
# Create instruction image
|
|
||||||
img = Image.new('RGB', (800, 600), color=(240, 240, 240))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
try:
|
|
||||||
font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
|
|
||||||
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
|
|
||||||
except:
|
|
||||||
font_large = ImageFont.load_default()
|
|
||||||
font_small = ImageFont.load_default()
|
|
||||||
|
|
||||||
# Draw instructions
|
|
||||||
y = 50
|
|
||||||
draw.text((400, y), "Swipe Detection Test", fill=(0, 0, 0), font=font_large, anchor="mt")
|
|
||||||
|
|
||||||
y += 80
|
|
||||||
instructions = [
|
|
||||||
"Click and DRAG to create swipes:",
|
|
||||||
"",
|
|
||||||
"← Drag LEFT = Next Page",
|
|
||||||
"→ Drag RIGHT = Previous Page",
|
|
||||||
"↑ Drag UP = Scroll Up",
|
|
||||||
"↓ Drag DOWN = Scroll Down",
|
|
||||||
"",
|
|
||||||
"Minimum drag distance: 30 pixels",
|
|
||||||
"",
|
|
||||||
"Or use keyboard:",
|
|
||||||
"Space/Right Arrow = Next",
|
|
||||||
"Left Arrow = Previous",
|
|
||||||
"",
|
|
||||||
"Press Q or ESC to quit"
|
|
||||||
]
|
|
||||||
|
|
||||||
for line in instructions:
|
|
||||||
draw.text((400, y), line, fill=(0, 0, 0), font=font_small, anchor="mt")
|
|
||||||
y += 30
|
|
||||||
|
|
||||||
await hal.show_image(img)
|
|
||||||
|
|
||||||
# Event loop
|
|
||||||
hal.running = True
|
|
||||||
gesture_count = 0
|
|
||||||
last_gesture = None
|
|
||||||
|
|
||||||
print("\nWaiting for gestures... (window is now open)")
|
|
||||||
|
|
||||||
while hal.running:
|
|
||||||
event = await hal.get_touch_event()
|
|
||||||
|
|
||||||
if event:
|
|
||||||
gesture_count += 1
|
|
||||||
last_gesture = event.gesture
|
|
||||||
|
|
||||||
print(f"\n[{gesture_count}] Detected: {event.gesture.value}")
|
|
||||||
print(f" Position: ({event.x}, {event.y})")
|
|
||||||
|
|
||||||
# Visual feedback
|
|
||||||
feedback_img = img.copy()
|
|
||||||
feedback_draw = ImageDraw.Draw(feedback_img)
|
|
||||||
|
|
||||||
# Draw gesture type
|
|
||||||
gesture_text = f"Gesture #{gesture_count}: {event.gesture.value.upper()}"
|
|
||||||
feedback_draw.rectangle([(0, 550), (800, 600)], fill=(50, 150, 50))
|
|
||||||
feedback_draw.text((400, 575), gesture_text, fill=(255, 255, 255), font=font_large, anchor="mm")
|
|
||||||
|
|
||||||
await hal.show_image(feedback_img)
|
|
||||||
|
|
||||||
# Brief pause to show feedback
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
await hal.show_image(img)
|
|
||||||
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
|
|
||||||
await hal.cleanup()
|
|
||||||
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
print(f"Test complete! Detected {gesture_count} gestures.")
|
|
||||||
if last_gesture:
|
|
||||||
print(f"Last gesture: {last_gesture.value}")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(test_swipes())
|
|
||||||
85
tests/test_backward_nav_minimal.py
Normal file
85
tests/test_backward_nav_minimal.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"""
|
||||||
|
Minimal reproduction test for backward navigation bug.
|
||||||
|
|
||||||
|
BUG: Backward navigation cannot reach block_index=0 from block_index=1.
|
||||||
|
|
||||||
|
This is a pyWebLayout issue, not a dreader-application issue.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dreader.application import EbookReader
|
||||||
|
|
||||||
|
|
||||||
|
class TestBackwardNavigationBug(unittest.TestCase):
|
||||||
|
"""Minimal reproduction of backward navigation bug"""
|
||||||
|
|
||||||
|
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_minimal_backward_navigation_bug(self):
|
||||||
|
"""
|
||||||
|
MINIMAL REPRODUCTION:
|
||||||
|
|
||||||
|
1. Start at block_index=0
|
||||||
|
2. Go forward once (to block_index=1)
|
||||||
|
3. Go backward once
|
||||||
|
4. BUG: Lands at block_index=1 instead of block_index=0
|
||||||
|
|
||||||
|
This proves backward navigation cannot reach the first block.
|
||||||
|
"""
|
||||||
|
reader = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
# Starting position
|
||||||
|
pos_start = reader.manager.current_position.copy()
|
||||||
|
print(f"\n1. Starting at block_index={pos_start.block_index}")
|
||||||
|
self.assertEqual(pos_start.block_index, 0, "Should start at block 0")
|
||||||
|
|
||||||
|
# Go forward
|
||||||
|
reader.next_page()
|
||||||
|
pos_forward = reader.manager.current_position.copy()
|
||||||
|
print(f"2. After next_page(): block_index={pos_forward.block_index}")
|
||||||
|
self.assertEqual(pos_forward.block_index, 1, "Should be at block 1")
|
||||||
|
|
||||||
|
# Go backward
|
||||||
|
reader.previous_page()
|
||||||
|
pos_final = reader.manager.current_position.copy()
|
||||||
|
print(f"3. After previous_page(): block_index={pos_final.block_index}")
|
||||||
|
|
||||||
|
# THE BUG: This assertion will fail
|
||||||
|
print(f"\nEXPECTED: block_index=0")
|
||||||
|
print(f"ACTUAL: block_index={pos_final.block_index}")
|
||||||
|
|
||||||
|
if pos_final.block_index != 0:
|
||||||
|
print("\n❌ BUG CONFIRMED: Cannot navigate backward to block_index=0")
|
||||||
|
print(" This is a pyWebLayout bug in the previous_page() method.")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
pos_final.block_index,
|
||||||
|
0,
|
||||||
|
"BUG: Backward navigation from block 1 should return to block 0"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
239
tests/test_backward_navigation_detailed.py
Normal file
239
tests/test_backward_navigation_detailed.py
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
"""
|
||||||
|
Detailed test for backward navigation issues.
|
||||||
|
|
||||||
|
This test explores the backward navigation behavior more thoroughly
|
||||||
|
to understand if the issue is:
|
||||||
|
1. Complete failure (previous_page returns None)
|
||||||
|
2. Imprecise positioning (lands on wrong block)
|
||||||
|
3. Only occurs after resume
|
||||||
|
4. Occurs during continuous navigation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dreader.application import EbookReader
|
||||||
|
|
||||||
|
|
||||||
|
class TestBackwardNavigationDetailed(unittest.TestCase):
|
||||||
|
"""Detailed backward navigation 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 test_continuous_backward_navigation_no_resume(self):
|
||||||
|
"""
|
||||||
|
Test backward navigation without closing/resuming.
|
||||||
|
This checks if the issue is specific to resume or general.
|
||||||
|
"""
|
||||||
|
reader = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
print("\n=== Test: Continuous backward navigation (no resume) ===")
|
||||||
|
|
||||||
|
# Record starting position
|
||||||
|
pos0 = reader.manager.current_position.copy()
|
||||||
|
print(f"Starting position: {pos0}")
|
||||||
|
|
||||||
|
# Go forward 5 pages, recording positions
|
||||||
|
forward_positions = [pos0]
|
||||||
|
for i in range(5):
|
||||||
|
page = reader.next_page()
|
||||||
|
if page is None:
|
||||||
|
print(f"Reached end at page {i}")
|
||||||
|
break
|
||||||
|
pos = reader.manager.current_position.copy()
|
||||||
|
forward_positions.append(pos)
|
||||||
|
print(f"Forward page {i+1}: block_index={pos.block_index}")
|
||||||
|
|
||||||
|
num_forward = len(forward_positions) - 1
|
||||||
|
print(f"\nNavigated forward {num_forward} pages")
|
||||||
|
|
||||||
|
# Now go backward the same number of times
|
||||||
|
print("\n--- Going backward ---")
|
||||||
|
backward_positions = []
|
||||||
|
for i in range(num_forward):
|
||||||
|
page = reader.previous_page()
|
||||||
|
|
||||||
|
if page is None:
|
||||||
|
print(f"ERROR: previous_page() returned None at step {i+1}")
|
||||||
|
self.fail(f"Backward navigation failed at step {i+1}")
|
||||||
|
|
||||||
|
pos = reader.manager.current_position.copy()
|
||||||
|
backward_positions.append(pos)
|
||||||
|
print(f"Backward step {i+1}: block_index={pos.block_index}")
|
||||||
|
|
||||||
|
# Check final position
|
||||||
|
final_pos = reader.manager.current_position.copy()
|
||||||
|
print(f"\nFinal position: {final_pos}")
|
||||||
|
print(f"Expected (pos0): {pos0}")
|
||||||
|
|
||||||
|
if final_pos != pos0:
|
||||||
|
print(f"WARNING: Position mismatch!")
|
||||||
|
print(f" Expected block_index: {pos0.block_index}")
|
||||||
|
print(f" Actual block_index: {final_pos.block_index}")
|
||||||
|
print(f" Difference: {final_pos.block_index - pos0.block_index} blocks")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
final_pos,
|
||||||
|
pos0,
|
||||||
|
f"After {num_forward} forward and {num_forward} backward, should be at start"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
def test_backward_navigation_at_start(self):
|
||||||
|
"""
|
||||||
|
Test that previous_page() behaves correctly when at the start of the book.
|
||||||
|
"""
|
||||||
|
reader = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
print("\n=== Test: Backward navigation at start ===")
|
||||||
|
|
||||||
|
pos_start = reader.manager.current_position.copy()
|
||||||
|
print(f"At start: {pos_start}")
|
||||||
|
|
||||||
|
# Try to go back from the very first page
|
||||||
|
page = reader.previous_page()
|
||||||
|
|
||||||
|
print(f"previous_page() returned: {page is not None}")
|
||||||
|
|
||||||
|
pos_after = reader.manager.current_position.copy()
|
||||||
|
print(f"Position after previous_page(): {pos_after}")
|
||||||
|
|
||||||
|
# Should either return None or stay at same position
|
||||||
|
if page is not None:
|
||||||
|
self.assertEqual(
|
||||||
|
pos_after,
|
||||||
|
pos_start,
|
||||||
|
"If previous_page() returns a page at start, position should not change"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
def test_alternating_navigation(self):
|
||||||
|
"""
|
||||||
|
Test alternating forward/backward navigation.
|
||||||
|
"""
|
||||||
|
reader = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
print("\n=== Test: Alternating forward/backward navigation ===")
|
||||||
|
|
||||||
|
pos0 = reader.manager.current_position.copy()
|
||||||
|
print(f"Start: block_index={pos0.block_index}")
|
||||||
|
|
||||||
|
# Go forward, back, forward, back pattern
|
||||||
|
operations = [
|
||||||
|
("forward", 1),
|
||||||
|
("backward", 1),
|
||||||
|
("forward", 2),
|
||||||
|
("backward", 1),
|
||||||
|
("forward", 1),
|
||||||
|
("backward", 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
for op, count in operations:
|
||||||
|
for i in range(count):
|
||||||
|
if op == "forward":
|
||||||
|
page = reader.next_page()
|
||||||
|
else:
|
||||||
|
page = reader.previous_page()
|
||||||
|
|
||||||
|
self.assertIsNotNone(
|
||||||
|
page,
|
||||||
|
f"{op} navigation failed at iteration {i+1}"
|
||||||
|
)
|
||||||
|
|
||||||
|
pos = reader.manager.current_position.copy()
|
||||||
|
print(f"After {count}x {op}: block_index={pos.block_index}")
|
||||||
|
|
||||||
|
# We should end up at the starting position (net: +5 -4 = +1, then +1 -2 = -1, total = 0)
|
||||||
|
# Actually: +1 -1 +2 -1 +1 -2 = 0
|
||||||
|
final_pos = reader.manager.current_position.copy()
|
||||||
|
print(f"\nFinal: block_index={final_pos.block_index}")
|
||||||
|
print(f"Expected: block_index={pos0.block_index}")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
final_pos,
|
||||||
|
pos0,
|
||||||
|
"Alternating navigation should return to start"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
def test_backward_then_forward(self):
|
||||||
|
"""
|
||||||
|
Test that forward navigation works correctly after backward navigation.
|
||||||
|
"""
|
||||||
|
reader = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
print("\n=== Test: Backward then forward ===")
|
||||||
|
|
||||||
|
# Go forward 3 pages
|
||||||
|
positions = [reader.manager.current_position.copy()]
|
||||||
|
for i in range(3):
|
||||||
|
reader.next_page()
|
||||||
|
positions.append(reader.manager.current_position.copy())
|
||||||
|
|
||||||
|
print(f"Forward positions: {[p.block_index for p in positions]}")
|
||||||
|
|
||||||
|
# Go back 3 pages
|
||||||
|
for i in range(3):
|
||||||
|
reader.previous_page()
|
||||||
|
|
||||||
|
pos_after_back = reader.manager.current_position.copy()
|
||||||
|
print(f"After going back: block_index={pos_after_back.block_index}")
|
||||||
|
|
||||||
|
# Now go forward 3 pages again
|
||||||
|
for i in range(3):
|
||||||
|
reader.next_page()
|
||||||
|
|
||||||
|
final_pos = reader.manager.current_position.copy()
|
||||||
|
print(f"After going forward again: block_index={final_pos.block_index}")
|
||||||
|
print(f"Expected: block_index={positions[3].block_index}")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
final_pos,
|
||||||
|
positions[3],
|
||||||
|
"Forward after backward should reach same position"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
230
tests/test_backward_navigation_resume.py
Normal file
230
tests/test_backward_navigation_resume.py
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
"""
|
||||||
|
Test backward navigation after resuming from a saved position.
|
||||||
|
|
||||||
|
This test specifically checks if backward navigation works correctly
|
||||||
|
after opening an epub, navigating forward, closing it, then resuming
|
||||||
|
and attempting to navigate backward.
|
||||||
|
|
||||||
|
This may reveal issues with pyWebLayout's backward navigation handling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from dreader.application import EbookReader
|
||||||
|
|
||||||
|
|
||||||
|
class TestBackwardNavigationAfterResume(unittest.TestCase):
|
||||||
|
"""Test backward navigation behavior after resume"""
|
||||||
|
|
||||||
|
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_backward_navigation_after_resume(self):
|
||||||
|
"""
|
||||||
|
Test that backward navigation works after closing and resuming.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Open EPUB
|
||||||
|
2. Navigate forward 3 pages
|
||||||
|
3. Save positions and pages
|
||||||
|
4. Close reader
|
||||||
|
5. Open new reader (resume)
|
||||||
|
6. Try to navigate backward
|
||||||
|
7. Verify we can reach previous pages
|
||||||
|
"""
|
||||||
|
# Phase 1: Initial session - navigate forward
|
||||||
|
reader1 = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0 # Disable buffering for consistent testing
|
||||||
|
)
|
||||||
|
|
||||||
|
success = reader1.load_epub(self.epub_path)
|
||||||
|
self.assertTrue(success, "Failed to load test EPUB")
|
||||||
|
|
||||||
|
# Capture initial page
|
||||||
|
page0 = reader1.get_current_page()
|
||||||
|
self.assertIsNotNone(page0, "Initial page should not be None")
|
||||||
|
pos0 = reader1.manager.current_position.copy()
|
||||||
|
|
||||||
|
print(f"\nInitial position: {pos0}")
|
||||||
|
|
||||||
|
# Navigate forward 3 pages, capturing each page
|
||||||
|
pages = [page0]
|
||||||
|
positions = [pos0]
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
page = reader1.next_page()
|
||||||
|
self.assertIsNotNone(page, f"Page {i+1} should not be None")
|
||||||
|
pages.append(page)
|
||||||
|
positions.append(reader1.manager.current_position.copy())
|
||||||
|
print(f"Forward page {i+1} position: {positions[-1]}")
|
||||||
|
|
||||||
|
# We should now be at page 3 (0-indexed)
|
||||||
|
self.assertEqual(len(pages), 4, "Should have 4 pages total (0-3)")
|
||||||
|
|
||||||
|
# Save the current position before closing
|
||||||
|
final_position = reader1.manager.current_position.copy()
|
||||||
|
print(f"Final position before close: {final_position}")
|
||||||
|
|
||||||
|
# Close reader (this should save the position)
|
||||||
|
reader1.close()
|
||||||
|
|
||||||
|
# Phase 2: Resume session - navigate backward
|
||||||
|
reader2 = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
success = reader2.load_epub(self.epub_path)
|
||||||
|
self.assertTrue(success, "Failed to load test EPUB on resume")
|
||||||
|
|
||||||
|
# Verify we resumed at the correct position
|
||||||
|
resumed_position = reader2.manager.current_position.copy()
|
||||||
|
print(f"Resumed at position: {resumed_position}")
|
||||||
|
self.assertEqual(
|
||||||
|
resumed_position,
|
||||||
|
final_position,
|
||||||
|
"Should resume at the last saved position"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the current page (should match page 3)
|
||||||
|
resumed_page = reader2.get_current_page()
|
||||||
|
self.assertIsNotNone(resumed_page, "Resumed page should not be None")
|
||||||
|
|
||||||
|
# Now try to navigate backward
|
||||||
|
print("\nAttempting backward navigation...")
|
||||||
|
|
||||||
|
backward_pages = []
|
||||||
|
backward_positions = []
|
||||||
|
|
||||||
|
# Try to go back 3 times
|
||||||
|
for i in range(3):
|
||||||
|
prev_page = reader2.previous_page()
|
||||||
|
print(f"Backward step {i+1}: page={'Not None' if prev_page else 'None'}")
|
||||||
|
|
||||||
|
if prev_page is None:
|
||||||
|
print(f"WARNING: previous_page() returned None at step {i+1}")
|
||||||
|
# This is the bug we're testing for!
|
||||||
|
self.fail(f"Backward navigation failed at step {i+1}: previous_page() returned None")
|
||||||
|
|
||||||
|
backward_pages.append(prev_page)
|
||||||
|
backward_positions.append(reader2.manager.current_position.copy())
|
||||||
|
print(f" Position after backward: {backward_positions[-1]}")
|
||||||
|
|
||||||
|
# We should have successfully gone back 3 pages
|
||||||
|
self.assertEqual(len(backward_pages), 3, "Should have navigated back 3 pages")
|
||||||
|
|
||||||
|
# Verify final position matches original position
|
||||||
|
final_backward_position = reader2.manager.current_position.copy()
|
||||||
|
print(f"\nFinal position after backward navigation: {final_backward_position}")
|
||||||
|
print(f"Original position (page 0): {pos0}")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
final_backward_position,
|
||||||
|
pos0,
|
||||||
|
"After going forward 3 and back 3, should be at initial position"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the page content matches
|
||||||
|
final_page = reader2.get_current_page()
|
||||||
|
self.assertTrue(
|
||||||
|
self.compare_images(page0, final_page),
|
||||||
|
"Final page should match initial page after forward/backward navigation"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader2.close()
|
||||||
|
|
||||||
|
print("\n✓ Test passed: Backward navigation works correctly after resume")
|
||||||
|
|
||||||
|
def test_backward_navigation_single_step(self):
|
||||||
|
"""
|
||||||
|
Simplified test: Open, go forward 1 page, close, resume, go back 1 page.
|
||||||
|
This is a minimal reproduction case.
|
||||||
|
"""
|
||||||
|
# Session 1: Navigate forward one page
|
||||||
|
reader1 = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
reader1.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
page0 = reader1.get_current_page()
|
||||||
|
pos0 = reader1.manager.current_position.copy()
|
||||||
|
|
||||||
|
page1 = reader1.next_page()
|
||||||
|
self.assertIsNotNone(page1, "Should be able to navigate forward")
|
||||||
|
pos1 = reader1.manager.current_position.copy()
|
||||||
|
|
||||||
|
reader1.close()
|
||||||
|
|
||||||
|
# Session 2: Resume and navigate backward
|
||||||
|
reader2 = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0
|
||||||
|
)
|
||||||
|
|
||||||
|
reader2.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
# Verify we're at page 1
|
||||||
|
self.assertEqual(
|
||||||
|
reader2.manager.current_position,
|
||||||
|
pos1,
|
||||||
|
"Should resume at page 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to go back
|
||||||
|
prev_page = reader2.previous_page()
|
||||||
|
|
||||||
|
# This is the critical assertion - if this fails, backward nav is broken
|
||||||
|
self.assertIsNotNone(
|
||||||
|
prev_page,
|
||||||
|
"CRITICAL: previous_page() returned None after resume - this indicates a pyWebLayout bug"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify we're back at page 0
|
||||||
|
final_pos = reader2.manager.current_position.copy()
|
||||||
|
self.assertEqual(
|
||||||
|
final_pos,
|
||||||
|
pos0,
|
||||||
|
"Should be back at initial position"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader2.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
132
tests/test_epub_images.py
Normal file
132
tests/test_epub_images.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test that images render correctly in EPUB files.
|
||||||
|
|
||||||
|
This test verifies that:
|
||||||
|
1. All images in the EPUB are loaded with correct dimensions
|
||||||
|
2. Images can be navigated to without errors
|
||||||
|
3. Pages with images render successfully
|
||||||
|
4. The rendered pages contain actual image content (not blank)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from dreader.application import EbookReader
|
||||||
|
from pyWebLayout.abstract.block import Image as AbstractImage
|
||||||
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def test_epub_images():
|
||||||
|
"""Test that EPUB images render correctly."""
|
||||||
|
|
||||||
|
# Create reader
|
||||||
|
reader = EbookReader(page_size=(800, 1200))
|
||||||
|
|
||||||
|
# Load EPUB
|
||||||
|
epub_path = "tests/data/library-epub/pg11-images-3.epub"
|
||||||
|
success = reader.load_epub(epub_path)
|
||||||
|
|
||||||
|
assert success, "Failed to load EPUB"
|
||||||
|
assert reader.book_title == "Alice's Adventures in Wonderland"
|
||||||
|
|
||||||
|
# Check that images were parsed
|
||||||
|
images = [b for b in reader.blocks if isinstance(b, AbstractImage)]
|
||||||
|
assert len(images) >= 1, f"Expected at least 1 image, found {len(images)}"
|
||||||
|
|
||||||
|
# Check that all images have dimensions set
|
||||||
|
for img in images:
|
||||||
|
assert img.width is not None, f"Image {img.source} has no width"
|
||||||
|
assert img.height is not None, f"Image {img.source} has no height"
|
||||||
|
assert img.width > 0, f"Image {img.source} has invalid width: {img.width}"
|
||||||
|
assert img.height > 0, f"Image {img.source} has invalid height: {img.height}"
|
||||||
|
|
||||||
|
# Check that image is loaded into memory
|
||||||
|
assert hasattr(img, '_loaded_image'), f"Image {img.source} not loaded"
|
||||||
|
assert img._loaded_image is not None, f"Image {img.source} _loaded_image is None"
|
||||||
|
|
||||||
|
# Test navigation through first 15 pages (which should include all images)
|
||||||
|
for page_num in range(15):
|
||||||
|
page_img = reader.get_current_page()
|
||||||
|
|
||||||
|
assert page_img is not None, f"Page {page_num + 1} failed to render"
|
||||||
|
assert isinstance(page_img, Image.Image), f"Page {page_num + 1} is not a PIL Image"
|
||||||
|
assert page_img.size == (800, 1200), f"Page {page_num + 1} has wrong size: {page_img.size}"
|
||||||
|
|
||||||
|
# Check that page has some non-white content
|
||||||
|
arr = np.array(page_img.convert('RGB'))
|
||||||
|
non_white_pixels = np.sum(arr < 255)
|
||||||
|
|
||||||
|
assert non_white_pixels > 100, f"Page {page_num + 1} appears to be blank (only {non_white_pixels} non-white pixels)"
|
||||||
|
|
||||||
|
# Navigate to next page
|
||||||
|
if page_num < 14:
|
||||||
|
next_result = reader.next_page()
|
||||||
|
if next_result is None:
|
||||||
|
# It's OK to reach end of book early
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def test_cover_image():
|
||||||
|
"""Specifically test that the cover image renders."""
|
||||||
|
|
||||||
|
reader = EbookReader(page_size=(800, 1200))
|
||||||
|
reader.load_epub("tests/data/library-epub/pg11-images-3.epub")
|
||||||
|
|
||||||
|
# The first page should have the cover image
|
||||||
|
page_img = reader.get_current_page()
|
||||||
|
assert page_img is not None, "Cover page failed to render"
|
||||||
|
|
||||||
|
# Save for visual inspection
|
||||||
|
output_path = "/tmp/epub_cover_test.png"
|
||||||
|
page_img.save(output_path)
|
||||||
|
|
||||||
|
# Check that it has significant content (the cover image)
|
||||||
|
arr = np.array(page_img.convert('RGB'))
|
||||||
|
non_white_pixels = np.sum(arr < 255)
|
||||||
|
|
||||||
|
# The cover page should have substantial content
|
||||||
|
assert non_white_pixels > 10000, f"Cover page has too few non-white pixels: {non_white_pixels}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_epub_images():
|
||||||
|
"""Test images across multiple EPUB files."""
|
||||||
|
|
||||||
|
epub_files = [
|
||||||
|
("tests/data/library-epub/pg11-images-3.epub", "Alice's Adventures in Wonderland"),
|
||||||
|
("tests/data/library-epub/pg16328-images-3.epub", "Beowulf: An Anglo-Saxon Epic Poem"),
|
||||||
|
("tests/data/library-epub/pg5200-images-3.epub", "Metamorphosis"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for epub_path, expected_title in epub_files:
|
||||||
|
reader = EbookReader(page_size=(800, 1200))
|
||||||
|
success = reader.load_epub(epub_path)
|
||||||
|
|
||||||
|
assert success, f"Failed to load {epub_path}"
|
||||||
|
assert reader.book_title == expected_title
|
||||||
|
|
||||||
|
# Check that at least one image exists
|
||||||
|
images = [b for b in reader.blocks if isinstance(b, AbstractImage)]
|
||||||
|
assert len(images) >= 1, f"{epub_path} should have at least 1 image"
|
||||||
|
|
||||||
|
# Check first image is valid
|
||||||
|
img = images[0]
|
||||||
|
assert img.width > 0 and img.height > 0, f"Invalid dimensions in {epub_path}"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run tests directly
|
||||||
|
print("Testing EPUB images...")
|
||||||
|
|
||||||
|
print("\n1. Testing all images load and render...")
|
||||||
|
test_epub_images()
|
||||||
|
print("✓ PASSED")
|
||||||
|
|
||||||
|
print("\n2. Testing cover image...")
|
||||||
|
test_cover_image()
|
||||||
|
print("✓ PASSED")
|
||||||
|
|
||||||
|
print("\n3. Testing multiple EPUB images...")
|
||||||
|
test_multiple_epub_images()
|
||||||
|
print("✓ PASSED")
|
||||||
|
|
||||||
|
print("\n✓ All tests passed!")
|
||||||
@ -7,6 +7,7 @@ and verify that tap detection works correctly.
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
from dreader import LibraryManager
|
from dreader import LibraryManager
|
||||||
|
|
||||||
|
|
||||||
@ -46,9 +47,12 @@ class TestLibraryInteraction(unittest.TestCase):
|
|||||||
# Table should exist
|
# Table should exist
|
||||||
self.assertIsNotNone(table)
|
self.assertIsNotNone(table)
|
||||||
|
|
||||||
# Table should have body rows matching book count
|
# Table should have body rows for 2-column grid layout
|
||||||
|
# Each pair of books gets 2 rows (cover row + detail row)
|
||||||
|
# So N books = ceil(N/2) * 2 rows
|
||||||
body_rows = list(table.body_rows())
|
body_rows = list(table.body_rows())
|
||||||
self.assertEqual(len(body_rows), len(books))
|
expected_rows = ((len(books) + 1) // 2) * 2 # Round up to nearest even number, then double
|
||||||
|
self.assertEqual(len(body_rows), expected_rows)
|
||||||
|
|
||||||
def test_library_rendering(self):
|
def test_library_rendering(self):
|
||||||
"""Test that library can be rendered to image"""
|
"""Test that library can be rendered to image"""
|
||||||
@ -136,7 +140,7 @@ class TestLibraryInteraction(unittest.TestCase):
|
|||||||
self.assertIsNone(selected_path, "Tap below last book should not select anything")
|
self.assertIsNone(selected_path, "Tap below last book should not select anything")
|
||||||
|
|
||||||
def test_multiple_taps(self):
|
def test_multiple_taps(self):
|
||||||
"""Test that multiple taps work correctly"""
|
"""Test that multiple taps work correctly with 2-column grid layout"""
|
||||||
books = self.library.scan_library()
|
books = self.library.scan_library()
|
||||||
|
|
||||||
if len(books) < 3:
|
if len(books) < 3:
|
||||||
@ -145,16 +149,20 @@ class TestLibraryInteraction(unittest.TestCase):
|
|||||||
self.library.create_library_table()
|
self.library.create_library_table()
|
||||||
self.library.render_library()
|
self.library.render_library()
|
||||||
|
|
||||||
# Tap first book (row 0: y=60-180)
|
# In 2-column layout:
|
||||||
|
# Books 0 and 1 are in the first pair (rows 0-1: cover and detail)
|
||||||
|
# Books 2 and 3 are in the second pair (rows 2-3: cover and detail)
|
||||||
|
|
||||||
|
# Tap first book (left column, first pair cover row)
|
||||||
path1 = self.library.handle_library_tap(x=100, y=100)
|
path1 = self.library.handle_library_tap(x=100, y=100)
|
||||||
self.assertEqual(path1, books[0]['path'])
|
self.assertEqual(path1, books[0]['path'])
|
||||||
|
|
||||||
# Tap second book (row 1: y=181-301)
|
# Tap second book (right column, first pair cover row)
|
||||||
path2 = self.library.handle_library_tap(x=400, y=250)
|
path2 = self.library.handle_library_tap(x=500, y=100)
|
||||||
self.assertEqual(path2, books[1]['path'])
|
self.assertEqual(path2, books[1]['path'])
|
||||||
|
|
||||||
# Tap third book (row 2: y=302-422)
|
# Tap third book (left column, second pair cover row)
|
||||||
path3 = self.library.handle_library_tap(x=400, y=360)
|
path3 = self.library.handle_library_tap(x=100, y=360)
|
||||||
self.assertEqual(path3, books[2]['path'])
|
self.assertEqual(path3, books[2]['path'])
|
||||||
|
|
||||||
# All should be different
|
# All should be different
|
||||||
@ -162,6 +170,79 @@ class TestLibraryInteraction(unittest.TestCase):
|
|||||||
self.assertNotEqual(path2, path3)
|
self.assertNotEqual(path2, path3)
|
||||||
self.assertNotEqual(path1, path3)
|
self.assertNotEqual(path1, path3)
|
||||||
|
|
||||||
|
def test_pagination(self):
|
||||||
|
"""Test library pagination with fake book data"""
|
||||||
|
# Create fake books (20 books to ensure multiple pages)
|
||||||
|
fake_books = []
|
||||||
|
for i in range(20):
|
||||||
|
fake_books.append({
|
||||||
|
'path': f'/fake/path/book_{i}.epub',
|
||||||
|
'title': f'Book Title {i}',
|
||||||
|
'author': f'Author {i}',
|
||||||
|
'filename': f'book_{i}.epub',
|
||||||
|
'cover_data': None,
|
||||||
|
'cover_path': None
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create library with 6 books per page
|
||||||
|
library = LibraryManager(
|
||||||
|
library_path=str(self.library_path),
|
||||||
|
page_size=(800, 1200),
|
||||||
|
books_per_page=6
|
||||||
|
)
|
||||||
|
library.books = fake_books
|
||||||
|
|
||||||
|
# Test initial state
|
||||||
|
self.assertEqual(library.current_page, 0)
|
||||||
|
self.assertEqual(library.get_total_pages(), 4) # 20 books / 6 per page = 4 pages
|
||||||
|
|
||||||
|
# Test creating table for first page
|
||||||
|
table = library.create_library_table()
|
||||||
|
self.assertIsNotNone(table)
|
||||||
|
# 6 books = 3 pairs = 6 rows (3 cover rows + 3 detail rows)
|
||||||
|
body_rows = list(table.body_rows())
|
||||||
|
self.assertEqual(len(body_rows), 6)
|
||||||
|
|
||||||
|
# Test navigation to next page
|
||||||
|
self.assertTrue(library.next_page())
|
||||||
|
self.assertEqual(library.current_page, 1)
|
||||||
|
|
||||||
|
# Create table for second page
|
||||||
|
table = library.create_library_table()
|
||||||
|
body_rows = list(table.body_rows())
|
||||||
|
self.assertEqual(len(body_rows), 6) # Still 6 books on page 2
|
||||||
|
|
||||||
|
# Test navigation to last page
|
||||||
|
library.set_page(3)
|
||||||
|
self.assertEqual(library.current_page, 3)
|
||||||
|
table = library.create_library_table()
|
||||||
|
body_rows = list(table.body_rows())
|
||||||
|
# Page 4 has 2 books (20 - 18 = 2) = 1 pair = 2 rows
|
||||||
|
self.assertEqual(len(body_rows), 2)
|
||||||
|
|
||||||
|
# Test can't go beyond last page
|
||||||
|
self.assertFalse(library.next_page())
|
||||||
|
self.assertEqual(library.current_page, 3)
|
||||||
|
|
||||||
|
# Test navigation to previous page
|
||||||
|
self.assertTrue(library.previous_page())
|
||||||
|
self.assertEqual(library.current_page, 2)
|
||||||
|
|
||||||
|
# Test navigation to first page
|
||||||
|
library.set_page(0)
|
||||||
|
self.assertEqual(library.current_page, 0)
|
||||||
|
|
||||||
|
# Test can't go before first page
|
||||||
|
self.assertFalse(library.previous_page())
|
||||||
|
self.assertEqual(library.current_page, 0)
|
||||||
|
|
||||||
|
# Test invalid page number
|
||||||
|
self.assertFalse(library.set_page(-1))
|
||||||
|
self.assertFalse(library.set_page(100))
|
||||||
|
self.assertEqual(library.current_page, 0) # Should stay on current page
|
||||||
|
|
||||||
|
library.cleanup()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user