test_component.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. from typing import Dict, List, Type
  2. import pytest
  3. import reflex as rx
  4. from reflex.components.component import Component, CustomComponent, custom_component
  5. from reflex.components.layout.box import Box
  6. from reflex.event import EVENT_ARG, EVENT_TRIGGERS, EventHandler
  7. from reflex.state import State
  8. from reflex.style import Style
  9. from reflex.utils import imports
  10. from reflex.vars import ImportVar, Var
  11. @pytest.fixture
  12. def test_state():
  13. class TestState(State):
  14. num: int
  15. def do_something(self):
  16. pass
  17. def do_something_arg(self, arg):
  18. pass
  19. return TestState
  20. @pytest.fixture
  21. def component1() -> Type[Component]:
  22. """A test component.
  23. Returns:
  24. A test component.
  25. """
  26. class TestComponent1(Component):
  27. # A test string prop.
  28. text: Var[str]
  29. # A test number prop.
  30. number: Var[int]
  31. def _get_imports(self) -> imports.ImportDict:
  32. return {"react": {ImportVar(tag="Component")}}
  33. def _get_custom_code(self) -> str:
  34. return "console.log('component1')"
  35. return TestComponent1
  36. @pytest.fixture
  37. def component2() -> Type[Component]:
  38. """A test component.
  39. Returns:
  40. A test component.
  41. """
  42. class TestComponent2(Component):
  43. # A test list prop.
  44. arr: Var[List[str]]
  45. def get_controlled_triggers(self) -> Dict[str, Var]:
  46. """Test controlled triggers.
  47. Returns:
  48. Test controlled triggers.
  49. """
  50. return {
  51. "on_open": EVENT_ARG,
  52. "on_close": EVENT_ARG,
  53. }
  54. def _get_imports(self) -> imports.ImportDict:
  55. return {"react-redux": {ImportVar(tag="connect")}}
  56. def _get_custom_code(self) -> str:
  57. return "console.log('component2')"
  58. return TestComponent2
  59. @pytest.fixture
  60. def component3() -> Type[Component]:
  61. """A test component with hook defined.
  62. Returns:
  63. A test component.
  64. """
  65. class TestComponent3(Component):
  66. def _get_hooks(self) -> str:
  67. return "const a = () => true"
  68. return TestComponent3
  69. @pytest.fixture
  70. def component4() -> Type[Component]:
  71. """A test component with hook defined.
  72. Returns:
  73. A test component.
  74. """
  75. class TestComponent4(Component):
  76. def _get_hooks(self) -> str:
  77. return "const b = () => false"
  78. return TestComponent4
  79. @pytest.fixture
  80. def component5() -> Type[Component]:
  81. """A test component.
  82. Returns:
  83. A test component.
  84. """
  85. class TestComponent5(Component):
  86. tag = "RandomComponent"
  87. invalid_children: List[str] = ["Text"]
  88. valid_children: List[str] = ["Text"]
  89. return TestComponent5
  90. @pytest.fixture
  91. def component6() -> Type[Component]:
  92. """A test component.
  93. Returns:
  94. A test component.
  95. """
  96. class TestComponent6(Component):
  97. tag = "RandomComponent"
  98. invalid_children: List[str] = ["Text"]
  99. return TestComponent6
  100. @pytest.fixture
  101. def component7() -> Type[Component]:
  102. """A test component.
  103. Returns:
  104. A test component.
  105. """
  106. class TestComponent7(Component):
  107. tag = "RandomComponent"
  108. valid_children: List[str] = ["Text"]
  109. return TestComponent7
  110. @pytest.fixture
  111. def on_click1() -> EventHandler:
  112. """A sample on click function.
  113. Returns:
  114. A sample on click function.
  115. """
  116. def on_click1():
  117. pass
  118. return EventHandler(fn=on_click1)
  119. @pytest.fixture
  120. def on_click2() -> EventHandler:
  121. """A sample on click function.
  122. Returns:
  123. A sample on click function.
  124. """
  125. def on_click2():
  126. pass
  127. return EventHandler(fn=on_click2)
  128. @pytest.fixture
  129. def my_component():
  130. """A test component function.
  131. Returns:
  132. A test component function.
  133. """
  134. def my_component(prop1: Var[str], prop2: Var[int]):
  135. return Box.create(prop1, prop2)
  136. return my_component
  137. def test_set_style_attrs(component1):
  138. """Test that style attributes are set in the dict.
  139. Args:
  140. component1: A test component.
  141. """
  142. component = component1(color="white", text_align="center")
  143. assert component.style["color"] == "white"
  144. assert component.style["textAlign"] == "center"
  145. def test_custom_attrs(component1):
  146. """Test that custom attributes are set in the dict.
  147. Args:
  148. component1: A test component.
  149. """
  150. component = component1(custom_attrs={"attr1": "1", "attr2": "attr2"})
  151. assert component.custom_attrs == {"attr1": "1", "attr2": "attr2"}
  152. def test_create_component(component1):
  153. """Test that the component is created correctly.
  154. Args:
  155. component1: A test component.
  156. """
  157. children = [component1() for _ in range(3)]
  158. attrs = {"color": "white", "text_align": "center"}
  159. c = component1.create(*children, **attrs)
  160. assert isinstance(c, component1)
  161. assert c.children == children
  162. assert c.style == {"color": "white", "textAlign": "center"}
  163. def test_add_style(component1, component2):
  164. """Test adding a style to a component.
  165. Args:
  166. component1: A test component.
  167. component2: A test component.
  168. """
  169. style = {
  170. component1: Style({"color": "white"}),
  171. component2: Style({"color": "black"}),
  172. }
  173. c1 = component1().add_style(style) # type: ignore
  174. c2 = component2().add_style(style) # type: ignore
  175. assert c1.style["color"] == "white"
  176. assert c2.style["color"] == "black"
  177. def test_get_imports(component1, component2):
  178. """Test getting the imports of a component.
  179. Args:
  180. component1: A test component.
  181. component2: A test component.
  182. """
  183. c1 = component1.create()
  184. c2 = component2.create(c1)
  185. assert c1.get_imports() == {"react": {ImportVar(tag="Component")}}
  186. assert c2.get_imports() == {
  187. "react-redux": {ImportVar(tag="connect")},
  188. "react": {ImportVar(tag="Component")},
  189. }
  190. def test_get_custom_code(component1, component2):
  191. """Test getting the custom code of a component.
  192. Args:
  193. component1: A test component.
  194. component2: A test component.
  195. """
  196. # Check that the code gets compiled correctly.
  197. c1 = component1.create()
  198. c2 = component2.create()
  199. assert c1.get_custom_code() == {"console.log('component1')"}
  200. assert c2.get_custom_code() == {"console.log('component2')"}
  201. # Check that nesting components compiles both codes.
  202. c1 = component1.create(c2)
  203. assert c1.get_custom_code() == {
  204. "console.log('component1')",
  205. "console.log('component2')",
  206. }
  207. # Check that code is not duplicated.
  208. c1 = component1.create(c2, c2, c1, c1)
  209. assert c1.get_custom_code() == {
  210. "console.log('component1')",
  211. "console.log('component2')",
  212. }
  213. def test_get_props(component1, component2):
  214. """Test that the props are set correctly.
  215. Args:
  216. component1: A test component.
  217. component2: A test component.
  218. """
  219. assert component1.get_props() == {"text", "number"}
  220. assert component2.get_props() == {"arr"}
  221. @pytest.mark.parametrize(
  222. "text,number",
  223. [
  224. ("", 0),
  225. ("test", 1),
  226. ("hi", -13),
  227. ],
  228. )
  229. def test_valid_props(component1, text: str, number: int):
  230. """Test that we can construct a component with valid props.
  231. Args:
  232. component1: A test component.
  233. text: A test string.
  234. number: A test number.
  235. """
  236. c = component1.create(text=text, number=number)
  237. assert c.text == text
  238. assert c.number == number
  239. @pytest.mark.parametrize(
  240. "text,number", [("", "bad_string"), (13, 1), (None, 1), ("test", [1, 2, 3])]
  241. )
  242. def test_invalid_prop_type(component1, text: str, number: int):
  243. """Test that an invalid prop type raises an error.
  244. Args:
  245. component1: A test component.
  246. text: A test string.
  247. number: A test number.
  248. """
  249. # Check that
  250. with pytest.raises(TypeError):
  251. component1.create(text=text, number=number)
  252. def test_var_props(component1, test_state):
  253. """Test that we can set a Var prop.
  254. Args:
  255. component1: A test component.
  256. test_state: A test state.
  257. """
  258. c1 = component1.create(text="hello", number=test_state.num)
  259. assert c1.number == test_state.num
  260. def test_get_controlled_triggers(component1, component2):
  261. """Test that we can get the controlled triggers of a component.
  262. Args:
  263. component1: A test component.
  264. component2: A test component.
  265. """
  266. assert component1().get_controlled_triggers() == dict()
  267. assert set(component2().get_controlled_triggers()) == {"on_open", "on_close"}
  268. def test_get_triggers(component1, component2):
  269. """Test that we can get the triggers of a component.
  270. Args:
  271. component1: A test component.
  272. component2: A test component.
  273. """
  274. assert component1().get_triggers() == EVENT_TRIGGERS
  275. assert component2().get_triggers() == {"on_open", "on_close"} | EVENT_TRIGGERS
  276. def test_create_custom_component(my_component):
  277. """Test that we can create a custom component.
  278. Args:
  279. my_component: A test custom component.
  280. """
  281. component = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  282. assert component.tag == "MyComponent"
  283. assert component.get_props() == set()
  284. assert component.get_custom_components() == {component}
  285. def test_custom_component_hash(my_component):
  286. """Test that the hash of a custom component is correct.
  287. Args:
  288. my_component: A test custom component.
  289. """
  290. component1 = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  291. component2 = CustomComponent(component_fn=my_component, prop1="test", prop2=2)
  292. assert {component1, component2} == {component1}
  293. def test_custom_component_wrapper():
  294. """Test that the wrapper of a custom component is correct."""
  295. @custom_component
  296. def my_component(width: Var[int], color: Var[str]):
  297. return rx.box(
  298. width=width,
  299. color=color,
  300. )
  301. ccomponent = my_component(
  302. rx.text("child"), width=Var.create(1), color=Var.create("red")
  303. )
  304. assert isinstance(ccomponent, CustomComponent)
  305. assert len(ccomponent.children) == 1
  306. assert isinstance(ccomponent.children[0], rx.Text)
  307. component = ccomponent.get_component()
  308. assert isinstance(component, Box)
  309. def test_invalid_event_handler_args(component2, test_state):
  310. """Test that an invalid event handler raises an error.
  311. Args:
  312. component2: A test component.
  313. test_state: A test state.
  314. """
  315. # Uncontrolled event handlers should not take args.
  316. # This is okay.
  317. component2.create(on_click=test_state.do_something)
  318. # This is not okay.
  319. with pytest.raises(ValueError):
  320. component2.create(on_click=test_state.do_something_arg)
  321. # However lambdas are okay.
  322. component2.create(on_click=lambda: test_state.do_something_arg(1))
  323. component2.create(
  324. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something]
  325. )
  326. component2.create(
  327. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something()]
  328. )
  329. # Controlled event handlers should take args.
  330. # This is okay.
  331. component2.create(on_open=test_state.do_something_arg)
  332. # do_something is allowed and will simply run while ignoring the arg
  333. component2.create(on_open=test_state.do_something)
  334. component2.create(on_open=[test_state.do_something_arg, test_state.do_something])
  335. def test_get_hooks_nested(component1, component2, component3):
  336. """Test that a component returns hooks from child components.
  337. Args:
  338. component1: test component.
  339. component2: another component.
  340. component3: component with hooks defined.
  341. """
  342. c = component1.create(
  343. component2.create(arr=[]),
  344. component3.create(),
  345. component3.create(),
  346. component3.create(),
  347. text="a",
  348. number=1,
  349. )
  350. assert c.get_hooks() == component3().get_hooks()
  351. def test_get_hooks_nested2(component3, component4):
  352. """Test that a component returns both when parent and child have hooks.
  353. Args:
  354. component3: component with hooks defined.
  355. component4: component with different hooks defined.
  356. """
  357. exp_hooks = component3().get_hooks().union(component4().get_hooks())
  358. assert component3.create(component4.create()).get_hooks() == exp_hooks
  359. assert component4.create(component3.create()).get_hooks() == exp_hooks
  360. assert (
  361. component4.create(
  362. component3.create(),
  363. component4.create(),
  364. component3.create(),
  365. ).get_hooks()
  366. == exp_hooks
  367. )
  368. @pytest.mark.parametrize("fixture", ["component5", "component6"])
  369. def test_unsupported_child_components(fixture, request):
  370. """Test that a value error is raised when an unsupported component (a child component found in the
  371. component's invalid children list) is provided as a child.
  372. Args:
  373. fixture: the test component as a fixture.
  374. request: Pytest request.
  375. """
  376. component = request.getfixturevalue(fixture)
  377. with pytest.raises(ValueError) as err:
  378. comp = component.create(rx.text("testing component"))
  379. comp.render()
  380. assert (
  381. err.value.args[0]
  382. == f"The component `{component.__name__}` cannot have `Text` as a child component"
  383. )
  384. @pytest.mark.parametrize("fixture", ["component5", "component7"])
  385. def test_component_with_only_valid_children(fixture, request):
  386. """Test that a value error is raised when an unsupported component (a child component not found in the
  387. component's valid children list) is provided as a child.
  388. Args:
  389. fixture: the test component as a fixture.
  390. request: Pytest request.
  391. """
  392. component = request.getfixturevalue(fixture)
  393. with pytest.raises(ValueError) as err:
  394. comp = component.create(rx.box("testing component"))
  395. comp.render()
  396. assert (
  397. err.value.args[0]
  398. == f"The component `{component.__name__}` only allows the components: `Text` as children. "
  399. f"Got `Box` instead."
  400. )