Files
rio/tests/test_events.py
T
2025-10-05 17:27:35 +02:00

306 lines
9.6 KiB
Python

import asyncio
import typing as t
import rio.testing
from rio.debug.layouter import Layouter
class ResizeEventRecorder(rio.Component):
recorded_events: list[rio.ComponentResizeEvent] = []
@rio.event.on_resize
def on_resize(self, resize_event: rio.ComponentResizeEvent) -> None:
self.recorded_events.append(resize_event)
def build(self):
return rio.Rectangle(
fill=rio.Color.BLUE,
min_width=5.0,
min_height=10.0,
)
class ChildMounter(rio.Component):
child: rio.Component
child_mounted: bool = False
def toggle(self) -> None:
self.child_mounted = not self.child_mounted
def build(self) -> rio.Component:
if self.child_mounted:
return self.child
else:
return rio.Spacer()
class EventCounter(rio.Component):
child: rio.Component
# Rio mustn't interpret these as state, otherwise it will cause unexpected
# rebuilds.
if t.TYPE_CHECKING:
mount_count: int = 0
unmount_count: int = 0
def __post_init__(self):
self.mount_count = 0
self.unmount_count = 0
@rio.event.on_mount
def _on_mount(self):
self.mount_count += 1
@rio.event.on_unmount
def _on_unmount(self):
self.unmount_count += 1
def build(self) -> rio.Component:
return self.child
async def test_mounted():
class NestedComponent(rio.Component):
def build(self) -> rio.Component:
return rio.Text("hi")
def build():
return ChildMounter(EventCounter(NestedComponent()))
async with rio.testing.DummyClient(build) as test_client:
mounter = test_client.get_component(ChildMounter)
event_counter = t.cast(EventCounter, mounter.child)
assert event_counter.mount_count == 0
assert event_counter.unmount_count == 0
mounter.toggle()
await test_client.wait_for_refresh()
assert event_counter.mount_count == 1
assert event_counter.unmount_count == 0
# Make sure the newly mounted components were sent to the client
nested_component = test_client.get_component(NestedComponent)
text_component = test_client.get_component(rio.Text)
assert test_client._last_updated_components == {
mounter,
event_counter,
nested_component,
text_component,
}
mounter.toggle()
await test_client.wait_for_refresh()
assert event_counter.unmount_count == 1
async def test_double_mount():
def build():
return ChildMounter(EventCounter(rio.Text("hello!")))
async with rio.testing.DummyClient(build) as test_client:
mounter = test_client.get_component(ChildMounter)
event_counter = t.cast(EventCounter, mounter.child)
for _ in range(4):
mounter.toggle()
await test_client.wait_for_refresh()
if mounter.child_mounted:
assert event_counter in test_client._last_updated_components
assert event_counter.mount_count == 2
assert event_counter.unmount_count == 2
async def test_unmount_and_remount() -> None:
class DemoComponent(rio.Component):
content: rio.Component
show_child: bool
def build(self) -> rio.Component:
children = [self.content] if self.show_child else []
return rio.Row(*children)
def build() -> rio.Component:
return DemoComponent(
rio.Text("hi"),
show_child=True,
)
async with rio.testing.DummyClient(build) as test_client:
root_component = test_client.get_component(DemoComponent)
child_component = root_component.content
row_component = test_client.get_component(rio.Row)
root_component.show_child = False
await test_client.wait_for_refresh()
assert not child_component._is_in_component_tree_({})
assert test_client._last_updated_components == {
root_component,
row_component,
}
root_component.show_child = True
await test_client.wait_for_refresh()
assert child_component._is_in_component_tree_({})
assert test_client._last_updated_components == {
root_component,
row_component,
child_component,
}
async def test_nested_unmount_and_remount():
def build():
return ChildMounter(
EventCounter(
ChildMounter(
EventCounter(
rio.Text("hello!"),
key="inner_counter",
),
child_mounted=True,
key="inner_mounter",
),
key="outer_counter",
),
child_mounted=True,
key="outer_mounter",
)
async with rio.testing.DummyClient(build) as client:
outer_mounter = client.get_component(ChildMounter, key="outer_mounter")
outer_counter = client.get_component(EventCounter, key="outer_counter")
inner_counter = client.get_component(EventCounter, key="inner_counter")
assert outer_counter.mount_count == 1
assert outer_counter.unmount_count == 0
assert inner_counter.mount_count == 1
assert inner_counter.unmount_count == 0
outer_mounter.child_mounted = False
await client.wait_for_refresh()
assert outer_counter.unmount_count == 1
assert inner_counter.unmount_count == 1
outer_mounter.child_mounted = True
await client.wait_for_refresh()
assert outer_counter.mount_count == 2
assert inner_counter.mount_count == 2
async def test_refresh_after_synchronous_mount_handler():
class DemoComponent(rio.Component):
mounted: bool = False
@rio.event.on_mount
def on_mount(self):
self.mounted = True
def build(self) -> rio.Component:
return rio.Switch(self.mounted)
async with rio.testing.DummyClient(DemoComponent) as test_client:
demo_component = test_client.get_component(DemoComponent)
switch = test_client.get_component(rio.Switch)
assert demo_component.mounted
last_component_state_changes = test_client._last_component_state_changes
assert switch in last_component_state_changes
assert last_component_state_changes[switch].get("is_on") is True
async def test_periodic():
ticks = 0
class DemoComponent(rio.Component):
@rio.event.periodic(0.05)
def tick(self):
nonlocal ticks
ticks += 1
def build(self) -> rio.Component:
return rio.Spacer()
async with rio.testing.DummyClient(DemoComponent) as test_client:
ticks_before = ticks
await asyncio.sleep(0.1)
ticks_after = ticks
assert ticks_after > ticks_before
await test_client._simulate_interrupted_connection()
ticks_before = ticks
await asyncio.sleep(0.1)
ticks_after = ticks
assert ticks_after == ticks_before
await test_client._simulate_reconnect()
ticks_before = ticks
await asyncio.sleep(0.1)
ticks_after = ticks
assert ticks_after > ticks_before
async def test_populate_dead_child():
class DemoComponent(rio.Component):
text: str = "alive"
@rio.event.on_populate
async def _on_populate(self):
await asyncio.sleep(1)
self.text = "dead"
def build(self) -> rio.Component:
return rio.Text(self.text)
def build():
return ChildMounter(DemoComponent())
async with rio.testing.DummyClient(build) as test_client:
mounter = test_client.get_component(ChildMounter)
# Unmount the child before its `on_populate` handler makes it dirty
mounter.child_mounted = False
await test_client.wait_for_refresh()
# Wait for the `on_populate` handler and the subsequent refresh
test_client._received_messages.clear()
await asyncio.sleep(1.5)
# Make sure the dead component wasn't sent to the frontend
#
# Note: Even though we cleared the outgoing messages, it's possible that
# some `registerFont` messages were sent afterwards. So unfortunately we
# can't assert that no message was sent at all, but we can assert that
# no components were updated.
assert not test_client._last_updated_components, (
"Unmounted component was sent to the frontend"
)
async def test_size_observer_reports_content_dimensions():
async with rio.testing.BrowserClient(ResizeEventRecorder) as client:
resize_event_recorder = client.get_component(ResizeEventRecorder)
rectangle = client.get_component(rio.Rectangle)
layouter = await Layouter.create(client.session)
recorder_layout = layouter.get_layout_is(resize_event_recorder)
rectangle_layout = layouter.get_layout_is(rectangle)
assert (
recorder_layout.allocated_outer_width
== rectangle_layout.allocated_outer_width
)
assert (
recorder_layout.allocated_outer_height
== rectangle_layout.allocated_outer_height
)
event = resize_event_recorder.recorded_events[-1]
assert event.width == recorder_layout.allocated_outer_width
assert event.height == recorder_layout.allocated_outer_height