diff --git a/tests/concrete/test_linkedword_hyphenation.py b/tests/concrete/test_linkedword_hyphenation.py new file mode 100644 index 0000000..3586fbf --- /dev/null +++ b/tests/concrete/test_linkedword_hyphenation.py @@ -0,0 +1,137 @@ +""" +Test that LinkedWord objects remain as LinkText even when hyphenated. + +This is a regression test for the bug where hyphenated LinkedWords +were being converted to regular Text objects instead of LinkText. +""" + +import unittest +from PIL import Image, ImageDraw +from pyWebLayout.concrete.text import Line +from pyWebLayout.concrete.functional import LinkText +from pyWebLayout.abstract.inline import LinkedWord +from pyWebLayout.abstract.functional import LinkType +from pyWebLayout.style import Font, Alignment + + +class TestLinkedWordHyphenation(unittest.TestCase): + """Test that LinkedWords become LinkText objects even when hyphenated.""" + + def setUp(self): + """Set up test canvas and drawing context.""" + self.canvas = Image.new('RGB', (800, 600), color='white') + self.draw = ImageDraw.Draw(self.canvas) + self.font = Font(font_size=12) + + def test_short_linkedword_no_hyphenation(self): + """Test that a short LinkedWord that fits becomes a LinkText.""" + # Create a line with enough space + line = Line( + spacing=(5, 15), + origin=(0, 0), + size=(200, 30), + draw=self.draw, + halign=Alignment.LEFT + ) + + # Create a LinkedWord that will fit without hyphenation + linked_word = LinkedWord( + text="click", + style=self.font, + location="action:test", + link_type=LinkType.API + ) + + # Add the word to the line + success, overflow = line.add_word(linked_word) + + # Verify it was added successfully + self.assertTrue(success) + self.assertIsNone(overflow) + + # Verify it became a LinkText object + self.assertEqual(len(line._text_objects), 1) + self.assertIsInstance(line._text_objects[0], LinkText) + self.assertEqual(line._text_objects[0].link.location, "action:test") + + def test_long_linkedword_with_hyphenation(self): + """Test that a long LinkedWord that needs hyphenation preserves LinkText.""" + # Create a narrow line to force hyphenation + line = Line( + spacing=(5, 15), + origin=(0, 0), + size=(80, 30), + draw=self.draw, + halign=Alignment.LEFT + ) + + # Create a long LinkedWord that will need hyphenation + linked_word = LinkedWord( + text="https://example.com/very-long-url", + style=self.font, + location="https://example.com/very-long-url", + link_type=LinkType.EXTERNAL + ) + + # Add the word to the line + success, overflow = line.add_word(linked_word) + + # The word should either: + # 1. Fit completely and be a LinkText + # 2. Be hyphenated, and BOTH parts should be LinkText + + if overflow is not None: + # Word was hyphenated + # The first part should be in the line + self.assertTrue(success) + self.assertGreater(len(line._text_objects), 0) + + # Both parts should be LinkText (this is the bug we're testing for) + for text_obj in line._text_objects: + self.assertIsInstance(text_obj, LinkText, + f"Hyphenated LinkedWord part should be LinkText, got {type(text_obj)}") + self.assertEqual(text_obj.link.location, linked_word.location) + + # The overflow should also be LinkText if it's hyphenated + if isinstance(overflow, LinkText): + self.assertEqual(overflow.link.location, linked_word.location) + else: + # Word fit without hyphenation + self.assertTrue(success) + self.assertEqual(len(line._text_objects), 1) + self.assertIsInstance(line._text_objects[0], LinkText) + + def test_linkedword_title_preserved_after_hyphenation(self): + """Test that link metadata (title) is preserved when hyphenated.""" + # Create a narrow line + line = Line( + spacing=(5, 15), + origin=(0, 0), + size=(60, 30), + draw=self.draw, + halign=Alignment.LEFT + ) + + # Create a LinkedWord with title that will likely be hyphenated + linked_word = LinkedWord( + text="documentation", + style=self.font, + location="https://docs.example.com", + link_type=LinkType.EXTERNAL, + title="View Documentation" + ) + + # Add the word + success, overflow = line.add_word(linked_word) + + # Verify metadata is preserved + if overflow is not None: + # If hyphenated, both parts should have link metadata + for text_obj in line._text_objects: + if isinstance(text_obj, LinkText): + self.assertEqual(text_obj.link.location, "https://docs.example.com") + self.assertEqual(text_obj.link.title, "View Documentation") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/layout/test_html_links_in_ereader.py b/tests/layout/test_html_links_in_ereader.py new file mode 100644 index 0000000..7d220e8 --- /dev/null +++ b/tests/layout/test_html_links_in_ereader.py @@ -0,0 +1,142 @@ +""" +End-to-end test for HTML links in EreaderLayoutManager. + +This test mimics exactly what the dreader application does: +1. Load HTML with links via parse_html_string +2. Create an EreaderLayoutManager +3. Render a page +4. Query for interactive elements + +This should reveal if links are actually interactive after full rendering. +""" + +import unittest +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.layout.ereader_manager import EreaderLayoutManager +from pyWebLayout.abstract.inline import LinkedWord +from pyWebLayout.concrete.functional import LinkText + + +class TestHTMLLinksInEreader(unittest.TestCase): + """Test HTML link interactivity in the full ereader pipeline.""" + + def test_settings_overlay_links_are_interactive(self): + """Test that settings overlay HTML creates interactive links.""" + # This is realistic settings overlay HTML + html = ''' +
+

