pyWebLayout/tests/layout/test_ereader_application.py

306 lines
11 KiB
Python

"""
Tests for the EbookReader application interface.
Tests the high-level EbookReader API including bidirectional navigation,
image rendering consistency, and position management.
"""
import unittest
import tempfile
import shutil
from pathlib import Path
import numpy as np
from PIL import Image
from pyWebLayout.layout.ereader_application import EbookReader
class TestEbookReaderNavigation(unittest.TestCase):
"""Test EbookReader navigation functionality"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.epub_path = "tests/data/test.epub"
# Verify test EPUB exists
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.
Args:
img1: First image
img2: Second image
Returns:
True if images are identical, False otherwise
"""
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_bidirectional_navigation_20_pages(self):
"""
Test that navigating forward 20 pages and then backward 20 pages
produces identical page renderings for the first page.
This validates that the bidirectional layout system maintains
perfect consistency during extended navigation.
Note: This test uses position save/load as a workaround for incomplete
backward rendering implementation.
"""
# Create reader with standard page size and disable buffer to avoid pickle issues
reader = EbookReader(
page_size=(800, 1000),
bookmarks_dir=self.temp_dir,
buffer_size=0 # Disable multiprocess buffering
)
# Load the test EPUB
success = reader.load_epub(self.epub_path)
self.assertTrue(success, "Failed to load test EPUB")
self.assertTrue(reader.is_loaded(), "Reader should be loaded")
# Capture initial page (page 0) and save its position
initial_page = reader.get_current_page()
self.assertIsNotNone(initial_page, "Initial page should not be None")
# Save the initial position for later comparison
initial_position = reader.manager.current_position.copy()
# Store forward navigation pages and positions
forward_pages = [initial_page]
forward_positions = [initial_position]
pages_to_navigate = 20
# Navigate forward, capturing each page and position
for i in range(pages_to_navigate):
page = reader.next_page()
if page is None:
# Reached end of document
print(f"Reached end of document at page {i + 1}")
break
forward_pages.append(page)
forward_positions.append(reader.manager.current_position.copy())
actual_pages_navigated = len(forward_pages) - 1
print(f"Navigated forward through {actual_pages_navigated} pages")
# Now navigate backward using position jumps (more reliable than previous_page)
backward_pages = []
# Traverse backwards through our saved positions
for i in range(len(forward_positions) - 1, -1, -1):
position = forward_positions[i]
page_obj = reader.manager.jump_to_position(position)
# Render the Page object to get PIL Image
page_img = page_obj.render()
backward_pages.append(page_img)
# The last page from backward navigation should be page 0
final_page = backward_pages[-1]
# Debug: Save images to inspect differences
initial_page.save("/tmp/initial_page.png")
final_page.save("/tmp/final_page.png")
# Check image sizes first
print(f"Initial page size: {initial_page.size}")
print(f"Final page size: {final_page.size}")
print(f"Initial position: {initial_position}")
print(f"Final position: {forward_positions[0]}")
# Compare arrays to see differences
arr1 = np.array(initial_page)
arr2 = np.array(final_page)
if arr1.shape == arr2.shape:
diff = np.abs(arr1.astype(int) - arr2.astype(int))
diff_pixels = np.count_nonzero(diff)
total_pixels = arr1.shape[0] * arr1.shape[1] * arr1.shape[2]
print(f"Different pixels: {diff_pixels} out of {total_pixels} ({100*diff_pixels/total_pixels:.2f}%)")
if diff_pixels > 0:
# Save difference map
diff_img = Image.fromarray(np.clip(diff.sum(axis=2) * 10, 0, 255).astype(np.uint8))
diff_img.save("/tmp/diff_page.png")
# Critical assertion: first page should be identical after round trip
self.assertTrue(
self.compare_images(initial_page, final_page),
"First page should be identical after forward/backward navigation"
)
# Extended validation: compare all pages
# forward_pages[i] should match backward_pages[-(i+1)]
mismatches = []
for i in range(len(forward_pages)):
forward_page = forward_pages[i]
backward_page = backward_pages[-(i + 1)]
if not self.compare_images(forward_page, backward_page):
mismatches.append(i)
# Save mismatched pages for debugging
if i < 3: # Only save first few mismatches
forward_page.save(f"/tmp/forward_page_{i}.png")
backward_page.save(f"/tmp/backward_page_{i}.png")
if mismatches:
self.fail(f"Page mismatches detected at indices: {mismatches[:10]}... (showing first 10)")
print(f"Successfully validated bidirectional navigation consistency")
print(f"Tested {actual_pages_navigated} pages forward and backward")
print(f"All {len(forward_pages)} pages matched perfectly")
# Clean up
reader.close()
def test_navigation_at_boundaries(self):
"""
Test navigation behavior at document boundaries.
"""
reader = EbookReader(
page_size=(800, 1000),
bookmarks_dir=self.temp_dir
)
success = reader.load_epub(self.epub_path)
self.assertTrue(success, "Failed to load test EPUB")
# Try to go backward from first page
# Should stay on first page or return None gracefully
page = reader.previous_page()
# Behavior depends on implementation - could be None or same page
# Navigate forward until end
pages_forward = 0
max_pages = 100 # Safety limit
while pages_forward < max_pages:
page = reader.next_page()
if page is None:
break
pages_forward += 1
print(f"Document has approximately {pages_forward} pages")
# Try to go forward from last page
page = reader.next_page()
self.assertIsNone(page, "Should return None at end of document")
reader.close()
class TestEbookReaderFeatures(unittest.TestCase):
"""Test other EbookReader features"""
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_epub_loading(self):
"""Test EPUB loading functionality"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
# Test with valid EPUB
success = reader.load_epub(self.epub_path)
self.assertTrue(success)
self.assertTrue(reader.is_loaded())
# Get book info
info = reader.get_book_info()
self.assertIsNotNone(info)
self.assertIn('title', info)
self.assertIn('author', info)
self.assertGreater(info['total_blocks'], 0)
reader.close()
def test_page_rendering(self):
"""Test that pages render correctly as PIL Images"""
reader = EbookReader(
page_size=(400, 600),
bookmarks_dir=self.temp_dir
)
reader.load_epub(self.epub_path)
page = reader.get_current_page()
self.assertIsNotNone(page)
self.assertIsInstance(page, Image.Image)
self.assertEqual(page.size, (400, 600))
reader.close()
def test_position_tracking(self):
"""Test position save/load functionality"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
reader.load_epub(self.epub_path)
# Navigate to a specific position
for _ in range(3):
reader.next_page()
# Save position
success = reader.save_position("test_position")
self.assertTrue(success)
# Navigate away
for _ in range(5):
reader.next_page()
# Load saved position
page = reader.load_position("test_position")
self.assertIsNotNone(page)
# List positions
positions = reader.list_saved_positions()
self.assertIn("test_position", positions)
# Delete position
success = reader.delete_position("test_position")
self.assertTrue(success)
reader.close()
def test_chapter_navigation(self):
"""Test chapter listing and navigation"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
reader.load_epub(self.epub_path)
# Get chapters
chapters = reader.get_chapters()
self.assertIsInstance(chapters, list)
# If book has chapters, test navigation
if len(chapters) > 0:
# Jump to first chapter
page = reader.jump_to_chapter(0)
self.assertIsNotNone(page)
reader.close()
if __name__ == '__main__':
unittest.main()