improved library screen, fixed issues with image rendering and navigation
Some checks failed
Python CI / test (3.12) (push) Successful in 6m50s
Python CI / test (3.13) (push) Has been cancelled

This commit is contained in:
Duncan Tourolle 2025-11-10 14:13:59 +01:00
parent 8f6205dcfe
commit dd392d2e15
11 changed files with 990 additions and 642 deletions

View File

@ -45,7 +45,8 @@ class LibraryManager:
self,
library_path: str,
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.
@ -54,9 +55,11 @@ class LibraryManager:
library_path: Path to directory containing EPUB files
cache_dir: Optional cache directory for covers. If None, uses default.
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.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
if cache_dir:
@ -75,6 +78,7 @@ class LibraryManager:
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.table_renderer: Optional[TableRenderer] = None # Store renderer for bounds info
self.current_page: int = 0 # Current page index for pagination
@staticmethod
def _get_default_cache_dir() -> Path:
@ -149,12 +153,13 @@ class LibraryManager:
print(f"Error caching cover for {book['title']}: {e}")
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:
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:
Table object ready for rendering
@ -162,83 +167,133 @@ class LibraryManager:
if books is None:
books = self.books
if page is None:
page = self.current_page
if not books:
print("No books to display in library")
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
table = Table(caption="My Library", style=Font(font_size=18, weight="bold"))
print(f"Creating library table with {len(page_books)} books (page {page + 1}/{total_pages})...")
# Add books as rows
for i, book in enumerate(books):
row = table.create_row("body")
# Create table with caption showing page info
caption_text = f"My Library (Page {page + 1}/{total_pages})" if total_pages > 1 else "My Library"
table = Table(caption=caption_text, style=Font(font_size=18, weight="bold"))
# Cover cell with interactive image
cover_cell = row.create_cell()
cover_path = book.get('cover_path')
book_path = book['path']
# 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")
# Create callback that returns book path
callback = lambda point, path=book_path: path
# Add first book's cover (left column)
self._add_book_cover(cover_row, page_books[i])
if cover_path and Path(cover_path).exists():
# Use cached cover with callback
img = InteractiveImage.create_and_add_to(
cover_cell,
source=cover_path,
alt_text=book['title'],
callback=callback
)
elif book.get('cover_data'):
# Decode base64 and save to temp file for InteractiveImage
try:
img_data = base64.b64decode(book['cover_data'])
img = Image.open(BytesIO(img_data))
# Save to temp file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
img.save(tmp.name, 'PNG')
temp_path = tmp.name
self.temp_cover_files.append(temp_path)
img = InteractiveImage.create_and_add_to(
cover_cell,
source=temp_path,
alt_text=book['title'],
callback=callback
)
except Exception as e:
print(f"Error creating cover image for {book['title']}: {e}")
self._add_no_cover_text(cover_cell)
# 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:
# No cover available
self._add_no_cover_text(cover_cell)
# Add empty cell if odd number of books
cover_row.create_cell()
# Book info cell
info_cell = row.create_cell()
# Row 2: Details for this pair
details_row = table.create_row("body")
# Title paragraph
title_para = info_cell.create_paragraph()
for word in book['title'].split():
title_para.add_word(Word(word, Font(font_size=14, weight="bold")))
# Add first book's details (left column)
self._add_book_details(details_row, page_books[i])
# Author paragraph
author_para = info_cell.create_paragraph()
for word in book.get('author', 'Unknown').split():
author_para.add_word(Word(word, Font(font_size=12)))
# Filename paragraph (small, gray)
filename_para = info_cell.create_paragraph()
filename_para.add_word(Word(
Path(book['path']).name,
Font(font_size=10, colour=(150, 150, 150))
))
# 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_path = book.get('cover_path')
book_path = book['path']
# Create callback that returns book path
callback = lambda point, path=book_path: path
# Add cover image
if cover_path and Path(cover_path).exists():
# Use cached cover with callback
img = InteractiveImage.create_and_add_to(
cover_cell,
source=cover_path,
alt_text=book['title'],
callback=callback
)
elif book.get('cover_data'):
# Decode base64 and save to temp file for InteractiveImage
try:
img_data = base64.b64decode(book['cover_data'])
img = Image.open(BytesIO(img_data))
# Save to temp file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
img.save(tmp.name, 'PNG')
temp_path = tmp.name
self.temp_cover_files.append(temp_path)
img = InteractiveImage.create_and_add_to(
cover_cell,
source=temp_path,
alt_text=book['title'],
callback=callback
)
except Exception as e:
print(f"Error creating cover image for {book['title']}: {e}")
self._add_no_cover_text(cover_cell)
else:
# No cover available
self._add_no_cover_text(cover_cell)
def _add_book_details(self, row, book: Dict):
"""
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_para = details_cell.create_paragraph()
for word in book['title'].split():
title_para.add_word(Word(word, Font(font_size=14, weight="bold")))
# Author paragraph
author_para = details_cell.create_paragraph()
for word in book.get('author', 'Unknown').split():
author_para.add_word(Word(word, Font(font_size=12)))
# Filename paragraph (small, gray)
filename_para = details_cell.create_paragraph()
filename_para.add_word(Word(
Path(book['path']).name,
Font(font_size=10, colour=(150, 150, 150))
))
def _add_no_cover_text(self, cell):
"""Add placeholder text when no cover is available"""
para = cell.create_paragraph()
@ -306,10 +361,11 @@ class LibraryManager:
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
book path. This makes the entire row interactive, not just the cover image.
The layout has alternating rows: cover rows and detail rows.
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:
x: X coordinate of tap
@ -323,6 +379,11 @@ class LibraryManager:
return None
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
all_rows = list(self.library_table.all_rows())
@ -345,11 +406,43 @@ class LibraryManager:
# Find which body row this is (0-indexed)
body_row_index = sum(1 for s, _ in all_rows[:row_idx] if s == "body")
# Return the corresponding book
if body_row_index < len(self.books):
book_path = self.books[body_row_index]['path']
print(f"Book selected (row {body_row_index}): {book_path}")
return book_path
# Each pair of books uses 2 rows (cover row + detail row)
# Determine which book pair this row belongs to
book_pair_index = body_row_index // 2 # Which pair of books (0, 1, 2, ...)
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
print(f"No book tapped at ({x}, {y})")
return None
@ -374,6 +467,56 @@ class LibraryManager:
return self.books[index]
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:
"""
Get current library state for persistence.

