test_component.py 15 KB

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