test_component.py 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145
  1. from contextlib import nullcontext
  2. from typing import Any, Dict, List, Optional, Type, Union
  3. import pytest
  4. import reflex_chakra as rc
  5. from reflex_chakra.components.layout.box import Box
  6. import reflex as rx
  7. from reflex.base import Base
  8. from reflex.compiler.compiler import compile_components
  9. from reflex.components.base.bare import Bare
  10. from reflex.components.base.fragment import Fragment
  11. from reflex.components.component import (
  12. Component,
  13. CustomComponent,
  14. StatefulComponent,
  15. custom_component,
  16. )
  17. from reflex.constants import EventTriggers
  18. from reflex.event import EventChain, EventHandler, parse_args_spec
  19. from reflex.state import BaseState
  20. from reflex.style import Style
  21. from reflex.utils import imports
  22. from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports
  23. from reflex.vars import BaseVar, Var, VarData
  24. @pytest.fixture
  25. def test_state():
  26. class TestState(BaseState):
  27. num: int
  28. def do_something(self):
  29. pass
  30. def do_something_arg(self, arg):
  31. pass
  32. return TestState
  33. @pytest.fixture
  34. def component1() -> Type[Component]:
  35. """A test component.
  36. Returns:
  37. A test component.
  38. """
  39. class TestComponent1(Component):
  40. # A test string prop.
  41. text: Var[str]
  42. # A test number prop.
  43. number: Var[int]
  44. # A test string/number prop.
  45. text_or_number: Var[Union[int, str]]
  46. def _get_imports(self) -> ParsedImportDict:
  47. return {"react": [ImportVar(tag="Component")]}
  48. def _get_custom_code(self) -> str:
  49. return "console.log('component1')"
  50. return TestComponent1
  51. @pytest.fixture
  52. def component2() -> Type[Component]:
  53. """A test component.
  54. Returns:
  55. A test component.
  56. """
  57. class TestComponent2(Component):
  58. # A test list prop.
  59. arr: Var[List[str]]
  60. def get_event_triggers(self) -> Dict[str, Any]:
  61. """Test controlled triggers.
  62. Returns:
  63. Test controlled triggers.
  64. """
  65. return {
  66. **super().get_event_triggers(),
  67. "on_open": lambda e0: [e0],
  68. "on_close": lambda e0: [e0],
  69. }
  70. def _get_imports(self) -> ParsedImportDict:
  71. return {"react-redux": [ImportVar(tag="connect")]}
  72. def _get_custom_code(self) -> str:
  73. return "console.log('component2')"
  74. return TestComponent2
  75. @pytest.fixture
  76. def component3() -> Type[Component]:
  77. """A test component with hook defined.
  78. Returns:
  79. A test component.
  80. """
  81. class TestComponent3(Component):
  82. def _get_hooks(self) -> str:
  83. return "const a = () => true"
  84. return TestComponent3
  85. @pytest.fixture
  86. def component4() -> Type[Component]:
  87. """A test component with hook defined.
  88. Returns:
  89. A test component.
  90. """
  91. class TestComponent4(Component):
  92. def _get_hooks(self) -> str:
  93. return "const b = () => false"
  94. return TestComponent4
  95. @pytest.fixture
  96. def component5() -> Type[Component]:
  97. """A test component.
  98. Returns:
  99. A test component.
  100. """
  101. class TestComponent5(Component):
  102. tag = "RandomComponent"
  103. _invalid_children: List[str] = ["Text"]
  104. _valid_children: List[str] = ["Text"]
  105. _valid_parents: List[str] = ["Text"]
  106. return TestComponent5
  107. @pytest.fixture
  108. def component6() -> Type[Component]:
  109. """A test component.
  110. Returns:
  111. A test component.
  112. """
  113. class TestComponent6(Component):
  114. tag = "RandomComponent"
  115. _invalid_children: List[str] = ["Text"]
  116. return TestComponent6
  117. @pytest.fixture
  118. def component7() -> Type[Component]:
  119. """A test component.
  120. Returns:
  121. A test component.
  122. """
  123. class TestComponent7(Component):
  124. tag = "RandomComponent"
  125. _valid_children: List[str] = ["Text"]
  126. return TestComponent7
  127. @pytest.fixture
  128. def on_click1() -> EventHandler:
  129. """A sample on click function.
  130. Returns:
  131. A sample on click function.
  132. """
  133. def on_click1():
  134. pass
  135. return EventHandler(fn=on_click1)
  136. @pytest.fixture
  137. def on_click2() -> EventHandler:
  138. """A sample on click function.
  139. Returns:
  140. A sample on click function.
  141. """
  142. def on_click2():
  143. pass
  144. return EventHandler(fn=on_click2)
  145. @pytest.fixture
  146. def my_component():
  147. """A test component function.
  148. Returns:
  149. A test component function.
  150. """
  151. def my_component(prop1: Var[str], prop2: Var[int]):
  152. return Box.create(prop1, prop2)
  153. return my_component
  154. def test_set_style_attrs(component1):
  155. """Test that style attributes are set in the dict.
  156. Args:
  157. component1: A test component.
  158. """
  159. component = component1(color="white", text_align="center")
  160. assert component.style["color"] == "white"
  161. assert component.style["textAlign"] == "center"
  162. def test_custom_attrs(component1):
  163. """Test that custom attributes are set in the dict.
  164. Args:
  165. component1: A test component.
  166. """
  167. component = component1(custom_attrs={"attr1": "1", "attr2": "attr2"})
  168. assert component.custom_attrs == {"attr1": "1", "attr2": "attr2"}
  169. def test_create_component(component1):
  170. """Test that the component is created correctly.
  171. Args:
  172. component1: A test component.
  173. """
  174. children = [component1() for _ in range(3)]
  175. attrs = {"color": "white", "text_align": "center"}
  176. c = component1.create(*children, **attrs)
  177. assert isinstance(c, component1)
  178. assert c.children == children
  179. assert c.style == {"color": "white", "textAlign": "center"}
  180. @pytest.mark.parametrize(
  181. "prop_name,var,expected",
  182. [
  183. pytest.param(
  184. "text",
  185. Var.create("hello"),
  186. None,
  187. id="text",
  188. ),
  189. pytest.param(
  190. "text",
  191. BaseVar(_var_name="hello", _var_type=Optional[str]),
  192. None,
  193. id="text-optional",
  194. ),
  195. pytest.param(
  196. "text",
  197. BaseVar(_var_name="hello", _var_type=Union[str, None]),
  198. None,
  199. id="text-union-str-none",
  200. ),
  201. pytest.param(
  202. "text",
  203. BaseVar(_var_name="hello", _var_type=Union[None, str]),
  204. None,
  205. id="text-union-none-str",
  206. ),
  207. pytest.param(
  208. "text",
  209. Var.create(1),
  210. TypeError,
  211. id="text-int",
  212. ),
  213. pytest.param(
  214. "number",
  215. Var.create(1),
  216. None,
  217. id="number",
  218. ),
  219. pytest.param(
  220. "number",
  221. BaseVar(_var_name="1", _var_type=Optional[int]),
  222. None,
  223. id="number-optional",
  224. ),
  225. pytest.param(
  226. "number",
  227. BaseVar(_var_name="1", _var_type=Union[int, None]),
  228. None,
  229. id="number-union-int-none",
  230. ),
  231. pytest.param(
  232. "number",
  233. BaseVar(_var_name="1", _var_type=Union[None, int]),
  234. None,
  235. id="number-union-none-int",
  236. ),
  237. pytest.param(
  238. "number",
  239. Var.create("1"),
  240. TypeError,
  241. id="number-str",
  242. ),
  243. pytest.param(
  244. "text_or_number",
  245. Var.create("hello"),
  246. None,
  247. id="text_or_number-str",
  248. ),
  249. pytest.param(
  250. "text_or_number",
  251. Var.create(1),
  252. None,
  253. id="text_or_number-int",
  254. ),
  255. pytest.param(
  256. "text_or_number",
  257. BaseVar(_var_name="hello", _var_type=Optional[str]),
  258. None,
  259. id="text_or_number-optional-str",
  260. ),
  261. pytest.param(
  262. "text_or_number",
  263. BaseVar(_var_name="hello", _var_type=Union[str, None]),
  264. None,
  265. id="text_or_number-union-str-none",
  266. ),
  267. pytest.param(
  268. "text_or_number",
  269. BaseVar(_var_name="hello", _var_type=Union[None, str]),
  270. None,
  271. id="text_or_number-union-none-str",
  272. ),
  273. pytest.param(
  274. "text_or_number",
  275. BaseVar(_var_name="1", _var_type=Optional[int]),
  276. None,
  277. id="text_or_number-optional-int",
  278. ),
  279. pytest.param(
  280. "text_or_number",
  281. BaseVar(_var_name="1", _var_type=Union[int, None]),
  282. None,
  283. id="text_or_number-union-int-none",
  284. ),
  285. pytest.param(
  286. "text_or_number",
  287. BaseVar(_var_name="1", _var_type=Union[None, int]),
  288. None,
  289. id="text_or_number-union-none-int",
  290. ),
  291. pytest.param(
  292. "text_or_number",
  293. Var.create(1.0),
  294. TypeError,
  295. id="text_or_number-float",
  296. ),
  297. pytest.param(
  298. "text_or_number",
  299. BaseVar(_var_name="hello", _var_type=Optional[Union[str, int]]),
  300. None,
  301. id="text_or_number-optional-union-str-int",
  302. ),
  303. ],
  304. )
  305. def test_create_component_prop_validation(
  306. component1: Type[Component],
  307. prop_name: str,
  308. var: Union[Var, str, int],
  309. expected: Type[Exception],
  310. ):
  311. """Test that component props are validated correctly.
  312. Args:
  313. component1: A test component.
  314. prop_name: The name of the prop.
  315. var: The value of the prop.
  316. expected: The expected exception.
  317. """
  318. ctx = pytest.raises(expected) if expected else nullcontext()
  319. kwargs = {prop_name: var}
  320. with ctx:
  321. c = component1.create(**kwargs)
  322. assert isinstance(c, component1)
  323. assert c.children == []
  324. assert c.style == {}
  325. def test_add_style(component1, component2):
  326. """Test adding a style to a component.
  327. Args:
  328. component1: A test component.
  329. component2: A test component.
  330. """
  331. style = {
  332. component1: Style({"color": "white"}),
  333. component2: Style({"color": "black"}),
  334. }
  335. c1 = component1()._add_style_recursive(style) # type: ignore
  336. c2 = component2()._add_style_recursive(style) # type: ignore
  337. assert c1.style["color"] == "white"
  338. assert c2.style["color"] == "black"
  339. def test_add_style_create(component1, component2):
  340. """Test that adding style works with the create method.
  341. Args:
  342. component1: A test component.
  343. component2: A test component.
  344. """
  345. style = {
  346. component1.create: Style({"color": "white"}),
  347. component2.create: Style({"color": "black"}),
  348. }
  349. c1 = component1()._add_style_recursive(style) # type: ignore
  350. c2 = component2()._add_style_recursive(style) # type: ignore
  351. assert c1.style["color"] == "white"
  352. assert c2.style["color"] == "black"
  353. def test_get_imports(component1, component2):
  354. """Test getting the imports of a component.
  355. Args:
  356. component1: A test component.
  357. component2: A test component.
  358. """
  359. c1 = component1.create()
  360. c2 = component2.create(c1)
  361. assert c1._get_all_imports() == {"react": [ImportVar(tag="Component")]}
  362. assert c2._get_all_imports() == {
  363. "react-redux": [ImportVar(tag="connect")],
  364. "react": [ImportVar(tag="Component")],
  365. }
  366. def test_get_custom_code(component1, component2):
  367. """Test getting the custom code of a component.
  368. Args:
  369. component1: A test component.
  370. component2: A test component.
  371. """
  372. # Check that the code gets compiled correctly.
  373. c1 = component1.create()
  374. c2 = component2.create()
  375. assert c1._get_all_custom_code() == {"console.log('component1')"}
  376. assert c2._get_all_custom_code() == {"console.log('component2')"}
  377. # Check that nesting components compiles both codes.
  378. c1 = component1.create(c2)
  379. assert c1._get_all_custom_code() == {
  380. "console.log('component1')",
  381. "console.log('component2')",
  382. }
  383. # Check that code is not duplicated.
  384. c1 = component1.create(c2, c2, c1, c1)
  385. assert c1._get_all_custom_code() == {
  386. "console.log('component1')",
  387. "console.log('component2')",
  388. }
  389. def test_get_props(component1, component2):
  390. """Test that the props are set correctly.
  391. Args:
  392. component1: A test component.
  393. component2: A test component.
  394. """
  395. assert component1.get_props() == {"text", "number", "text_or_number"}
  396. assert component2.get_props() == {"arr"}
  397. @pytest.mark.parametrize(
  398. "text,number",
  399. [
  400. ("", 0),
  401. ("test", 1),
  402. ("hi", -13),
  403. ],
  404. )
  405. def test_valid_props(component1, text: str, number: int):
  406. """Test that we can construct a component with valid props.
  407. Args:
  408. component1: A test component.
  409. text: A test string.
  410. number: A test number.
  411. """
  412. c = component1.create(text=text, number=number)
  413. assert c.text._decode() == text
  414. assert c.number._decode() == number
  415. @pytest.mark.parametrize(
  416. "text,number", [("", "bad_string"), (13, 1), ("test", [1, 2, 3])]
  417. )
  418. def test_invalid_prop_type(component1, text: str, number: int):
  419. """Test that an invalid prop type raises an error.
  420. Args:
  421. component1: A test component.
  422. text: A test string.
  423. number: A test number.
  424. """
  425. # Check that
  426. with pytest.raises(TypeError):
  427. component1.create(text=text, number=number)
  428. def test_var_props(component1, test_state):
  429. """Test that we can set a Var prop.
  430. Args:
  431. component1: A test component.
  432. test_state: A test state.
  433. """
  434. c1 = component1.create(text="hello", number=test_state.num)
  435. assert c1.number.equals(test_state.num)
  436. def test_get_event_triggers(component1, component2):
  437. """Test that we can get the triggers of a component.
  438. Args:
  439. component1: A test component.
  440. component2: A test component.
  441. """
  442. default_triggers = {
  443. EventTriggers.ON_FOCUS,
  444. EventTriggers.ON_BLUR,
  445. EventTriggers.ON_CLICK,
  446. EventTriggers.ON_CONTEXT_MENU,
  447. EventTriggers.ON_DOUBLE_CLICK,
  448. EventTriggers.ON_MOUSE_DOWN,
  449. EventTriggers.ON_MOUSE_ENTER,
  450. EventTriggers.ON_MOUSE_LEAVE,
  451. EventTriggers.ON_MOUSE_MOVE,
  452. EventTriggers.ON_MOUSE_OUT,
  453. EventTriggers.ON_MOUSE_OVER,
  454. EventTriggers.ON_MOUSE_UP,
  455. EventTriggers.ON_SCROLL,
  456. EventTriggers.ON_MOUNT,
  457. EventTriggers.ON_UNMOUNT,
  458. }
  459. assert component1().get_event_triggers().keys() == default_triggers
  460. assert (
  461. component2().get_event_triggers().keys()
  462. == {"on_open", "on_close"} | default_triggers
  463. )
  464. @pytest.fixture
  465. def test_component() -> Type[Component]:
  466. """A test component.
  467. Returns:
  468. A test component.
  469. """
  470. class TestComponent(Component):
  471. pass
  472. return TestComponent
  473. # Write a test case to check if the create method filters out None props
  474. def test_create_filters_none_props(test_component):
  475. child1 = test_component()
  476. child2 = test_component()
  477. props = {
  478. "prop1": "value1",
  479. "prop2": None,
  480. "prop3": "value3",
  481. "prop4": None,
  482. "style": {"color": "white", "text-align": "center"}, # Adding a style prop
  483. }
  484. component = test_component.create(child1, child2, **props)
  485. # Assert that None props are not present in the component's props
  486. assert "prop2" not in component.get_props()
  487. assert "prop4" not in component.get_props()
  488. # Assert that the style prop is present in the component's props
  489. assert component.style["color"] == "white"
  490. assert component.style["text-align"] == "center"
  491. @pytest.mark.parametrize("children", [((None,),), ("foo", ("bar", (None,)))])
  492. def test_component_create_unallowed_types(children, test_component):
  493. with pytest.raises(TypeError) as err:
  494. test_component.create(*children)
  495. assert (
  496. err.value.args[0]
  497. == "Children of Reflex components must be other components, state vars, or primitive Python types. Got child None of type <class 'NoneType'>."
  498. )
  499. @pytest.mark.parametrize(
  500. "element, expected",
  501. [
  502. (
  503. (rx.text("first_text"),),
  504. {
  505. "name": "Fragment",
  506. "props": [],
  507. "contents": "",
  508. "args": None,
  509. "special_props": set(),
  510. "children": [
  511. {
  512. "name": "RadixThemesText",
  513. "props": ["as={`p`}"],
  514. "contents": "",
  515. "args": None,
  516. "special_props": set(),
  517. "children": [
  518. {
  519. "name": "",
  520. "props": [],
  521. "contents": "{`first_text`}",
  522. "args": None,
  523. "special_props": set(),
  524. "children": [],
  525. "autofocus": False,
  526. }
  527. ],
  528. "autofocus": False,
  529. }
  530. ],
  531. "autofocus": False,
  532. },
  533. ),
  534. (
  535. (rx.text("first_text"), rx.text("second_text")),
  536. {
  537. "args": None,
  538. "autofocus": False,
  539. "children": [
  540. {
  541. "args": None,
  542. "autofocus": False,
  543. "children": [
  544. {
  545. "args": None,
  546. "autofocus": False,
  547. "children": [],
  548. "contents": "{`first_text`}",
  549. "name": "",
  550. "props": [],
  551. "special_props": set(),
  552. }
  553. ],
  554. "contents": "",
  555. "name": "RadixThemesText",
  556. "props": ["as={`p`}"],
  557. "special_props": set(),
  558. },
  559. {
  560. "args": None,
  561. "autofocus": False,
  562. "children": [
  563. {
  564. "args": None,
  565. "autofocus": False,
  566. "children": [],
  567. "contents": "{`second_text`}",
  568. "name": "",
  569. "props": [],
  570. "special_props": set(),
  571. }
  572. ],
  573. "contents": "",
  574. "name": "RadixThemesText",
  575. "props": ["as={`p`}"],
  576. "special_props": set(),
  577. },
  578. ],
  579. "contents": "",
  580. "name": "Fragment",
  581. "props": [],
  582. "special_props": set(),
  583. },
  584. ),
  585. (
  586. (rx.text("first_text"), rx.box((rx.text("second_text"),))),
  587. {
  588. "args": None,
  589. "autofocus": False,
  590. "children": [
  591. {
  592. "args": None,
  593. "autofocus": False,
  594. "children": [
  595. {
  596. "args": None,
  597. "autofocus": False,
  598. "children": [],
  599. "contents": "{`first_text`}",
  600. "name": "",
  601. "props": [],
  602. "special_props": set(),
  603. }
  604. ],
  605. "contents": "",
  606. "name": "RadixThemesText",
  607. "props": ["as={`p`}"],
  608. "special_props": set(),
  609. },
  610. {
  611. "args": None,
  612. "autofocus": False,
  613. "children": [
  614. {
  615. "args": None,
  616. "autofocus": False,
  617. "children": [
  618. {
  619. "args": None,
  620. "autofocus": False,
  621. "children": [
  622. {
  623. "args": None,
  624. "autofocus": False,
  625. "children": [],
  626. "contents": "{`second_text`}",
  627. "name": "",
  628. "props": [],
  629. "special_props": set(),
  630. }
  631. ],
  632. "contents": "",
  633. "name": "RadixThemesText",
  634. "props": ["as={`p`}"],
  635. "special_props": set(),
  636. }
  637. ],
  638. "contents": "",
  639. "name": "Fragment",
  640. "props": [],
  641. "special_props": set(),
  642. }
  643. ],
  644. "contents": "",
  645. "name": "RadixThemesBox",
  646. "props": [],
  647. "special_props": set(),
  648. },
  649. ],
  650. "contents": "",
  651. "name": "Fragment",
  652. "props": [],
  653. "special_props": set(),
  654. },
  655. ),
  656. ],
  657. )
  658. def test_component_create_unpack_tuple_child(test_component, element, expected):
  659. """Test that component in tuples are unwrapped into an rx.Fragment.
  660. Args:
  661. test_component: Component fixture.
  662. element: The children to pass to the component.
  663. expected: The expected render dict.
  664. """
  665. comp = test_component.create(element)
  666. assert len(comp.children) == 1
  667. assert isinstance((fragment_wrapper := comp.children[0]), Fragment)
  668. assert fragment_wrapper.render() == expected
  669. class C1State(BaseState):
  670. """State for testing C1 component."""
  671. def mock_handler(self, _e, _bravo, _charlie):
  672. """Mock handler."""
  673. pass
  674. def test_component_event_trigger_arbitrary_args():
  675. """Test that we can define arbitrary types for the args of an event trigger."""
  676. class Obj(Base):
  677. custom: int = 0
  678. def on_foo_spec(_e, alpha: str, bravo: Dict[str, Any], charlie: Obj):
  679. return [_e.target.value, bravo["nested"], charlie.custom + 42]
  680. class C1(Component):
  681. library = "/local"
  682. tag = "C1"
  683. def get_event_triggers(self) -> Dict[str, Any]:
  684. return {
  685. **super().get_event_triggers(),
  686. "on_foo": on_foo_spec,
  687. }
  688. comp = C1.create(on_foo=C1State.mock_handler)
  689. assert comp.render()["props"][0] == (
  690. "onFoo={(__e,_alpha,_bravo,_charlie) => addEvents("
  691. f'[Event("{C1State.get_full_name()}.mock_handler", {{_e:__e.target.value,_bravo:_bravo["nested"],_charlie:((_charlie.custom) + (42))}})], '
  692. "[__e,_alpha,_bravo,_charlie], {})}"
  693. )
  694. def test_create_custom_component(my_component):
  695. """Test that we can create a custom component.
  696. Args:
  697. my_component: A test custom component.
  698. """
  699. component = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  700. assert component.tag == "MyComponent"
  701. assert component.get_props() == set()
  702. assert component._get_all_custom_components() == {component}
  703. def test_custom_component_hash(my_component):
  704. """Test that the hash of a custom component is correct.
  705. Args:
  706. my_component: A test custom component.
  707. """
  708. component1 = CustomComponent(component_fn=my_component, prop1="test", prop2=1)
  709. component2 = CustomComponent(component_fn=my_component, prop1="test", prop2=2)
  710. assert {component1, component2} == {component1}
  711. def test_custom_component_wrapper():
  712. """Test that the wrapper of a custom component is correct."""
  713. @custom_component
  714. def my_component(width: Var[int], color: Var[str]):
  715. return rx.box(
  716. width=width,
  717. color=color,
  718. )
  719. from reflex.components.radix.themes.typography.text import Text
  720. ccomponent = my_component(
  721. rx.text("child"), width=Var.create(1), color=Var.create("red")
  722. )
  723. assert isinstance(ccomponent, CustomComponent)
  724. assert len(ccomponent.children) == 1
  725. assert isinstance(ccomponent.children[0], Text)
  726. component = ccomponent.get_component(ccomponent)
  727. assert isinstance(component, Box)
  728. def test_invalid_event_handler_args(component2, test_state):
  729. """Test that an invalid event handler raises an error.
  730. Args:
  731. component2: A test component.
  732. test_state: A test state.
  733. """
  734. # Uncontrolled event handlers should not take args.
  735. # This is okay.
  736. component2.create(on_click=test_state.do_something)
  737. # This is not okay.
  738. with pytest.raises(ValueError):
  739. component2.create(on_click=test_state.do_something_arg)
  740. component2.create(on_open=test_state.do_something)
  741. component2.create(
  742. on_open=[test_state.do_something_arg, test_state.do_something]
  743. )
  744. # However lambdas are okay.
  745. component2.create(on_click=lambda: test_state.do_something_arg(1))
  746. component2.create(
  747. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something]
  748. )
  749. component2.create(
  750. on_click=lambda: [test_state.do_something_arg(1), test_state.do_something()]
  751. )
  752. # Controlled event handlers should take args.
  753. # This is okay.
  754. component2.create(on_open=test_state.do_something_arg)
  755. def test_get_hooks_nested(component1, component2, component3):
  756. """Test that a component returns hooks from child components.
  757. Args:
  758. component1: test component.
  759. component2: another component.
  760. component3: component with hooks defined.
  761. """
  762. c = component1.create(
  763. component2.create(arr=[]),
  764. component3.create(),
  765. component3.create(),
  766. component3.create(),
  767. text="a",
  768. number=1,
  769. )
  770. assert c._get_all_hooks() == component3()._get_all_hooks()
  771. def test_get_hooks_nested2(component3, component4):
  772. """Test that a component returns both when parent and child have hooks.
  773. Args:
  774. component3: component with hooks defined.
  775. component4: component with different hooks defined.
  776. """
  777. exp_hooks = {**component3()._get_all_hooks(), **component4()._get_all_hooks()}
  778. assert component3.create(component4.create())._get_all_hooks() == exp_hooks
  779. assert component4.create(component3.create())._get_all_hooks() == exp_hooks
  780. assert (
  781. component4.create(
  782. component3.create(),
  783. component4.create(),
  784. component3.create(),
  785. )._get_all_hooks()
  786. == exp_hooks
  787. )
  788. @pytest.mark.parametrize("fixture", ["component5", "component6"])
  789. def test_unsupported_child_components(fixture, request):
  790. """Test that a value error is raised when an unsupported component (a child component found in the
  791. component's invalid children list) is provided as a child.
  792. Args:
  793. fixture: the test component as a fixture.
  794. request: Pytest request.
  795. """
  796. component = request.getfixturevalue(fixture)
  797. with pytest.raises(ValueError) as err:
  798. comp = component.create(rx.text("testing component"))
  799. comp.render()
  800. assert (
  801. err.value.args[0]
  802. == f"The component `{component.__name__}` cannot have `Text` as a child component"
  803. )
  804. def test_unsupported_parent_components(component5):
  805. """Test that a value error is raised when an component is not in _valid_parents of one of its children.
  806. Args:
  807. component5: component with valid parent of "Text" only
  808. """
  809. with pytest.raises(ValueError) as err:
  810. rx.box(component5.create())
  811. assert (
  812. err.value.args[0]
  813. == f"The component `{component5.__name__}` can only be a child of the components: `{component5._valid_parents[0]}`. Got `Box` instead."
  814. )
  815. @pytest.mark.parametrize("fixture", ["component5", "component7"])
  816. def test_component_with_only_valid_children(fixture, request):
  817. """Test that a value error is raised when an unsupported component (a child component not found in the
  818. component's valid children list) is provided as a child.
  819. Args:
  820. fixture: the test component as a fixture.
  821. request: Pytest request.
  822. """
  823. component = request.getfixturevalue(fixture)
  824. with pytest.raises(ValueError) as err:
  825. comp = component.create(rx.box("testing component"))
  826. comp.render()
  827. assert (
  828. err.value.args[0]
  829. == f"The component `{component.__name__}` only allows the components: `Text` as children. "
  830. f"Got `Box` instead."
  831. )
  832. @pytest.mark.parametrize(
  833. "component,rendered",
  834. [
  835. (rx.text("hi"), "<RadixThemesText as={`p`}>\n {`hi`}\n</RadixThemesText>"),
  836. (
  837. rx.box(rc.heading("test", size="md")),
  838. "<RadixThemesBox>\n <Heading size={`md`}>\n {`test`}\n</Heading>\n</RadixThemesBox>",
  839. ),
  840. ],
  841. )
  842. def test_format_component(component, rendered):
  843. """Test that a component is formatted correctly.
  844. Args:
  845. component: The component to format.
  846. rendered: The expected rendered component.
  847. """
  848. assert str(component) == rendered
  849. def test_stateful_component(test_state):
  850. """Test that a stateful component is created correctly.
  851. Args:
  852. test_state: A test state.
  853. """
  854. text_component = rx.text(test_state.num)
  855. stateful_component = StatefulComponent.compile_from(text_component)
  856. assert isinstance(stateful_component, StatefulComponent)
  857. assert stateful_component.tag is not None
  858. assert stateful_component.tag.startswith("Text_")
  859. assert stateful_component.references == 1
  860. sc2 = StatefulComponent.compile_from(rx.text(test_state.num))
  861. assert isinstance(sc2, StatefulComponent)
  862. assert stateful_component.references == 2
  863. assert sc2.references == 2
  864. def test_stateful_component_memoize_event_trigger(test_state):
  865. """Test that a stateful component is created correctly with events.
  866. Args:
  867. test_state: A test state.
  868. """
  869. button_component = rx.button("Click me", on_click=test_state.do_something)
  870. stateful_component = StatefulComponent.compile_from(button_component)
  871. assert isinstance(stateful_component, StatefulComponent)
  872. # No event trigger? No StatefulComponent
  873. assert not isinstance(
  874. StatefulComponent.compile_from(rx.button("Click me")), StatefulComponent
  875. )
  876. def test_stateful_banner():
  877. """Test that a stateful component is created correctly with events."""
  878. connection_modal_component = rx.connection_modal()
  879. stateful_component = StatefulComponent.compile_from(connection_modal_component)
  880. assert isinstance(stateful_component, StatefulComponent)
  881. TEST_VAR = Var.create_safe("test")._replace(
  882. merge_var_data=VarData(
  883. hooks={"useTest": None},
  884. imports={"test": [ImportVar(tag="test")]},
  885. state="Test",
  886. interpolations=[],
  887. )
  888. )
  889. FORMATTED_TEST_VAR = Var.create(f"foo{TEST_VAR}bar")
  890. STYLE_VAR = TEST_VAR._replace(_var_name="style", _var_is_local=False)
  891. EVENT_CHAIN_VAR = TEST_VAR._replace(_var_type=EventChain)
  892. ARG_VAR = Var.create("arg")
  893. TEST_VAR_DICT_OF_DICT = Var.create_safe({"a": {"b": "test"}})._replace(
  894. merge_var_data=TEST_VAR._var_data
  895. )
  896. FORMATTED_TEST_VAR_DICT_OF_DICT = Var.create_safe({"a": {"b": f"footestbar"}})._replace(
  897. merge_var_data=TEST_VAR._var_data
  898. )
  899. TEST_VAR_LIST_OF_LIST = Var.create_safe([["test"]])._replace(
  900. merge_var_data=TEST_VAR._var_data
  901. )
  902. FORMATTED_TEST_VAR_LIST_OF_LIST = Var.create_safe([["footestbar"]])._replace(
  903. merge_var_data=TEST_VAR._var_data
  904. )
  905. TEST_VAR_LIST_OF_LIST_OF_LIST = Var.create_safe([[["test"]]])._replace(
  906. merge_var_data=TEST_VAR._var_data
  907. )
  908. FORMATTED_TEST_VAR_LIST_OF_LIST_OF_LIST = Var.create_safe([[["footestbar"]]])._replace(
  909. merge_var_data=TEST_VAR._var_data
  910. )
  911. TEST_VAR_LIST_OF_DICT = Var.create_safe([{"a": "test"}])._replace(
  912. merge_var_data=TEST_VAR._var_data
  913. )
  914. FORMATTED_TEST_VAR_LIST_OF_DICT = Var.create_safe([{"a": "footestbar"}])._replace(
  915. merge_var_data=TEST_VAR._var_data
  916. )
  917. class ComponentNestedVar(Component):
  918. """A component with nested Var types."""
  919. dict_of_dict: Var[Dict[str, Dict[str, str]]]
  920. list_of_list: Var[List[List[str]]]
  921. list_of_list_of_list: Var[List[List[List[str]]]]
  922. list_of_dict: Var[List[Dict[str, str]]]
  923. class EventState(rx.State):
  924. """State for testing event handlers with _get_vars."""
  925. v: int = 42
  926. def handler(self):
  927. """A handler that does nothing."""
  928. def handler2(self, arg):
  929. """A handler that takes an arg.
  930. Args:
  931. arg: An arg.
  932. """
  933. @pytest.mark.parametrize(
  934. ("component", "exp_vars"),
  935. (
  936. pytest.param(
  937. Bare.create(TEST_VAR),
  938. [TEST_VAR],
  939. id="direct-bare",
  940. ),
  941. pytest.param(
  942. Bare.create(f"foo{TEST_VAR}bar"),
  943. [FORMATTED_TEST_VAR],
  944. id="fstring-bare",
  945. ),
  946. pytest.param(
  947. rx.text(as_=TEST_VAR),
  948. [TEST_VAR],
  949. id="direct-prop",
  950. ),
  951. pytest.param(
  952. rx.heading(as_=f"foo{TEST_VAR}bar"),
  953. [FORMATTED_TEST_VAR],
  954. id="fstring-prop",
  955. ),
  956. pytest.param(
  957. rx.fragment(id=TEST_VAR),
  958. [TEST_VAR],
  959. id="direct-id",
  960. ),
  961. pytest.param(
  962. rx.fragment(id=f"foo{TEST_VAR}bar"),
  963. [FORMATTED_TEST_VAR],
  964. id="fstring-id",
  965. ),
  966. pytest.param(
  967. rx.fragment(key=TEST_VAR),
  968. [TEST_VAR],
  969. id="direct-key",
  970. ),
  971. pytest.param(
  972. rx.fragment(key=f"foo{TEST_VAR}bar"),
  973. [FORMATTED_TEST_VAR],
  974. id="fstring-key",
  975. ),
  976. pytest.param(
  977. rx.fragment(class_name=TEST_VAR),
  978. [TEST_VAR],
  979. id="direct-class_name",
  980. ),
  981. pytest.param(
  982. rx.fragment(class_name=f"foo{TEST_VAR}bar"),
  983. [FORMATTED_TEST_VAR],
  984. id="fstring-class_name",
  985. ),
  986. pytest.param(
  987. rx.fragment(special_props={TEST_VAR}),
  988. [TEST_VAR],
  989. id="direct-special_props",
  990. ),
  991. pytest.param(
  992. rx.fragment(special_props={Var.create(f"foo{TEST_VAR}bar")}),
  993. [FORMATTED_TEST_VAR],
  994. id="fstring-special_props",
  995. ),
  996. pytest.param(
  997. # custom_attrs cannot accept a Var directly as a value
  998. rx.fragment(custom_attrs={"href": f"{TEST_VAR}"}),
  999. [TEST_VAR],
  1000. id="fstring-custom_attrs-nofmt",
  1001. ),
  1002. pytest.param(
  1003. rx.fragment(custom_attrs={"href": f"foo{TEST_VAR}bar"}),
  1004. [FORMATTED_TEST_VAR],
  1005. id="fstring-custom_attrs",
  1006. ),
  1007. pytest.param(
  1008. rx.fragment(background_color=TEST_VAR),
  1009. [STYLE_VAR],
  1010. id="direct-background_color",
  1011. ),
  1012. pytest.param(
  1013. rx.fragment(background_color=f"foo{TEST_VAR}bar"),
  1014. [STYLE_VAR],
  1015. id="fstring-background_color",
  1016. ),
  1017. pytest.param(
  1018. rx.fragment(style={"background_color": TEST_VAR}), # type: ignore
  1019. [STYLE_VAR],
  1020. id="direct-style-background_color",
  1021. ),
  1022. pytest.param(
  1023. rx.fragment(style={"background_color": f"foo{TEST_VAR}bar"}), # type: ignore
  1024. [STYLE_VAR],
  1025. id="fstring-style-background_color",
  1026. ),
  1027. pytest.param(
  1028. rx.fragment(on_click=EVENT_CHAIN_VAR), # type: ignore
  1029. [EVENT_CHAIN_VAR],
  1030. id="direct-event-chain",
  1031. ),
  1032. pytest.param(
  1033. rx.fragment(on_click=EventState.handler),
  1034. [],
  1035. id="direct-event-handler",
  1036. ),
  1037. pytest.param(
  1038. rx.fragment(on_click=EventState.handler2(TEST_VAR)), # type: ignore
  1039. [ARG_VAR, TEST_VAR],
  1040. id="direct-event-handler-arg",
  1041. ),
  1042. pytest.param(
  1043. rx.fragment(on_click=EventState.handler2(EventState.v)), # type: ignore
  1044. [ARG_VAR, EventState.v],
  1045. id="direct-event-handler-arg2",
  1046. ),
  1047. pytest.param(
  1048. rx.fragment(on_click=lambda: EventState.handler2(TEST_VAR)), # type: ignore
  1049. [ARG_VAR, TEST_VAR],
  1050. id="direct-event-handler-lambda",
  1051. ),
  1052. pytest.param(
  1053. ComponentNestedVar.create(dict_of_dict={"a": {"b": TEST_VAR}}),
  1054. [TEST_VAR_DICT_OF_DICT],
  1055. id="direct-dict_of_dict",
  1056. ),
  1057. pytest.param(
  1058. ComponentNestedVar.create(dict_of_dict={"a": {"b": f"foo{TEST_VAR}bar"}}),
  1059. [FORMATTED_TEST_VAR_DICT_OF_DICT],
  1060. id="fstring-dict_of_dict",
  1061. ),
  1062. pytest.param(
  1063. ComponentNestedVar.create(list_of_list=[[TEST_VAR]]),
  1064. [TEST_VAR_LIST_OF_LIST],
  1065. id="direct-list_of_list",
  1066. ),
  1067. pytest.param(
  1068. ComponentNestedVar.create(list_of_list=[[f"foo{TEST_VAR}bar"]]),
  1069. [FORMATTED_TEST_VAR_LIST_OF_LIST],
  1070. id="fstring-list_of_list",
  1071. ),
  1072. pytest.param(
  1073. ComponentNestedVar.create(list_of_list_of_list=[[[TEST_VAR]]]),
  1074. [TEST_VAR_LIST_OF_LIST_OF_LIST],
  1075. id="direct-list_of_list_of_list",
  1076. ),
  1077. pytest.param(
  1078. ComponentNestedVar.create(list_of_list_of_list=[[[f"foo{TEST_VAR}bar"]]]),
  1079. [FORMATTED_TEST_VAR_LIST_OF_LIST_OF_LIST],
  1080. id="fstring-list_of_list_of_list",
  1081. ),
  1082. pytest.param(
  1083. ComponentNestedVar.create(list_of_dict=[{"a": TEST_VAR}]),
  1084. [TEST_VAR_LIST_OF_DICT],
  1085. id="direct-list_of_dict",
  1086. ),
  1087. pytest.param(
  1088. ComponentNestedVar.create(list_of_dict=[{"a": f"foo{TEST_VAR}bar"}]),
  1089. [FORMATTED_TEST_VAR_LIST_OF_DICT],
  1090. id="fstring-list_of_dict",
  1091. ),
  1092. ),
  1093. )
  1094. def test_get_vars(component, exp_vars):
  1095. comp_vars = sorted(component._get_vars(), key=lambda v: v._var_name)
  1096. assert len(comp_vars) == len(exp_vars)
  1097. for comp_var, exp_var in zip(
  1098. comp_vars,
  1099. sorted(exp_vars, key=lambda v: v._var_name),
  1100. ):
  1101. assert comp_var.equals(exp_var)
  1102. def test_instantiate_all_components():
  1103. """Test that all components can be instantiated."""
  1104. # These components all have required arguments and cannot be trivially instantiated.
  1105. untested_components = {
  1106. "Card",
  1107. "Cond",
  1108. "DebounceInput",
  1109. "Foreach",
  1110. "FormControl",
  1111. "Html",
  1112. "Icon",
  1113. "Match",
  1114. "Markdown",
  1115. "MultiSelect",
  1116. "Option",
  1117. "Popover",
  1118. "Radio",
  1119. "Script",
  1120. "Tag",
  1121. "Tfoot",
  1122. "Thead",
  1123. }
  1124. component_nested_list = [
  1125. *rx.RADIX_MAPPING.values(),
  1126. *rx.COMPONENTS_BASE_MAPPING.values(),
  1127. *rx.COMPONENTS_CORE_MAPPING.values(),
  1128. ]
  1129. for component_name in [
  1130. comp_name
  1131. for submodule_list in component_nested_list
  1132. for comp_name in submodule_list
  1133. ]: # type: ignore
  1134. if component_name in untested_components:
  1135. continue
  1136. component = getattr(
  1137. rx,
  1138. component_name
  1139. if not isinstance(component_name, tuple)
  1140. else component_name[1],
  1141. )
  1142. if isinstance(component, type) and issubclass(component, Component):
  1143. component.create()
  1144. class InvalidParentComponent(Component):
  1145. """Invalid Parent Component."""
  1146. ...
  1147. class ValidComponent1(Component):
  1148. """Test valid component."""
  1149. _valid_children = ["ValidComponent2"]
  1150. class ValidComponent2(Component):
  1151. """Test valid component."""
  1152. ...
  1153. class ValidComponent3(Component):
  1154. """Test valid component."""
  1155. _valid_parents = ["ValidComponent2"]
  1156. class ValidComponent4(Component):
  1157. """Test valid component."""
  1158. _invalid_children = ["InvalidComponent"]
  1159. class InvalidComponent(Component):
  1160. """Test invalid component."""
  1161. ...
  1162. valid_component1 = ValidComponent1.create
  1163. valid_component2 = ValidComponent2.create
  1164. invalid_component = InvalidComponent.create
  1165. valid_component3 = ValidComponent3.create
  1166. invalid_parent = InvalidParentComponent.create
  1167. valid_component4 = ValidComponent4.create
  1168. def test_validate_valid_children():
  1169. valid_component1(valid_component2())
  1170. valid_component1(
  1171. rx.fragment(valid_component2()),
  1172. )
  1173. valid_component1(
  1174. rx.fragment(
  1175. rx.fragment(
  1176. rx.fragment(valid_component2()),
  1177. ),
  1178. ),
  1179. )
  1180. valid_component1(
  1181. rx.cond( # type: ignore
  1182. True,
  1183. rx.fragment(valid_component2()),
  1184. rx.fragment(
  1185. rx.foreach(Var.create([1, 2, 3]), lambda x: valid_component2(x)) # type: ignore
  1186. ),
  1187. )
  1188. )
  1189. valid_component1(
  1190. rx.cond(
  1191. True,
  1192. valid_component2(),
  1193. rx.fragment(
  1194. rx.match(
  1195. "condition",
  1196. ("first", valid_component2()),
  1197. rx.fragment(valid_component2(rx.text("default"))),
  1198. )
  1199. ),
  1200. )
  1201. )
  1202. valid_component1(
  1203. rx.match(
  1204. "condition",
  1205. ("first", valid_component2()),
  1206. ("second", "third", rx.fragment(valid_component2())),
  1207. (
  1208. "fourth",
  1209. rx.cond(True, valid_component2(), rx.fragment(valid_component2())),
  1210. ),
  1211. (
  1212. "fifth",
  1213. rx.match(
  1214. "nested_condition",
  1215. ("nested_first", valid_component2()),
  1216. rx.fragment(valid_component2()),
  1217. ),
  1218. valid_component2(),
  1219. ),
  1220. )
  1221. )
  1222. def test_validate_valid_parents():
  1223. valid_component2(valid_component3())
  1224. valid_component2(
  1225. rx.fragment(valid_component3()),
  1226. )
  1227. valid_component1(
  1228. rx.fragment(
  1229. valid_component2(
  1230. rx.fragment(valid_component3()),
  1231. ),
  1232. ),
  1233. )
  1234. valid_component2(
  1235. rx.cond( # type: ignore
  1236. True,
  1237. rx.fragment(valid_component3()),
  1238. rx.fragment(
  1239. rx.foreach(
  1240. Var.create([1, 2, 3]), # type: ignore
  1241. lambda x: valid_component2(valid_component3(x)),
  1242. )
  1243. ),
  1244. )
  1245. )
  1246. valid_component2(
  1247. rx.cond(
  1248. True,
  1249. valid_component3(),
  1250. rx.fragment(
  1251. rx.match(
  1252. "condition",
  1253. ("first", valid_component3()),
  1254. rx.fragment(valid_component3(rx.text("default"))),
  1255. )
  1256. ),
  1257. )
  1258. )
  1259. valid_component2(
  1260. rx.match(
  1261. "condition",
  1262. ("first", valid_component3()),
  1263. ("second", "third", rx.fragment(valid_component3())),
  1264. (
  1265. "fourth",
  1266. rx.cond(True, valid_component3(), rx.fragment(valid_component3())),
  1267. ),
  1268. (
  1269. "fifth",
  1270. rx.match(
  1271. "nested_condition",
  1272. ("nested_first", valid_component3()),
  1273. rx.fragment(valid_component3()),
  1274. ),
  1275. valid_component3(),
  1276. ),
  1277. )
  1278. )
  1279. def test_validate_invalid_children():
  1280. with pytest.raises(ValueError):
  1281. valid_component4(invalid_component())
  1282. with pytest.raises(ValueError):
  1283. valid_component4(
  1284. rx.fragment(invalid_component()),
  1285. )
  1286. with pytest.raises(ValueError):
  1287. valid_component2(
  1288. rx.fragment(
  1289. valid_component4(
  1290. rx.fragment(invalid_component()),
  1291. ),
  1292. ),
  1293. )
  1294. with pytest.raises(ValueError):
  1295. valid_component4(
  1296. rx.cond( # type: ignore
  1297. True,
  1298. rx.fragment(invalid_component()),
  1299. rx.fragment(
  1300. rx.foreach(Var.create([1, 2, 3]), lambda x: invalid_component(x)) # type: ignore
  1301. ),
  1302. )
  1303. )
  1304. with pytest.raises(ValueError):
  1305. valid_component4(
  1306. rx.cond(
  1307. True,
  1308. invalid_component(),
  1309. rx.fragment(
  1310. rx.match(
  1311. "condition",
  1312. ("first", invalid_component()),
  1313. rx.fragment(invalid_component(rx.text("default"))),
  1314. )
  1315. ),
  1316. )
  1317. )
  1318. with pytest.raises(ValueError):
  1319. valid_component4(
  1320. rx.match(
  1321. "condition",
  1322. ("first", invalid_component()),
  1323. ("second", "third", rx.fragment(invalid_component())),
  1324. (
  1325. "fourth",
  1326. rx.cond(True, invalid_component(), rx.fragment(valid_component2())),
  1327. ),
  1328. (
  1329. "fifth",
  1330. rx.match(
  1331. "nested_condition",
  1332. ("nested_first", invalid_component()),
  1333. rx.fragment(invalid_component()),
  1334. ),
  1335. invalid_component(),
  1336. ),
  1337. )
  1338. )
  1339. def test_rename_props():
  1340. """Test that _rename_props works and is inherited."""
  1341. class C1(Component):
  1342. tag = "C1"
  1343. prop1: Var[str]
  1344. prop2: Var[str]
  1345. _rename_props = {"prop1": "renamed_prop1", "prop2": "renamed_prop2"}
  1346. class C2(C1):
  1347. tag = "C2"
  1348. prop3: Var[str]
  1349. _rename_props = {"prop2": "subclass_prop2", "prop3": "renamed_prop3"}
  1350. c1 = C1.create(prop1="prop1_1", prop2="prop2_1")
  1351. rendered_c1 = c1.render()
  1352. assert "renamed_prop1={`prop1_1`}" in rendered_c1["props"]
  1353. assert "renamed_prop2={`prop2_1`}" in rendered_c1["props"]
  1354. c2 = C2.create(prop1="prop1_2", prop2="prop2_2", prop3="prop3_2")
  1355. rendered_c2 = c2.render()
  1356. assert "renamed_prop1={`prop1_2`}" in rendered_c2["props"]
  1357. assert "subclass_prop2={`prop2_2`}" in rendered_c2["props"]
  1358. assert "renamed_prop3={`prop3_2`}" in rendered_c2["props"]
  1359. def test_deprecated_props(capsys):
  1360. """Assert that deprecated underscore suffix props are translated.
  1361. Args:
  1362. capsys: Pytest fixture for capturing stdout and stderr.
  1363. """
  1364. class C1(Component):
  1365. tag = "C1"
  1366. type: Var[str]
  1367. min: Var[str]
  1368. max: Var[str]
  1369. # No warnings are emitted when using the new prop names.
  1370. c1_1 = C1.create(type="type1", min="min1", max="max1")
  1371. out_err = capsys.readouterr()
  1372. assert not out_err.err
  1373. assert not out_err.out
  1374. c1_1_render = c1_1.render()
  1375. assert "type={`type1`}" in c1_1_render["props"]
  1376. assert "min={`min1`}" in c1_1_render["props"]
  1377. assert "max={`max1`}" in c1_1_render["props"]
  1378. # Deprecation warning is emitted with underscore suffix,
  1379. # but the component still works.
  1380. c1_2 = C1.create(type_="type2", min_="min2", max_="max2")
  1381. out_err = capsys.readouterr()
  1382. assert out_err.out.count("DeprecationWarning:") == 3
  1383. assert not out_err.err
  1384. c1_2_render = c1_2.render()
  1385. assert "type={`type2`}" in c1_2_render["props"]
  1386. assert "min={`min2`}" in c1_2_render["props"]
  1387. assert "max={`max2`}" in c1_2_render["props"]
  1388. class C2(Component):
  1389. tag = "C2"
  1390. type_: Var[str]
  1391. min_: Var[str]
  1392. max_: Var[str]
  1393. # No warnings are emitted if the actual prop has an underscore suffix
  1394. c2_1 = C2.create(type_="type1", min_="min1", max_="max1")
  1395. out_err = capsys.readouterr()
  1396. assert not out_err.err
  1397. assert not out_err.out
  1398. c2_1_render = c2_1.render()
  1399. assert "type={`type1`}" in c2_1_render["props"]
  1400. assert "min={`min1`}" in c2_1_render["props"]
  1401. assert "max={`max1`}" in c2_1_render["props"]
  1402. def test_custom_component_get_imports():
  1403. class Inner(Component):
  1404. tag = "Inner"
  1405. library = "inner"
  1406. class Other(Component):
  1407. tag = "Other"
  1408. library = "other"
  1409. @rx.memo
  1410. def wrapper():
  1411. return Inner.create()
  1412. @rx.memo
  1413. def outer(c: Component):
  1414. return Other.create(c)
  1415. custom_comp = wrapper()
  1416. # Inner is not imported directly, but it is imported by the custom component.
  1417. assert "inner" not in custom_comp._get_all_imports()
  1418. # The imports are only resolved during compilation.
  1419. _, _, imports_inner = compile_components(custom_comp._get_all_custom_components())
  1420. assert "inner" in imports_inner
  1421. outer_comp = outer(c=wrapper())
  1422. # Libraries are not imported directly, but are imported by the custom component.
  1423. assert "inner" not in outer_comp._get_all_imports()
  1424. assert "other" not in outer_comp._get_all_imports()
  1425. # The imports are only resolved during compilation.
  1426. _, _, imports_outer = compile_components(outer_comp._get_all_custom_components())
  1427. assert "inner" in imports_outer
  1428. assert "other" in imports_outer
  1429. def test_custom_component_declare_event_handlers_in_fields():
  1430. class ReferenceComponent(Component):
  1431. def get_event_triggers(self) -> Dict[str, Any]:
  1432. """Test controlled triggers.
  1433. Returns:
  1434. Test controlled triggers.
  1435. """
  1436. return {
  1437. **super().get_event_triggers(),
  1438. "on_a": lambda e0: [e0],
  1439. "on_b": lambda e0: [e0.target.value],
  1440. "on_c": lambda e0: [],
  1441. "on_d": lambda: [],
  1442. "on_e": lambda: [],
  1443. "on_f": lambda a, b, c: [c, b, a],
  1444. }
  1445. class TestComponent(Component):
  1446. on_a: EventHandler[lambda e0: [e0]]
  1447. on_b: EventHandler[lambda e0: [e0.target.value]]
  1448. on_c: EventHandler[lambda e0: []]
  1449. on_d: EventHandler[lambda: []]
  1450. on_e: EventHandler
  1451. on_f: EventHandler[lambda a, b, c: [c, b, a]]
  1452. custom_component = ReferenceComponent.create()
  1453. test_component = TestComponent.create()
  1454. custom_triggers = custom_component.get_event_triggers()
  1455. test_triggers = test_component.get_event_triggers()
  1456. assert custom_triggers.keys() == test_triggers.keys()
  1457. for trigger_name in custom_component.get_event_triggers():
  1458. for v1, v2 in zip(
  1459. parse_args_spec(test_triggers[trigger_name]),
  1460. parse_args_spec(custom_triggers[trigger_name]),
  1461. ):
  1462. assert v1.equals(v2)
  1463. def test_invalid_event_trigger():
  1464. class TriggerComponent(Component):
  1465. on_push: Var[bool]
  1466. def get_event_triggers(self) -> Dict[str, Any]:
  1467. """Test controlled triggers.
  1468. Returns:
  1469. Test controlled triggers.
  1470. """
  1471. return {
  1472. **super().get_event_triggers(),
  1473. "on_a": lambda: [],
  1474. }
  1475. trigger_comp = TriggerComponent.create
  1476. # test that these do not throw errors.
  1477. trigger_comp(on_push=True)
  1478. trigger_comp(on_a=rx.console_log("log"))
  1479. with pytest.raises(ValueError):
  1480. trigger_comp(on_b=rx.console_log("log"))
  1481. @pytest.mark.parametrize(
  1482. "tags",
  1483. (
  1484. ["Component"],
  1485. ["Component", "useState"],
  1486. [ImportVar(tag="Component")],
  1487. [ImportVar(tag="Component"), ImportVar(tag="useState")],
  1488. ["Component", ImportVar(tag="useState")],
  1489. ),
  1490. )
  1491. def test_component_add_imports(tags):
  1492. class BaseComponent(Component):
  1493. def _get_imports(self) -> ImportDict:
  1494. return {}
  1495. class Reference(Component):
  1496. def _get_imports(self) -> ParsedImportDict:
  1497. return imports.merge_imports(
  1498. super()._get_imports(),
  1499. parse_imports({"react": tags}),
  1500. {"foo": [ImportVar(tag="bar")]},
  1501. )
  1502. class TestBase(Component):
  1503. def add_imports(
  1504. self,
  1505. ) -> Dict[str, Union[str, ImportVar, List[str], List[ImportVar]]]:
  1506. return {"foo": "bar"}
  1507. class Test(TestBase):
  1508. def add_imports(
  1509. self,
  1510. ) -> Dict[str, Union[str, ImportVar, List[str], List[ImportVar]]]:
  1511. return {"react": (tags[0] if len(tags) == 1 else tags)}
  1512. baseline = Reference.create()
  1513. test = Test.create()
  1514. assert baseline._get_all_imports() == parse_imports(
  1515. {
  1516. "react": tags,
  1517. "foo": [ImportVar(tag="bar")],
  1518. }
  1519. )
  1520. assert test._get_all_imports() == baseline._get_all_imports()
  1521. def test_component_add_hooks():
  1522. class BaseComponent(Component):
  1523. def _get_hooks(self):
  1524. return "const hook1 = 42"
  1525. class ChildComponent1(BaseComponent):
  1526. pass
  1527. class GrandchildComponent1(ChildComponent1):
  1528. def add_hooks(self):
  1529. return [
  1530. "const hook2 = 43",
  1531. "const hook3 = 44",
  1532. ]
  1533. class GreatGrandchildComponent1(GrandchildComponent1):
  1534. def add_hooks(self):
  1535. return [
  1536. "const hook4 = 45",
  1537. ]
  1538. class GrandchildComponent2(ChildComponent1):
  1539. def _get_hooks(self):
  1540. return "const hook5 = 46"
  1541. class GreatGrandchildComponent2(GrandchildComponent2):
  1542. def add_hooks(self):
  1543. return [
  1544. "const hook2 = 43",
  1545. "const hook6 = 47",
  1546. ]
  1547. assert list(BaseComponent()._get_all_hooks()) == ["const hook1 = 42"]
  1548. assert list(ChildComponent1()._get_all_hooks()) == ["const hook1 = 42"]
  1549. assert list(GrandchildComponent1()._get_all_hooks()) == [
  1550. "const hook1 = 42",
  1551. "const hook2 = 43",
  1552. "const hook3 = 44",
  1553. ]
  1554. assert list(GreatGrandchildComponent1()._get_all_hooks()) == [
  1555. "const hook1 = 42",
  1556. "const hook2 = 43",
  1557. "const hook3 = 44",
  1558. "const hook4 = 45",
  1559. ]
  1560. assert list(GrandchildComponent2()._get_all_hooks()) == ["const hook5 = 46"]
  1561. assert list(GreatGrandchildComponent2()._get_all_hooks()) == [
  1562. "const hook5 = 46",
  1563. "const hook2 = 43",
  1564. "const hook6 = 47",
  1565. ]
  1566. assert list(
  1567. BaseComponent.create(
  1568. GrandchildComponent1.create(GreatGrandchildComponent2()),
  1569. GreatGrandchildComponent1(),
  1570. )._get_all_hooks(),
  1571. ) == [
  1572. "const hook1 = 42",
  1573. "const hook2 = 43",
  1574. "const hook3 = 44",
  1575. "const hook5 = 46",
  1576. "const hook6 = 47",
  1577. "const hook4 = 45",
  1578. ]
  1579. assert list(
  1580. Fragment.create(
  1581. GreatGrandchildComponent2(),
  1582. GreatGrandchildComponent1(),
  1583. )._get_all_hooks()
  1584. ) == [
  1585. "const hook5 = 46",
  1586. "const hook2 = 43",
  1587. "const hook6 = 47",
  1588. "const hook1 = 42",
  1589. "const hook3 = 44",
  1590. "const hook4 = 45",
  1591. ]
  1592. def test_component_add_custom_code():
  1593. class BaseComponent(Component):
  1594. def _get_custom_code(self):
  1595. return "const custom_code1 = 42"
  1596. class ChildComponent1(BaseComponent):
  1597. pass
  1598. class GrandchildComponent1(ChildComponent1):
  1599. def add_custom_code(self):
  1600. return [
  1601. "const custom_code2 = 43",
  1602. "const custom_code3 = 44",
  1603. ]
  1604. class GreatGrandchildComponent1(GrandchildComponent1):
  1605. def add_custom_code(self):
  1606. return [
  1607. "const custom_code4 = 45",
  1608. ]
  1609. class GrandchildComponent2(ChildComponent1):
  1610. def _get_custom_code(self):
  1611. return "const custom_code5 = 46"
  1612. class GreatGrandchildComponent2(GrandchildComponent2):
  1613. def add_custom_code(self):
  1614. return [
  1615. "const custom_code2 = 43",
  1616. "const custom_code6 = 47",
  1617. ]
  1618. assert BaseComponent()._get_all_custom_code() == {"const custom_code1 = 42"}
  1619. assert ChildComponent1()._get_all_custom_code() == {"const custom_code1 = 42"}
  1620. assert GrandchildComponent1()._get_all_custom_code() == {
  1621. "const custom_code1 = 42",
  1622. "const custom_code2 = 43",
  1623. "const custom_code3 = 44",
  1624. }
  1625. assert GreatGrandchildComponent1()._get_all_custom_code() == {
  1626. "const custom_code1 = 42",
  1627. "const custom_code2 = 43",
  1628. "const custom_code3 = 44",
  1629. "const custom_code4 = 45",
  1630. }
  1631. assert GrandchildComponent2()._get_all_custom_code() == {"const custom_code5 = 46"}
  1632. assert GreatGrandchildComponent2()._get_all_custom_code() == {
  1633. "const custom_code2 = 43",
  1634. "const custom_code5 = 46",
  1635. "const custom_code6 = 47",
  1636. }
  1637. assert BaseComponent.create(
  1638. GrandchildComponent1.create(GreatGrandchildComponent2()),
  1639. GreatGrandchildComponent1(),
  1640. )._get_all_custom_code() == {
  1641. "const custom_code1 = 42",
  1642. "const custom_code2 = 43",
  1643. "const custom_code3 = 44",
  1644. "const custom_code4 = 45",
  1645. "const custom_code5 = 46",
  1646. "const custom_code6 = 47",
  1647. }
  1648. assert Fragment.create(
  1649. GreatGrandchildComponent2(),
  1650. GreatGrandchildComponent1(),
  1651. )._get_all_custom_code() == {
  1652. "const custom_code1 = 42",
  1653. "const custom_code2 = 43",
  1654. "const custom_code3 = 44",
  1655. "const custom_code4 = 45",
  1656. "const custom_code5 = 46",
  1657. "const custom_code6 = 47",
  1658. }
  1659. def test_component_add_hooks_var():
  1660. class HookComponent(Component):
  1661. def add_hooks(self):
  1662. return [
  1663. "const hook3 = useRef(null)",
  1664. "const hook1 = 42",
  1665. Var.create(
  1666. "useEffect(() => () => {}, [])",
  1667. _var_data=VarData(
  1668. hooks={
  1669. "const hook2 = 43": None,
  1670. "const hook3 = useRef(null)": None,
  1671. },
  1672. imports={"react": [ImportVar(tag="useEffect")]},
  1673. ),
  1674. ),
  1675. Var.create(
  1676. "const hook3 = useRef(null)",
  1677. _var_data=VarData(
  1678. imports={"react": [ImportVar(tag="useRef")]},
  1679. ),
  1680. ),
  1681. ]
  1682. assert list(HookComponent()._get_all_hooks()) == [
  1683. "const hook3 = useRef(null)",
  1684. "const hook1 = 42",
  1685. "const hook2 = 43",
  1686. "useEffect(() => () => {}, [])",
  1687. ]
  1688. imports = HookComponent()._get_all_imports()
  1689. assert len(imports) == 1
  1690. assert "react" in imports
  1691. assert len(imports["react"]) == 2
  1692. assert ImportVar(tag="useRef") in imports["react"]
  1693. assert ImportVar(tag="useEffect") in imports["react"]
  1694. def test_add_style_embedded_vars(test_state: BaseState):
  1695. """Test that add_style works with embedded vars when returning a plain dict.
  1696. Args:
  1697. test_state: A test state.
  1698. """
  1699. v0 = Var.create_safe("parent")._replace(
  1700. merge_var_data=VarData(hooks={"useParent": None}), # type: ignore
  1701. )
  1702. v1 = rx.color("plum", 10)
  1703. v2 = Var.create_safe("text")._replace(
  1704. merge_var_data=VarData(hooks={"useText": None}), # type: ignore
  1705. )
  1706. class ParentComponent(Component):
  1707. def add_style(self):
  1708. return Style(
  1709. {
  1710. "fake_parent": v0,
  1711. }
  1712. )
  1713. class StyledComponent(ParentComponent):
  1714. tag = "StyledComponent"
  1715. def add_style(self):
  1716. return {
  1717. "color": v1,
  1718. "fake": v2,
  1719. "margin": f"{test_state.num}%",
  1720. }
  1721. page = rx.vstack(StyledComponent.create())
  1722. page._add_style_recursive(Style())
  1723. assert (
  1724. f"const {test_state.get_name()} = useContext(StateContexts.{test_state.get_name()})"
  1725. in page._get_all_hooks_internal()
  1726. )
  1727. assert "useText" in page._get_all_hooks_internal()
  1728. assert "useParent" in page._get_all_hooks_internal()
  1729. assert (
  1730. str(page).count(
  1731. f'css={{{{"fakeParent": "parent", "color": "var(--plum-10)", "fake": "text", "margin": `${{{test_state.get_name()}.num}}%`}}}}'
  1732. )
  1733. == 1
  1734. )
  1735. def test_add_style_foreach():
  1736. class StyledComponent(Component):
  1737. tag = "StyledComponent"
  1738. ix: Var[int]
  1739. def add_style(self):
  1740. return Style({"color": "red"})
  1741. page = rx.vstack(rx.foreach(Var.range(3), lambda i: StyledComponent.create(i)))
  1742. page._add_style_recursive(Style())
  1743. # Expect only a single child of the foreach on the python side
  1744. assert len(page.children[0].children) == 1
  1745. # Expect the style to be added to the child of the foreach
  1746. assert 'css={{"color": "red"}}' in str(page.children[0].children[0])
  1747. # Expect only one instance of this CSS dict in the rendered page
  1748. assert str(page).count('css={{"color": "red"}}') == 1
  1749. class TriggerState(rx.State):
  1750. """Test state with event handlers."""
  1751. def do_something(self):
  1752. """Sample event handler."""
  1753. pass
  1754. @pytest.mark.parametrize(
  1755. "component, output",
  1756. [
  1757. (rx.box(rx.text("random text")), False),
  1758. (
  1759. rx.box(rx.text("random text", on_click=rx.console_log("log"))),
  1760. False,
  1761. ),
  1762. (
  1763. rx.box(
  1764. rx.text("random text", on_click=TriggerState.do_something),
  1765. rx.text(
  1766. "random text",
  1767. on_click=BaseVar(_var_name="toggleColorMode", _var_type=EventChain),
  1768. ),
  1769. ),
  1770. True,
  1771. ),
  1772. (
  1773. rx.box(
  1774. rx.text("random text", on_click=rx.console_log("log")),
  1775. rx.text(
  1776. "random text",
  1777. on_click=BaseVar(_var_name="toggleColorMode", _var_type=EventChain),
  1778. ),
  1779. ),
  1780. False,
  1781. ),
  1782. (
  1783. rx.box(rx.text("random text", on_click=TriggerState.do_something)),
  1784. True,
  1785. ),
  1786. (
  1787. rx.box(
  1788. rx.text(
  1789. "random text",
  1790. on_click=[rx.console_log("log"), rx.window_alert("alert")],
  1791. ),
  1792. ),
  1793. False,
  1794. ),
  1795. (
  1796. rx.box(
  1797. rx.text(
  1798. "random text",
  1799. on_click=[rx.console_log("log"), TriggerState.do_something],
  1800. ),
  1801. ),
  1802. True,
  1803. ),
  1804. (
  1805. rx.box(
  1806. rx.text(
  1807. "random text",
  1808. on_blur=lambda: TriggerState.do_something,
  1809. ),
  1810. ),
  1811. True,
  1812. ),
  1813. ],
  1814. )
  1815. def test_has_state_event_triggers(component, output):
  1816. assert component._has_stateful_event_triggers() == output