test_component.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. from typing import Any, Dict, List, Type
  2. import pytest
  3. import reflex as rx
  4. from reflex.base import Base
  5. from reflex.components.base.bare import Bare
  6. from reflex.components.component import Component, CustomComponent, custom_component
  7. from reflex.components.layout.box import Box
  8. from reflex.constants import EventTriggers
  9. from reflex.event import EventChain, EventHandler
  10. from reflex.state import State
  11. from reflex.style import Style
  12. from reflex.utils import imports
  13. from reflex.utils.imports import ImportVar
  14. from reflex.vars import Var, VarData
  15. @pytest.fixture
  16. def test_state():
  17. class TestState(State):
  18. num: int
  19. def do_something(self):
  20. pass
  21. def do_something_arg(self, arg):
  22. pass
  23. return TestState
  24. @pytest.fixture
  25. def component1() -> Type[Component]:
  26. """A test component.
  27. Returns:
  28. A test component.
  29. """
  30. class TestComponent1(Component):
  31. # A test string prop.
  32. text: Var[str]
  33. # A test number prop.
  34. number: Var[int]
  35. def _get_imports(self) -> imports.ImportDict:
  36. return {"react": [ImportVar(tag="Component")]}
  37. def _get_custom_code(self) -> str:
  38. return "console.log('component1')"
  39. return TestComponent1
  40. @pytest.fixture
  41. def component2() -> Type[Component]:
  42. """A test component.
  43. Returns:
  44. A test component.
  45. """
  46. class TestComponent2(Component):
  47. # A test list prop.
  48. arr: Var[List[str]]
  49. def get_event_triggers(self) -> Dict[str, Any]:
  50. """Test controlled triggers.
  51. Returns:
  52. Test controlled triggers.
  53. """
  54. return {
  55. **super().get_event_triggers(),
  56. "on_open": lambda e0: [e0],
  57. "on_close": lambda e0: [e0],
  58. }
  59. def _get_imports(self) -> imports.ImportDict:
  60. return {"react-redux": [ImportVar(tag="connect")]}
  61. def _get_custom_code(self) -> str:
  62. return "console.log('component2')"
  63. return TestComponent2
  64. @pytest.fixture
  65. def component3() -> Type[Component]:
  66. """A test component with hook defined.
  67. Returns:
  68. A test component.
  69. """
  70. class TestComponent3(Component):
  71. def _get_hooks(self) -> str:
  72. return "const a = () => true"
  73. return TestComponent3
  74. @pytest.fixture
  75. def component4() -> Type[Component]:
  76. """A test component with hook defined.
  77. Returns:
  78. A test component.
  79. """
  80. class TestComponent4(Component):
  81. def _get_hooks(self) -> str:
  82. return "const b = () => false"
  83. return TestComponent4
  84. @pytest.fixture
  85. def component5() -> Type[Component]:
  86. """A test component.
  87. Returns:
  88. A test component.
  89. """
  90. class TestComponent5(Component):
  91. tag = "RandomComponent"
  92. _invalid_children: List[str] = ["Text"]
  93. _valid_children: List[str] = ["Text"]
  94. return TestComponent5
  95. @pytest.fixture
  96. def component6() -> Type[Component]:
  97. """A test component.
  98. Returns:
  99. A test component.
  100. """
  101. class TestComponent6(Component):
  102. tag = "RandomComponent"
  103. _invalid_children: List[str] = ["Text"]
  104. return TestComponent6
  105. @pytest.fixture
  106. def component7() -> Type[Component]:
  107. """A test component.
  108. Returns:
  109. A test component.
  110. """
  111. class TestComponent7(Component):
  112. tag = "RandomComponent"
  113. _valid_children: List[str] = ["Text"]
  114. return TestComponent7
  115. @pytest.fixture
  116. def on_click1() -> EventHandler:
  117. """A sample on click function.
  118. Returns:
  119. A sample on click function.
  120. """
  121. def on_click1():
  122. pass
  123. return EventHandler(fn=on_click1)
  124. @pytest.fixture
  125. def on_click2() -> EventHandler:
  126. """A sample on click function.
  127. Returns:
  128. A sample on click function.
  129. """
  130. def on_click2():
  131. pass
  132. return EventHandler(fn=on_click2)
  133. @pytest.fixture
  134. def my_component():
  135. """A test component function.
  136. Returns:
  137. A test component function.
  138. """
  139. def my_component(prop1: Var[str], prop2: Var[int]):
  140. return Box.create(prop1, prop2)
  141. return my_component
  142. def test_set_style_attrs(component1):
  143. """Test that style attributes are set in the dict.
  144. Args:
  145. component1: A test component.
  146. """
  147. component = component1(color="white", text_align="center")
  148. assert component.style["color"] == "white"
  149. assert component.style["textAlign"] == "center"
  150. def test_custom_attrs(component1):
  151. """Test that custom attributes are set in the dict.
  152. Args:
  153. component1: A test component.
  154. """
  155. component = component1(custom_attrs={"attr1": "1", "attr2": "attr2"})
  156. assert component.custom_attrs == {"attr1": "1", "attr2": "attr2"}
  157. def test_create_component(component1):
  158. """Test that the component is created correctly.
  159. Args:
  160. component1: A test component.
  161. """
  162. children = [component1() for _ in range(3)]
  163. attrs = {"color": "white", "text_align": "center"}
  164. c = component1.create(*children, **attrs)
  165. assert isinstance(c, component1)
  166. assert c.children == children
  167. assert c.style == {"color": "white", "textAlign": "center"}
  168. def test_add_style(component1, component2):
  169. """Test adding a style to a component.
  170. Args:
  171. component1: A test component.
  172. component2: A test component.
  173. """
  174. style = {
  175. component1: Style({"color": "white"}),
  176. component2: Style({"color": "black"}),
  177. }
  178. c1 = component1().add_style(style) # type: ignore
  179. c2 = component2().add_style(style) # type: ignore
  180. assert c1.style["color"] == "white"
  181. assert c2.style["color"] == "black"
  182. def test_get_imports(component1, component2):
  183. """Test getting the imports of a component.
  184. Args:
  185. component1: A test component.
  186. component2: A test component.
  187. """
  188. c1 = component1.create()
  189. c2 = component2.create(c1)
  190. assert c1.get_imports() == {"react": [ImportVar(tag="Component")]}
  191. assert c2.get_imports() == {
  192. "react-redux": [ImportVar(tag="connect")],
  193. "react": [ImportVar(tag="Component")],
  194. }
  195. def test_get_custom_code(component1, component2):
  196. """Test getting the custom code of a component.
  197. Args:
  198. component1: A test component.
  199. component2: A test component.
  200. """
  201. # Check that the code gets compiled correctly.
  202. c1 = component1.create()
  203. c2 = component2.create()
  204. assert c1.get_custom_code() == {"console.log('component1')"}
  205. assert c2.get_custom_code() == {"console.log('component2')"}
  206. # Check that nesting components compiles both codes.
  207. c1 = component1.create(c2)
  208. assert c1.get_custom_code() == {
  209. "console.log('component1')",
  210. "console.log('component2')",
  211. }
  212. # Check that code is not duplicated.
  213. c1 = component1.create(c2, c2, c1, c1)
  214. assert c1.get_custom_code() == {
  215. "console.log('component1')",
  216. "console.log('component2')",
  217. }
  218. def test_get_props(component1, component2):
  219. """Test that the props are set correctly.
  220. Args:
  221. component1: A test component.
  222. component2: A test component.
  223. """
  224. assert component1.get_props() == {"text", "number"}
  225. assert component2.get_props() == {"arr"}
  226. @pytest.mark.parametrize(
  227. "text,number",
  228. [
  229. ("", 0),
  230. ("test", 1),
  231. ("hi", -13),
  232. ],
  233. )
  234. def test_valid_props(component1, text: str, number: int):
  235. """Test that we can construct a component with valid props.
  236. Args:
  237. component1: A test component.
  238. text: A test string.
  239. number: A test number.
  240. """
  241. c = component1.create(text=text, number=number)
  242. assert c.text._decode() == text
  243. assert c.number._decode() == number
  244. @pytest.mark.parametrize(
  245. "text,number", [("", "bad_string"), (13, 1), (None, 1), ("test", [1, 2, 3])]
  246. )
  247. def test_invalid_prop_type(component1, text: str, number: int):
  248. """Test that an invalid prop type raises an error.
  249. Args:
  250. component1: A test component.
  251. text: A test string.
  252. number: A test number.
  253. """
  254. # Check that
  255. with pytest.raises(TypeError):
  256. component1.create(text=text, number=number)
  257. def test_var_props(component1, test_state):
  258. """Test that we can set a Var prop.
  259. Args:
  260. component1: A test component.
  261. test_state: A test state.
  262. """
  263. c1 = component1.create(text="hello", number=test_state.num)
  264. assert c1.number.equals(test_state.num)
  265. def test_get_event_triggers(component1, component2):
  266. """Test that we can get the triggers of a component.
  267. Args:
  268. component1: A test component.
  269. component2: A test component.
  270. """
  271. default_triggers = {
  272. EventTriggers.ON_FOCUS,
  273. EventTriggers.ON_BLUR,
  274. EventTriggers.ON_CLICK,
  275. EventTriggers.ON_CONTEXT_MENU,
  276. EventTriggers.ON_DOUBLE_CLICK,
  277. EventTriggers.ON_MOUSE_DOWN,
  278. EventTriggers.ON_MOUSE_ENTER,
  279. EventTriggers.ON_MOUSE_LEAVE,
  280. EventTriggers.ON_MOUSE_MOVE,
  281. EventTriggers.ON_MOUSE_OUT,
  282. EventTriggers.ON_MOUSE_OVER,
  283. EventTriggers.ON_MOUSE_UP,
  284. EventTriggers.ON_SCROLL,
  285. EventTriggers.ON_MOUNT,
  286. EventTriggers.ON_UNMOUNT,
  287. }
  288. assert set(component1().get_event_triggers().keys()) == default_triggers
  289. assert (
  290. component2().get_event_triggers().keys()
  291. == {"on_open", "on_close"} | default_triggers
  292. )
  293. class C1State(State):
  294. """State for testing C1 component."""
  295. def mock_handler(self, _e, _bravo, _charlie):
  296. """Mock handler."""
  297. pass
  298. def test_component_event_trigger_arbitrary_args():
  299. """Test that we can define arbitrary types for the args of an event trigger."""
  300. class Obj(Base):
  301. custom: int = 0
  302. def on_foo_spec(_e, alpha: str, bravo: Dict[str, Any], charlie: Obj):
  303. return [_e.target.value, bravo["nested"], charlie.custom + 42]
  304. class C1(Component):
  305. library = "/local"
  306. tag = "C1"
  307. def get_event_triggers(self) -> Dict[str, Any]:
  308. return {
  309. **super().get_event_triggers(),
  310. "on_foo": on_foo_spec,
  311. }
  312. comp = C1.create(on_foo=C1State.mock_handler)
  313. assert comp.render()["props"][0] == (
  314. "onFoo={(__e,_alpha,_bravo,_charlie) => addEvents("
  315. '[Event("c1_state.mock_handler", {_e:__e.target.value,_bravo:_bravo["nested"],_charlie:(_charlie.custom + 42)})], '
  316. "(__e,_alpha,_bravo,_charlie), {})}"
  317. )
  318. def test_create_custom_component(my_component):
  319. """Test that we can create a custom component.
  320. Args:
  321. my_component: A test custom component.
  322. """
  323. component = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  324. assert component.tag == "MyComponent"
  325. assert component.get_props() == set()
  326. assert component.get_custom_components() == {component}
  327. def test_custom_component_hash(my_component):
  328. """Test that the hash of a custom component is correct.
  329. Args:
  330. my_component: A test custom component.
  331. """
  332. component1 = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  333. component2 = CustomComponent(component_fn=my_component, prop1="test", prop2=2)
  334. assert {component1, component2} == {component1}
  335. def test_custom_component_wrapper():
  336. """Test that the wrapper of a custom component is correct."""
  337. @custom_component
  338. def my_component(width: Var[int], color: Var[str]):
  339. return rx.box(
  340. width=width,
  341. color=color,
  342. )
  343. ccomponent = my_component(
  344. rx.text("child"), width=Var.create(1), color=Var.create("red")
  345. )
  346. assert isinstance(ccomponent, CustomComponent)
  347. assert len(ccomponent.children) == 1
  348. assert isinstance(ccomponent.children[0], rx.Text)
  349. component = ccomponent.get_component(ccomponent)
  350. assert isinstance(component, Box)
  351. def test_invalid_event_handler_args(component2, test_state):
  352. """Test that an invalid event handler raises an error.
  353. Args:
  354. component2: A test component.
  355. test_state: A test state.
  356. """
  357. # Uncontrolled event handlers should not take args.
  358. # This is okay.
  359. component2.create(on_click=test_state.do_something)
  360. # This is not okay.
  361. with pytest.raises(ValueError):
  362. component2.create(on_click=test_state.do_something_arg)
  363. component2.create(on_open=test_state.do_something)
  364. component2.create(
  365. on_open=[test_state.do_something_arg, test_state.do_something]
  366. )
  367. # However lambdas are okay.
  368. component2.create(on_click=lambda: test_state.do_something_arg(1))
  369. component2.create(
  370. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something]
  371. )
  372. component2.create(
  373. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something()]
  374. )
  375. # Controlled event handlers should take args.
  376. # This is okay.
  377. component2.create(on_open=test_state.do_something_arg)
  378. def test_get_hooks_nested(component1, component2, component3):
  379. """Test that a component returns hooks from child components.
  380. Args:
  381. component1: test component.
  382. component2: another component.
  383. component3: component with hooks defined.
  384. """
  385. c = component1.create(
  386. component2.create(arr=[]),
  387. component3.create(),
  388. component3.create(),
  389. component3.create(),
  390. text="a",
  391. number=1,
  392. )
  393. assert c.get_hooks() == component3().get_hooks()
  394. def test_get_hooks_nested2(component3, component4):
  395. """Test that a component returns both when parent and child have hooks.
  396. Args:
  397. component3: component with hooks defined.
  398. component4: component with different hooks defined.
  399. """
  400. exp_hooks = component3().get_hooks().union(component4().get_hooks())
  401. assert component3.create(component4.create()).get_hooks() == exp_hooks
  402. assert component4.create(component3.create()).get_hooks() == exp_hooks
  403. assert (
  404. component4.create(
  405. component3.create(),
  406. component4.create(),
  407. component3.create(),
  408. ).get_hooks()
  409. == exp_hooks
  410. )
  411. @pytest.mark.parametrize("fixture", ["component5", "component6"])
  412. def test_unsupported_child_components(fixture, request):
  413. """Test that a value error is raised when an unsupported component (a child component found in the
  414. component's invalid children list) is provided as a child.
  415. Args:
  416. fixture: the test component as a fixture.
  417. request: Pytest request.
  418. """
  419. component = request.getfixturevalue(fixture)
  420. with pytest.raises(ValueError) as err:
  421. comp = component.create(rx.text("testing component"))
  422. comp.render()
  423. assert (
  424. err.value.args[0]
  425. == f"The component `{component.__name__}` cannot have `Text` as a child component"
  426. )
  427. @pytest.mark.parametrize("fixture", ["component5", "component7"])
  428. def test_component_with_only_valid_children(fixture, request):
  429. """Test that a value error is raised when an unsupported component (a child component not found in the
  430. component's valid children list) is provided as a child.
  431. Args:
  432. fixture: the test component as a fixture.
  433. request: Pytest request.
  434. """
  435. component = request.getfixturevalue(fixture)
  436. with pytest.raises(ValueError) as err:
  437. comp = component.create(rx.box("testing component"))
  438. comp.render()
  439. assert (
  440. err.value.args[0]
  441. == f"The component `{component.__name__}` only allows the components: `Text` as children. "
  442. f"Got `Box` instead."
  443. )
  444. @pytest.mark.parametrize(
  445. "component,rendered",
  446. [
  447. (rx.text("hi"), "<Text>\n {`hi`}\n</Text>"),
  448. (
  449. rx.box(rx.heading("test", size="md")),
  450. "<Box>\n <Heading size={`md`}>\n {`test`}\n</Heading>\n</Box>",
  451. ),
  452. ],
  453. )
  454. def test_format_component(component, rendered):
  455. """Test that a component is formatted correctly.
  456. Args:
  457. component: The component to format.
  458. rendered: The expected rendered component.
  459. """
  460. assert str(component) == rendered
  461. TEST_VAR = Var.create_safe("test")._replace(
  462. merge_var_data=VarData(
  463. hooks={"useTest"}, imports={"test": {ImportVar(tag="test")}}, state="Test"
  464. )
  465. )
  466. FORMATTED_TEST_VAR = Var.create(f"foo{TEST_VAR}bar")
  467. STYLE_VAR = TEST_VAR._replace(_var_name="style", _var_is_local=False)
  468. EVENT_CHAIN_VAR = TEST_VAR._replace(_var_type=EventChain)
  469. ARG_VAR = Var.create("arg")
  470. class EventState(rx.State):
  471. """State for testing event handlers with _get_vars."""
  472. v: int = 42
  473. def handler(self):
  474. """A handler that does nothing."""
  475. def handler2(self, arg):
  476. """A handler that takes an arg.
  477. Args:
  478. arg: An arg.
  479. """
  480. @pytest.mark.parametrize(
  481. ("component", "exp_vars"),
  482. (
  483. pytest.param(
  484. Bare.create(TEST_VAR),
  485. [TEST_VAR],
  486. id="direct-bare",
  487. ),
  488. pytest.param(
  489. Bare.create(f"foo{TEST_VAR}bar"),
  490. [FORMATTED_TEST_VAR],
  491. id="fstring-bare",
  492. ),
  493. pytest.param(
  494. rx.text(as_=TEST_VAR),
  495. [TEST_VAR],
  496. id="direct-prop",
  497. ),
  498. pytest.param(
  499. rx.text(as_=f"foo{TEST_VAR}bar"),
  500. [FORMATTED_TEST_VAR],
  501. id="fstring-prop",
  502. ),
  503. pytest.param(
  504. rx.fragment(id=TEST_VAR),
  505. [TEST_VAR],
  506. id="direct-id",
  507. ),
  508. pytest.param(
  509. rx.fragment(id=f"foo{TEST_VAR}bar"),
  510. [FORMATTED_TEST_VAR],
  511. id="fstring-id",
  512. ),
  513. pytest.param(
  514. rx.fragment(key=TEST_VAR),
  515. [TEST_VAR],
  516. id="direct-key",
  517. ),
  518. pytest.param(
  519. rx.fragment(key=f"foo{TEST_VAR}bar"),
  520. [FORMATTED_TEST_VAR],
  521. id="fstring-key",
  522. ),
  523. pytest.param(
  524. rx.fragment(class_name=TEST_VAR),
  525. [TEST_VAR],
  526. id="direct-class_name",
  527. ),
  528. pytest.param(
  529. rx.fragment(class_name=f"foo{TEST_VAR}bar"),
  530. [FORMATTED_TEST_VAR],
  531. id="fstring-class_name",
  532. ),
  533. pytest.param(
  534. rx.fragment(special_props={TEST_VAR}),
  535. [TEST_VAR],
  536. id="direct-special_props",
  537. ),
  538. pytest.param(
  539. rx.fragment(special_props={Var.create(f"foo{TEST_VAR}bar")}),
  540. [FORMATTED_TEST_VAR],
  541. id="fstring-special_props",
  542. ),
  543. pytest.param(
  544. # custom_attrs cannot accept a Var directly as a value
  545. rx.fragment(custom_attrs={"href": f"{TEST_VAR}"}),
  546. [TEST_VAR],
  547. id="fstring-custom_attrs-nofmt",
  548. ),
  549. pytest.param(
  550. rx.fragment(custom_attrs={"href": f"foo{TEST_VAR}bar"}),
  551. [FORMATTED_TEST_VAR],
  552. id="fstring-custom_attrs",
  553. ),
  554. pytest.param(
  555. rx.fragment(background_color=TEST_VAR),
  556. [STYLE_VAR],
  557. id="direct-background_color",
  558. ),
  559. pytest.param(
  560. rx.fragment(background_color=f"foo{TEST_VAR}bar"),
  561. [STYLE_VAR],
  562. id="fstring-background_color",
  563. ),
  564. pytest.param(
  565. rx.fragment(style={"background_color": TEST_VAR}), # type: ignore
  566. [STYLE_VAR],
  567. id="direct-style-background_color",
  568. ),
  569. pytest.param(
  570. rx.fragment(style={"background_color": f"foo{TEST_VAR}bar"}), # type: ignore
  571. [STYLE_VAR],
  572. id="fstring-style-background_color",
  573. ),
  574. pytest.param(
  575. rx.fragment(on_click=EVENT_CHAIN_VAR), # type: ignore
  576. [EVENT_CHAIN_VAR],
  577. id="direct-event-chain",
  578. ),
  579. pytest.param(
  580. rx.fragment(on_click=EventState.handler),
  581. [],
  582. id="direct-event-handler",
  583. ),
  584. pytest.param(
  585. rx.fragment(on_click=EventState.handler2(TEST_VAR)), # type: ignore
  586. [ARG_VAR, TEST_VAR],
  587. id="direct-event-handler-arg",
  588. ),
  589. pytest.param(
  590. rx.fragment(on_click=EventState.handler2(EventState.v)), # type: ignore
  591. [ARG_VAR, EventState.v],
  592. id="direct-event-handler-arg2",
  593. ),
  594. pytest.param(
  595. rx.fragment(on_click=lambda: EventState.handler2(TEST_VAR)), # type: ignore
  596. [ARG_VAR, TEST_VAR],
  597. id="direct-event-handler-lambda",
  598. ),
  599. ),
  600. )
  601. def test_get_vars(component, exp_vars):
  602. comp_vars = sorted(component._get_vars(), key=lambda v: v._var_name)
  603. assert len(comp_vars) == len(exp_vars)
  604. for comp_var, exp_var in zip(
  605. comp_vars,
  606. sorted(exp_vars, key=lambda v: v._var_name),
  607. ):
  608. assert comp_var.equals(exp_var)