From 72e03ec1675ac127f9340d8314c8cc55748ac569 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sun, 4 Apr 2021 00:26:27 -0700 Subject: [PATCH] add tests for callback identity preservation with keys --- docs/source/core-concepts.rst | 8 +-- src/idom/core/layout.py | 4 +- tests/test_core/test_dispatcher.py | 2 +- tests/test_core/test_layout.py | 84 ++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/docs/source/core-concepts.rst b/docs/source/core-concepts.rst index 9e338d870..9d00bdbe5 100644 --- a/docs/source/core-concepts.rst +++ b/docs/source/core-concepts.rst @@ -88,7 +88,7 @@ have to re-render the layout and see what changed: async with idom.Layout(ClickCount(key="something")) as layout: patch_1 = await layout.render() - fake_event = LayoutEvent("something.onClick", [{}]) + fake_event = LayoutEvent("/something/onClick", [{}]) await layout.dispatch(fake_event) patch_2 = await layout.render() @@ -129,7 +129,7 @@ callback that's called by the dispatcher to events it should execute. async def recv(): - event = LayoutEvent(event_handler_id, [{}]) + event = LayoutEvent("/my-component/onClick", [{}]) # We need this so we don't flood the render loop with events. # In practice this is never an issue since events won't arrive @@ -139,7 +139,9 @@ callback that's called by the dispatcher to events it should execute. return event - async with SingleViewDispatcher(idom.Layout(ClickCount())) as dispatcher: + async with SingleViewDispatcher( + idom.Layout(ClickCount(key="my-component")) + ) as dispatcher: context = None # see note below await dispatcher.run(send, recv, context) diff --git a/src/idom/core/layout.py b/src/idom/core/layout.py index 14ee78e5b..3d9327134 100644 --- a/src/idom/core/layout.py +++ b/src/idom/core/layout.py @@ -207,7 +207,7 @@ def _render_model_children( component_state, cast(VdomDict, child), patch_path=f"{patch_path}/children/{index}", - key_path=f"{key_path}/{index}", + key_path=f"{key_path}/{child.get('key') or hex(id(child))[2:]}", ) ) elif isinstance(child, AbstractComponent): @@ -250,7 +250,7 @@ def _render_model_event_targets( handlers_by_target: Dict[str, EventHandler] = {} model_event_targets: Dict[str, _EventTarget] = {} for event, handler in handlers_by_event.items(): - target = f"{key_path}.{event}" + target = f"{key_path}/{event}" handlers_by_target[target] = handler model_event_targets[event] = { "target": target, diff --git a/tests/test_core/test_dispatcher.py b/tests/test_core/test_dispatcher.py index f7015ee49..487c24a49 100644 --- a/tests/test_core/test_dispatcher.py +++ b/tests/test_core/test_dispatcher.py @@ -19,7 +19,7 @@ async def test_shared_state_dispatcher(): changes_2 = [] key = "test-element" event_name = "onEvent" - target_id = f"/{key}.{event_name}" + target_id = f"/{key}/{event_name}" events_to_inject = [LayoutEvent(target=target_id, data=[])] * 4 diff --git a/tests/test_core/test_layout.py b/tests/test_core/test_layout.py index 6a5c30aad..cd13ca69e 100644 --- a/tests/test_core/test_layout.py +++ b/tests/test_core/test_layout.py @@ -275,3 +275,87 @@ def SomeComponent(): "Ignored event - handler 'missing' does not exist or its component unmounted", next(iter(caplog.records)).msg, ) + + +def use_toggle(init=False): + state, set_state = idom.hooks.use_state(init) + return state, lambda: set_state(lambda old: not old) + + +async def test_model_key_preserves_callback_identity_for_common_elements(): + called_good_trigger = idom.Ref(False) + + @idom.component + def MyComponent(): + reverse_children, set_reverse_children = use_toggle() + + def good_trigger(): + called_good_trigger.current = True + set_reverse_children() + + def bad_trigger(): + raise ValueError("Called bad trigger") + + children = [ + idom.html.button( + {"onClick": good_trigger, "id": "good"}, "good", key="good" + ), + idom.html.button({"onClick": bad_trigger, "id": "bad"}, "bad", key="bad"), + ] + + if reverse_children: + children.reverse() + + return idom.html.div(children) + + async with idom.Layout(MyComponent(key="component")) as layout: + await layout.render() + for i in range(3): + event = LayoutEvent("/component/good/onClick", []) + await layout.dispatch(event) + + assert called_good_trigger.current + # reset after checking + called_good_trigger.current = False + + await layout.render() + + +async def test_model_key_preserves_callback_identity_for_components(): + called_good_trigger = idom.Ref(False) + + @idom.component + def RootComponent(): + reverse_children, set_reverse_children = use_toggle() + + children = [ + Trigger(name, set_reverse_children, key=name) for name in ["good", "bad"] + ] + + if reverse_children: + children.reverse() + + return idom.html.div(children) + + @idom.component + def Trigger(name, set_reverse_children): + def callback(): + if name == "good": + called_good_trigger.current = True + set_reverse_children() + else: + raise ValueError("Called bad trigger") + + return idom.html.button({"onClick": callback, "id": "good"}, "good") + + async with idom.Layout(RootComponent(key="root")) as layout: + await layout.render() + for i in range(3): + event = LayoutEvent("/root/good/onClick", []) + await layout.dispatch(event) + + assert called_good_trigger.current + # reset after checking + called_good_trigger.current = False + + await layout.render()