""" Tests for pointer interaction on EreaderLayoutManager (R7). concrete/interaction_handler.py was 310 lines reachable only from examples/07_pressed_state_demo.py - no library code, no tests. These cover the wiring; the press/hover state on the elements themselves lives in tests/concrete/. """ import pytest from PIL import Image from pyWebLayout.io.readers.html_extraction import parse_html_string from pyWebLayout.layout.ereader_manager import EreaderLayoutManager @pytest.fixture def manager(tmp_path): blocks = parse_html_string( '

Tap this link please.

' '

' + " ".join(f"w{i}" for i in range(400)) + '

') manager = EreaderLayoutManager(blocks, page_size=(400, 600), document_id="interaction", bookmarks_dir=str(tmp_path)) yield manager manager.shutdown() @pytest.fixture def link_point(manager): """A page coordinate that lands on the interactive link.""" page = manager.get_current_page() page.render() for y in range(0, 120, 2): for x in range(0, 400, 2): result = page.query_point((x, y)) if result is not None and result.is_interactive: return (x, y) pytest.fail("fixture document rendered no interactive element") EMPTY_POINT = (399, 599) class TestHover: def test_hovering_an_element_produces_a_frame(self, manager, link_point): assert isinstance(manager.handle_hover(link_point), Image.Image) def test_hovering_the_same_element_again_reports_no_change(self, manager, link_point): manager.handle_hover(link_point) assert manager.handle_hover(link_point) is None, \ "an unchanged hover should not force the caller to redraw" def test_moving_off_the_element_clears_the_hover(self, manager, link_point): manager.handle_hover(link_point) assert isinstance(manager.handle_hover(EMPTY_POINT), Image.Image) class TestPress: def test_pressing_an_element_produces_a_frame(self, manager, link_point): assert isinstance(manager.handle_touch_down(link_point), Image.Image) def test_pressing_empty_space_does_nothing(self, manager): manager.get_current_page().render() assert manager.handle_touch_down(EMPTY_POINT) is None def test_release_runs_the_link_action(self, manager, link_point): manager.handle_touch_down(link_point) frame, result = manager.handle_touch_up(link_point) assert isinstance(frame, Image.Image) assert result == "action:go" def test_release_without_a_press_is_a_no_op(self, manager): manager.get_current_page().render() assert manager.handle_touch_up(EMPTY_POINT) == (None, None) def test_a_full_press_release_cycle_leaves_no_state(self, manager, link_point): manager.handle_touch_down(link_point) manager.handle_touch_up(link_point) assert manager.handle_touch_up(link_point) == (None, None) class TestStateFollowsTheDisplayedPage: def test_navigating_rebinds_the_state_machine(self, manager, link_point): before = manager._interaction_state() manager.next_page() assert manager._interaction_state() is not before, \ "press state belongs to one rendered page" def test_state_survives_repeated_access_on_one_page(self, manager, link_point): assert manager._interaction_state() is manager._interaction_state() def test_reset_is_safe_before_any_interaction(self, manager): manager.reset_interaction_state() # must not raise def test_reset_clears_a_pending_press(self, manager, link_point): manager.handle_touch_down(link_point) manager.reset_interaction_state() assert manager.handle_touch_up(link_point) == (None, None) class TestPressedRenderingRegression: """ LinkText.render passed [origin, origin + size] - two numpy arrays - to PIL's draw.rectangle, which needs a flat four-scalar box. Rendering any hovered or pressed link raised TypeError. Nothing caught it because the only caller was an example. """ def test_rendering_a_hovered_link_does_not_raise(self, manager, link_point): manager.handle_hover(link_point) assert isinstance(manager.get_current_page().render(), Image.Image) def test_rendering_a_pressed_link_does_not_raise(self, manager, link_point): manager.handle_touch_down(link_point) assert isinstance(manager.get_current_page().render(), Image.Image)