GitHub Viewer
"""Unit tests for the breadth expansion: new components and the new
props added to existing components.
These assert the Element shape produced by each factory (type, props,
children, dropped ``None`` defaults), which is the platform-agnostic
contract the native handlers consume.
"""
from __future__ import annotations
import pytest
from pythonnative.components import (
ActivityIndicator,
Checkbox,
DatePicker,
FlatList,
Image,
ImageBackground,
Modal,
Pressable,
PressState,
ProgressBar,
Ripple,
ScrollView,
SegmentedControl,
StatusBar,
Text,
TextInput,
TouchableOpacity,
View,
WebView,
)
from pythonnative.components._base import _SpanPressDispatcher
# ======================================================================
# TouchableOpacity
# ======================================================================
def test_touchable_opacity_is_pressable_with_active_opacity() -> None:
cb = lambda: None # noqa: E731
el = TouchableOpacity(Text("Tap"), on_press=cb, active_opacity=0.3)
assert el.type == "Pressable"
assert el.props["pressed_opacity"] == 0.3
assert el.props["on_press"] is cb
assert el.props["accessibility_role"] == "button"
assert len(el.children) == 1
def test_touchable_opacity_disabled_forwards_disabled_and_dims() -> None:
cb = lambda: None # noqa: E731
el = TouchableOpacity(Text("Tap"), on_press=cb, disabled=True)
assert el.props["disabled"] is True
assert el.props["accessibility_state"] == {"disabled": True}
assert el.props["opacity"] == 0.4
# ======================================================================
# Pressable: disabled, delay, ripple, callable child
# ======================================================================
def test_pressable_disabled_sets_prop_and_accessibility_state() -> None:
el = Pressable(Text("Save"), on_press=lambda: None, disabled=True)
assert el.props["disabled"] is True
assert el.props["accessibility_state"] == {"disabled": True}
# The user's explicit state wins.
el2 = Pressable(Text("Save"), disabled=True, accessibility_state={"selected": True})
assert el2.props["accessibility_state"] == {"selected": True}
assert "disabled" not in Pressable(Text("Save")).props
def test_pressable_delay_long_press_and_ripple() -> None:
el = Pressable(Text("Hold"), delay_long_press=400, android_ripple=Ripple(color="#33000000", borderless=True))
assert el.props["delay_long_press"] == 400
assert el.props["android_ripple"] == {"color": "#33000000", "borderless": True, "radius": None, "foreground": False}
assert "delay_long_press" not in Pressable(Text("Hold")).props
with pytest.raises(TypeError, match="pn.Ripple"):
Pressable(Text("Hold"), android_ripple={"color": "#000"}) # type: ignore[arg-type]
def test_pressable_callable_child_becomes_stateful_composite() -> None:
el = Pressable(lambda state: Text("Pressed" if state.pressed else "Idle"), on_press=lambda: None)
assert callable(el.type)
assert el.children == ()
with pytest.raises(TypeError, match="single callable child"):
Pressable(Text("a"), lambda state: Text("b"))
def test_press_state_is_frozen() -> None:
state = PressState(pressed=True)
assert state.pressed is True
with pytest.raises(Exception):
state.pressed = False # type: ignore[misc]
# ======================================================================
# Text: truncation, selection, pressable spans
# ======================================================================
def test_text_truncation_and_selection_props() -> None:
on_press = lambda: None # noqa: E731
el = Text("Hello", ellipsize_mode="middle", selectable=True, allow_font_scaling=False, on_press=on_press)
assert el.props["ellipsize_mode"] == "middle"
assert el.props["selectable"] is True
assert el.props["allow_font_scaling"] is False
assert el.props["on_press"] is on_press
plain = Text("Hello")
for key in ("ellipsize_mode", "selectable", "allow_font_scaling", "on_press", "on_span_press"):
assert key not in plain.props
def test_text_nested_span_press_is_flagged_and_routed_by_index() -> None:
pressed: list = []
el = Text(
"Read the ",
Text("terms", on_press=lambda: pressed.append("terms"), style={"color": "#06F"}),
" and ",
Text("policy", on_press=lambda: pressed.append("policy")),
".",
)
spans = el.props["spans"]
assert [s.get("pressable") for s in spans] == [None, True, None, True, None]
assert spans[1] == {"text": "terms", "color": "#06F", "pressable": True}
assert "on_press" not in el.props
dispatcher = el.props["on_span_press"]
assert isinstance(dispatcher, _SpanPressDispatcher)
dispatcher(1)
dispatcher(3)
dispatcher(0)
dispatcher(99)
assert pressed == ["terms", "policy"]
def test_text_pressable_span_inherits_through_nesting() -> None:
hits: list = []
inner = Text("deep", on_press=lambda: hits.append("deep"))
middle = Text("mid ", inner, on_press=lambda: hits.append("mid"))
el = Text("outer ", middle)
spans = el.props["spans"]
assert [s["text"] for s in spans] == ["outer ", "mid ", "deep"]
assert [s.get("pressable") for s in spans] == [None, True, True]
el.props["on_span_press"](1)
el.props["on_span_press"](2)
assert hits == ["mid", "deep"]
# ======================================================================
# Accessibility surface shared by every view
# ======================================================================
def test_accessibility_value_actions_and_importance_flow_to_props() -> None:
on_action = lambda _name: None # noqa: E731
el = View(
accessibility_value={"min": 0, "max": 10, "now": 3},
accessibility_actions=[{"name": "increment"}, {"name": "archive", "label": "Archive"}],
on_accessibility_action=on_action,
important_for_accessibility="no_hide_descendants",
)
assert el.props["accessibility_value"] == {"min": 0, "max": 10, "now": 3}
assert el.props["accessibility_actions"] == [{"name": "increment"}, {"name": "archive", "label": "Archive"}]
assert el.props["on_accessibility_action"] is on_action
assert el.props["important_for_accessibility"] == "no_hide_descendants"
assert Text("x", accessibility_value="3 of 10").props["accessibility_value"] == "3 of 10"
plain = View()
for key in ("accessibility_value", "accessibility_actions", "important_for_accessibility"):
assert key not in plain.props
@pytest.mark.parametrize("factory", [Text, TextInput, Image, Checkbox, SegmentedControl, DatePicker])
def test_every_view_factory_accepts_the_accessibility_surface(factory) -> None: # type: ignore[no-untyped-def]
el = factory(
accessibility_value="v",
accessibility_actions=[{"name": "activate"}],
on_accessibility_action=lambda _n: None,
important_for_accessibility="yes",
)
assert el.props["accessibility_value"] == "v"
assert el.props["important_for_accessibility"] == "yes"
# ======================================================================
# Image, StatusBar
# ======================================================================
def test_image_load_lifecycle_fade_and_headers() -> None:
start = lambda: None # noqa: E731
end = lambda: None # noqa: E731
el = Image(
"https://example.com/a.png",
on_load_start=start,
on_load_end=end,
fade_duration=300,
headers={"Authorization": "Bearer x"},
)
assert el.props["on_load_start"] is start
assert el.props["on_load_end"] is end
assert el.props["fade_duration"] == 300
assert el.props["headers"] == {"Authorization": "Bearer x"}
plain = Image("a.png")
for key in ("on_load_start", "on_load_end", "fade_duration", "headers"):
assert key not in plain.props
def test_status_bar_translucent_and_animated() -> None:
el = StatusBar(translucent=True, animated=True)
assert el.props == {"translucent": True, "animated": True}
assert StatusBar().props == {}
# ======================================================================
# ImageBackground
# ======================================================================
def test_image_background_layers_image_then_content() -> None:
el = ImageBackground(Text("Overlay"), source="bg.png")
assert el.type == "View"
assert len(el.children) == 2
background, content = el.children
assert background.type == "Image"
assert background.props["source"] == "bg.png"
assert background.props["scale_type"] == "cover"
assert background.props["position"] == "absolute"
assert background.props["top"] == 0
assert content.type == "View"
assert content.children[0].props["text"] == "Overlay"
def test_image_background_respects_explicit_scale_type() -> None:
el = ImageBackground(source="bg.png", scale_type="contain")
assert el.children[0].props["scale_type"] == "contain"
# ======================================================================
# Checkbox
# ======================================================================
def test_checkbox_props_and_default_role() -> None:
cb = lambda _v: None # noqa: E731
el = Checkbox(value=True, on_change=cb, label="Accept", color="#FF0000")
assert el.type == "Checkbox"
assert el.props["value"] is True
assert el.props["on_change"] is cb
assert el.props["label"] == "Accept"
assert el.props["color"] == "#FF0000"
assert el.props["accessibility_role"] == "checkbox"
def test_checkbox_disabled_flag_dropped_when_false() -> None:
el = Checkbox(value=False)
assert "disabled" not in el.props
assert el.props["value"] is False
# ======================================================================
# SegmentedControl
# ======================================================================
def test_segmented_control_props() -> None:
cb = lambda _i: None # noqa: E731
el = SegmentedControl(segments=["Day", "Week", "Month"], selected_index=1, on_change=cb)
assert el.type == "SegmentedControl"
assert el.props["segments"] == ["Day", "Week", "Month"]
assert el.props["selected_index"] == 1
assert el.props["on_change"] is cb
def test_segmented_control_defaults_empty() -> None:
el = SegmentedControl()
assert el.props["segments"] == []
assert el.props["selected_index"] == 0
# ======================================================================
# DatePicker
# ======================================================================
def test_date_picker_props_and_default_role() -> None:
cb = lambda _v: None # noqa: E731
el = DatePicker(value="2026-05-31", mode="date", on_change=cb, minimum="2026-01-01")
assert el.type == "DatePicker"
assert el.props["value"] == "2026-05-31"
assert el.props["mode"] == "date"
assert el.props["on_change"] is cb
assert el.props["minimum"] == "2026-01-01"
assert el.props["accessibility_role"] == "button"
def test_date_picker_time_mode() -> None:
el = DatePicker(mode="time")
assert el.props["mode"] == "time"
# ======================================================================
# Enriched existing components
# ======================================================================
def test_progress_bar_color_and_indeterminate() -> None:
el = ProgressBar(value=0.5, color="#00FF00", track_color="#EEEEEE", indeterminate=True)
assert el.props["value"] == 0.5
assert el.props["color"] == "#00FF00"
assert el.props["track_color"] == "#EEEEEE"
assert el.props["indeterminate"] is True
def test_progress_bar_indeterminate_dropped_when_false() -> None:
el = ProgressBar(value=0.25)
assert "indeterminate" not in el.props
def test_activity_indicator_color_and_size() -> None:
el = ActivityIndicator(color="#123456", size="large")
assert el.props["color"] == "#123456"
assert el.props["size"] == "large"
def test_web_view_html_and_callbacks() -> None:
on_load = lambda _u: None # noqa: E731
on_message = lambda _m: None # noqa: E731
el = WebView(html="
Hi
", on_load=on_load, on_message=on_message, inject_javascript="1;")
assert el.props["html"] == "Hi
"
assert el.props["on_load"] is on_load
assert el.props["on_message"] is on_message
assert el.props["inject_javascript"] == "1;"
def test_web_view_scroll_enabled_dropped_when_true() -> None:
el = WebView(url="https://example.com")
assert "scroll_enabled" not in el.props
el2 = WebView(url="https://example.com", scroll_enabled=False)
assert el2.props["scroll_enabled"] is False
def test_text_input_new_props() -> None:
on_focus = lambda: None # noqa: E731
on_blur = lambda: None # noqa: E731
el = TextInput(
value="x",
editable=False,
clear_button=True,
on_focus=on_focus,
on_blur=on_blur,
selection_color="#FF00FF",
text_content_type="password",
)
assert el.props["editable"] is False
assert el.props["clear_button"] is True
assert el.props["on_focus"] is on_focus
assert el.props["on_blur"] is on_blur
assert el.props["selection_color"] == "#FF00FF"
assert el.props["text_content_type"] == "password"
def test_text_input_editable_dropped_when_true() -> None:
el = TextInput(value="x")
assert "editable" not in el.props
assert "clear_button" not in el.props
def test_text_input_selection_key_and_keyboard_props() -> None:
on_selection_change = lambda _e: None # noqa: E731
on_key_press = lambda _e: None # noqa: E731
on_content_size_change = lambda _e: None # noqa: E731
el = TextInput(
value="hello",
selection=(1, 3),
on_selection_change=on_selection_change,
on_key_press=on_key_press,
on_content_size_change=on_content_size_change,
select_text_on_focus=True,
blur_on_submit=False,
keyboard_appearance="dark",
keyboard_type="numbers_and_punctuation",
)
assert el.props["selection"] == {"start": 1, "end": 3}
assert el.props["on_selection_change"] is on_selection_change
assert el.props["on_key_press"] is on_key_press
assert el.props["on_content_size_change"] is on_content_size_change
assert el.props["select_text_on_focus"] is True
assert el.props["blur_on_submit"] is False
assert el.props["keyboard_appearance"] == "dark"
assert el.props["keyboard_type"] == "numbers_and_punctuation"
def test_text_input_new_defaults_are_omitted() -> None:
el = TextInput(value="x")
for key in ("selection", "select_text_on_focus", "blur_on_submit", "keyboard_appearance"):
assert key not in el.props
def test_text_input_rejects_inverted_selection() -> None:
with pytest.raises(ValueError, match="start