|
13 | 13 | from reactpy.core.component import component
|
14 | 14 | from reactpy.core.hooks import use_effect, use_state
|
15 | 15 | from reactpy.core.layout import Layout
|
| 16 | +from reactpy.core.types import State |
16 | 17 | from reactpy.testing import (
|
17 | 18 | HookCatcher,
|
18 | 19 | StaticEventHandler,
|
19 | 20 | assert_reactpy_did_log,
|
20 | 21 | capture_reactpy_logs,
|
21 | 22 | )
|
22 | 23 | from reactpy.utils import Ref
|
| 24 | +from tests.tooling import select |
23 | 25 | from tests.tooling.common import event_message, update_message
|
24 | 26 | from tests.tooling.hooks import use_force_render, use_toggle
|
| 27 | +from tests.tooling.layout import layout_runner |
| 28 | +from tests.tooling.select import element_exists, find_element |
25 | 29 |
|
26 | 30 |
|
27 | 31 | @pytest.fixture(autouse=True)
|
@@ -1190,3 +1194,59 @@ def Child():
|
1190 | 1194 | done, pending = await asyncio.wait([render_task], timeout=0.1)
|
1191 | 1195 | assert not done and pending
|
1192 | 1196 | render_task.cancel()
|
| 1197 | + |
| 1198 | + |
| 1199 | +async def test_ensure_model_path_udpates(): |
| 1200 | + """ |
| 1201 | + This is regression test for a bug in which we failed to update the path of a bug |
| 1202 | + that arose when the "path" of a component within the overall model was not updated |
| 1203 | + when the component changes position amongst its siblings. This meant that when |
| 1204 | + a component whose position had changed would attempt to update the view at its old |
| 1205 | + position. |
| 1206 | + """ |
| 1207 | + |
| 1208 | + @component |
| 1209 | + def Item(item: str, all_items: State[list[str]]): |
| 1210 | + color = use_state(None) |
| 1211 | + |
| 1212 | + def deleteme(event): |
| 1213 | + all_items.set_value([i for i in all_items.value if (i != item)]) |
| 1214 | + |
| 1215 | + def colorize(event): |
| 1216 | + color.set_value("blue" if not color.value else None) |
| 1217 | + |
| 1218 | + return html.div( |
| 1219 | + {"id": item, "color": color.value}, |
| 1220 | + html.button({"on_click": colorize}, f"Color {item}"), |
| 1221 | + html.button({"on_click": deleteme}, f"Delete {item}"), |
| 1222 | + ) |
| 1223 | + |
| 1224 | + @component |
| 1225 | + def App(): |
| 1226 | + items = use_state(["A", "B", "C"]) |
| 1227 | + return html._([Item(item, items, key=item) for item in items.value]) |
| 1228 | + |
| 1229 | + async with layout_runner(reactpy.Layout(App())) as runner: |
| 1230 | + tree = await runner.render() |
| 1231 | + |
| 1232 | + # Delete item B |
| 1233 | + b, b_info = find_element(tree, select.id_equals("B")) |
| 1234 | + assert b_info.path == (0, 1, 0) |
| 1235 | + b_delete, _ = find_element(b, select.text_equals("Delete B")) |
| 1236 | + await runner.trigger(b_delete, "on_click", {}) |
| 1237 | + |
| 1238 | + tree = await runner.render() |
| 1239 | + |
| 1240 | + # Set color of item C |
| 1241 | + assert not element_exists(tree, select.id_equals("B")) |
| 1242 | + c, c_info = find_element(tree, select.id_equals("C")) |
| 1243 | + assert c_info.path == (0, 1, 0) |
| 1244 | + c_color, _ = find_element(c, select.text_equals("Color C")) |
| 1245 | + await runner.trigger(c_color, "on_click", {}) |
| 1246 | + |
| 1247 | + tree = await runner.render() |
| 1248 | + |
| 1249 | + # Ensure position and color of item C are correct |
| 1250 | + c, c_info = find_element(tree, select.id_equals("C")) |
| 1251 | + assert c_info.path == (0, 1, 0) |
| 1252 | + assert c["attributes"]["color"] == "blue" |
0 commit comments