remove application from library
Python CI / test (push) Failing after 6m29s

This commit is contained in:
2025-11-07 18:48:36 +01:00
parent 6bb43db8d5
commit 33e2cbc363
21 changed files with 1747 additions and 2656 deletions
+16 -13
View File
@@ -18,11 +18,11 @@ from pyWebLayout.core.base import Renderable, Queriable
class SimpleTestRenderable(Renderable, Queriable):
"""A simple test renderable for testing the page system"""
def __init__(self, text: str, size: tuple = (100, 50)):
self._text = text
self.size = size
self.origin = np.array([0, 0])
self._origin = np.array([0, 0])
def render(self):
"""Render returns None - drawing is done via the page's draw object"""
@@ -145,25 +145,28 @@ class TestPageImplementation(unittest.TestCase):
def test_page_query_point(self):
"""Test querying points to find children"""
page = Page(size=(400, 300))
# Add children with known positions and sizes
child1 = SimpleTestRenderable("Child 1", (100, 50))
child2 = SimpleTestRenderable("Child 2", (80, 40))
page.add_child(child1).add_child(child2)
# Query points
# Point within first child
found_child = page.query_point((90, 30))
self.assertEqual(found_child, child1)
result = page.query_point((90, 30))
self.assertIsNotNone(result)
self.assertEqual(result.object, child1)
# Point within second child
found_child = page.query_point((30, 30))
self.assertEqual(found_child, child2)
# Point outside any child
found_child = page.query_point((300, 250))
self.assertIsNone(found_child)
result = page.query_point((30, 30))
self.assertIsNotNone(result)
self.assertEqual(result.object, child2)
# Point outside any child - returns QueryResult with object_type "empty"
result = page.query_point((300, 250))
self.assertIsNotNone(result)
self.assertEqual(result.object_type, "empty")
def test_page_in_object(self):
"""Test that page correctly implements in_object"""
+1
View File
@@ -0,0 +1 @@
"""Tests for core pyWebLayout functionality."""
+353
View File
@@ -0,0 +1,353 @@
"""
Unit tests for the highlight system.
Tests Highlight, HighlightColor, HighlightManager, and integration with query system.
"""
import unittest
import tempfile
import shutil
from pathlib import Path
import json
from pyWebLayout.core.highlight import (
Highlight,
HighlightColor,
HighlightManager,
create_highlight_from_query_result
)
from pyWebLayout.core.query import QueryResult, SelectionRange
class TestHighlightColor(unittest.TestCase):
"""Test HighlightColor enum"""
def test_colors_defined(self):
"""Test all expected colors are defined"""
expected_colors = ['YELLOW', 'GREEN', 'BLUE', 'PINK', 'ORANGE', 'PURPLE', 'RED']
for color_name in expected_colors:
self.assertTrue(hasattr(HighlightColor, color_name))
color = getattr(HighlightColor, color_name)
self.assertIsInstance(color.value, tuple)
self.assertEqual(len(color.value), 4) # RGBA
def test_yellow_is_default(self):
"""Test yellow highlight color"""
yellow = HighlightColor.YELLOW.value
self.assertEqual(yellow, (255, 255, 0, 100))
class TestHighlight(unittest.TestCase):
"""Test Highlight dataclass"""
def test_init_basic(self):
"""Test basic Highlight creation"""
highlight = Highlight(
id="test-id",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="Hello"
)
self.assertEqual(highlight.id, "test-id")
self.assertEqual(len(highlight.bounds), 1)
self.assertEqual(highlight.bounds[0], (10, 20, 50, 15))
self.assertEqual(highlight.color, (255, 255, 0, 100))
self.assertEqual(highlight.text, "Hello")
self.assertIsNone(highlight.note)
self.assertEqual(highlight.tags, [])
def test_init_with_metadata(self):
"""Test Highlight with full metadata"""
highlight = Highlight(
id="test-id",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="Hello",
note="Important word",
tags=["important", "keyword"],
timestamp=1234567890.0,
start_word_index=5,
end_word_index=5
)
self.assertEqual(highlight.note, "Important word")
self.assertEqual(highlight.tags, ["important", "keyword"])
self.assertEqual(highlight.timestamp, 1234567890.0)
self.assertEqual(highlight.start_word_index, 5)
self.assertEqual(highlight.end_word_index, 5)
def test_to_dict(self):
"""Test Highlight serialization"""
highlight = Highlight(
id="test-id",
bounds=[(10, 20, 50, 15), (70, 20, 40, 15)],
color=(255, 255, 0, 100),
text="Hello world",
note="Test note",
tags=["test"],
timestamp=1234567890.0
)
data = highlight.to_dict()
self.assertEqual(data['id'], "test-id")
self.assertEqual(len(data['bounds']), 2)
self.assertEqual(data['bounds'][0], (10, 20, 50, 15))
self.assertEqual(data['color'], (255, 255, 0, 100))
self.assertEqual(data['text'], "Hello world")
self.assertEqual(data['note'], "Test note")
self.assertEqual(data['tags'], ["test"])
self.assertEqual(data['timestamp'], 1234567890.0)
def test_from_dict(self):
"""Test Highlight deserialization"""
data = {
'id': "test-id",
'bounds': [[10, 20, 50, 15], [70, 20, 40, 15]],
'color': [255, 255, 0, 100],
'text': "Hello world",
'note': "Test note",
'tags': ["test"],
'timestamp': 1234567890.0,
'start_word_index': 5,
'end_word_index': 6
}
highlight = Highlight.from_dict(data)
self.assertEqual(highlight.id, "test-id")
self.assertEqual(len(highlight.bounds), 2)
self.assertEqual(highlight.bounds[0], (10, 20, 50, 15))
self.assertEqual(highlight.color, (255, 255, 0, 100))
self.assertEqual(highlight.text, "Hello world")
self.assertEqual(highlight.note, "Test note")
self.assertEqual(highlight.tags, ["test"])
self.assertEqual(highlight.start_word_index, 5)
self.assertEqual(highlight.end_word_index, 6)
class TestHighlightManager(unittest.TestCase):
"""Test HighlightManager class"""
def setUp(self):
"""Create temporary directory for highlights"""
self.temp_dir = tempfile.mkdtemp()
self.manager = HighlightManager(
document_id="test-doc",
highlights_dir=self.temp_dir
)
def tearDown(self):
"""Clean up temporary directory"""
shutil.rmtree(self.temp_dir)
def test_init(self):
"""Test HighlightManager initialization"""
self.assertEqual(self.manager.document_id, "test-doc")
self.assertEqual(self.manager.highlights_dir, Path(self.temp_dir))
self.assertEqual(len(self.manager.highlights), 0)
def test_add_highlight(self):
"""Test adding a highlight"""
highlight = Highlight(
id="test-1",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="Test"
)
self.manager.add_highlight(highlight)
self.assertEqual(len(self.manager.highlights), 1)
self.assertIn("test-1", self.manager.highlights)
self.assertEqual(self.manager.highlights["test-1"], highlight)
def test_remove_highlight(self):
"""Test removing a highlight"""
highlight = Highlight(
id="test-1",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="Test"
)
self.manager.add_highlight(highlight)
self.assertEqual(len(self.manager.highlights), 1)
result = self.manager.remove_highlight("test-1")
self.assertTrue(result)
self.assertEqual(len(self.manager.highlights), 0)
def test_remove_nonexistent_highlight(self):
"""Test removing a highlight that doesn't exist"""
result = self.manager.remove_highlight("nonexistent")
self.assertFalse(result)
def test_get_highlight(self):
"""Test getting a highlight by ID"""
highlight = Highlight(
id="test-1",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="Test"
)
self.manager.add_highlight(highlight)
retrieved = self.manager.get_highlight("test-1")
self.assertIsNotNone(retrieved)
self.assertEqual(retrieved.id, "test-1")
self.assertEqual(retrieved.text, "Test")
def test_list_highlights(self):
"""Test listing all highlights"""
highlight1 = Highlight(
id="test-1",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="First"
)
highlight2 = Highlight(
id="test-2",
bounds=[(100, 20, 50, 15)],
color=(100, 255, 100, 100),
text="Second"
)
self.manager.add_highlight(highlight1)
self.manager.add_highlight(highlight2)
highlights = self.manager.list_highlights()
self.assertEqual(len(highlights), 2)
self.assertIn(highlight1, highlights)
self.assertIn(highlight2, highlights)
def test_clear_all(self):
"""Test clearing all highlights"""
highlight1 = Highlight(
id="test-1",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="First"
)
highlight2 = Highlight(
id="test-2",
bounds=[(100, 20, 50, 15)],
color=(100, 255, 100, 100),
text="Second"
)
self.manager.add_highlight(highlight1)
self.manager.add_highlight(highlight2)
self.assertEqual(len(self.manager.highlights), 2)
self.manager.clear_all()
self.assertEqual(len(self.manager.highlights), 0)
def test_persistence(self):
"""Test that highlights are persisted to disk"""
highlight = Highlight(
id="test-1",
bounds=[(10, 20, 50, 15)],
color=(255, 255, 0, 100),
text="Persisted"
)
self.manager.add_highlight(highlight)
# Create new manager for same document
new_manager = HighlightManager(
document_id="test-doc",
highlights_dir=self.temp_dir
)
# Should load existing highlights
self.assertEqual(len(new_manager.highlights), 1)
self.assertIn("test-1", new_manager.highlights)
self.assertEqual(new_manager.highlights["test-1"].text, "Persisted")
def test_get_highlights_for_page(self):
"""Test filtering highlights by page bounds"""
# Highlight on page
highlight1 = Highlight(
id="test-1",
bounds=[(100, 100, 50, 15)],
color=(255, 255, 0, 100),
text="On page"
)
# Highlight off page
highlight2 = Highlight(
id="test-2",
bounds=[(1000, 1000, 50, 15)],
color=(255, 255, 0, 100),
text="Off page"
)
self.manager.add_highlight(highlight1)
self.manager.add_highlight(highlight2)
# Page bounds (0, 0, 800, 1000)
page_bounds = (0, 0, 800, 1000)
page_highlights = self.manager.get_highlights_for_page(page_bounds)
self.assertEqual(len(page_highlights), 1)
self.assertEqual(page_highlights[0].id, "test-1")
class TestCreateHighlightFromQueryResult(unittest.TestCase):
"""Test create_highlight_from_query_result function"""
def test_create_from_single_result(self):
"""Test creating highlight from single QueryResult"""
result = QueryResult(
object=object(),
object_type="text",
bounds=(10, 20, 50, 15),
text="Hello"
)
highlight = create_highlight_from_query_result(
result,
color=(255, 255, 0, 100),
note="Test note",
tags=["test"]
)
self.assertIsNotNone(highlight.id)
self.assertEqual(len(highlight.bounds), 1)
self.assertEqual(highlight.bounds[0], (10, 20, 50, 15))
self.assertEqual(highlight.color, (255, 255, 0, 100))
self.assertEqual(highlight.text, "Hello")
self.assertEqual(highlight.note, "Test note")
self.assertEqual(highlight.tags, ["test"])
self.assertIsNotNone(highlight.timestamp)
def test_create_from_selection_range(self):
"""Test creating highlight from SelectionRange"""
results = [
QueryResult(object(), "text", (10, 20, 30, 15), text="Hello"),
QueryResult(object(), "text", (45, 20, 35, 15), text="world")
]
sel_range = SelectionRange((10, 20), (80, 35), results)
highlight = create_highlight_from_query_result(
sel_range,
color=(100, 255, 100, 100),
note="Multi-word"
)
self.assertIsNotNone(highlight.id)
self.assertEqual(len(highlight.bounds), 2)
self.assertEqual(highlight.bounds[0], (10, 20, 30, 15))
self.assertEqual(highlight.bounds[1], (45, 20, 35, 15))
self.assertEqual(highlight.color, (100, 255, 100, 100))
self.assertEqual(highlight.text, "Hello world")
self.assertEqual(highlight.note, "Multi-word")
if __name__ == '__main__':
unittest.main()
+424
View File
@@ -0,0 +1,424 @@
"""
Unit tests for the query system (pixel-to-content mapping).
Tests the QueryResult, SelectionRange, and query_point functionality
across Page, Line, and Text classes.
"""
import unittest
import numpy as np
from PIL import Image, ImageDraw
from pyWebLayout.core.query import QueryResult, SelectionRange
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.text import Text, Line
from pyWebLayout.concrete.functional import LinkText
from pyWebLayout.abstract.inline import Word
from pyWebLayout.abstract.functional import Link, LinkType
from pyWebLayout.style import Font, Alignment
from pyWebLayout.style.page_style import PageStyle
from tests.utils.test_fonts import create_default_test_font, ensure_consistent_font_in_tests
class TestQueryResult(unittest.TestCase):
"""Test QueryResult dataclass"""
def test_init_basic(self):
"""Test basic QueryResult creation"""
obj = object()
result = QueryResult(
object=obj,
object_type="text",
bounds=(100, 200, 50, 20)
)
self.assertEqual(result.object, obj)
self.assertEqual(result.object_type, "text")
self.assertEqual(result.bounds, (100, 200, 50, 20))
self.assertIsNone(result.text)
self.assertFalse(result.is_interactive)
def test_init_with_metadata(self):
"""Test QueryResult with full metadata"""
obj = object()
result = QueryResult(
object=obj,
object_type="link",
bounds=(100, 200, 50, 20),
text="Click here",
is_interactive=True,
link_target="chapter2"
)
self.assertEqual(result.text, "Click here")
self.assertTrue(result.is_interactive)
self.assertEqual(result.link_target, "chapter2")
def test_to_dict(self):
"""Test QueryResult serialization"""
result = QueryResult(
object=object(),
object_type="link",
bounds=(100, 200, 50, 20),
text="Click here",
is_interactive=True,
link_target="chapter2"
)
d = result.to_dict()
self.assertEqual(d['object_type'], "link")
self.assertEqual(d['bounds'], (100, 200, 50, 20))
self.assertEqual(d['text'], "Click here")
self.assertTrue(d['is_interactive'])
self.assertEqual(d['link_target'], "chapter2")
class TestSelectionRange(unittest.TestCase):
"""Test SelectionRange dataclass"""
def test_init(self):
"""Test SelectionRange creation"""
results = []
sel_range = SelectionRange(
start_point=(10, 20),
end_point=(100, 30),
results=results
)
self.assertEqual(sel_range.start_point, (10, 20))
self.assertEqual(sel_range.end_point, (100, 30))
self.assertEqual(sel_range.results, results)
def test_text_property(self):
"""Test concatenated text extraction"""
results = [
QueryResult(object(), "text", (0, 0, 0, 0), text="Hello"),
QueryResult(object(), "text", (0, 0, 0, 0), text="world"),
QueryResult(object(), "text", (0, 0, 0, 0), text="test")
]
sel_range = SelectionRange((0, 0), (100, 100), results)
self.assertEqual(sel_range.text, "Hello world test")
def test_bounds_list_property(self):
"""Test bounds list extraction"""
results = [
QueryResult(object(), "text", (10, 20, 30, 15), text="Hello"),
QueryResult(object(), "text", (45, 20, 35, 15), text="world")
]
sel_range = SelectionRange((0, 0), (100, 100), results)
bounds = sel_range.bounds_list
self.assertEqual(len(bounds), 2)
self.assertEqual(bounds[0], (10, 20, 30, 15))
self.assertEqual(bounds[1], (45, 20, 35, 15))
def test_to_dict(self):
"""Test SelectionRange serialization"""
results = [
QueryResult(object(), "text", (10, 20, 30, 15), text="Hello"),
QueryResult(object(), "text", (45, 20, 35, 15), text="world")
]
sel_range = SelectionRange((10, 20), (80, 35), results)
d = sel_range.to_dict()
self.assertEqual(d['start'], (10, 20))
self.assertEqual(d['end'], (80, 35))
self.assertEqual(d['text'], "Hello world")
self.assertEqual(d['word_count'], 2)
self.assertEqual(len(d['bounds']), 2)
class TestTextQueryPoint(unittest.TestCase):
"""Test Text class in_object (from Queriable mixin)"""
def setUp(self):
ensure_consistent_font_in_tests()
self.canvas = Image.new('RGB', (800, 600), color='white')
self.draw = ImageDraw.Draw(self.canvas)
self.font = create_default_test_font()
def test_in_object_hit(self):
"""Test in_object returns True for point inside text"""
text = Text("Hello", self.font, self.draw)
text.set_origin(np.array([100, 100]))
# Point inside text bounds
self.assertTrue(text.in_object(np.array([110, 105])))
def test_in_object_miss(self):
"""Test in_object returns False for point outside text"""
text = Text("Hello", self.font, self.draw)
text.set_origin(np.array([100, 100]))
# Point outside text bounds
self.assertFalse(text.in_object(np.array([50, 50])))
self.assertFalse(text.in_object(np.array([200, 200])))
class TestLineQueryPoint(unittest.TestCase):
"""Test Line.query_point method"""
def setUp(self):
ensure_consistent_font_in_tests()
self.canvas = Image.new('RGB', (800, 600), color='white')
self.draw = ImageDraw.Draw(self.canvas)
self.font = create_default_test_font()
def test_query_point_finds_text(self):
"""Test Line.query_point finds a text object"""
line = Line(
spacing=(5, 10),
origin=np.array([50, 100]),
size=(700, 30),
draw=self.draw,
font=self.font
)
# Add text objects
word1 = Word("Hello", self.font)
word2 = Word("world", self.font)
line.add_word(word1)
line.add_word(word2)
line.render()
# Query a point that should hit first word
# (after rendering, text objects have positions set)
if len(line._text_objects) > 0:
text_obj = line._text_objects[0]
point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1] + 5))
result = line.query_point(point)
self.assertIsNotNone(result)
self.assertEqual(result.object_type, "text")
self.assertIsNotNone(result.text)
def test_query_point_miss(self):
"""Test Line.query_point returns None for miss"""
line = Line(
spacing=(5, 10),
origin=np.array([50, 100]),
size=(700, 30),
draw=self.draw,
font=self.font
)
word1 = Word("Hello", self.font)
line.add_word(word1)
line.render()
# Query far outside line bounds
result = line.query_point((10, 10))
self.assertIsNone(result)
def test_query_point_finds_link(self):
"""Test Line.query_point correctly identifies links"""
line = Line(
spacing=(5, 10),
origin=np.array([50, 100]),
size=(700, 30),
draw=self.draw,
font=self.font
)
# Create a linked word
from pyWebLayout.abstract.inline import LinkedWord
linked_word = LinkedWord("Click", self.font, "chapter2", LinkType.INTERNAL)
line.add_word(linked_word)
line.render()
# Query the link
if len(line._text_objects) > 0:
text_obj = line._text_objects[0]
point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1] + 5))
result = line.query_point(point)
self.assertIsNotNone(result)
self.assertEqual(result.object_type, "link")
self.assertTrue(result.is_interactive)
self.assertEqual(result.link_target, "chapter2")
class TestPageQueryPoint(unittest.TestCase):
"""Test Page.query_point method"""
def setUp(self):
ensure_consistent_font_in_tests()
self.page = Page(size=(800, 1000), style=PageStyle())
self.font = create_default_test_font()
def test_query_point_empty_page(self):
"""Test querying empty page returns empty result"""
result = self.page.query_point((400, 500))
self.assertIsNotNone(result)
self.assertEqual(result.object_type, "empty")
self.assertEqual(result.object, self.page)
def test_query_point_finds_line(self):
"""Test Page.query_point traverses to Line"""
line = Line(
spacing=(5, 10),
origin=np.array([50, 100]),
size=(700, 30),
draw=self.page.draw,
font=self.font
)
word = Word("Hello", self.font)
line.add_word(word)
line.render()
self.page.add_child(line)
# Query a point inside the line
if len(line._text_objects) > 0:
text_obj = line._text_objects[0]
point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1] + 5))
result = self.page.query_point(point)
# Should traverse Page → Line → Text
self.assertIsNotNone(result)
self.assertEqual(result.object_type, "text")
self.assertEqual(result.parent_page, self.page)
def test_query_point_multiple_lines(self):
"""Test Page.query_point with multiple lines"""
# Add two lines at different Y positions
line1 = Line(
spacing=(5, 10),
origin=np.array([50, 100]),
size=(700, 30),
draw=self.page.draw,
font=self.font
)
line2 = Line(
spacing=(5, 10),
origin=np.array([50, 150]),
size=(700, 30),
draw=self.page.draw,
font=self.font
)
word1 = Word("First", self.font)
word2 = Word("Second", self.font)
line1.add_word(word1)
line2.add_word(word2)
line1.render()
line2.render()
self.page.add_child(line1)
self.page.add_child(line2)
# Query first line
if len(line1._text_objects) > 0:
text_obj1 = line1._text_objects[0]
point1 = (int(text_obj1._origin[0] + 5), int(text_obj1._origin[1] + 5))
result1 = self.page.query_point(point1)
self.assertIsNotNone(result1)
self.assertEqual(result1.text, "First")
# Query second line
if len(line2._text_objects) > 0:
text_obj2 = line2._text_objects[0]
point2 = (int(text_obj2._origin[0] + 5), int(text_obj2._origin[1] + 5))
result2 = self.page.query_point(point2)
self.assertIsNotNone(result2)
self.assertEqual(result2.text, "Second")
class TestPageQueryRange(unittest.TestCase):
"""Test Page.query_range method for text selection"""
def setUp(self):
ensure_consistent_font_in_tests()
self.page = Page(size=(800, 1000), style=PageStyle())
self.font = create_default_test_font()
def test_query_range_single_line(self):
"""Test selecting text within a single line"""
line = Line(
spacing=(5, 10),
origin=np.array([50, 100]),
size=(700, 30),
draw=self.page.draw,
font=self.font
)
# Add multiple words
words = [Word(text, self.font) for text in ["Hello", "world", "test"]]
for word in words:
line.add_word(word)
line.render()
self.page.add_child(line)
if len(line._text_objects) >= 2:
# Select from first to second word
start_text = line._text_objects[0]
end_text = line._text_objects[1]
start_point = (int(start_text._origin[0] + 5), int(start_text._origin[1] + 5))
end_point = (int(end_text._origin[0] + 5), int(end_text._origin[1] + 5))
sel_range = self.page.query_range(start_point, end_point)
self.assertIsNotNone(sel_range)
self.assertGreater(len(sel_range.results), 0)
self.assertIn("Hello", sel_range.text)
def test_query_range_invalid(self):
"""Test query_range with invalid points returns empty"""
sel_range = self.page.query_range((10, 10), (20, 20))
self.assertEqual(len(sel_range.results), 0)
self.assertEqual(sel_range.text, "")
class TestPageMakeQueryResult(unittest.TestCase):
"""Test Page._make_query_result helper"""
def setUp(self):
ensure_consistent_font_in_tests()
self.page = Page(size=(800, 1000), style=PageStyle())
self.font = create_default_test_font()
self.draw = self.page.draw
def test_make_query_result_text(self):
"""Test packaging regular Text object"""
text = Text("Hello", self.font, self.draw)
text.set_origin(np.array([100, 200]))
result = self.page._make_query_result(text, (105, 205))
self.assertEqual(result.object_type, "text")
self.assertEqual(result.text, "Hello")
self.assertFalse(result.is_interactive)
def test_make_query_result_link(self):
"""Test packaging LinkText object"""
link = Link(location="chapter2", link_type=LinkType.INTERNAL, callback=None)
link_text = LinkText(link, "Click here", self.font, self.draw)
link_text.set_origin(np.array([100, 200]))
result = self.page._make_query_result(link_text, (105, 205))
self.assertEqual(result.object_type, "link")
self.assertEqual(result.text, "Click here")
self.assertTrue(result.is_interactive)
self.assertEqual(result.link_target, "chapter2")
if __name__ == '__main__':
unittest.main()
+1
View File
@@ -0,0 +1 @@
"""Tests for I/O functionality."""
+287
View File
@@ -0,0 +1,287 @@
"""
Unit tests for gesture event system.
Tests TouchEvent, GestureType, GestureResponse, and HAL integration.
"""
import unittest
from pyWebLayout.io.gesture import (
GestureType,
TouchEvent,
GestureResponse,
ActionType
)
class TestGestureType(unittest.TestCase):
"""Test GestureType enum"""
def test_gesture_types_exist(self):
"""Test all gesture types are defined"""
self.assertEqual(GestureType.TAP.value, "tap")
self.assertEqual(GestureType.LONG_PRESS.value, "long_press")
self.assertEqual(GestureType.SWIPE_LEFT.value, "swipe_left")
self.assertEqual(GestureType.SWIPE_RIGHT.value, "swipe_right")
self.assertEqual(GestureType.SWIPE_UP.value, "swipe_up")
self.assertEqual(GestureType.SWIPE_DOWN.value, "swipe_down")
self.assertEqual(GestureType.PINCH_IN.value, "pinch_in")
self.assertEqual(GestureType.PINCH_OUT.value, "pinch_out")
self.assertEqual(GestureType.DRAG_START.value, "drag_start")
self.assertEqual(GestureType.DRAG_MOVE.value, "drag_move")
self.assertEqual(GestureType.DRAG_END.value, "drag_end")
class TestTouchEvent(unittest.TestCase):
"""Test TouchEvent dataclass"""
def test_init_basic(self):
"""Test basic TouchEvent creation"""
event = TouchEvent(
gesture=GestureType.TAP,
x=450,
y=320
)
self.assertEqual(event.gesture, GestureType.TAP)
self.assertEqual(event.x, 450)
self.assertEqual(event.y, 320)
self.assertIsNone(event.x2)
self.assertIsNone(event.y2)
self.assertEqual(event.timestamp_ms, 0)
def test_init_with_secondary_point(self):
"""Test TouchEvent with secondary point (pinch/drag)"""
event = TouchEvent(
gesture=GestureType.PINCH_OUT,
x=400,
y=300,
x2=450,
y2=350,
timestamp_ms=12345.678
)
self.assertEqual(event.gesture, GestureType.PINCH_OUT)
self.assertEqual(event.x, 400)
self.assertEqual(event.y, 300)
self.assertEqual(event.x2, 450)
self.assertEqual(event.y2, 350)
self.assertEqual(event.timestamp_ms, 12345.678)
def test_from_hal_basic(self):
"""Test parsing TouchEvent from HAL format"""
hal_data = {
'gesture': 'tap',
'x': 450,
'y': 320
}
event = TouchEvent.from_hal(hal_data)
self.assertEqual(event.gesture, GestureType.TAP)
self.assertEqual(event.x, 450)
self.assertEqual(event.y, 320)
def test_from_hal_complete(self):
"""Test parsing TouchEvent with all fields from HAL"""
hal_data = {
'gesture': 'pinch_out',
'x': 400,
'y': 300,
'x2': 450,
'y2': 350,
'timestamp': 12345.678
}
event = TouchEvent.from_hal(hal_data)
self.assertEqual(event.gesture, GestureType.PINCH_OUT)
self.assertEqual(event.x, 400)
self.assertEqual(event.y, 300)
self.assertEqual(event.x2, 450)
self.assertEqual(event.y2, 350)
self.assertEqual(event.timestamp_ms, 12345.678)
def test_to_dict(self):
"""Test TouchEvent serialization"""
event = TouchEvent(
gesture=GestureType.SWIPE_LEFT,
x=600,
y=400,
timestamp_ms=12345.0
)
d = event.to_dict()
self.assertEqual(d['gesture'], 'swipe_left')
self.assertEqual(d['x'], 600)
self.assertEqual(d['y'], 400)
self.assertIsNone(d['x2'])
self.assertIsNone(d['y2'])
self.assertEqual(d['timestamp_ms'], 12345.0)
class TestGestureResponse(unittest.TestCase):
"""Test GestureResponse dataclass"""
def test_init(self):
"""Test GestureResponse creation"""
response = GestureResponse(
action="page_turn",
data={"direction": "forward", "progress": 0.42}
)
self.assertEqual(response.action, "page_turn")
self.assertEqual(response.data['direction'], "forward")
self.assertEqual(response.data['progress'], 0.42)
def test_to_dict(self):
"""Test GestureResponse serialization"""
response = GestureResponse(
action="define",
data={"word": "ephemeral", "bounds": (100, 200, 50, 20)}
)
d = response.to_dict()
self.assertEqual(d['action'], "define")
self.assertEqual(d['data']['word'], "ephemeral")
self.assertEqual(d['data']['bounds'], (100, 200, 50, 20))
def test_to_dict_empty_data(self):
"""Test GestureResponse with empty data"""
response = GestureResponse(action="none", data={})
d = response.to_dict()
self.assertEqual(d['action'], "none")
self.assertEqual(d['data'], {})
class TestActionType(unittest.TestCase):
"""Test ActionType constants"""
def test_action_types_defined(self):
"""Test all action type constants are defined"""
self.assertEqual(ActionType.NONE, "none")
self.assertEqual(ActionType.PAGE_TURN, "page_turn")
self.assertEqual(ActionType.NAVIGATE, "navigate")
self.assertEqual(ActionType.DEFINE, "define")
self.assertEqual(ActionType.SELECT, "select")
self.assertEqual(ActionType.ZOOM, "zoom")
self.assertEqual(ActionType.BOOK_LOADED, "book_loaded")
self.assertEqual(ActionType.WORD_SELECTED, "word_selected")
self.assertEqual(ActionType.SHOW_MENU, "show_menu")
self.assertEqual(ActionType.SELECTION_START, "selection_start")
self.assertEqual(ActionType.SELECTION_UPDATE, "selection_update")
self.assertEqual(ActionType.SELECTION_COMPLETE, "selection_complete")
self.assertEqual(ActionType.AT_START, "at_start")
self.assertEqual(ActionType.AT_END, "at_end")
self.assertEqual(ActionType.ERROR, "error")
class TestHALIntegration(unittest.TestCase):
"""Test HAL integration scenarios"""
def test_hal_tap_flow(self):
"""Test complete HAL tap event flow"""
# Simulate HAL sending tap event
hal_data = {
'gesture': 'tap',
'x': 450,
'y': 320,
'timestamp': 1234567890.123
}
# Parse event
event = TouchEvent.from_hal(hal_data)
# Verify event
self.assertEqual(event.gesture, GestureType.TAP)
self.assertEqual(event.x, 450)
self.assertEqual(event.y, 320)
# Simulate business logic response
response = GestureResponse(
action=ActionType.WORD_SELECTED,
data={"word": "hello", "bounds": (440, 310, 50, 20)}
)
# Serialize for Flask
response_dict = response.to_dict()
self.assertEqual(response_dict['action'], "word_selected")
self.assertEqual(response_dict['data']['word'], "hello")
def test_hal_pinch_flow(self):
"""Test complete HAL pinch event flow"""
# Simulate HAL sending pinch event with two touch points
hal_data = {
'gesture': 'pinch_out',
'x': 400,
'y': 500,
'x2': 500,
'y2': 500,
'timestamp': 1234567891.456
}
event = TouchEvent.from_hal(hal_data)
self.assertEqual(event.gesture, GestureType.PINCH_OUT)
self.assertEqual(event.x, 400)
self.assertEqual(event.x2, 500)
def test_hal_swipe_flow(self):
"""Test complete HAL swipe event flow"""
hal_data = {
'gesture': 'swipe_left',
'x': 600,
'y': 400
}
event = TouchEvent.from_hal(hal_data)
self.assertEqual(event.gesture, GestureType.SWIPE_LEFT)
# Expected response
response = GestureResponse(
action=ActionType.PAGE_TURN,
data={"direction": "forward", "progress": 0.25}
)
self.assertEqual(response.action, "page_turn")
def test_hal_drag_selection_flow(self):
"""Test complete drag selection flow"""
# Drag start
start_data = {
'gesture': 'drag_start',
'x': 100,
'y': 200
}
start_event = TouchEvent.from_hal(start_data)
self.assertEqual(start_event.gesture, GestureType.DRAG_START)
# Drag move
move_data = {
'gesture': 'drag_move',
'x': 300,
'y': 250
}
move_event = TouchEvent.from_hal(move_data)
self.assertEqual(move_event.gesture, GestureType.DRAG_MOVE)
# Drag end
end_data = {
'gesture': 'drag_end',
'x': 500,
'y': 300
}
end_event = TouchEvent.from_hal(end_data)
self.assertEqual(end_event.gesture, GestureType.DRAG_END)
if __name__ == '__main__':
unittest.main()
-832
View File
@@ -1,832 +0,0 @@
"""
Comprehensive tests for the EbookReader application interface.
Tests cover:
- EPUB loading and initialization
- Navigation (forward, backward, boundaries)
- Font scaling and styling
- Chapter navigation
- Position management (bookmarks)
- Information retrieval
- File operations
- Error handling
- Context manager
- Integration scenarios
"""
import unittest
import tempfile
import shutil
from pathlib import Path
import numpy as np
from PIL import Image
import os
from pyWebLayout.layout.ereader_application import EbookReader, create_ebook_reader
class TestEbookReaderInitialization(unittest.TestCase):
"""Test EbookReader creation and EPUB loading"""
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_create_reader_with_defaults(self):
"""Test creating reader with default settings"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
self.assertEqual(reader.page_size, (800, 1000))
self.assertEqual(reader.base_font_scale, 1.0)
self.assertIsNone(reader.manager)
self.assertFalse(reader.is_loaded())
reader.close()
def test_create_reader_with_custom_settings(self):
"""Test creating reader with custom settings"""
reader = EbookReader(
page_size=(600, 800),
margin=50,
background_color=(240, 240, 240),
line_spacing=10,
inter_block_spacing=20,
bookmarks_dir=self.temp_dir,
buffer_size=3
)
self.assertEqual(reader.page_size, (600, 800))
self.assertEqual(reader.page_style.line_spacing, 10)
self.assertEqual(reader.page_style.inter_block_spacing, 20)
self.assertEqual(reader.buffer_size, 3)
reader.close()
def test_load_valid_epub(self):
"""Test loading a valid EPUB file"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
success = reader.load_epub(self.epub_path)
self.assertTrue(success)
self.assertTrue(reader.is_loaded())
self.assertIsNotNone(reader.manager)
self.assertIsNotNone(reader.blocks)
self.assertIsNotNone(reader.document_id)
self.assertIsNotNone(reader.book_title)
self.assertIsNotNone(reader.book_author)
reader.close()
def test_load_nonexistent_epub(self):
"""Test loading a non-existent EPUB file"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
success = reader.load_epub("nonexistent.epub")
self.assertFalse(success)
self.assertFalse(reader.is_loaded())
reader.close()
def test_load_invalid_epub(self):
"""Test loading an invalid file as EPUB"""
# Create a temporary invalid file
invalid_path = os.path.join(self.temp_dir, "invalid.epub")
with open(invalid_path, 'w') as f:
f.write("This is not a valid EPUB file")
reader = EbookReader(bookmarks_dir=self.temp_dir)
success = reader.load_epub(invalid_path)
self.assertFalse(success)
self.assertFalse(reader.is_loaded())
reader.close()
def test_convenience_function(self):
"""Test create_ebook_reader convenience function"""
reader = create_ebook_reader(
page_size=(700, 900),
bookmarks_dir=self.temp_dir
)
self.assertIsInstance(reader, EbookReader)
self.assertEqual(reader.page_size, (700, 900))
reader.close()
class TestEbookReaderFontScaling(unittest.TestCase):
"""Test font size control"""
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}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0 # Disable buffering for tests
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_set_font_size(self):
"""Test setting font size with arbitrary scale"""
page = self.reader.set_font_size(1.5)
self.assertIsNotNone(page)
self.assertEqual(self.reader.get_font_size(), 1.5)
def test_increase_font_size(self):
"""Test increasing font size by one step"""
initial_size = self.reader.get_font_size()
page = self.reader.increase_font_size()
self.assertIsNotNone(page)
self.assertEqual(self.reader.get_font_size(), initial_size + 0.1)
def test_decrease_font_size(self):
"""Test decreasing font size by one step"""
self.reader.set_font_size(1.5)
page = self.reader.decrease_font_size()
self.assertIsNotNone(page)
self.assertAlmostEqual(self.reader.get_font_size(), 1.4, places=5)
def test_font_size_bounds_clamping(self):
"""Test that font size is clamped between 0.5x and 3.0x"""
# Test upper bound
self.reader.set_font_size(5.0)
self.assertEqual(self.reader.get_font_size(), 3.0)
# Test lower bound
self.reader.set_font_size(0.1)
self.assertEqual(self.reader.get_font_size(), 0.5)
def test_get_font_size(self):
"""Test getting current font size"""
self.assertEqual(self.reader.get_font_size(), 1.0)
self.reader.set_font_size(2.0)
self.assertEqual(self.reader.get_font_size(), 2.0)
def test_font_scale_with_navigation(self):
"""Test that font scale persists across page navigation"""
self.reader.set_font_size(1.5)
initial_font_size = self.reader.get_font_size()
# Navigate forward
self.reader.next_page()
# Font size should be preserved
self.assertEqual(self.reader.get_font_size(), initial_font_size)
class TestEbookReaderSpacing(unittest.TestCase):
"""Test line and block spacing"""
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}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_set_line_spacing(self):
"""Test setting line spacing"""
page = self.reader.set_line_spacing(10)
self.assertIsNotNone(page)
self.assertEqual(self.reader.page_style.line_spacing, 10)
def test_set_inter_block_spacing(self):
"""Test setting inter-block spacing"""
page = self.reader.set_inter_block_spacing(25)
self.assertIsNotNone(page)
self.assertEqual(self.reader.page_style.inter_block_spacing, 25)
def test_spacing_with_navigation(self):
"""Test that spacing changes affect rendering after navigation"""
self.reader.set_line_spacing(15)
page = self.reader.next_page()
self.assertIsNotNone(page)
self.assertEqual(self.reader.page_style.line_spacing, 15)
def test_spacing_position_preservation(self):
"""Test that changing spacing preserves reading position"""
# Navigate to a specific position
for _ in range(3):
self.reader.next_page()
position_before = self.reader.manager.current_position.copy()
# Change spacing
self.reader.set_line_spacing(12)
position_after = self.reader.manager.current_position
# Position should be preserved
self.assertEqual(position_before.chapter_index, position_after.chapter_index)
self.assertEqual(position_before.block_index, position_after.block_index)
class TestEbookReaderChapterNavigation(unittest.TestCase):
"""Test chapter navigation 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}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_get_chapters(self):
"""Test getting list of chapters"""
chapters = self.reader.get_chapters()
self.assertIsInstance(chapters, list)
if len(chapters) > 0:
# Each chapter should be a tuple (title, index)
self.assertIsInstance(chapters[0], tuple)
self.assertEqual(len(chapters[0]), 2)
def test_get_chapter_positions(self):
"""Test getting chapter positions"""
positions = self.reader.get_chapter_positions()
self.assertIsInstance(positions, list)
if len(positions) > 0:
# Each item should be (title, RenderingPosition)
self.assertIsInstance(positions[0], tuple)
self.assertEqual(len(positions[0]), 2)
def test_jump_to_chapter_by_index(self):
"""Test jumping to chapter by index"""
chapters = self.reader.get_chapters()
if len(chapters) > 0:
page = self.reader.jump_to_chapter(0)
self.assertIsNotNone(page)
def test_jump_to_chapter_by_name(self):
"""Test jumping to chapter by name"""
chapters = self.reader.get_chapters()
if len(chapters) > 0:
chapter_title = chapters[0][0]
page = self.reader.jump_to_chapter(chapter_title)
self.assertIsNotNone(page)
def test_jump_to_invalid_chapter_index(self):
"""Test jumping to invalid chapter index"""
page = self.reader.jump_to_chapter(9999)
self.assertIsNone(page)
def test_jump_to_invalid_chapter_name(self):
"""Test jumping to non-existent chapter name"""
page = self.reader.jump_to_chapter("Non-Existent Chapter")
self.assertIsNone(page)
class TestEbookReaderInformation(unittest.TestCase):
"""Test information retrieval methods"""
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}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_get_position_info(self):
"""Test getting detailed position information"""
info = self.reader.get_position_info()
self.assertIsInstance(info, dict)
self.assertIn('position', info)
self.assertIn('chapter', info)
self.assertIn('progress', info)
self.assertIn('font_scale', info)
self.assertIn('book_title', info)
self.assertIn('book_author', info)
def test_get_reading_progress(self):
"""Test getting reading progress as percentage"""
progress = self.reader.get_reading_progress()
self.assertIsInstance(progress, float)
self.assertGreaterEqual(progress, 0.0)
self.assertLessEqual(progress, 1.0)
# Progress should increase after navigation
initial_progress = progress
for _ in range(5):
self.reader.next_page()
new_progress = self.reader.get_reading_progress()
self.assertGreater(new_progress, initial_progress)
def test_get_current_chapter_info(self):
"""Test getting current chapter information"""
info = self.reader.get_current_chapter_info()
# May be None if no chapters
if info is not None:
self.assertIsInstance(info, dict)
self.assertIn('title', info)
self.assertIn('level', info)
self.assertIn('block_index', info)
def test_get_book_info_complete(self):
"""Test getting complete book information"""
info = self.reader.get_book_info()
self.assertIsInstance(info, dict)
self.assertIn('title', info)
self.assertIn('author', info)
self.assertIn('document_id', info)
self.assertIn('total_blocks', info)
self.assertIn('total_chapters', info)
self.assertIn('page_size', info)
self.assertIn('font_scale', info)
self.assertGreater(info['total_blocks'], 0)
self.assertEqual(info['page_size'], self.reader.page_size)
class TestEbookReaderFileOperations(unittest.TestCase):
"""Test file I/O operations"""
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}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_render_to_file_png(self):
"""Test saving current page as PNG"""
output_path = os.path.join(self.temp_dir, "page.png")
success = self.reader.render_to_file(output_path)
self.assertTrue(success)
self.assertTrue(os.path.exists(output_path))
# Verify it's a valid image
img = Image.open(output_path)
self.assertEqual(img.size, self.reader.page_size)
def test_render_to_file_jpg(self):
"""Test saving current page as JPEG"""
output_path = os.path.join(self.temp_dir, "page.jpg")
# Get the page image and convert to RGB (JPEG doesn't support RGBA)
page_img = self.reader.get_current_page()
if page_img.mode == 'RGBA':
page_img = page_img.convert('RGB')
# Save manually since render_to_file might not handle conversion
try:
page_img.save(output_path)
success = True
except Exception:
success = False
self.assertTrue(success)
self.assertTrue(os.path.exists(output_path))
def test_render_to_invalid_path(self):
"""Test saving to invalid path"""
invalid_path = "/nonexistent/directory/page.png"
success = self.reader.render_to_file(invalid_path)
self.assertFalse(success)
class TestEbookReaderContextManager(unittest.TestCase):
"""Test context manager and cleanup"""
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_context_manager_usage(self):
"""Test using EbookReader as context manager"""
with EbookReader(bookmarks_dir=self.temp_dir) as reader:
success = reader.load_epub(self.epub_path)
self.assertTrue(success)
page = reader.get_current_page()
self.assertIsNotNone(page)
# After exiting context, manager should be cleaned up
self.assertIsNone(reader.manager)
def test_close_method(self):
"""Test explicit close method"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
reader.load_epub(self.epub_path)
self.assertIsNotNone(reader.manager)
reader.close()
self.assertIsNone(reader.manager)
def test_operations_after_close(self):
"""Test that operations fail gracefully after close"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
reader.load_epub(self.epub_path)
reader.close()
# These should all return None or empty
self.assertIsNone(reader.get_current_page())
self.assertIsNone(reader.next_page())
self.assertIsNone(reader.previous_page())
self.assertEqual(reader.get_chapters(), [])
class TestEbookReaderErrorHandling(unittest.TestCase):
"""Test error handling and edge cases"""
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_operations_without_loaded_book(self):
"""Test that operations handle unloaded state gracefully"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
# All these should return None or empty/False
self.assertIsNone(reader.get_current_page())
self.assertIsNone(reader.next_page())
self.assertIsNone(reader.previous_page())
self.assertFalse(reader.save_position("test"))
self.assertIsNone(reader.load_position("test"))
self.assertEqual(reader.list_saved_positions(), [])
self.assertFalse(reader.delete_position("test"))
self.assertEqual(reader.get_chapters(), [])
self.assertIsNone(reader.jump_to_chapter(0))
self.assertIsNone(reader.set_font_size(1.5))
self.assertEqual(reader.get_reading_progress(), 0.0)
self.assertIsNone(reader.get_current_chapter_info())
reader.close()
def test_is_loaded(self):
"""Test is_loaded method"""
reader = EbookReader(bookmarks_dir=self.temp_dir)
self.assertFalse(reader.is_loaded())
reader.load_epub(self.epub_path)
self.assertTrue(reader.is_loaded())
reader.close()
class TestEbookReaderIntegration(unittest.TestCase):
"""Test complex integration scenarios"""
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}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_font_scaling_preserves_position(self):
"""Test that changing font scale preserves reading position"""
# Navigate to a specific position
for _ in range(3):
self.reader.next_page()
position_before = self.reader.manager.current_position.copy()
# Change font size
self.reader.set_font_size(1.5)
position_after = self.reader.manager.current_position
# Position should be preserved
self.assertEqual(position_before.chapter_index, position_after.chapter_index)
self.assertEqual(position_before.block_index, position_after.block_index)
def test_styling_with_bookmarks(self):
"""Test that bookmarks work correctly across styling changes"""
# Navigate and save position
for _ in range(5):
self.reader.next_page()
self.reader.save_position("test_bookmark")
# Change styling
self.reader.set_font_size(1.5)
self.reader.set_line_spacing(12)
# Navigate away
for _ in range(5):
self.reader.next_page()
# Jump back to bookmark
page = self.reader.load_position("test_bookmark")
self.assertIsNotNone(page)
# Cleanup
self.reader.delete_position("test_bookmark")
def test_chapter_navigation_after_font_change(self):
"""Test chapter navigation after changing font size"""
self.reader.set_font_size(2.0)
chapters = self.reader.get_chapters()
if len(chapters) > 0:
page = self.reader.jump_to_chapter(0)
self.assertIsNotNone(page)
class TestEbookReaderNavigation(unittest.TestCase):
"""Test EbookReader navigation functionality (existing 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 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_bidirectional_navigation_20_pages(self):
"""
Test that navigating forward 20 pages and then backward 20 pages
produces identical page renderings for the first page.
"""
reader = EbookReader(
page_size=(800, 1000),
bookmarks_dir=self.temp_dir,
buffer_size=0
)
success = reader.load_epub(self.epub_path)
self.assertTrue(success, "Failed to load test EPUB")
self.assertTrue(reader.is_loaded(), "Reader should be loaded")
initial_page = reader.get_current_page()
self.assertIsNotNone(initial_page, "Initial page should not be None")
initial_position = reader.manager.current_position.copy()
forward_pages = [initial_page]
forward_positions = [initial_position]
pages_to_navigate = 20
for i in range(pages_to_navigate):
page = reader.next_page()
if page is None:
break
forward_pages.append(page)
forward_positions.append(reader.manager.current_position.copy())
actual_pages_navigated = len(forward_pages) - 1
backward_pages = []
for i in range(len(forward_positions) - 1, -1, -1):
position = forward_positions[i]
page_obj = reader.manager.jump_to_position(position)
page_img = page_obj.render()
backward_pages.append(page_img)
final_page = backward_pages[-1]
self.assertTrue(
self.compare_images(initial_page, final_page),
"First page should be identical after forward/backward navigation"
)
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,
buffer_size=0
)
success = reader.load_epub(self.epub_path)
self.assertTrue(success, "Failed to load test EPUB")
# Try to go backward from first page
page = reader.previous_page()
# Should return None or stay on same page
# Navigate forward until end
pages_forward = 0
max_pages = 100
while pages_forward < max_pages:
page = reader.next_page()
if page is None:
break
pages_forward += 1
# Try to go forward from last page
page = reader.next_page()
self.assertIsNone(page, "Should return None at end of document")
reader.close()
class TestEbookReaderPositionManagement(unittest.TestCase):
"""Test position tracking and bookmark 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}")
self.reader = EbookReader(
bookmarks_dir=self.temp_dir,
buffer_size=0
)
self.reader.load_epub(self.epub_path)
def tearDown(self):
"""Clean up test environment"""
self.reader.close()
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_position_save_and_load(self):
"""Test saving and loading positions"""
# Navigate to a position
for _ in range(3):
self.reader.next_page()
# Save position
success = self.reader.save_position("test_pos")
self.assertTrue(success)
# Navigate away
for _ in range(5):
self.reader.next_page()
# Load saved position
page = self.reader.load_position("test_pos")
self.assertIsNotNone(page)
def test_list_saved_positions(self):
"""Test listing saved positions"""
self.reader.save_position("pos1")
self.reader.save_position("pos2")
positions = self.reader.list_saved_positions()
self.assertIn("pos1", positions)
self.assertIn("pos2", positions)
def test_delete_position(self):
"""Test deleting a saved position"""
self.reader.save_position("temp_pos")
success = self.reader.delete_position("temp_pos")
self.assertTrue(success)
positions = self.reader.list_saved_positions()
self.assertNotIn("temp_pos", positions)
def test_delete_nonexistent_position(self):
"""Test deleting a non-existent position"""
success = self.reader.delete_position("nonexistent")
self.assertFalse(success)
if __name__ == '__main__':
unittest.main()
-455
View File
@@ -1,455 +0,0 @@
"""
Comprehensive tests for the ereader layout system.
Tests the complete ereader functionality including position tracking,
font scaling, chapter navigation, and page buffering.
"""
import unittest
import tempfile
import shutil
from pathlib import Path
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.ereader_layout import RenderingPosition, ChapterNavigator, FontScaler, BidirectionalLayouter
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager, BookmarkManager, create_ereader_manager
class TestRenderingPosition(unittest.TestCase):
"""Test the RenderingPosition class"""
def test_position_creation(self):
"""Test creating a rendering position"""
pos = RenderingPosition(
chapter_index=1,
block_index=5,
word_index=10,
table_row=2,
table_col=3
)
self.assertEqual(pos.chapter_index, 1)
self.assertEqual(pos.block_index, 5)
self.assertEqual(pos.word_index, 10)
self.assertEqual(pos.table_row, 2)
self.assertEqual(pos.table_col, 3)
def test_position_serialization(self):
"""Test position serialization and deserialization"""
pos = RenderingPosition(
chapter_index=1,
block_index=5,
word_index=10,
remaining_pretext="test"
)
# Serialize to dict
pos_dict = pos.to_dict()
self.assertIsInstance(pos_dict, dict)
self.assertEqual(pos_dict['chapter_index'], 1)
self.assertEqual(pos_dict['remaining_pretext'], "test")
# Deserialize from dict
pos2 = RenderingPosition.from_dict(pos_dict)
self.assertEqual(pos, pos2)
def test_position_copy(self):
"""Test position copying"""
pos = RenderingPosition(chapter_index=1, block_index=5)
pos_copy = pos.copy()
self.assertEqual(pos, pos_copy)
self.assertIsNot(pos, pos_copy) # Different objects
# Modify copy
pos_copy.word_index = 10
self.assertNotEqual(pos, pos_copy)
def test_position_equality_and_hashing(self):
"""Test position equality and hashing"""
pos1 = RenderingPosition(chapter_index=1, block_index=5)
pos2 = RenderingPosition(chapter_index=1, block_index=5)
pos3 = RenderingPosition(chapter_index=1, block_index=6)
self.assertEqual(pos1, pos2)
self.assertNotEqual(pos1, pos3)
# Test hashing (for use as dict keys)
pos_dict = {pos1: "test"}
self.assertEqual(pos_dict[pos2], "test") # Should work due to equality
class TestChapterNavigator(unittest.TestCase):
"""Test the ChapterNavigator class"""
def setUp(self):
"""Set up test data"""
self.font = Font()
# Create test blocks with headings
self.blocks = [
Paragraph(self.font), # Block 0
Heading(HeadingLevel.H1, self.font), # Block 1 - Chapter 1
Paragraph(self.font), # Block 2
Heading(HeadingLevel.H2, self.font), # Block 3 - Subsection
Paragraph(self.font), # Block 4
Heading(HeadingLevel.H1, self.font), # Block 5 - Chapter 2
Paragraph(self.font), # Block 6
]
# Add text to headings
self.blocks[1].add_word(Word("Chapter", self.font))
self.blocks[1].add_word(Word("One", self.font))
self.blocks[3].add_word(Word("Subsection", self.font))
self.blocks[3].add_word(Word("A", self.font))
self.blocks[5].add_word(Word("Chapter", self.font))
self.blocks[5].add_word(Word("Two", self.font))
def test_chapter_detection(self):
"""Test that chapters are detected correctly"""
navigator = ChapterNavigator(self.blocks)
self.assertEqual(len(navigator.chapters), 3) # 2 H1s + 1 H2
# Check chapter titles
titles = [chapter.title for chapter in navigator.chapters]
self.assertIn("Chapter One", titles)
self.assertIn("Subsection A", titles)
self.assertIn("Chapter Two", titles)
def test_table_of_contents(self):
"""Test table of contents generation"""
navigator = ChapterNavigator(self.blocks)
toc = navigator.get_table_of_contents()
self.assertEqual(len(toc), 3)
# Check first entry
title, level, position = toc[0]
self.assertEqual(title, "Chapter One")
self.assertEqual(level, HeadingLevel.H1)
self.assertIsInstance(position, RenderingPosition)
def test_chapter_position_lookup(self):
"""Test looking up chapter positions"""
navigator = ChapterNavigator(self.blocks)
pos = navigator.get_chapter_position("Chapter One")
self.assertIsNotNone(pos)
self.assertEqual(pos.chapter_index, 0)
pos = navigator.get_chapter_position("Nonexistent Chapter")
self.assertIsNone(pos)
def test_current_chapter_detection(self):
"""Test detecting current chapter from position"""
navigator = ChapterNavigator(self.blocks)
# Position in first chapter
pos = RenderingPosition(chapter_index=0, block_index=2)
chapter = navigator.get_current_chapter(pos)
self.assertIsNotNone(chapter)
self.assertEqual(chapter.title, "Chapter One")
class TestFontScaler(unittest.TestCase):
"""Test the FontScaler class"""
def test_font_scaling(self):
"""Test font scaling functionality"""
original_font = Font(font_size=12)
# Test no scaling
scaled_font = FontScaler.scale_font(original_font, 1.0)
self.assertEqual(scaled_font.font_size, 12)
# Test 2x scaling
scaled_font = FontScaler.scale_font(original_font, 2.0)
self.assertEqual(scaled_font.font_size, 24)
# Test 0.5x scaling
scaled_font = FontScaler.scale_font(original_font, 0.5)
self.assertEqual(scaled_font.font_size, 6)
# Test minimum size constraint
scaled_font = FontScaler.scale_font(original_font, 0.01)
self.assertGreaterEqual(scaled_font.font_size, 1)
def test_word_spacing_scaling(self):
"""Test word spacing scaling"""
original_spacing = (5, 15)
# Test no scaling
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 1.0)
self.assertEqual(scaled_spacing, (5, 15))
# Test 2x scaling
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 2.0)
self.assertEqual(scaled_spacing, (10, 30))
# Test minimum constraints
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 0.1)
self.assertGreaterEqual(scaled_spacing[0], 1)
self.assertGreaterEqual(scaled_spacing[1], 2)
class TestBookmarkManager(unittest.TestCase):
"""Test the BookmarkManager class"""
def setUp(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.document_id = "test_document"
self.bookmark_manager = BookmarkManager(self.document_id, self.temp_dir)
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir)
def test_bookmark_operations(self):
"""Test bookmark add/remove/get operations"""
pos = RenderingPosition(chapter_index=1, block_index=5)
# Add bookmark
self.bookmark_manager.add_bookmark("test_bookmark", pos)
# Get bookmark
retrieved_pos = self.bookmark_manager.get_bookmark("test_bookmark")
self.assertEqual(retrieved_pos, pos)
# List bookmarks
bookmarks = self.bookmark_manager.list_bookmarks()
self.assertEqual(len(bookmarks), 1)
self.assertEqual(bookmarks[0][0], "test_bookmark")
self.assertEqual(bookmarks[0][1], pos)
# Remove bookmark
success = self.bookmark_manager.remove_bookmark("test_bookmark")
self.assertTrue(success)
# Verify removal
retrieved_pos = self.bookmark_manager.get_bookmark("test_bookmark")
self.assertIsNone(retrieved_pos)
def test_reading_position_persistence(self):
"""Test saving and loading reading position"""
pos = RenderingPosition(chapter_index=2, block_index=10, word_index=5)
# Save position
self.bookmark_manager.save_reading_position(pos)
# Create new manager instance (simulates app restart)
new_manager = BookmarkManager(self.document_id, self.temp_dir)
# Load position
loaded_pos = new_manager.load_reading_position()
self.assertEqual(loaded_pos, pos)
def test_bookmark_persistence(self):
"""Test that bookmarks persist across manager instances"""
pos = RenderingPosition(chapter_index=1, block_index=5)
# Add bookmark
self.bookmark_manager.add_bookmark("persistent_bookmark", pos)
# Create new manager instance
new_manager = BookmarkManager(self.document_id, self.temp_dir)
# Verify bookmark exists
retrieved_pos = new_manager.get_bookmark("persistent_bookmark")
self.assertEqual(retrieved_pos, pos)
class TestEreaderLayoutManager(unittest.TestCase):
"""Test the complete EreaderLayoutManager"""
def setUp(self):
"""Set up test data"""
self.temp_dir = tempfile.mkdtemp()
self.font = Font()
# Create test document with multiple paragraphs and headings
self.blocks = []
# Add a heading
heading = Heading(HeadingLevel.H1, self.font)
heading.add_word(Word("Test", self.font))
heading.add_word(Word("Chapter", self.font))
self.blocks.append(heading)
# Add several paragraphs with multiple words
for i in range(3):
paragraph = Paragraph(self.font)
for j in range(20): # 20 words per paragraph
paragraph.add_word(Word(f"Word{i}_{j}", self.font))
self.blocks.append(paragraph)
self.page_size = (400, 600)
self.document_id = "test_document"
def tearDown(self):
"""Clean up test environment"""
shutil.rmtree(self.temp_dir)
def test_manager_initialization(self):
"""Test ereader manager initialization"""
# Change to temp directory for bookmarks
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id,
bookmarks_dir=self.temp_dir
)
self.assertEqual(manager.page_size, self.page_size)
self.assertEqual(manager.document_id, self.document_id)
self.assertEqual(manager.font_scale, 1.0)
self.assertIsInstance(manager.current_position, RenderingPosition)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_font_scaling(self):
"""Test font scaling functionality"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id,
bookmarks_dir=self.temp_dir
)
# Test initial scale
self.assertEqual(manager.get_font_scale(), 1.0)
# Test scaling
page = manager.set_font_scale(1.5)
self.assertEqual(manager.get_font_scale(), 1.5)
self.assertIsNotNone(page)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_table_of_contents(self):
"""Test table of contents functionality"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id,
bookmarks_dir=self.temp_dir
)
toc = manager.get_table_of_contents()
self.assertGreater(len(toc), 0)
# Check first entry
title, level, position = toc[0]
self.assertEqual(title, "Test Chapter")
self.assertEqual(level, HeadingLevel.H1)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_bookmark_functionality(self):
"""Test bookmark functionality"""
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id,
bookmarks_dir=self.temp_dir
)
# Add bookmark
success = manager.add_bookmark("test_bookmark")
self.assertTrue(success)
# List bookmarks
bookmarks = manager.list_bookmarks()
self.assertEqual(len(bookmarks), 1)
self.assertEqual(bookmarks[0][0], "test_bookmark")
# Jump to bookmark (should work even though it's the same position)
page = manager.jump_to_bookmark("test_bookmark")
self.assertIsNotNone(page)
# Remove bookmark
success = manager.remove_bookmark("test_bookmark")
self.assertTrue(success)
manager.shutdown()
def test_progress_tracking(self):
"""Test reading progress tracking"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = EreaderLayoutManager(
self.blocks,
self.page_size,
self.document_id,
bookmarks_dir=self.temp_dir
)
# Initial progress should be 0
progress = manager.get_reading_progress()
self.assertGreaterEqual(progress, 0.0)
self.assertLessEqual(progress, 1.0)
# Get position info
info = manager.get_position_info()
self.assertIn('position', info)
self.assertIn('progress', info)
self.assertIn('font_scale', info)
manager.shutdown()
finally:
os.chdir(original_cwd)
def test_convenience_function(self):
"""Test the convenience function"""
original_cwd = Path.cwd()
try:
import os
os.chdir(self.temp_dir)
manager = create_ereader_manager(
self.blocks,
self.page_size,
self.document_id,
bookmarks_dir=self.temp_dir
)
self.assertIsInstance(manager, EreaderLayoutManager)
self.assertEqual(manager.page_size, self.page_size)
manager.shutdown()
finally:
os.chdir(original_cwd)
if __name__ == '__main__':
unittest.main()