mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-06 05:09:43 -06:00
391 lines
12 KiB
Python
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
|
|
)
|