async def test_model_key_preserves_callback_identity_for_common_elements( caplog): called_good_trigger = idom.Ref(False) good_handler = StaticEventHandler() bad_handler = StaticEventHandler() @idom.component def MyComponent(): reverse_children, set_reverse_children = use_toggle() @good_handler.use def good_trigger(): called_good_trigger.current = True set_reverse_children() @bad_handler.use 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()) as layout: await layout.render() for i in range(3): event = LayoutEvent(good_handler.target, []) await layout.deliver(event) assert called_good_trigger.current # reset after checking called_good_trigger.current = False await layout.render() assert not caplog.records
async def test_model_key_preserves_callback_identity_for_components(): called_good_trigger = idom.Ref(False) good_handler = StaticEventHandler() bad_handler = StaticEventHandler() @idom.component def RootComponent(): reverse_children, set_reverse_children = use_toggle() children = [ Trigger(set_reverse_children, name=name, key=name) for name in ["good", "bad"] ] if reverse_children: children.reverse() return idom.html.div(children) @idom.component def Trigger(set_reverse_children, name): if name == "good": @good_handler.use def callback(): called_good_trigger.current = True set_reverse_children() else: @bad_handler.use def callback(): raise ValueError("Called bad trigger") return idom.html.button({"onClick": callback, "id": "good"}, "good") async with idom.Layout(RootComponent()) as layout: await layout.render() for _ in range(3): event = LayoutEvent(good_handler.target, []) await layout.deliver(event) assert called_good_trigger.current # reset after checking called_good_trigger.current = False await layout.render()
async def test_element_keys_inside_components_do_not_reset_state_of_component( ): """This is a regression test for a bug. You would not expect that calling `set_child_key_num` would trigger state to be reset in any `Child()` components but there was a bug where that happened. """ effect_calls_without_state = [] set_child_key_num = StaticEventHandler() did_call_effect = asyncio.Event() @component def Parent(): state, set_state = use_state(0) return html.div( html.button( { "onClick": set_child_key_num.use(lambda: set_state(state + 1)) }, "click me", ), Child("some-key"), Child(f"key-{state}"), ) @component def Child(child_key): state, set_state = use_state(0) @use_effect async def record_if_state_is_reset(): if state: return effect_calls_without_state.append(child_key) set_state(1) did_call_effect.set() return html.div( child_key, key=child_key, ) async with idom.Layout(Parent()) as layout: await layout.render() await did_call_effect.wait() assert effect_calls_without_state == ["some-key", "key-0"] did_call_effect.clear() for i in range(1, 5): await layout.deliver(LayoutEvent(set_child_key_num.target, [])) await layout.render() assert effect_calls_without_state == ["some-key", "key-0"] did_call_effect.clear()
async def test_dispatcher_handles_more_than_one_event_at_a_time(): block_and_never_set = asyncio.Event() will_block = asyncio.Event() second_event_did_execute = asyncio.Event() blocked_handler = StaticEventHandler() non_blocked_handler = StaticEventHandler() @idom.component def ComponentWithTwoEventHandlers(): @blocked_handler.use async def block_forever(): will_block.set() await block_and_never_set.wait() @non_blocked_handler.use async def handle_event(): second_event_did_execute.set() return idom.html.div( idom.html.button({"onClick": block_forever}), idom.html.button({"onClick": handle_event}), ) send_queue = asyncio.Queue() recv_queue = asyncio.Queue() asyncio.ensure_future( serve_json_patch( idom.Layout(ComponentWithTwoEventHandlers()), send_queue.put, recv_queue.get, )) await recv_queue.put(LayoutEvent(blocked_handler.target, [])) await will_block.wait() await recv_queue.put(LayoutEvent(non_blocked_handler.target, [])) await second_event_did_execute.wait()
async def test_switching_node_type_with_event_handlers(): toggle_type = idom.Ref() element_static_handler = StaticEventHandler() component_static_handler = StaticEventHandler() @idom.component def Root(): toggle, toggle_type.current = use_toggle(True) handler = element_static_handler.use(lambda: None) if toggle: return html.div(html.button({"onEvent": handler})) else: return html.div(SomeComponent()) @idom.component def SomeComponent(): handler = component_static_handler.use(lambda: None) return html.button({"onAnotherEvent": handler}) async with idom.Layout(Root()) as layout: await layout.render() assert element_static_handler.target in layout._event_handlers assert component_static_handler.target not in layout._event_handlers toggle_type.current() await layout.render() assert element_static_handler.target not in layout._event_handlers assert component_static_handler.target in layout._event_handlers toggle_type.current() await layout.render() assert element_static_handler.target in layout._event_handlers assert component_static_handler.target not in layout._event_handlers
async def test_log_error_on_bad_event_handler(): bad_handler = StaticEventHandler() @idom.component def ComponentWithBadEventHandler(): @bad_handler.use def raise_error(): raise Exception("bad event handler") return idom.html.button({"onClick": raise_error}) with assert_idom_did_log(match_error="bad event handler"): async with idom.Layout(ComponentWithBadEventHandler()) as layout: await layout.render() event = LayoutEvent(bad_handler.target, []) await layout.deliver(event)
async def test_changing_event_handlers_in_the_next_render(): set_event_name = Ref() event_handler = StaticEventHandler() did_trigger = Ref(False) @component def Root(): event_name, set_event_name.current = use_state("first") return html.button({ event_name: event_handler.use(lambda: did_trigger.set_current(True)) }) async with Layout(Root()) as layout: await layout.render() await layout.deliver(LayoutEvent(event_handler.target, [])) assert did_trigger.current did_trigger.current = False set_event_name.current("second") await layout.render() await layout.deliver(LayoutEvent(event_handler.target, [])) assert did_trigger.current did_trigger.current = False
import asyncio from typing import Any, Sequence import idom from idom.core.layout import Layout, LayoutEvent, LayoutUpdate from idom.core.serve import VdomJsonPatch, serve_json_patch from idom.testing import StaticEventHandler EVENT_NAME = "onEvent" STATIC_EVENT_HANDLER = StaticEventHandler() def test_vdom_json_patch_create_from_apply_to(): update = LayoutUpdate("", {"a": 1, "b": [1]}, {"a": 2, "b": [1, 2]}) patch = VdomJsonPatch.create_from(update) result = patch.apply_to({"a": 1, "b": [1]}) assert result == {"a": 2, "b": [1, 2]} def make_send_recv_callbacks(events_to_inject): changes = [] # We need a semaphor here to simulate recieving an event after each update is sent. # The effect is that the send() and recv() callbacks trade off control. If we did # not do this, it would easy to determine when to halt because, while we might have # received all the events, they might not have been sent since the two callbacks are # executed in separate loops. sem = asyncio.Semaphore(0) async def send(patch): changes.append(patch)