Settings

+

+ Back to Library +

+

+ Font Size: + [-] + [+] +

+
+ ''' + + # Step 1: Parse HTML to blocks + blocks = parse_html_string(html) + + # Verify LinkedWords were created + all_linked_words = [] + for block in blocks: + if hasattr(block, 'words'): + for word in block.words: + if isinstance(word, LinkedWord): + all_linked_words.append(word) + + self.assertGreater(len(all_linked_words), 0, "Should create LinkedWords from HTML") + print(f"\n Created {len(all_linked_words)} LinkedWords from HTML") + + # Step 2: Create EreaderLayoutManager (like the dreader app does) + page_size = (400, 600) + manager = EreaderLayoutManager( + blocks=blocks, + page_size=page_size, + document_id="test_settings" + ) + + # Step 3: Get the rendered page + page = manager.get_current_page() + self.assertIsNotNone(page) + + # Step 4: Render to image + rendered_image = page.render() + self.assertIsNotNone(rendered_image) + print(f" Rendered page: {rendered_image.size}") + + # Step 5: Find all interactive elements by scanning the page + # This is the CRITICAL test - are there LinkText objects in the page? + interactive_elements = [] + + # Scan through all children of the page + if hasattr(page, '_children'): + for child in page._children: + # Check if child is a Line + if hasattr(child, '_text_objects'): + for text_obj in child._text_objects: + if isinstance(text_obj, LinkText): + interactive_elements.append({ + 'type': 'LinkText', + 'text': text_obj._text, + 'location': text_obj.link.location, + 'is_interactive': hasattr(text_obj, 'execute') + }) + + print(f" Found {len(interactive_elements)} LinkText objects in rendered page") + for elem in interactive_elements: + print(f" - '{elem['text']}' -> {elem['location']}") + + # THIS IS THE KEY ASSERTION + self.assertGreater(len(interactive_elements), 0, + "Settings overlay should have interactive LinkText objects after rendering!") + + # Verify the expected links are present + locations = {elem['location'] for elem in interactive_elements} + self.assertIn("action:back_to_library", locations, + "Should find 'Back to Library' link") + self.assertIn("setting:font_decrease", locations, + "Should find font decrease link") + self.assertIn("setting:font_increase", locations, + "Should find font increase link") + + def test_query_point_detects_links(self): + """Test that query_point can detect LinkText objects.""" + html = '

Click here to test.

' + + blocks = parse_html_string(html) + manager = EreaderLayoutManager( + blocks=blocks, + page_size=(400, 200), + document_id="test_query" + ) + + page = manager.get_current_page() + page.render() + + # Try to query various points on the page + # We don't know exact coordinates, so scan a grid + found_link = False + for y in range(20, 100, 10): + for x in range(20, 380, 20): + result = page.query_point((x, y)) + if result and result.is_interactive: + print(f"\n Found interactive element at ({x}, {y})") + print(f" Type: {result.object_type}") + print(f" Link target: {result.link_target}") + print(f" Text: {result.text}") + found_link = True + self.assertEqual(result.link_target, "action:test") + break + if found_link: + break + + self.assertTrue(found_link, + "Should be able to detect link via query_point somewhere on the page") + + +if __name__ == '__main__': + unittest.main()