1
0

test_component.py 16 KB

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