def _process_property_change(self, msg): if msg['msg'] is None: return {} from idom.core.layout import LayoutEvent dispatch = self._idom_layout.dispatch(LayoutEvent(**msg['msg'])) asyncio.run_coroutine_threadsafe(dispatch, loop=self._idom_loop) for ref, (m, _) in self._models.items(): m.msg = None push_on_root(ref) return {}
async def recv_1(): sent = await data_sent_1.get() element_id = sent["root"] element_data = sent["new"][element_id] if element_data["attributes"]["count"] == 4: done.set() raise asyncio.CancelledError() return LayoutEvent(target="an-event", data=[])
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_log_on_dispatch_to_missing_event_handler(caplog): @idom.component def SomeComponent(): return idom.html.div() async with idom.Layout(SomeComponent()) as layout: await layout.dispatch(LayoutEvent(target="missing", data=[])) assert re.match( "Ignored event - handler 'missing' does not exist or its component unmounted", next(iter(caplog.records)).msg, )
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_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_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
async def test_layout_cannot_be_used_outside_context_manager(caplog): @idom.component def Component(): ... component = Component() layout = idom.Layout(component) with pytest.raises(Exception): await layout.deliver(LayoutEvent("something", [])) with pytest.raises(Exception): layout.update(component) with pytest.raises(Exception): await layout.render()
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_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)
def _idom_on_msg(self, message, buffers): m_type = message.get("type") if m_type == "client-ready": v_id = message["viewID"] self._idom_views.add(v_id) update = LayoutUpdate("", None, self._idom_model) diff = VdomJsonPatch.create_from(update) self.send({"viewID": v_id, "data": diff}) elif m_type == "dom-event": asyncio.run_coroutine_threadsafe( self._idom_layout.deliver(LayoutEvent(**message["data"])), loop=self._idom_loop, ) elif m_type == "client-removed": v_id = message["viewID"] if v_id in self._idom_views: self._idom_views.remove(message["viewID"])
def make_events_and_expected_model(): events = [LayoutEvent(STATIC_EVENT_HANDLER.target, [])] * 4 expected_model = { "tagName": "", "children": [{ "tagName": "div", "attributes": { "count": 4 }, "eventHandlers": { EVENT_NAME: { "target": STATIC_EVENT_HANDLER.target, "preventDefault": False, "stopPropagation": False, } }, }], } return events, expected_model
async def sock_recv() -> LayoutEvent: data = await socket.recv() if data is None: raise Stop() return LayoutEvent(**json.loads(data))
async def sock_recv() -> LayoutEvent: return LayoutEvent(**json.loads(await socket.receive_text()))
async def recv(): await asyncio.sleep(0) return LayoutEvent(target_id, [])
async def test_shared_state_dispatcher(): done = asyncio.Event() changes_1 = [] changes_2 = [] target_id = "an-event" events_to_inject = [LayoutEvent(target=target_id, data=[])] * 4 async def send_1(patch): changes_1.append(patch.changes) async def recv_1(): await asyncio.sleep(0) try: return events_to_inject.pop(0) except IndexError: done.set() raise asyncio.CancelledError() async def send_2(patch): changes_2.append(patch.changes) async def recv_2(): await done.wait() raise asyncio.CancelledError() @idom.component def Clickable(): count, set_count = idom.hooks.use_state(0) @idom.event(target_id=target_id) async def an_event(): set_count(count + 1) return idom.html.div({"anEvent": an_event, "count": count}) async with SharedViewDispatcher(Layout(Clickable())) as dispatcher: await dispatcher.run(send_1, recv_1, "1") await dispatcher.run(send_2, recv_2, "2") expected_changes = [ [ { "op": "add", "path": "/eventHandlers", "value": { "anEvent": { "target": "an-event", "preventDefault": False, "stopPropagation": False, } }, }, { "op": "add", "path": "/attributes", "value": { "count": 0 } }, { "op": "add", "path": "/tagName", "value": "div" }, ], [{ "op": "replace", "path": "/attributes/count", "value": 1 }], [{ "op": "replace", "path": "/attributes/count", "value": 2 }], [{ "op": "replace", "path": "/attributes/count", "value": 3 }], ] for c_2, expected_c in zip(changes_2, expected_changes): assert_same_items(c_2, expected_c) assert changes_1 == changes_2
async def recv() -> LayoutEvent: return LayoutEvent(**json.loads(await message_queue.get()))
def recv() -> LayoutEvent: return LayoutEvent(**json.loads(ws.receive()))
async def sock_recv() -> LayoutEvent: message = json.loads(await socket.recv()) event = message["body"]["event"] return LayoutEvent(event["target"], event["data"])
def recv() -> Optional[LayoutEvent]: event = ws.receive() if event is not None: return LayoutEvent(**json.loads(event)) else: return None