[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/tests/test_handles.py [Back]  [Original]

"""Unit tests for the typed imperative handles in ``pythonnative.handles``."""

from __future__ import annotations

import asyncio
from typing import Any, Dict, List, Optional, Tuple

import pytest

import pythonnative as pn
from pythonnative.handles import HANDLE_TYPES, make_handle


class _RecordingBackend:
    """Minimal backend: records ``command`` calls and answers from a canned table."""

    def __init__(self, answers: Optional[Dict[str, Any]] = None) -> None:
        self.commands: List[Tuple[int, str, Dict[str, Any]]] = []
        self.answers = dict(answers or {})

    def command(self, tag: int, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
        self.commands.append((tag, name, dict(args or {})))
        return self.answers.get(name)


def _run(coro: Any) -> Any:
    return asyncio.run(coro)


# ======================================================================
# make_handle / ViewHandle
# ======================================================================


def test_make_handle_picks_the_type_specific_class() -> None:
    backend = _RecordingBackend()
    assert type(make_handle("TextInput", 1, backend)) is pn.TextInputHandle
    assert type(make_handle("ScrollView", 2, backend)) is pn.ScrollViewHandle
    assert type(make_handle("WebView", 3, backend)) is pn.WebViewHandle
    assert type(make_handle("View", 4, backend)) is pn.ViewHandle
    assert type(make_handle("Text", 5, backend)) is pn.ViewHandle
    assert set(HANDLE_TYPES) == {"TextInput", "ScrollView", "WebView"}
    for cls in HANDLE_TYPES.values():
        assert issubclass(cls, pn.ViewHandle)


def test_view_handle_exposes_tag_type_and_writable_frame() -> None:
    handle = pn.ViewHandle(7, "View", _RecordingBackend())
    assert handle.tag == 7
    assert handle.type_name == "View"
    assert handle.frame is None
    handle.frame = pn.LayoutEvent(x=1, y=2, width=30, height=40)
    assert handle.frame.width == 30
    assert "View #7" in repr(handle)


def test_view_handle_command_forwards_tag_name_and_args() -> None:
    backend = _RecordingBackend({"custom": {"ok": True}})
    handle = pn.ViewHandle(9, "Badge", backend)
    assert handle.command("custom", level=2, label="x") == {"ok": True}
    assert backend.commands == [(9, "custom", {"level": 2, "label": "x"})]


# ======================================================================
# TextInputHandle
# ======================================================================


def test_text_input_handle_commands() -> None:
    backend = _RecordingBackend({"get_value": "hello"})
    handle = pn.TextInputHandle(3, "TextInput", backend)
    handle.focus()
    handle.blur()
    handle.clear()
    handle.select_all()
    handle.set_selection(2)
    handle.set_selection(1, 4)
    assert [(name, args) for _tag, name, args in backend.commands] == [
        ("focus", {}),
        ("blur", {}),
        ("clear", {}),
        ("select_all", {}),
        ("set_selection", {"start": 2, "end": 2}),
        ("set_selection", {"start": 1, "end": 4}),
    ]
    assert _run(handle.get_value()) == "hello"


def test_text_input_handle_get_value_defaults_to_empty_string() -> None:
    handle = pn.TextInputHandle(3, "TextInput", _RecordingBackend())
    assert _run(handle.get_value()) == ""


# ======================================================================
# ScrollViewHandle
# ======================================================================


def test_scroll_view_handle_commands() -> None:
    backend = _RecordingBackend({"get_scroll_offset": {"x": 0, "y": 120.5}})
    handle = pn.ScrollViewHandle(4, "ScrollView", backend)
    handle.scroll_to(y=200)
    handle.scroll_to(x=10, y=20, animated=False)
    handle.scroll_to_end()
    handle.flash_scroll_indicators()
    assert [(name, args) for _tag, name, args in backend.commands] == [
        ("scroll_to_offset", {"animated": True, "y": 200.0}),
        ("scroll_to_offset", {"animated": False, "x": 10.0, "y": 20.0}),
        ("scroll_to_end", {"animated": True}),
        ("flash_scroll_indicators", {}),
    ]
    offset = _run(handle.get_scroll_offset())
    assert offset == pn.ScrollOffset(x=0.0, y=120.5)
    assert isinstance(offset.y, float)


def test_scroll_offset_is_frozen() -> None:
    offset = pn.ScrollOffset(x=1.0, y=2.0)
    with pytest.raises(Exception):
        offset.x = 5.0  # type: ignore[misc]


# ======================================================================
# WebViewHandle
# ======================================================================


def test_web_view_handle_commands() -> None:
    backend = _RecordingBackend({"can_go_back": True, "can_go_forward": False, "get_url": "https://example.com/"})
    handle = pn.WebViewHandle(5, "WebView", backend)
    handle.reload()
    handle.go_back()
    handle.go_forward()
    handle.stop_loading()
    handle.load_url("https://example.com/next")
    handle.inject_javascript("document.title = 'x'")
    assert [(name, args) for _tag, name, args in backend.commands] == [
        ("reload", {}),
        ("go_back", {}),
        ("go_forward", {}),
        ("stop_loading", {}),
        ("load_url", {"url": "https://example.com/next"}),
        ("inject_javascript", {"script": "document.title = 'x'"}),
    ]
    assert _run(handle.can_go_back()) is True
    assert _run(handle.can_go_forward()) is False
    assert _run(handle.get_url()) == "https://example.com/"


def test_handle_commands_match_the_native_contract() -> None:
    """Every handle method issues a command the component's contract declares with valid arguments."""
    from pythonnative.sdk.schema import COMPONENTS, ModuleSchema

    cases = [
        ("TextInput", pn.TextInputHandle, lambda h: (h.focus(), h.set_selection(1, 2), _run(h.get_value()))),
        (
            "ScrollView",
            pn.ScrollViewHandle,
            lambda h: (h.scroll_to(y=5), h.scroll_to_end(), h.flash_scroll_indicators()),
        ),
        (
            "WebView",
            pn.WebViewHandle,
            lambda h: (h.reload(), h.load_url("https://a.b"), h.inject_javascript("1")),
        ),
    ]
    for type_name, cls, drive in cases:
        backend = _RecordingBackend()
        drive(cls(1, type_name, backend))
        schema = COMPONENTS[type_name]
        contract = ModuleSchema(schema.name, schema.commands)
        for _tag, name, args in backend.commands:
            contract.validate_call(name, args)


def test_web_view_eval_js_awaits_the_webviews_module() -> None:
    from pythonnative.native_modules.registry import register_python_module

    calls = []

    class Page:
        def eval_js(self, tag: int, script: str) -> str:
            calls.append((tag, script))
            return "42"

    register_python_module("WebViews", Page())
    try:
        backend = _RecordingBackend()
        handle = pn.WebViewHandle(5, "WebView", backend)
        assert _run(handle.eval_js("6 * 7")) == "42"
        assert calls == [(5, "6 * 7")]
        assert backend.commands == [], "eval_js is a module call, not a view command"
    finally:
        from pythonnative.native_modules import fallback

        register_python_module("WebViews", fallback.FallbackWebViews())


def test_web_view_eval_js_answers_empty_off_device() -> None:
    handle = pn.WebViewHandle(5, "WebView", _RecordingBackend())
    assert _run(handle.eval_js("1 + 1")) == ""

Web Proxy Viewer  |  New URL  |  Original Page