View File

@ -52,7 +52,7 @@ class DocumentManager:
# Extract metadata
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
self.document_id = Path(epub_path).stem

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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())

View 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()

View 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()

View 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
View 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!")

View File

@ -7,6 +7,7 @@ and verify that tap detection works correctly.
import unittest
from pathlib import Path
from unittest.mock import MagicMock
from dreader import LibraryManager
@ -46,9 +47,12 @@ class TestLibraryInteraction(unittest.TestCase):
# Table should exist
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())
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):
"""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")
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()
if len(books) < 3:
@ -145,16 +149,20 @@ class TestLibraryInteraction(unittest.TestCase):
self.library.create_library_table()
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)
self.assertEqual(path1, books[0]['path'])
# Tap second book (row 1: y=181-301)
path2 = self.library.handle_library_tap(x=400, y=250)
# Tap second book (right column, first pair cover row)
path2 = self.library.handle_library_tap(x=500, y=100)
self.assertEqual(path2, books[1]['path'])
# Tap third book (row 2: y=302-422)
path3 = self.library.handle_library_tap(x=400, y=360)
# Tap third book (left column, second pair cover row)
path3 = self.library.handle_library_tap(x=100, y=360)
self.assertEqual(path3, books[2]['path'])
# All should be different
@ -162,6 +170,79 @@ class TestLibraryInteraction(unittest.TestCase):
self.assertNotEqual(path2, 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__':
unittest.main()