This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for core pyWebLayout functionality."""
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user