@@ -37,6 +37,28 @@ def before_render(self):
3737 self .state ["doubled" ] = self .state .get ("value" , 0 ) * 2
3838
3939
40+ class AsyncSampleComponent (Component ):
41+ """Test component with async event handlers."""
42+
43+ template_name = "async_sample.html"
44+
45+ def mount (self ):
46+ super ().mount ()
47+ self .state ["value" ] = self .params .get ("initial" , 0 )
48+
49+ async def on_update (self , value : int ):
50+ self .state ["value" ] = value
51+
52+ async def on_failing_event (self ):
53+ raise ValueError ("intentional async error" )
54+
55+ def on_sync_event (self , value : int = 0 ):
56+ self .state ["value" ] = value
57+
58+ def before_render (self ):
59+ self .state ["doubled" ] = self .state .get ("value" , 0 ) * 2
60+
61+
4062# ---------- Component Lifecycle ----------
4163
4264
@@ -236,3 +258,95 @@ def test_serialize_non_json_types_uses_str(self):
236258 def test_deserialize_invalid_json_raises (self ):
237259 with pytest .raises (Exception ):
238260 StateSerializer .deserialize ("not json" )
261+
262+
263+ # ---------- Async Event Handling ----------
264+
265+
266+ class TestAsyncHandleEvent :
267+ @pytest .mark .asyncio
268+ async def test_async_handle_event_routes_to_async_handler (self ):
269+ Component .renderer = MockRenderer ()
270+ comp = AsyncSampleComponent ()
271+ comp .mount ()
272+ await comp .async_handle_event ("update" , {"value" : 99 })
273+ assert comp .state ["value" ] == 99
274+
275+ @pytest .mark .asyncio
276+ async def test_async_handle_event_works_with_sync_handler (self ):
277+ Component .renderer = MockRenderer ()
278+ comp = AsyncSampleComponent ()
279+ comp .mount ()
280+ await comp .async_handle_event ("sync_event" , {"value" : 42 })
281+ assert comp .state ["value" ] == 42
282+
283+ @pytest .mark .asyncio
284+ async def test_async_handle_event_missing_handler_raises (self ):
285+ comp = AsyncSampleComponent ()
286+ with pytest .raises (EventNotFoundError , match = "No handler for event: nonexistent" ):
287+ await comp .async_handle_event ("nonexistent" , {})
288+
289+ @pytest .mark .asyncio
290+ async def test_async_handle_event_invalid_payload_raises (self ):
291+ comp = AsyncSampleComponent ()
292+ comp .mount ()
293+ with pytest .raises (ComponentError , match = "Invalid payload for update" ):
294+ await comp .async_handle_event ("update" , {"wrong_param" : 1 })
295+
296+ @pytest .mark .asyncio
297+ async def test_async_handle_event_handler_exception_wrapped (self ):
298+ comp = AsyncSampleComponent ()
299+ comp .mount ()
300+ with pytest .raises (ComponentError , match = "Error handling failing_event" ):
301+ await comp .async_handle_event ("failing_event" , {})
302+
303+ def test_sync_handle_event_rejects_async_handler (self ):
304+ comp = AsyncSampleComponent ()
305+ comp .mount ()
306+ with pytest .raises (ComponentError , match = "is async" ):
307+ comp .handle_event ("update" , {"value" : 1 })
308+
309+
310+ # ---------- Async Dispatch ----------
311+
312+
313+ class TestAsyncDispatch :
314+ def setup_method (self ):
315+ Component .renderer = MockRenderer ()
316+
317+ @pytest .mark .asyncio
318+ async def test_async_dispatch_mount_path (self ):
319+ comp = AsyncSampleComponent (initial = 7 )
320+ result = await comp .async_dispatch ()
321+ assert result ["state" ]["value" ] == 7
322+ assert result ["state" ]["doubled" ] == 14
323+ assert "html" in result
324+ assert "component_id" in result
325+
326+ @pytest .mark .asyncio
327+ async def test_async_dispatch_hydrate_path (self ):
328+ comp = AsyncSampleComponent ()
329+ result = await comp .async_dispatch (state = {"value" : 20 })
330+ assert result ["state" ]["value" ] == 20
331+
332+ @pytest .mark .asyncio
333+ async def test_async_dispatch_with_async_event (self ):
334+ comp = AsyncSampleComponent ()
335+ result = await comp .async_dispatch (
336+ event = "update" , payload = {"value" : 50 }, state = {"value" : 0 }
337+ )
338+ assert result ["state" ]["value" ] == 50
339+
340+ @pytest .mark .asyncio
341+ async def test_async_dispatch_with_sync_event (self ):
342+ comp = AsyncSampleComponent ()
343+ result = await comp .async_dispatch (
344+ event = "sync_event" , payload = {"value" : 77 }, state = {"value" : 0 }
345+ )
346+ assert result ["state" ]["value" ] == 77
347+
348+ @pytest .mark .asyncio
349+ async def test_async_dispatch_event_error_propagates (self ):
350+ comp = AsyncSampleComponent ()
351+ with pytest .raises (ComponentError ):
352+ await comp .async_dispatch (event = "nonexistent" , state = {"value" : 0 })
0 commit comments