mirror of
https://github.com/rio-labs/rio.git
synced 2026-05-02 08:59:27 -05:00
435 lines
14 KiB
Python
435 lines
14 KiB
Python
import typing as t
|
|
|
|
import pytest
|
|
|
|
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.DummyClient(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.wait_for_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.wait_for_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.DummyClient(build) 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 test_client._dirty_components == {child, container}
|
|
await test_client.wait_for_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.DummyClient(build) as test_client:
|
|
test_client._received_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._received_messages
|
|
or test_client._last_updated_components == {root_component}
|
|
)
|
|
|
|
|
|
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.DummyClient(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.DummyClient(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.wait_for_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.DummyClient(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.wait_for_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.DummyClient(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.wait_for_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.DummyClient(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.wait_for_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.DummyClient(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.wait_for_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.DummyClient(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.wait_for_refresh()
|
|
|
|
assert text.text == "Hello"
|
|
|
|
|
|
async def test_child_containing_attribute_recognition():
|
|
class CompWithChildren(rio.Component):
|
|
content: str | t.Annotated[rio.Component, "foobar"]
|
|
children: t.MutableSequence[rio.Component]
|
|
|
|
def build(self) -> rio.Component:
|
|
content = self.content
|
|
if isinstance(content, str):
|
|
content = rio.Text(content)
|
|
|
|
return rio.Column(
|
|
content,
|
|
rio.Row(*self.children),
|
|
)
|
|
|
|
class RootComponent(rio.Component):
|
|
state: bool = False
|
|
|
|
def build(self) -> rio.Component:
|
|
return CompWithChildren(
|
|
content=rio.Switch(self.state),
|
|
children=[rio.Text(str(self.state))],
|
|
)
|
|
|
|
async with rio.testing.DummyClient(RootComponent) as client:
|
|
root_component = client.get_component(RootComponent)
|
|
comp_with_children = client.get_component(CompWithChildren)
|
|
content_switch = client.get_component(rio.Switch)
|
|
children_text = client.get_component(rio.Text)
|
|
|
|
root_component.state = True
|
|
await client.wait_for_refresh()
|
|
|
|
assert comp_with_children.content is content_switch
|
|
assert content_switch.is_on
|
|
|
|
assert comp_with_children.children[0] is children_text
|
|
assert children_text.text == "True"
|
|
|
|
|
|
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.DummyClient(RootComponent) as test_client:
|
|
root = test_client.get_component(RootComponent)
|
|
text = test_client.get_component(rio.Text)
|
|
|
|
root.text = "bar"
|
|
await test_client.wait_for_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.DummyClient(RootComponent) as test_client:
|
|
root = test_client.get_component(RootComponent)
|
|
grid = test_client.get_component(rio.Grid)
|
|
|
|
root.num_rows += 1
|
|
await test_client.wait_for_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.DummyClient(RootComponent) as test_client:
|
|
root = test_client.get_component(RootComponent)
|
|
texts = list(test_client.get_components(rio.Text))
|
|
|
|
root.switch = False
|
|
await test_client.wait_for_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
|
|
)
|
|
|
|
|
|
@pytest.mark.skip("Not implemented yet. Has some downsides.")
|
|
async def test_only_newly_instantiated_components_are_reconciled():
|
|
class RootComponent(rio.Component):
|
|
child: rio.Component
|
|
switch: bool = True
|
|
|
|
def build(self) -> rio.Component:
|
|
if self.switch:
|
|
return rio.Column(
|
|
rio.Text("foo"),
|
|
rio.Text("bar"),
|
|
self.child,
|
|
)
|
|
else:
|
|
return rio.Column(
|
|
rio.Text("1"),
|
|
self.child,
|
|
rio.Text("3"),
|
|
)
|
|
|
|
def build():
|
|
return RootComponent(rio.Text("qux"))
|
|
|
|
async with rio.testing.DummyClient(build) as test_client:
|
|
root = test_client.get_component(RootComponent)
|
|
text1, text2, text3 = test_client.get_components(rio.Text)
|
|
|
|
root.switch = False
|
|
await test_client.wait_for_refresh()
|
|
|
|
# The first Text should've been reconciled and thus have new text
|
|
assert text1.text == "1"
|
|
|
|
# The 2nd Text was replaced by a component that the RootComponent
|
|
# hasn't instantiated, so it shouldn't have been reconciled and should
|
|
# now be dead
|
|
assert text2.text == "bar"
|
|
assert text2 not in test_client._last_updated_components
|
|
|
|
# The 3rd Text wasn't instantiated by the RootComponent, so its text
|
|
# should have remained unchanged
|
|
assert text3.text == "qux"
|