-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathtest_event.py
More file actions
712 lines (570 loc) ยท 22.4 KB
/
test_event.py
File metadata and controls
712 lines (570 loc) ยท 22.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
import json
from collections.abc import Callable
import pytest
import reflex as rx
from reflex.constants.compiler import Hooks, Imports
from reflex.event import (
BACKGROUND_TASK_MARKER,
Event,
EventChain,
EventHandler,
EventSpec,
call_event_handler,
event,
fix_events,
)
from reflex.state import BaseState
from reflex.utils import format
from reflex.vars.base import Field, LiteralVar, Var, VarData, field
def make_var(value) -> Var:
"""Make a variable.
Args:
value: The value of the var.
Returns:
The var.
"""
return Var(_js_expr=value)
def test_create_event():
"""Test creating an event."""
event = Event(token="token", name="state.do_thing", payload={"arg": "value"})
assert event.token == "token"
assert event.name == "state.do_thing"
assert event.payload == {"arg": "value"}
def test_call_event_handler():
"""Test that calling an event handler creates an event spec."""
def test_fn():
pass
test_fn.__qualname__ = "test_fn"
def fn_with_args(_, arg1, arg2):
pass
fn_with_args.__qualname__ = "fn_with_args"
handler = EventHandler(fn=test_fn)
event_spec = handler()
assert event_spec.handler == handler
assert event_spec.args == ()
assert format.format_event(event_spec) == 'ReflexEvent("test_fn", {})'
handler = EventHandler(fn=fn_with_args)
event_spec = handler(make_var("first"), make_var("second"))
# Test passing vars as args.
assert event_spec.handler == handler
assert event_spec.args[0][0].equals(Var(_js_expr="arg1"))
assert event_spec.args[0][1].equals(Var(_js_expr="first"))
assert event_spec.args[1][0].equals(Var(_js_expr="arg2"))
assert event_spec.args[1][1].equals(Var(_js_expr="second"))
assert (
format.format_event(event_spec)
== 'ReflexEvent("fn_with_args", {arg1:first,arg2:second})'
)
# Passing args as strings should format differently.
event_spec = handler("first", "second")
assert (
format.format_event(event_spec)
== 'ReflexEvent("fn_with_args", {arg1:"first",arg2:"second"})'
)
first, second = 123, "456"
handler = EventHandler(fn=fn_with_args)
event_spec = handler(first, second)
assert (
format.format_event(event_spec)
== 'ReflexEvent("fn_with_args", {arg1:123,arg2:"456"})'
)
assert event_spec.handler == handler
assert event_spec.args[0][0].equals(Var(_js_expr="arg1"))
assert event_spec.args[0][1].equals(LiteralVar.create(first))
assert event_spec.args[1][0].equals(Var(_js_expr="arg2"))
assert event_spec.args[1][1].equals(LiteralVar.create(second))
handler = EventHandler(fn=fn_with_args)
with pytest.raises(TypeError):
handler(test_fn)
def test_call_event_handler_partial():
"""Calling an EventHandler with incomplete args returns an EventSpec that can be extended."""
def fn_with_args(_, arg1, arg2):
pass
fn_with_args.__qualname__ = "fn_with_args"
def spec(a2: Var[str]) -> list[Var[str]]:
return [a2]
handler = EventHandler(fn=fn_with_args, state_full_name="BigState")
event_spec = handler(make_var("first"))
event_spec2 = call_event_handler(event_spec, spec)
assert event_spec.handler == handler
assert len(event_spec.args) == 1
assert event_spec.args[0][0].equals(Var(_js_expr="arg1"))
assert event_spec.args[0][1].equals(Var(_js_expr="first"))
assert (
format.format_event(event_spec)
== 'ReflexEvent("BigState.fn_with_args", {arg1:first})'
)
assert event_spec2 is not event_spec
assert event_spec2.handler == handler
assert len(event_spec2.args) == 2
assert event_spec2.args[0][0].equals(Var(_js_expr="arg1"))
assert event_spec2.args[0][1].equals(Var(_js_expr="first"))
assert event_spec2.args[1][0].equals(Var(_js_expr="arg2"))
assert event_spec2.args[1][1].equals(Var(_js_expr="_a2", _var_type=str))
assert (
format.format_event(event_spec2)
== 'ReflexEvent("BigState.fn_with_args", {arg1:first,arg2:_a2})'
)
@pytest.mark.parametrize(
("arg1", "arg2"),
[
(1, 2),
(1, "2"),
({"a": 1}, {"b": 2}),
],
)
def test_fix_events(arg1, arg2):
"""Test that chaining an event handler with args formats the payload correctly.
Args:
arg1: The first arg passed to the handler.
arg2: The second arg passed to the handler.
"""
def fn_with_args(_, arg1, arg2):
pass
fn_with_args.__qualname__ = "fn_with_args"
handler = EventHandler(fn=fn_with_args)
event_spec = handler(arg1, arg2)
event = fix_events([event_spec], token="foo")[0]
assert event.name == fn_with_args.__qualname__
assert event.token == "foo"
assert event.payload == {"arg1": arg1, "arg2": arg2}
@pytest.mark.parametrize(
("input", "output"),
[
(
(path, is_external, replace, popup),
f'ReflexEvent("_redirect", {{path:{json.dumps(path) if isinstance(path, str) else path._js_expr},external:{"true" if is_external else "false"},popup:{"true" if popup else "false"},replace:{"true" if replace else "false"}}})',
)
for path in ("/path", Var(_js_expr="path"))
for is_external in (None, True, False)
for replace in (None, True, False)
for popup in (None, True, False)
],
)
def test_event_redirect(input, output):
"""Test the event redirect function.
Args:
input: The input for running the test.
output: The expected output to validate the test.
"""
path, is_external, replace, popup = input
kwargs = {}
if is_external is not None:
kwargs["is_external"] = is_external
if replace is not None:
kwargs["replace"] = replace
if popup is not None:
kwargs["popup"] = popup
spec = event.redirect(path, **kwargs)
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_redirect"
assert format.format_event(spec) == output
def test_event_console_log():
"""Test the event console log function."""
spec = event.console_log("message")
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_call_function"
assert spec.args[0][0].equals(Var(_js_expr="function"))
assert spec.args[0][1].equals(
Var('(() => (console?.["log"]("message")))', _var_type=Callable)
)
assert (
format.format_event(spec)
== 'ReflexEvent("_call_function", {function:(() => (console?.["log"]("message"))),callback:null})'
)
spec = event.console_log(Var(_js_expr="message"))
assert (
format.format_event(spec)
== 'ReflexEvent("_call_function", {function:(() => (console?.["log"](message))),callback:null})'
)
spec2 = event.console_log(Var(_js_expr="message2")).add_args(Var("throwaway"))
assert (
format.format_event(spec2)
== 'ReflexEvent("_call_function", {function:(() => (console?.["log"](message2))),callback:null})'
)
def test_event_window_alert():
"""Test the event window alert function."""
spec = event.window_alert("message")
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_call_function"
assert spec.args[0][0].equals(Var(_js_expr="function"))
assert spec.args[0][1].equals(
Var('(() => (window?.["alert"]("message")))', _var_type=Callable)
)
assert (
format.format_event(spec)
== 'ReflexEvent("_call_function", {function:(() => (window?.["alert"]("message"))),callback:null})'
)
spec = event.window_alert(Var(_js_expr="message"))
assert (
format.format_event(spec)
== 'ReflexEvent("_call_function", {function:(() => (window?.["alert"](message))),callback:null})'
)
spec2 = event.window_alert(Var(_js_expr="message2")).add_args(Var("throwaway"))
assert (
format.format_event(spec2)
== 'ReflexEvent("_call_function", {function:(() => (window?.["alert"](message2))),callback:null})'
)
@pytest.mark.parametrize(
("func", "qualname"), [("set_focus", "_set_focus"), ("blur_focus", "_blur_focus")]
)
def test_focus(func: str, qualname: str):
"""Test the event set focus function.
Args:
func: The event function name.
qualname: The sig qual name passed to JS.
"""
spec = getattr(event, func)("input1")
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == qualname
assert spec.args[0][0].equals(Var(_js_expr="ref"))
assert spec.args[0][1].equals(LiteralVar.create("ref_input1"))
assert (
format.format_event(spec) == f'ReflexEvent("{qualname}", {{ref:"ref_input1"}})'
)
spec = getattr(event, func)("input1")
assert (
format.format_event(spec) == f'ReflexEvent("{qualname}", {{ref:"ref_input1"}})'
)
def test_set_value():
"""Test the event window alert function."""
spec = event.set_value("input1", "")
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_set_value"
assert spec.args[0][0].equals(Var(_js_expr="ref"))
assert spec.args[0][1].equals(LiteralVar.create("ref_input1"))
assert spec.args[1][0].equals(Var(_js_expr="value"))
assert spec.args[1][1].equals(LiteralVar.create(""))
assert (
format.format_event(spec)
== 'ReflexEvent("_set_value", {ref:"ref_input1",value:""})'
)
spec = event.set_value("input1", Var(_js_expr="message"))
assert (
format.format_event(spec)
== 'ReflexEvent("_set_value", {ref:"ref_input1",value:message})'
)
def test_remove_cookie():
"""Test the event remove_cookie."""
spec = event.remove_cookie("testkey")
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_remove_cookie"
assert spec.args[0][0].equals(Var(_js_expr="key"))
assert spec.args[0][1].equals(LiteralVar.create("testkey"))
assert spec.args[1][0].equals(Var(_js_expr="options"))
assert spec.args[1][1].equals(LiteralVar.create({"path": "/"}))
assert (
format.format_event(spec)
== 'ReflexEvent("_remove_cookie", {key:"testkey",options:({ ["path"] : "/" })})'
)
def test_remove_cookie_with_options():
"""Test the event remove_cookie with options."""
options = {
"path": "/foo",
"domain": "example.com",
"secure": True,
"sameSite": "strict",
}
spec = event.remove_cookie("testkey", options)
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_remove_cookie"
assert spec.args[0][0].equals(Var(_js_expr="key"))
assert spec.args[0][1].equals(LiteralVar.create("testkey"))
assert spec.args[1][0].equals(Var(_js_expr="options"))
assert spec.args[1][1].equals(LiteralVar.create(options))
assert (
format.format_event(spec)
== f'ReflexEvent("_remove_cookie", {{key:"testkey",options:{LiteralVar.create(options)!s}}})'
)
def test_clear_local_storage():
"""Test the event clear_local_storage."""
spec = event.clear_local_storage()
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_clear_local_storage"
assert not spec.args
assert format.format_event(spec) == 'ReflexEvent("_clear_local_storage", {})'
def test_remove_local_storage():
"""Test the event remove_local_storage."""
spec = event.remove_local_storage("testkey")
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_remove_local_storage"
assert spec.args[0][0].equals(Var(_js_expr="key"))
assert spec.args[0][1].equals(LiteralVar.create("testkey"))
assert (
format.format_event(spec)
== 'ReflexEvent("_remove_local_storage", {key:"testkey"})'
)
def test_event_actions():
"""Test DOM event actions, like stopPropagation and preventDefault."""
# EventHandler
handler = EventHandler(fn=lambda: None)
assert not handler.event_actions
sp_handler = handler.stop_propagation
assert handler is not sp_handler
assert sp_handler.event_actions == {"stopPropagation": True}
pd_handler = handler.prevent_default
assert handler is not pd_handler
assert pd_handler.event_actions == {"preventDefault": True}
both_handler = sp_handler.prevent_default
assert both_handler is not sp_handler
assert both_handler.event_actions == {
"stopPropagation": True,
"preventDefault": True,
}
throttle_handler = handler.throttle(300)
assert handler is not throttle_handler
assert throttle_handler.event_actions == {"throttle": 300}
debounce_handler = handler.debounce(300)
assert handler is not debounce_handler
assert debounce_handler.event_actions == {"debounce": 300}
all_handler = handler.stop_propagation.prevent_default.throttle(200).debounce(100)
assert handler is not all_handler
assert all_handler.event_actions == {
"stopPropagation": True,
"preventDefault": True,
"throttle": 200,
"debounce": 100,
}
assert not handler.event_actions
# Convert to EventSpec should carry event actions
sp_handler2 = handler.stop_propagation.throttle(200)
spec = sp_handler2()
assert spec.event_actions == {"stopPropagation": True, "throttle": 200}
assert spec.event_actions == sp_handler2.event_actions
assert spec.event_actions is not sp_handler2.event_actions
# But it should be a copy!
assert spec.event_actions is not sp_handler2.event_actions
spec2 = spec.prevent_default
assert spec is not spec2
assert spec2.event_actions == {
"stopPropagation": True,
"preventDefault": True,
"throttle": 200,
}
assert spec2.event_actions != spec.event_actions
# The original handler should still not be touched.
assert not handler.event_actions
def test_event_actions_on_state():
class EventActionState(BaseState):
def handler(self):
pass
handler = EventActionState.handler
assert isinstance(handler, EventHandler)
assert not handler.event_actions
sp_handler = EventActionState.handler.stop_propagation # pyright: ignore [reportFunctionMemberAccess]
assert sp_handler.event_actions == {"stopPropagation": True}
# should NOT affect other references to the handler
assert not handler.event_actions
def test_event_var_data():
class S(BaseState):
x: Field[int] = field(0)
@event
def s(self, value: int):
pass
@event
def s2(self):
pass
# Handler doesn't have any _var_data because it's just a str
handler_var = Var.create(S.s2)
assert handler_var._get_all_var_data() is None
# Ensure spec carries _var_data
spec_var = Var.create(S.s(S.x))
assert spec_var._get_all_var_data() == S.x._get_all_var_data()
# Needed to instantiate the EventChain
def _args_spec(value: Var[int]) -> tuple[Var[int]]:
return (value,)
# Ensure chain carries _var_data
chain_var = Var.create(
EventChain(
events=[S.s(S.x)],
args_spec=_args_spec,
invocation=rx.vars.FunctionStringVar.create(""),
)
)
assert chain_var._get_all_var_data() == S.x._get_all_var_data()
chain_var_data = Var.create(
EventChain(
events=[],
args_spec=_args_spec,
)
)._get_all_var_data()
assert chain_var_data is not None
assert chain_var_data == VarData(
imports=Imports.EVENTS,
hooks={Hooks.EVENTS: None},
)
def test_event_bound_method() -> None:
class S(BaseState):
@event
def e(self, arg: str):
print(arg)
class Wrapper:
def get_handler(self, arg: Var[str]):
return S.e(arg)
w = Wrapper()
_ = rx.input(on_change=w.get_handler)
def test_event_decorator_with_event_actions():
"""Test that @rx.event decorator can accept event action parameters."""
class MyTestState(BaseState):
# Test individual event actions
@event(stop_propagation=True)
def handle_stop_prop(self):
pass
@event(prevent_default=True)
def handle_prevent_default(self):
pass
@event(throttle=500)
def handle_throttle(self):
pass
@event(debounce=300)
def handle_debounce(self):
pass
@event(temporal=True)
def handle_temporal(self):
pass
# Test multiple event actions combined
@event(stop_propagation=True, prevent_default=True, throttle=1000)
def handle_multiple(self):
pass
# Test with background parameter (existing functionality)
@event(background=True, temporal=True)
async def handle_background_temporal(self):
pass
# Test no event actions (existing behavior)
@event
def handle_no_actions(self):
pass
# Test individual event actions are applied
stop_prop_handler = MyTestState.handle_stop_prop
assert isinstance(stop_prop_handler, EventHandler)
assert stop_prop_handler.event_actions == {"stopPropagation": True}
prevent_default_handler = MyTestState.handle_prevent_default
assert prevent_default_handler.event_actions == {"preventDefault": True}
throttle_handler = MyTestState.handle_throttle
assert throttle_handler.event_actions == {"throttle": 500}
debounce_handler = MyTestState.handle_debounce
assert debounce_handler.event_actions == {"debounce": 300}
temporal_handler = MyTestState.handle_temporal
assert temporal_handler.event_actions == {"temporal": True}
# Test multiple event actions are combined correctly
multiple_handler = MyTestState.handle_multiple
assert multiple_handler.event_actions == {
"stopPropagation": True,
"preventDefault": True,
"throttle": 1000,
}
# Test background + event actions work together
bg_temporal_handler = MyTestState.handle_background_temporal
assert bg_temporal_handler.event_actions == {"temporal": True}
assert hasattr(bg_temporal_handler.fn, BACKGROUND_TASK_MARKER) # pyright: ignore [reportAttributeAccessIssue]
# Test no event actions (existing behavior preserved)
no_actions_handler = MyTestState.handle_no_actions
assert no_actions_handler.event_actions == {}
def test_event_decorator_actions_can_be_overridden():
"""Test that decorator event actions can still be overridden by chaining."""
class MyTestState(BaseState):
@event(throttle=500, stop_propagation=True)
def handle_with_defaults(self):
pass
# Get the handler with default actions
handler = MyTestState.handle_with_defaults
assert handler.event_actions == {"throttle": 500, "stopPropagation": True}
# Chain additional actions - should combine
handler_with_prevent_default = handler.prevent_default
assert handler_with_prevent_default.event_actions == {
"throttle": 500,
"stopPropagation": True,
"preventDefault": True,
}
# Chain throttle with different value - should override
handler_with_new_throttle = handler.throttle(1000)
assert handler_with_new_throttle.event_actions == {
"throttle": 1000, # New value overrides default
"stopPropagation": True,
}
# Original handler should be unchanged
assert handler.event_actions == {"throttle": 500, "stopPropagation": True}
def test_event_decorator_with_none_values():
"""Test that None values in decorator don't create event actions."""
class MyTestState(BaseState):
@event(stop_propagation=None, prevent_default=None, throttle=None)
def handle_all_none(self):
pass
@event(stop_propagation=True, prevent_default=None, throttle=500, debounce=None)
def handle_mixed(self):
pass
# All None should result in no event actions
all_none_handler = MyTestState.handle_all_none
assert all_none_handler.event_actions == {}
# Only non-None values should be included
mixed_handler = MyTestState.handle_mixed
assert mixed_handler.event_actions == {"stopPropagation": True, "throttle": 500}
def test_event_decorator_backward_compatibility():
"""Test that existing code without event action parameters continues to work."""
class MyTestState(BaseState):
@event
def handle_old_style(self):
pass
@event(background=True)
async def handle_old_background(self):
pass
# Old style without parameters should work unchanged
old_handler = MyTestState.handle_old_style
assert isinstance(old_handler, EventHandler)
assert old_handler.event_actions == {}
assert not hasattr(old_handler.fn, BACKGROUND_TASK_MARKER) # pyright: ignore [reportAttributeAccessIssue]
# Old background parameter should work unchanged
bg_handler = MyTestState.handle_old_background
assert bg_handler.event_actions == {}
assert hasattr(bg_handler.fn, BACKGROUND_TASK_MARKER) # pyright: ignore [reportAttributeAccessIssue]
def test_event_var_in_rx_cond():
"""Test that EventVar and EventChainVar cannot be used in rx.cond()."""
from reflex.components.core.cond import cond as rx_cond
class S(BaseState):
@event
def s(self):
pass
handler_var = Var.create(S.s)
with pytest.raises(TypeError) as err:
rx_cond(handler_var, rx.text("True"), rx.text("False"))
assert "Cannot convert" in str(err.value)
assert "to bool" in str(err.value)
def _args_spec() -> tuple:
return ()
chain_var = Var.create(
EventChain(
events=[S.s()],
args_spec=_args_spec,
)
)
with pytest.raises(TypeError) as err:
rx_cond(chain_var, rx.text("True"), rx.text("False"))
assert "Cannot convert" in str(err.value)
assert "to bool" in str(err.value)
def test_decentralized_event_with_args():
"""Test the decentralized event."""
class S(BaseState):
field: Field[str] = field("")
@event
def e(s: S, arg: str):
s.field = arg
_ = rx.input(on_change=e("foo"))
def test_decentralized_event_no_args():
"""Test the decentralized event with no args."""
class S(BaseState):
field: Field[str] = field("")
@event
def e(s: S):
s.field = "foo"
_ = rx.input(on_change=e())
_ = rx.input(on_change=e)
class GlobalState(BaseState):
"""Global state for testing decentralized events."""
field: Field[str] = field("")
@event
def f(s: GlobalState, arg: str):
s.field = arg
def test_decentralized_event_global_state():
"""Test the decentralized event with a global state."""
_ = rx.input(on_change=f("foo"))
_ = rx.input(on_change=f)