test_component.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379
  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.compiler.compiler import compile_components
  6. from reflex.components.base.bare import Bare
  7. from reflex.components.chakra.layout.box import Box
  8. from reflex.components.component import (
  9. Component,
  10. CustomComponent,
  11. StatefulComponent,
  12. custom_component,
  13. )
  14. from reflex.constants import EventTriggers
  15. from reflex.event import EventChain, EventHandler
  16. from reflex.state import BaseState
  17. from reflex.style import Style
  18. from reflex.utils import imports
  19. from reflex.utils.imports import ImportVar
  20. from reflex.vars import Var, VarData
  21. @pytest.fixture
  22. def test_state():
  23. class TestState(BaseState):
  24. num: int
  25. def do_something(self):
  26. pass
  27. def do_something_arg(self, arg):
  28. pass
  29. return TestState
  30. @pytest.fixture
  31. def component1() -> Type[Component]:
  32. """A test component.
  33. Returns:
  34. A test component.
  35. """
  36. class TestComponent1(Component):
  37. # A test string prop.
  38. text: Var[str]
  39. # A test number prop.
  40. number: Var[int]
  41. def _get_imports(self) -> imports.ImportDict:
  42. return {"react": [ImportVar(tag="Component")]}
  43. def _get_custom_code(self) -> str:
  44. return "console.log('component1')"
  45. return TestComponent1
  46. @pytest.fixture
  47. def component2() -> Type[Component]:
  48. """A test component.
  49. Returns:
  50. A test component.
  51. """
  52. class TestComponent2(Component):
  53. # A test list prop.
  54. arr: Var[List[str]]
  55. def get_event_triggers(self) -> Dict[str, Any]:
  56. """Test controlled triggers.
  57. Returns:
  58. Test controlled triggers.
  59. """
  60. return {
  61. **super().get_event_triggers(),
  62. "on_open": lambda e0: [e0],
  63. "on_close": lambda e0: [e0],
  64. }
  65. def _get_imports(self) -> imports.ImportDict:
  66. return {"react-redux": [ImportVar(tag="connect")]}
  67. def _get_custom_code(self) -> str:
  68. return "console.log('component2')"
  69. return TestComponent2
  70. @pytest.fixture
  71. def component3() -> Type[Component]:
  72. """A test component with hook defined.
  73. Returns:
  74. A test component.
  75. """
  76. class TestComponent3(Component):
  77. def _get_hooks(self) -> str:
  78. return "const a = () => true"
  79. return TestComponent3
  80. @pytest.fixture
  81. def component4() -> Type[Component]:
  82. """A test component with hook defined.
  83. Returns:
  84. A test component.
  85. """
  86. class TestComponent4(Component):
  87. def _get_hooks(self) -> str:
  88. return "const b = () => false"
  89. return TestComponent4
  90. @pytest.fixture
  91. def component5() -> Type[Component]:
  92. """A test component.
  93. Returns:
  94. A test component.
  95. """
  96. class TestComponent5(Component):
  97. tag = "RandomComponent"
  98. _invalid_children: List[str] = ["Text"]
  99. _valid_children: List[str] = ["Text"]
  100. _valid_parents: List[str] = ["Text"]
  101. return TestComponent5
  102. @pytest.fixture
  103. def component6() -> Type[Component]:
  104. """A test component.
  105. Returns:
  106. A test component.
  107. """
  108. class TestComponent6(Component):
  109. tag = "RandomComponent"
  110. _invalid_children: List[str] = ["Text"]
  111. return TestComponent6
  112. @pytest.fixture
  113. def component7() -> Type[Component]:
  114. """A test component.
  115. Returns:
  116. A test component.
  117. """
  118. class TestComponent7(Component):
  119. tag = "RandomComponent"
  120. _valid_children: List[str] = ["Text"]
  121. return TestComponent7
  122. @pytest.fixture
  123. def on_click1() -> EventHandler:
  124. """A sample on click function.
  125. Returns:
  126. A sample on click function.
  127. """
  128. def on_click1():
  129. pass
  130. return EventHandler(fn=on_click1)
  131. @pytest.fixture
  132. def on_click2() -> EventHandler:
  133. """A sample on click function.
  134. Returns:
  135. A sample on click function.
  136. """
  137. def on_click2():
  138. pass
  139. return EventHandler(fn=on_click2)
  140. @pytest.fixture
  141. def my_component():
  142. """A test component function.
  143. Returns:
  144. A test component function.
  145. """
  146. def my_component(prop1: Var[str], prop2: Var[int]):
  147. return Box.create(prop1, prop2)
  148. return my_component
  149. def test_set_style_attrs(component1):
  150. """Test that style attributes are set in the dict.
  151. Args:
  152. component1: A test component.
  153. """
  154. component = component1(color="white", text_align="center")
  155. assert component.style["color"] == "white"
  156. assert component.style["textAlign"] == "center"
  157. def test_custom_attrs(component1):
  158. """Test that custom attributes are set in the dict.
  159. Args:
  160. component1: A test component.
  161. """
  162. component = component1(custom_attrs={"attr1": "1", "attr2": "attr2"})
  163. assert component.custom_attrs == {"attr1": "1", "attr2": "attr2"}
  164. def test_create_component(component1):
  165. """Test that the component is created correctly.
  166. Args:
  167. component1: A test component.
  168. """
  169. children = [component1() for _ in range(3)]
  170. attrs = {"color": "white", "text_align": "center"}
  171. c = component1.create(*children, **attrs)
  172. assert isinstance(c, component1)
  173. assert c.children == children
  174. assert c.style == {"color": "white", "textAlign": "center"}
  175. def test_add_style(component1, component2):
  176. """Test adding a style to a component.
  177. Args:
  178. component1: A test component.
  179. component2: A test component.
  180. """
  181. style = {
  182. component1: Style({"color": "white"}),
  183. component2: Style({"color": "black"}),
  184. }
  185. c1 = component1().add_style(style) # type: ignore
  186. c2 = component2().add_style(style) # type: ignore
  187. assert c1.style["color"] == "white"
  188. assert c2.style["color"] == "black"
  189. def test_add_style_create(component1, component2):
  190. """Test that adding style works with the create method.
  191. Args:
  192. component1: A test component.
  193. component2: A test component.
  194. """
  195. style = {
  196. component1.create: Style({"color": "white"}),
  197. component2.create: Style({"color": "black"}),
  198. }
  199. c1 = component1().add_style(style) # type: ignore
  200. c2 = component2().add_style(style) # type: ignore
  201. assert c1.style["color"] == "white"
  202. assert c2.style["color"] == "black"
  203. def test_get_imports(component1, component2):
  204. """Test getting the imports of a component.
  205. Args:
  206. component1: A test component.
  207. component2: A test component.
  208. """
  209. c1 = component1.create()
  210. c2 = component2.create(c1)
  211. assert c1.get_imports() == {"react": [ImportVar(tag="Component")]}
  212. assert c2.get_imports() == {
  213. "react-redux": [ImportVar(tag="connect")],
  214. "react": [ImportVar(tag="Component")],
  215. }
  216. def test_get_custom_code(component1, component2):
  217. """Test getting the custom code of a component.
  218. Args:
  219. component1: A test component.
  220. component2: A test component.
  221. """
  222. # Check that the code gets compiled correctly.
  223. c1 = component1.create()
  224. c2 = component2.create()
  225. assert c1.get_custom_code() == {"console.log('component1')"}
  226. assert c2.get_custom_code() == {"console.log('component2')"}
  227. # Check that nesting components compiles both codes.
  228. c1 = component1.create(c2)
  229. assert c1.get_custom_code() == {
  230. "console.log('component1')",
  231. "console.log('component2')",
  232. }
  233. # Check that code is not duplicated.
  234. c1 = component1.create(c2, c2, c1, c1)
  235. assert c1.get_custom_code() == {
  236. "console.log('component1')",
  237. "console.log('component2')",
  238. }
  239. def test_get_props(component1, component2):
  240. """Test that the props are set correctly.
  241. Args:
  242. component1: A test component.
  243. component2: A test component.
  244. """
  245. assert component1.get_props() == {"text", "number"}
  246. assert component2.get_props() == {"arr"}
  247. @pytest.mark.parametrize(
  248. "text,number",
  249. [
  250. ("", 0),
  251. ("test", 1),
  252. ("hi", -13),
  253. ],
  254. )
  255. def test_valid_props(component1, text: str, number: int):
  256. """Test that we can construct a component with valid props.
  257. Args:
  258. component1: A test component.
  259. text: A test string.
  260. number: A test number.
  261. """
  262. c = component1.create(text=text, number=number)
  263. assert c.text._decode() == text
  264. assert c.number._decode() == number
  265. @pytest.mark.parametrize(
  266. "text,number", [("", "bad_string"), (13, 1), ("test", [1, 2, 3])]
  267. )
  268. def test_invalid_prop_type(component1, text: str, number: int):
  269. """Test that an invalid prop type raises an error.
  270. Args:
  271. component1: A test component.
  272. text: A test string.
  273. number: A test number.
  274. """
  275. # Check that
  276. with pytest.raises(TypeError):
  277. component1.create(text=text, number=number)
  278. def test_var_props(component1, test_state):
  279. """Test that we can set a Var prop.
  280. Args:
  281. component1: A test component.
  282. test_state: A test state.
  283. """
  284. c1 = component1.create(text="hello", number=test_state.num)
  285. assert c1.number.equals(test_state.num)
  286. def test_get_event_triggers(component1, component2):
  287. """Test that we can get the triggers of a component.
  288. Args:
  289. component1: A test component.
  290. component2: A test component.
  291. """
  292. default_triggers = {
  293. EventTriggers.ON_FOCUS,
  294. EventTriggers.ON_BLUR,
  295. EventTriggers.ON_CLICK,
  296. EventTriggers.ON_CONTEXT_MENU,
  297. EventTriggers.ON_DOUBLE_CLICK,
  298. EventTriggers.ON_MOUSE_DOWN,
  299. EventTriggers.ON_MOUSE_ENTER,
  300. EventTriggers.ON_MOUSE_LEAVE,
  301. EventTriggers.ON_MOUSE_MOVE,
  302. EventTriggers.ON_MOUSE_OUT,
  303. EventTriggers.ON_MOUSE_OVER,
  304. EventTriggers.ON_MOUSE_UP,
  305. EventTriggers.ON_SCROLL,
  306. EventTriggers.ON_MOUNT,
  307. EventTriggers.ON_UNMOUNT,
  308. }
  309. assert set(component1().get_event_triggers().keys()) == default_triggers
  310. assert (
  311. component2().get_event_triggers().keys()
  312. == {"on_open", "on_close"} | default_triggers
  313. )
  314. @pytest.fixture
  315. def test_component() -> Type[Component]:
  316. """A test component.
  317. Returns:
  318. A test component.
  319. """
  320. class TestComponent(Component):
  321. pass
  322. return TestComponent
  323. # Write a test case to check if the create method filters out None props
  324. def test_create_filters_none_props(test_component):
  325. child1 = test_component()
  326. child2 = test_component()
  327. props = {
  328. "prop1": "value1",
  329. "prop2": None,
  330. "prop3": "value3",
  331. "prop4": None,
  332. "style": {"color": "white", "text-align": "center"}, # Adding a style prop
  333. }
  334. component = test_component.create(child1, child2, **props)
  335. # Assert that None props are not present in the component's props
  336. assert "prop2" not in component.get_props()
  337. assert "prop4" not in component.get_props()
  338. # Assert that the style prop is present in the component's props
  339. assert component.style["color"] == "white"
  340. assert component.style["text-align"] == "center"
  341. class C1State(BaseState):
  342. """State for testing C1 component."""
  343. def mock_handler(self, _e, _bravo, _charlie):
  344. """Mock handler."""
  345. pass
  346. def test_component_event_trigger_arbitrary_args():
  347. """Test that we can define arbitrary types for the args of an event trigger."""
  348. class Obj(Base):
  349. custom: int = 0
  350. def on_foo_spec(_e, alpha: str, bravo: Dict[str, Any], charlie: Obj):
  351. return [_e.target.value, bravo["nested"], charlie.custom + 42]
  352. class C1(Component):
  353. library = "/local"
  354. tag = "C1"
  355. def get_event_triggers(self) -> Dict[str, Any]:
  356. return {
  357. **super().get_event_triggers(),
  358. "on_foo": on_foo_spec,
  359. }
  360. comp = C1.create(on_foo=C1State.mock_handler)
  361. assert comp.render()["props"][0] == (
  362. "onFoo={(__e,_alpha,_bravo,_charlie) => addEvents("
  363. '[Event("c1_state.mock_handler", {_e:__e.target.value,_bravo:_bravo["nested"],_charlie:((_charlie.custom) + (42))})], '
  364. "(__e,_alpha,_bravo,_charlie), {})}"
  365. )
  366. def test_create_custom_component(my_component):
  367. """Test that we can create a custom component.
  368. Args:
  369. my_component: A test custom component.
  370. """
  371. component = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  372. assert component.tag == "MyComponent"
  373. assert component.get_props() == set()
  374. assert component.get_custom_components() == {component}
  375. def test_custom_component_hash(my_component):
  376. """Test that the hash of a custom component is correct.
  377. Args:
  378. my_component: A test custom component.
  379. """
  380. component1 = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  381. component2 = CustomComponent(component_fn=my_component, prop1="test", prop2=2)
  382. assert {component1, component2} == {component1}
  383. def test_custom_component_wrapper():
  384. """Test that the wrapper of a custom component is correct."""
  385. @custom_component
  386. def my_component(width: Var[int], color: Var[str]):
  387. return rx.box(
  388. width=width,
  389. color=color,
  390. )
  391. from reflex.components.radix.themes.typography.text import Text
  392. ccomponent = my_component(
  393. rx.text("child"), width=Var.create(1), color=Var.create("red")
  394. )
  395. assert isinstance(ccomponent, CustomComponent)
  396. assert len(ccomponent.children) == 1
  397. assert isinstance(ccomponent.children[0], Text)
  398. component = ccomponent.get_component(ccomponent)
  399. assert isinstance(component, Box)
  400. def test_invalid_event_handler_args(component2, test_state):
  401. """Test that an invalid event handler raises an error.
  402. Args:
  403. component2: A test component.
  404. test_state: A test state.
  405. """
  406. # Uncontrolled event handlers should not take args.
  407. # This is okay.
  408. component2.create(on_click=test_state.do_something)
  409. # This is not okay.
  410. with pytest.raises(ValueError):
  411. component2.create(on_click=test_state.do_something_arg)
  412. component2.create(on_open=test_state.do_something)
  413. component2.create(
  414. on_open=[test_state.do_something_arg, test_state.do_something]
  415. )
  416. # However lambdas are okay.
  417. component2.create(on_click=lambda: test_state.do_something_arg(1))
  418. component2.create(
  419. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something]
  420. )
  421. component2.create(
  422. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something()]
  423. )
  424. # Controlled event handlers should take args.
  425. # This is okay.
  426. component2.create(on_open=test_state.do_something_arg)
  427. def test_get_hooks_nested(component1, component2, component3):
  428. """Test that a component returns hooks from child components.
  429. Args:
  430. component1: test component.
  431. component2: another component.
  432. component3: component with hooks defined.
  433. """
  434. c = component1.create(
  435. component2.create(arr=[]),
  436. component3.create(),
  437. component3.create(),
  438. component3.create(),
  439. text="a",
  440. number=1,
  441. )
  442. assert c.get_hooks() == component3().get_hooks()
  443. def test_get_hooks_nested2(component3, component4):
  444. """Test that a component returns both when parent and child have hooks.
  445. Args:
  446. component3: component with hooks defined.
  447. component4: component with different hooks defined.
  448. """
  449. exp_hooks = {**component3().get_hooks(), **component4().get_hooks()}
  450. assert component3.create(component4.create()).get_hooks() == exp_hooks
  451. assert component4.create(component3.create()).get_hooks() == exp_hooks
  452. assert (
  453. component4.create(
  454. component3.create(),
  455. component4.create(),
  456. component3.create(),
  457. ).get_hooks()
  458. == exp_hooks
  459. )
  460. @pytest.mark.parametrize("fixture", ["component5", "component6"])
  461. def test_unsupported_child_components(fixture, request):
  462. """Test that a value error is raised when an unsupported component (a child component found in the
  463. component's invalid children list) is provided as a child.
  464. Args:
  465. fixture: the test component as a fixture.
  466. request: Pytest request.
  467. """
  468. component = request.getfixturevalue(fixture)
  469. with pytest.raises(ValueError) as err:
  470. comp = component.create(rx.text("testing component"))
  471. comp.render()
  472. assert (
  473. err.value.args[0]
  474. == f"The component `{component.__name__}` cannot have `Text` as a child component"
  475. )
  476. def test_unsupported_parent_components(component5):
  477. """Test that a value error is raised when an component is not in _valid_parents of one of its children.
  478. Args:
  479. component5: component with valid parent of "Text" only
  480. """
  481. with pytest.raises(ValueError) as err:
  482. rx.box(component5.create())
  483. assert (
  484. err.value.args[0]
  485. == f"The component `{component5.__name__}` can only be a child of the components: `{component5._valid_parents[0]}`. Got `Box` instead."
  486. )
  487. @pytest.mark.parametrize("fixture", ["component5", "component7"])
  488. def test_component_with_only_valid_children(fixture, request):
  489. """Test that a value error is raised when an unsupported component (a child component not found in the
  490. component's valid children list) is provided as a child.
  491. Args:
  492. fixture: the test component as a fixture.
  493. request: Pytest request.
  494. """
  495. component = request.getfixturevalue(fixture)
  496. with pytest.raises(ValueError) as err:
  497. comp = component.create(rx.box("testing component"))
  498. comp.render()
  499. assert (
  500. err.value.args[0]
  501. == f"The component `{component.__name__}` only allows the components: `Text` as children. "
  502. f"Got `Box` instead."
  503. )
  504. @pytest.mark.parametrize(
  505. "component,rendered",
  506. [
  507. (rx.text("hi"), "<RadixThemesText as={`p`}>\n {`hi`}\n</RadixThemesText>"),
  508. (
  509. rx.box(rx.chakra.heading("test", size="md")),
  510. "<RadixThemesBox>\n <Heading size={`md`}>\n {`test`}\n</Heading>\n</RadixThemesBox>",
  511. ),
  512. ],
  513. )
  514. def test_format_component(component, rendered):
  515. """Test that a component is formatted correctly.
  516. Args:
  517. component: The component to format.
  518. rendered: The expected rendered component.
  519. """
  520. assert str(component) == rendered
  521. def test_stateful_component(test_state):
  522. """Test that a stateful component is created correctly.
  523. Args:
  524. test_state: A test state.
  525. """
  526. text_component = rx.text(test_state.num)
  527. stateful_component = StatefulComponent.compile_from(text_component)
  528. assert isinstance(stateful_component, StatefulComponent)
  529. assert stateful_component.tag is not None
  530. assert stateful_component.tag.startswith("Text_")
  531. assert stateful_component.references == 1
  532. sc2 = StatefulComponent.compile_from(rx.text(test_state.num))
  533. assert isinstance(sc2, StatefulComponent)
  534. assert stateful_component.references == 2
  535. assert sc2.references == 2
  536. def test_stateful_component_memoize_event_trigger(test_state):
  537. """Test that a stateful component is created correctly with events.
  538. Args:
  539. test_state: A test state.
  540. """
  541. button_component = rx.button("Click me", on_click=test_state.do_something)
  542. stateful_component = StatefulComponent.compile_from(button_component)
  543. assert isinstance(stateful_component, StatefulComponent)
  544. # No event trigger? No StatefulComponent
  545. assert not isinstance(
  546. StatefulComponent.compile_from(rx.button("Click me")), StatefulComponent
  547. )
  548. def test_stateful_banner():
  549. """Test that a stateful component is created correctly with events."""
  550. connection_modal_component = rx.connection_modal()
  551. stateful_component = StatefulComponent.compile_from(connection_modal_component)
  552. assert isinstance(stateful_component, StatefulComponent)
  553. TEST_VAR = Var.create_safe("test")._replace(
  554. merge_var_data=VarData(
  555. hooks={"useTest": None},
  556. imports={"test": {ImportVar(tag="test")}},
  557. state="Test",
  558. interpolations=[],
  559. )
  560. )
  561. FORMATTED_TEST_VAR = Var.create(f"foo{TEST_VAR}bar")
  562. STYLE_VAR = TEST_VAR._replace(_var_name="style", _var_is_local=False)
  563. EVENT_CHAIN_VAR = TEST_VAR._replace(_var_type=EventChain)
  564. ARG_VAR = Var.create("arg")
  565. TEST_VAR_DICT_OF_DICT = Var.create_safe({"a": {"b": "test"}})._replace(
  566. merge_var_data=TEST_VAR._var_data
  567. )
  568. FORMATTED_TEST_VAR_DICT_OF_DICT = Var.create_safe({"a": {"b": f"footestbar"}})._replace(
  569. merge_var_data=TEST_VAR._var_data
  570. )
  571. TEST_VAR_LIST_OF_LIST = Var.create_safe([["test"]])._replace(
  572. merge_var_data=TEST_VAR._var_data
  573. )
  574. FORMATTED_TEST_VAR_LIST_OF_LIST = Var.create_safe([["footestbar"]])._replace(
  575. merge_var_data=TEST_VAR._var_data
  576. )
  577. TEST_VAR_LIST_OF_LIST_OF_LIST = Var.create_safe([[["test"]]])._replace(
  578. merge_var_data=TEST_VAR._var_data
  579. )
  580. FORMATTED_TEST_VAR_LIST_OF_LIST_OF_LIST = Var.create_safe([[["footestbar"]]])._replace(
  581. merge_var_data=TEST_VAR._var_data
  582. )
  583. TEST_VAR_LIST_OF_DICT = Var.create_safe([{"a": "test"}])._replace(
  584. merge_var_data=TEST_VAR._var_data
  585. )
  586. FORMATTED_TEST_VAR_LIST_OF_DICT = Var.create_safe([{"a": "footestbar"}])._replace(
  587. merge_var_data=TEST_VAR._var_data
  588. )
  589. class ComponentNestedVar(Component):
  590. """A component with nested Var types."""
  591. dict_of_dict: Var[Dict[str, Dict[str, str]]]
  592. list_of_list: Var[List[List[str]]]
  593. list_of_list_of_list: Var[List[List[List[str]]]]
  594. list_of_dict: Var[List[Dict[str, str]]]
  595. class EventState(rx.State):
  596. """State for testing event handlers with _get_vars."""
  597. v: int = 42
  598. def handler(self):
  599. """A handler that does nothing."""
  600. def handler2(self, arg):
  601. """A handler that takes an arg.
  602. Args:
  603. arg: An arg.
  604. """
  605. @pytest.mark.parametrize(
  606. ("component", "exp_vars"),
  607. (
  608. pytest.param(
  609. Bare.create(TEST_VAR),
  610. [TEST_VAR],
  611. id="direct-bare",
  612. ),
  613. pytest.param(
  614. Bare.create(f"foo{TEST_VAR}bar"),
  615. [FORMATTED_TEST_VAR],
  616. id="fstring-bare",
  617. ),
  618. pytest.param(
  619. rx.text(as_=TEST_VAR),
  620. [TEST_VAR],
  621. id="direct-prop",
  622. ),
  623. pytest.param(
  624. rx.heading(as_=f"foo{TEST_VAR}bar"),
  625. [FORMATTED_TEST_VAR],
  626. id="fstring-prop",
  627. ),
  628. pytest.param(
  629. rx.fragment(id=TEST_VAR),
  630. [TEST_VAR],
  631. id="direct-id",
  632. ),
  633. pytest.param(
  634. rx.fragment(id=f"foo{TEST_VAR}bar"),
  635. [FORMATTED_TEST_VAR],
  636. id="fstring-id",
  637. ),
  638. pytest.param(
  639. rx.fragment(key=TEST_VAR),
  640. [TEST_VAR],
  641. id="direct-key",
  642. ),
  643. pytest.param(
  644. rx.fragment(key=f"foo{TEST_VAR}bar"),
  645. [FORMATTED_TEST_VAR],
  646. id="fstring-key",
  647. ),
  648. pytest.param(
  649. rx.fragment(class_name=TEST_VAR),
  650. [TEST_VAR],
  651. id="direct-class_name",
  652. ),
  653. pytest.param(
  654. rx.fragment(class_name=f"foo{TEST_VAR}bar"),
  655. [FORMATTED_TEST_VAR],
  656. id="fstring-class_name",
  657. ),
  658. pytest.param(
  659. rx.fragment(special_props={TEST_VAR}),
  660. [TEST_VAR],
  661. id="direct-special_props",
  662. ),
  663. pytest.param(
  664. rx.fragment(special_props={Var.create(f"foo{TEST_VAR}bar")}),
  665. [FORMATTED_TEST_VAR],
  666. id="fstring-special_props",
  667. ),
  668. pytest.param(
  669. # custom_attrs cannot accept a Var directly as a value
  670. rx.fragment(custom_attrs={"href": f"{TEST_VAR}"}),
  671. [TEST_VAR],
  672. id="fstring-custom_attrs-nofmt",
  673. ),
  674. pytest.param(
  675. rx.fragment(custom_attrs={"href": f"foo{TEST_VAR}bar"}),
  676. [FORMATTED_TEST_VAR],
  677. id="fstring-custom_attrs",
  678. ),
  679. pytest.param(
  680. rx.fragment(background_color=TEST_VAR),
  681. [STYLE_VAR],
  682. id="direct-background_color",
  683. ),
  684. pytest.param(
  685. rx.fragment(background_color=f"foo{TEST_VAR}bar"),
  686. [STYLE_VAR],
  687. id="fstring-background_color",
  688. ),
  689. pytest.param(
  690. rx.fragment(style={"background_color": TEST_VAR}), # type: ignore
  691. [STYLE_VAR],
  692. id="direct-style-background_color",
  693. ),
  694. pytest.param(
  695. rx.fragment(style={"background_color": f"foo{TEST_VAR}bar"}), # type: ignore
  696. [STYLE_VAR],
  697. id="fstring-style-background_color",
  698. ),
  699. pytest.param(
  700. rx.fragment(on_click=EVENT_CHAIN_VAR), # type: ignore
  701. [EVENT_CHAIN_VAR],
  702. id="direct-event-chain",
  703. ),
  704. pytest.param(
  705. rx.fragment(on_click=EventState.handler),
  706. [],
  707. id="direct-event-handler",
  708. ),
  709. pytest.param(
  710. rx.fragment(on_click=EventState.handler2(TEST_VAR)), # type: ignore
  711. [ARG_VAR, TEST_VAR],
  712. id="direct-event-handler-arg",
  713. ),
  714. pytest.param(
  715. rx.fragment(on_click=EventState.handler2(EventState.v)), # type: ignore
  716. [ARG_VAR, EventState.v],
  717. id="direct-event-handler-arg2",
  718. ),
  719. pytest.param(
  720. rx.fragment(on_click=lambda: EventState.handler2(TEST_VAR)), # type: ignore
  721. [ARG_VAR, TEST_VAR],
  722. id="direct-event-handler-lambda",
  723. ),
  724. pytest.param(
  725. ComponentNestedVar.create(dict_of_dict={"a": {"b": TEST_VAR}}),
  726. [TEST_VAR_DICT_OF_DICT],
  727. id="direct-dict_of_dict",
  728. ),
  729. pytest.param(
  730. ComponentNestedVar.create(dict_of_dict={"a": {"b": f"foo{TEST_VAR}bar"}}),
  731. [FORMATTED_TEST_VAR_DICT_OF_DICT],
  732. id="fstring-dict_of_dict",
  733. ),
  734. pytest.param(
  735. ComponentNestedVar.create(list_of_list=[[TEST_VAR]]),
  736. [TEST_VAR_LIST_OF_LIST],
  737. id="direct-list_of_list",
  738. ),
  739. pytest.param(
  740. ComponentNestedVar.create(list_of_list=[[f"foo{TEST_VAR}bar"]]),
  741. [FORMATTED_TEST_VAR_LIST_OF_LIST],
  742. id="fstring-list_of_list",
  743. ),
  744. pytest.param(
  745. ComponentNestedVar.create(list_of_list_of_list=[[[TEST_VAR]]]),
  746. [TEST_VAR_LIST_OF_LIST_OF_LIST],
  747. id="direct-list_of_list_of_list",
  748. ),
  749. pytest.param(
  750. ComponentNestedVar.create(list_of_list_of_list=[[[f"foo{TEST_VAR}bar"]]]),
  751. [FORMATTED_TEST_VAR_LIST_OF_LIST_OF_LIST],
  752. id="fstring-list_of_list_of_list",
  753. ),
  754. pytest.param(
  755. ComponentNestedVar.create(list_of_dict=[{"a": TEST_VAR}]),
  756. [TEST_VAR_LIST_OF_DICT],
  757. id="direct-list_of_dict",
  758. ),
  759. pytest.param(
  760. ComponentNestedVar.create(list_of_dict=[{"a": f"foo{TEST_VAR}bar"}]),
  761. [FORMATTED_TEST_VAR_LIST_OF_DICT],
  762. id="fstring-list_of_dict",
  763. ),
  764. ),
  765. )
  766. def test_get_vars(component, exp_vars):
  767. comp_vars = sorted(component._get_vars(), key=lambda v: v._var_name)
  768. assert len(comp_vars) == len(exp_vars)
  769. for comp_var, exp_var in zip(
  770. comp_vars,
  771. sorted(exp_vars, key=lambda v: v._var_name),
  772. ):
  773. assert comp_var.equals(exp_var)
  774. def test_instantiate_all_components():
  775. """Test that all components can be instantiated."""
  776. # These components all have required arguments and cannot be trivially instantiated.
  777. untested_components = {
  778. "Card",
  779. "Cond",
  780. "DebounceInput",
  781. "Foreach",
  782. "FormControl",
  783. "Html",
  784. "Icon",
  785. "Match",
  786. "Markdown",
  787. "MultiSelect",
  788. "Option",
  789. "Popover",
  790. "Radio",
  791. "Script",
  792. "Tag",
  793. "Tfoot",
  794. "Thead",
  795. }
  796. for component_name in rx._ALL_COMPONENTS: # type: ignore
  797. if component_name in untested_components:
  798. continue
  799. component = getattr(rx, component_name)
  800. if isinstance(component, type) and issubclass(component, Component):
  801. component.create()
  802. class InvalidParentComponent(Component):
  803. """Invalid Parent Component."""
  804. ...
  805. class ValidComponent1(Component):
  806. """Test valid component."""
  807. _valid_children = ["ValidComponent2"]
  808. class ValidComponent2(Component):
  809. """Test valid component."""
  810. ...
  811. class ValidComponent3(Component):
  812. """Test valid component."""
  813. _valid_parents = ["ValidComponent2"]
  814. class ValidComponent4(Component):
  815. """Test valid component."""
  816. _invalid_children = ["InvalidComponent"]
  817. class InvalidComponent(Component):
  818. """Test invalid component."""
  819. ...
  820. valid_component1 = ValidComponent1.create
  821. valid_component2 = ValidComponent2.create
  822. invalid_component = InvalidComponent.create
  823. valid_component3 = ValidComponent3.create
  824. invalid_parent = InvalidParentComponent.create
  825. valid_component4 = ValidComponent4.create
  826. def test_validate_valid_children():
  827. valid_component1(valid_component2())
  828. valid_component1(
  829. rx.fragment(valid_component2()),
  830. )
  831. valid_component1(
  832. rx.fragment(
  833. rx.fragment(
  834. rx.fragment(valid_component2()),
  835. ),
  836. ),
  837. )
  838. valid_component1(
  839. rx.cond( # type: ignore
  840. True,
  841. rx.fragment(valid_component2()),
  842. rx.fragment(
  843. rx.foreach(Var.create([1, 2, 3]), lambda x: valid_component2(x)) # type: ignore
  844. ),
  845. )
  846. )
  847. valid_component1(
  848. rx.cond(
  849. True,
  850. valid_component2(),
  851. rx.fragment(
  852. rx.match(
  853. "condition",
  854. ("first", valid_component2()),
  855. rx.fragment(valid_component2(rx.text("default"))),
  856. )
  857. ),
  858. )
  859. )
  860. valid_component1(
  861. rx.match(
  862. "condition",
  863. ("first", valid_component2()),
  864. ("second", "third", rx.fragment(valid_component2())),
  865. (
  866. "fourth",
  867. rx.cond(True, valid_component2(), rx.fragment(valid_component2())),
  868. ),
  869. (
  870. "fifth",
  871. rx.match(
  872. "nested_condition",
  873. ("nested_first", valid_component2()),
  874. rx.fragment(valid_component2()),
  875. ),
  876. valid_component2(),
  877. ),
  878. )
  879. )
  880. def test_validate_valid_parents():
  881. valid_component2(valid_component3())
  882. valid_component2(
  883. rx.fragment(valid_component3()),
  884. )
  885. valid_component1(
  886. rx.fragment(
  887. valid_component2(
  888. rx.fragment(valid_component3()),
  889. ),
  890. ),
  891. )
  892. valid_component2(
  893. rx.cond( # type: ignore
  894. True,
  895. rx.fragment(valid_component3()),
  896. rx.fragment(
  897. rx.foreach(
  898. Var.create([1, 2, 3]), # type: ignore
  899. lambda x: valid_component2(valid_component3(x)),
  900. )
  901. ),
  902. )
  903. )
  904. valid_component2(
  905. rx.cond(
  906. True,
  907. valid_component3(),
  908. rx.fragment(
  909. rx.match(
  910. "condition",
  911. ("first", valid_component3()),
  912. rx.fragment(valid_component3(rx.text("default"))),
  913. )
  914. ),
  915. )
  916. )
  917. valid_component2(
  918. rx.match(
  919. "condition",
  920. ("first", valid_component3()),
  921. ("second", "third", rx.fragment(valid_component3())),
  922. (
  923. "fourth",
  924. rx.cond(True, valid_component3(), rx.fragment(valid_component3())),
  925. ),
  926. (
  927. "fifth",
  928. rx.match(
  929. "nested_condition",
  930. ("nested_first", valid_component3()),
  931. rx.fragment(valid_component3()),
  932. ),
  933. valid_component3(),
  934. ),
  935. )
  936. )
  937. def test_validate_invalid_children():
  938. with pytest.raises(ValueError):
  939. valid_component4(invalid_component())
  940. with pytest.raises(ValueError):
  941. valid_component4(
  942. rx.fragment(invalid_component()),
  943. )
  944. with pytest.raises(ValueError):
  945. valid_component2(
  946. rx.fragment(
  947. valid_component4(
  948. rx.fragment(invalid_component()),
  949. ),
  950. ),
  951. )
  952. with pytest.raises(ValueError):
  953. valid_component4(
  954. rx.cond( # type: ignore
  955. True,
  956. rx.fragment(invalid_component()),
  957. rx.fragment(
  958. rx.foreach(Var.create([1, 2, 3]), lambda x: invalid_component(x)) # type: ignore
  959. ),
  960. )
  961. )
  962. with pytest.raises(ValueError):
  963. valid_component4(
  964. rx.cond(
  965. True,
  966. invalid_component(),
  967. rx.fragment(
  968. rx.match(
  969. "condition",
  970. ("first", invalid_component()),
  971. rx.fragment(invalid_component(rx.text("default"))),
  972. )
  973. ),
  974. )
  975. )
  976. with pytest.raises(ValueError):
  977. valid_component4(
  978. rx.match(
  979. "condition",
  980. ("first", invalid_component()),
  981. ("second", "third", rx.fragment(invalid_component())),
  982. (
  983. "fourth",
  984. rx.cond(True, invalid_component(), rx.fragment(valid_component2())),
  985. ),
  986. (
  987. "fifth",
  988. rx.match(
  989. "nested_condition",
  990. ("nested_first", invalid_component()),
  991. rx.fragment(invalid_component()),
  992. ),
  993. invalid_component(),
  994. ),
  995. )
  996. )
  997. def test_rename_props():
  998. """Test that _rename_props works and is inherited."""
  999. class C1(Component):
  1000. tag = "C1"
  1001. prop1: Var[str]
  1002. prop2: Var[str]
  1003. _rename_props = {"prop1": "renamed_prop1", "prop2": "renamed_prop2"}
  1004. class C2(C1):
  1005. tag = "C2"
  1006. prop3: Var[str]
  1007. _rename_props = {"prop2": "subclass_prop2", "prop3": "renamed_prop3"}
  1008. c1 = C1.create(prop1="prop1_1", prop2="prop2_1")
  1009. rendered_c1 = c1.render()
  1010. assert "renamed_prop1={`prop1_1`}" in rendered_c1["props"]
  1011. assert "renamed_prop2={`prop2_1`}" in rendered_c1["props"]
  1012. c2 = C2.create(prop1="prop1_2", prop2="prop2_2", prop3="prop3_2")
  1013. rendered_c2 = c2.render()
  1014. assert "renamed_prop1={`prop1_2`}" in rendered_c2["props"]
  1015. assert "subclass_prop2={`prop2_2`}" in rendered_c2["props"]
  1016. assert "renamed_prop3={`prop3_2`}" in rendered_c2["props"]
  1017. def test_deprecated_props(capsys):
  1018. """Assert that deprecated underscore suffix props are translated.
  1019. Args:
  1020. capsys: Pytest fixture for capturing stdout and stderr.
  1021. """
  1022. class C1(Component):
  1023. tag = "C1"
  1024. type: Var[str]
  1025. min: Var[str]
  1026. max: Var[str]
  1027. # No warnings are emitted when using the new prop names.
  1028. c1_1 = C1.create(type="type1", min="min1", max="max1")
  1029. out_err = capsys.readouterr()
  1030. assert not out_err.err
  1031. assert not out_err.out
  1032. c1_1_render = c1_1.render()
  1033. assert "type={`type1`}" in c1_1_render["props"]
  1034. assert "min={`min1`}" in c1_1_render["props"]
  1035. assert "max={`max1`}" in c1_1_render["props"]
  1036. # Deprecation warning is emitted with underscore suffix,
  1037. # but the component still works.
  1038. c1_2 = C1.create(type_="type2", min_="min2", max_="max2")
  1039. out_err = capsys.readouterr()
  1040. assert out_err.out.count("DeprecationWarning:") == 3
  1041. assert not out_err.err
  1042. c1_2_render = c1_2.render()
  1043. assert "type={`type2`}" in c1_2_render["props"]
  1044. assert "min={`min2`}" in c1_2_render["props"]
  1045. assert "max={`max2`}" in c1_2_render["props"]
  1046. class C2(Component):
  1047. tag = "C2"
  1048. type_: Var[str]
  1049. min_: Var[str]
  1050. max_: Var[str]
  1051. # No warnings are emitted if the actual prop has an underscore suffix
  1052. c2_1 = C2.create(type_="type1", min_="min1", max_="max1")
  1053. out_err = capsys.readouterr()
  1054. assert not out_err.err
  1055. assert not out_err.out
  1056. c2_1_render = c2_1.render()
  1057. assert "type={`type1`}" in c2_1_render["props"]
  1058. assert "min={`min1`}" in c2_1_render["props"]
  1059. assert "max={`max1`}" in c2_1_render["props"]
  1060. def test_custom_component_get_imports():
  1061. class Inner(Component):
  1062. tag = "Inner"
  1063. library = "inner"
  1064. class Other(Component):
  1065. tag = "Other"
  1066. library = "other"
  1067. @rx.memo
  1068. def wrapper():
  1069. return Inner.create()
  1070. @rx.memo
  1071. def outer(c: Component):
  1072. return Other.create(c)
  1073. custom_comp = wrapper()
  1074. # Inner is not imported directly, but it is imported by the custom component.
  1075. assert "inner" not in custom_comp.get_imports()
  1076. # The imports are only resolved during compilation.
  1077. _, _, imports_inner = compile_components(custom_comp.get_custom_components())
  1078. assert "inner" in imports_inner
  1079. outer_comp = outer(c=wrapper())
  1080. # Libraries are not imported directly, but are imported by the custom component.
  1081. assert "inner" not in outer_comp.get_imports()
  1082. assert "other" not in outer_comp.get_imports()
  1083. # The imports are only resolved during compilation.
  1084. _, _, imports_outer = compile_components(outer_comp.get_custom_components())
  1085. assert "inner" in imports_outer
  1086. assert "other" in imports_outer
  1087. def test_custom_component_declare_event_handlers_in_fields():
  1088. class ReferenceComponent(Component):
  1089. def get_event_triggers(self) -> Dict[str, Any]:
  1090. """Test controlled triggers.
  1091. Returns:
  1092. Test controlled triggers.
  1093. """
  1094. return {
  1095. **super().get_event_triggers(),
  1096. "on_a": lambda e: [e],
  1097. "on_b": lambda e: [e.target.value],
  1098. "on_c": lambda e: [],
  1099. "on_d": lambda: [],
  1100. "on_e": lambda: [],
  1101. }
  1102. class TestComponent(Component):
  1103. on_a: EventHandler[lambda e0: [e0]]
  1104. on_b: EventHandler[lambda e0: [e0.target.value]]
  1105. on_c: EventHandler[lambda e0: []]
  1106. on_d: EventHandler[lambda: []]
  1107. on_e: EventHandler
  1108. custom_component = ReferenceComponent.create()
  1109. test_component = TestComponent.create()
  1110. assert (
  1111. custom_component.get_event_triggers().keys()
  1112. == test_component.get_event_triggers().keys()
  1113. )