Files
rio/tests/test_reconciliation.py
2024-12-17 01:04:01 +01:00

391 lines
12 KiB
Python

import rio.testing
async def test_reconciliation():
class Toggler(rio.Component):
toggle: bool = True
def build(self) -> rio.Component:
if self.toggle:
return rio.TextInput("hi", min_width=10, min_height=10)
else:
return rio.TextInput(min_height=15, is_secret=True)
async with rio.testing.TestClient(Toggler) as test_client:
toggler = test_client.get_component(Toggler)
text_input = test_client.get_component(rio.TextInput)
text_input.text = "bye"
text_input.min_height = 5
toggler.toggle = False
await test_client.refresh()
# The text should carry over because it was assigned after the component
# was created, which essentially means that the user interactively
# changed it
assert text_input.text == "bye"
# The height that was explicitly passed into the constructor of the new
# component should win out over the late assignment
assert text_input.min_height == 15
# The min_width and is_secret should be taken from the new component
assert text_input.min_width == 0
assert text_input.is_secret
# If we toggle again, make sure the `is_secret` is back to its default
# value. (Not really sure how this is different from the `min_width`,
# but there was once a bug like this)
toggler.toggle = True
await test_client.refresh()
assert not text_input.is_secret
async def test_reconcile_instance_with_itself() -> None:
# There used to be a bug where, when a component was reconciled with itself,
# it was removed from `Session._dirty_components`. So any changes in that
# component weren't sent to the frontend.
class Container(rio.Component):
child: rio.Component
def build(self) -> rio.Component:
return self.child
def build() -> rio.Component:
return Container(rio.Text("foo"))
async with rio.testing.TestClient(
build, use_ordered_dirty_set=True
) as test_client:
container = test_client.get_component(Container)
child = test_client.get_component(rio.Text)
# Change the child's state and make its parent rebuild
child.text = "bar"
container.child = child
# In order for the bug to occur, the parent has to be rebuilt before the
# child
assert test_client._session is not None
assert list(test_client._session._dirty_components) == [
child,
container,
]
await test_client.refresh()
assert test_client._last_updated_components == {child, container}
async def test_reconcile_same_component_instance():
# TODO: How is this different from the test above?
def build():
return rio.Container(rio.Text("Hello"))
async with rio.testing.TestClient(build) as test_client:
test_client._outgoing_messages.clear()
root_component = test_client.get_component(rio.Container)
await root_component._force_refresh()
# Nothing changed, so there's no need to send any data to JS. But in
# order to know that nothing changed, the framework would have to track
# every individual attribute of every component. Since we forced the
# root_component to refresh, it's reasonable to send that component's
# data to JS.
assert (
not test_client._outgoing_messages
or test_client._last_updated_components == {root_component}
)
async def test_reconcile_not_dirty_high_level_component():
# Situation:
# HighLevelComponent1 contains HighLevelComponent2
# HighLevelComponent2 contains LowLevelContainer
# HighLevelComponent1 is rebuilt and changes the child of LowLevelContainer
# -> LowLevelContainer is reconciled and dirty (because it has new children)
# -> HighLevelComponent2 is reconciled but *not* dirty because its child was
# reconciled
# The end result is that there is a new component (the child of
# LowLevelContainer), whose builder (HighLevelComponent2) is not "dirty".
# Make sure the new component is initialized correctly despite this.
class HighLevelComponent1(rio.Component):
switch: bool = False
def build(self):
if self.switch:
child = rio.Switch()
else:
child = rio.Text("hi")
return HighLevelComponent2(rio.Column(child))
class HighLevelComponent2(rio.Component):
content: rio.Component
def build(self):
return self.content
async with rio.testing.TestClient(HighLevelComponent1) as test_client:
root_component = test_client.get_component(HighLevelComponent1)
root_component.switch = True
await test_client.refresh()
assert any(
isinstance(component, rio.Switch)
for component in test_client._last_updated_components
)
async def test_reconcile_unusual_types():
class Container(rio.Component):
def build(self) -> rio.Component:
return CustomComponent(
integer=4,
text="bar",
tuple=(2.0, rio.Text("baz")),
byte_array=bytearray(b"foo"),
)
class CustomComponent(rio.Component):
integer: int
text: str
tuple: tuple[float, rio.Component]
byte_array: bytearray
def build(self):
return rio.Text(self.text)
async with rio.testing.TestClient(Container) as test_client:
root_component = test_client.get_component(Container)
# As long as this doesn't crash, it's fine
await root_component._force_refresh()
async def test_reconcile_by_key():
class Toggler(rio.Component):
toggle: bool = False
def build(self):
if not self.toggle:
return rio.Text("Hello", key="foo")
else:
return rio.Container(rio.Text("World", key="foo"))
async with rio.testing.TestClient(Toggler) as test_client:
root_component = test_client.get_component(Toggler)
text = test_client.get_component(rio.Text)
root_component.toggle = True
await test_client.refresh()
assert text.text == "World"
async def test_key_prevents_structural_match():
class Toggler(rio.Component):
toggle: bool = False
def build(self):
if not self.toggle:
return rio.Text("Hello")
else:
return rio.Text("World", key="foo")
async with rio.testing.TestClient(Toggler) as test_client:
root_component = test_client.get_component(Toggler)
text = test_client.get_component(rio.Text)
root_component.toggle = True
await test_client.refresh()
assert text.text == "Hello"
async def test_key_interrupts_structure():
class Toggler(rio.Component):
key_: str = "abc"
def build(self):
return rio.Container(rio.Text(self.key_), key=self.key_)
async with rio.testing.TestClient(Toggler) as test_client:
root_component = test_client.get_component(Toggler)
text = test_client.get_component(rio.Text)
root_component.key_ = "123"
await test_client.refresh()
# The container's key changed, so even though the structure is the same,
# the old Text component should be unchanged.
assert text.text == "abc"
async def test_structural_matching_inside_keyed_component():
class Toggler(rio.Component):
toggle: bool = False
def build(self):
if not self.toggle:
return rio.Row(rio.Container(rio.Text("A"), key="foo"))
else:
return rio.Row(
rio.Container(rio.Text("B")),
rio.Container(rio.Text("C"), key="foo"),
)
async with rio.testing.TestClient(Toggler) as test_client:
root_component = test_client.get_component(Toggler)
text = test_client.get_component(rio.Text)
root_component.toggle = True
await test_client.refresh()
# The container with key "foo" has moved. Make sure the structure inside
# of it was reconciled correctly.
assert text.text == "C"
async def test_key_matching_inside_keyed_component():
class Toggler(rio.Component):
toggle: bool = False
def build(self):
if not self.toggle:
return rio.Container(
rio.Row(
rio.Container(rio.Text("A"), key="container"),
key="row",
),
)
else:
return rio.Row(
rio.Text("B"),
rio.Container(rio.Text("C"), key="container"),
key="row",
)
async with rio.testing.TestClient(Toggler) as test_client:
root_component = test_client.get_component(Toggler)
text = test_client.get_component(rio.Text)
root_component.toggle = True
await test_client.refresh()
# The container with key "foo" has moved. Make sure the structure inside
# of it was reconciled correctly.
assert text.text == "C"
async def test_same_key_on_different_component_type():
class ComponentWithText(rio.Component):
text: str
def build(self) -> rio.Component:
return rio.Text(self.text)
class Toggler(rio.Component):
toggle: bool = False
def build(self):
if not self.toggle:
return rio.Text("Hello", key="foo")
else:
return ComponentWithText("World", key="foo")
async with rio.testing.TestClient(Toggler) as test_client:
root_component = test_client.get_component(Toggler)
text = test_client.get_component(rio.Text)
root_component.toggle = True
await test_client.refresh()
assert text.text == "Hello"
async def test_text_reconciliation():
class RootComponent(rio.Component):
text: str = "foo"
def build(self) -> rio.Component:
return rio.Text(self.text)
async with rio.testing.TestClient(RootComponent) as test_client:
root = test_client.get_component(RootComponent)
text = test_client.get_component(rio.Text)
root.text = "bar"
await test_client.refresh()
assert text.text == root.text
async def test_grid_reconciliation():
class RootComponent(rio.Component):
num_rows: int = 1
def build(self) -> rio.Component:
rows = [[rio.Text(f"Row {n}")] for n in range(self.num_rows)]
return rio.Grid(*rows)
async with rio.testing.TestClient(RootComponent) as test_client:
root = test_client.get_component(RootComponent)
grid = test_client.get_component(rio.Grid)
root.num_rows += 1
await test_client.refresh()
assert {root, grid} < test_client._last_updated_components
assert len(grid._children) == root.num_rows
async def test_margin_reconciliation():
class RootComponent(rio.Component):
switch: bool = True
def build(self) -> rio.Component:
if self.switch:
return rio.Column(*[rio.Text("hi") for _ in range(7)])
else:
return rio.Column(
rio.Text("hi", margin_left=1),
rio.Text("hi", margin_right=1),
rio.Text("hi", margin_top=1),
rio.Text("hi", margin_bottom=1),
rio.Text("hi", margin_x=1),
rio.Text("hi", margin_y=1),
rio.Text("hi", margin=1),
)
async with rio.testing.TestClient(RootComponent) as test_client:
root = test_client.get_component(RootComponent)
texts = list(test_client.get_components(rio.Text))
root.switch = False
await test_client.refresh()
assert texts[0].margin_left == 1
assert texts[1].margin_right == 1
assert texts[2].margin_top == 1
assert texts[3].margin_bottom == 1
assert texts[4].margin_x == 1
assert texts[4].margin_left == texts[4].margin_right == None
assert texts[5].margin_y == 1
assert texts[5].margin_top == texts[5].margin_bottom == None
assert texts[6].margin == 1
assert (
texts[6].margin_left
== texts[6].margin_right
== texts[6].margin_top
== texts[6].margin_bottom
== None
)