test_app.py 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844
  1. from __future__ import annotations
  2. import functools
  3. import io
  4. import json
  5. import os.path
  6. import re
  7. import unittest.mock
  8. import uuid
  9. from collections.abc import Generator
  10. from contextlib import nullcontext as does_not_raise
  11. from pathlib import Path
  12. from unittest.mock import AsyncMock
  13. import pytest
  14. import sqlmodel
  15. from fastapi.responses import StreamingResponse
  16. from pytest_mock import MockerFixture
  17. from starlette.applications import Starlette
  18. from starlette.datastructures import UploadFile
  19. from starlette_admin.auth import AuthProvider
  20. from starlette_admin.contrib.sqla.admin import Admin
  21. from starlette_admin.contrib.sqla.view import ModelView
  22. import reflex as rx
  23. from reflex import AdminDash, constants
  24. from reflex.app import (
  25. App,
  26. ComponentCallable,
  27. OverlayFragment,
  28. default_overlay_component,
  29. process,
  30. upload,
  31. )
  32. from reflex.components import Component
  33. from reflex.components.base.bare import Bare
  34. from reflex.components.base.fragment import Fragment
  35. from reflex.components.core.cond import Cond
  36. from reflex.components.radix.themes.typography.text import Text
  37. from reflex.event import Event
  38. from reflex.middleware import HydrateMiddleware
  39. from reflex.model import Model
  40. from reflex.state import (
  41. BaseState,
  42. OnLoadInternalState,
  43. RouterData,
  44. State,
  45. StateManagerDisk,
  46. StateManagerMemory,
  47. StateManagerRedis,
  48. StateUpdate,
  49. _substate_key,
  50. )
  51. from reflex.style import Style
  52. from reflex.utils import console, exceptions, format
  53. from reflex.vars.base import computed_var
  54. from .conftest import chdir
  55. from .states import (
  56. ChildFileUploadState,
  57. FileStateBase1,
  58. FileUploadState,
  59. GenState,
  60. GrandChildFileUploadState,
  61. )
  62. class EmptyState(BaseState):
  63. """An empty state."""
  64. pass
  65. @pytest.fixture
  66. def index_page() -> ComponentCallable:
  67. """An index page.
  68. Returns:
  69. The index page.
  70. """
  71. def index():
  72. return rx.box("Index")
  73. return index
  74. @pytest.fixture
  75. def about_page() -> ComponentCallable:
  76. """An about page.
  77. Returns:
  78. The about page.
  79. """
  80. def about():
  81. return rx.box("About")
  82. return about
  83. class ATestState(BaseState):
  84. """A simple state for testing."""
  85. var: int
  86. @pytest.fixture()
  87. def test_state() -> type[BaseState]:
  88. """A default state.
  89. Returns:
  90. A default state.
  91. """
  92. return ATestState
  93. @pytest.fixture()
  94. def redundant_test_state() -> type[BaseState]:
  95. """A default state.
  96. Returns:
  97. A default state.
  98. """
  99. class RedundantTestState(BaseState):
  100. var: int
  101. return RedundantTestState
  102. @pytest.fixture(scope="session")
  103. def test_model() -> type[Model]:
  104. """A default model.
  105. Returns:
  106. A default model.
  107. """
  108. class TestModel(Model, table=True):
  109. pass
  110. return TestModel
  111. @pytest.fixture(scope="session")
  112. def test_model_auth() -> type[Model]:
  113. """A default model.
  114. Returns:
  115. A default model.
  116. """
  117. class TestModelAuth(Model, table=True):
  118. """A test model with auth."""
  119. pass
  120. return TestModelAuth
  121. @pytest.fixture()
  122. def test_get_engine():
  123. """A default database engine.
  124. Returns:
  125. A default database engine.
  126. """
  127. enable_admin = True
  128. url = "sqlite:///test.db"
  129. return sqlmodel.create_engine(
  130. url,
  131. echo=False,
  132. connect_args={"check_same_thread": False} if enable_admin else {},
  133. )
  134. @pytest.fixture()
  135. def test_custom_auth_admin() -> type[AuthProvider]:
  136. """A default auth provider.
  137. Returns:
  138. A default default auth provider.
  139. """
  140. class TestAuthProvider(AuthProvider):
  141. """A test auth provider."""
  142. login_path: str = "/login"
  143. logout_path: str = "/logout"
  144. def login(self): # pyright: ignore [reportIncompatibleMethodOverride]
  145. """Login."""
  146. pass
  147. def is_authenticated(self): # pyright: ignore [reportIncompatibleMethodOverride]
  148. """Is authenticated."""
  149. pass
  150. def get_admin_user(self): # pyright: ignore [reportIncompatibleMethodOverride]
  151. """Get admin user."""
  152. pass
  153. def logout(self): # pyright: ignore [reportIncompatibleMethodOverride]
  154. """Logout."""
  155. pass
  156. return TestAuthProvider
  157. def test_default_app(app: App):
  158. """Test creating an app with no args.
  159. Args:
  160. app: The app to test.
  161. """
  162. assert app._middlewares == [HydrateMiddleware()]
  163. assert app.style == Style()
  164. assert app.admin_dash is None
  165. def test_multiple_states_error(monkeypatch, test_state, redundant_test_state):
  166. """Test that an error is thrown when multiple classes subclass rx.BaseState.
  167. Args:
  168. monkeypatch: Pytest monkeypatch object.
  169. test_state: A test state subclassing rx.BaseState.
  170. redundant_test_state: Another test state subclassing rx.BaseState.
  171. """
  172. monkeypatch.delenv(constants.PYTEST_CURRENT_TEST)
  173. with pytest.raises(ValueError):
  174. App()
  175. def test_add_page_default_route(app: App, index_page, about_page):
  176. """Test adding a page to an app.
  177. Args:
  178. app: The app to test.
  179. index_page: The index page.
  180. about_page: The about page.
  181. """
  182. assert app._pages == {}
  183. assert app._unevaluated_pages == {}
  184. app.add_page(index_page)
  185. app._compile_page("index")
  186. assert app._pages.keys() == {"index"}
  187. app.add_page(about_page)
  188. app._compile_page("about")
  189. assert app._pages.keys() == {"index", "about"}
  190. def test_add_page_set_route(app: App, index_page, windows_platform: bool):
  191. """Test adding a page to an app.
  192. Args:
  193. app: The app to test.
  194. index_page: The index page.
  195. windows_platform: Whether the system is windows.
  196. """
  197. route = "test" if windows_platform else "/test"
  198. assert app._unevaluated_pages == {}
  199. app.add_page(index_page, route=route)
  200. app._compile_page("test")
  201. assert app._pages.keys() == {"test"}
  202. def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
  203. """Test adding a page with dynamic route variable to an app.
  204. Args:
  205. index_page: The index page.
  206. windows_platform: Whether the system is windows.
  207. """
  208. app = App(_state=EmptyState)
  209. assert app._state is not None
  210. route = "/test/[dynamic]"
  211. assert app._unevaluated_pages == {}
  212. app.add_page(index_page, route=route)
  213. app._compile_page("test/[dynamic]")
  214. assert app._pages.keys() == {"test/[dynamic]"}
  215. assert "dynamic" in app._state.computed_vars
  216. assert app._state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
  217. EmptyState.get_full_name(): {constants.ROUTER},
  218. }
  219. assert constants.ROUTER in app._state()._var_dependencies
  220. def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool):
  221. """Test adding a page to an app.
  222. Args:
  223. app: The app to test.
  224. index_page: The index page.
  225. windows_platform: Whether the system is windows.
  226. """
  227. route = "test\\nested" if windows_platform else "/test/nested"
  228. assert app._unevaluated_pages == {}
  229. app.add_page(index_page, route=route)
  230. assert app._unevaluated_pages.keys() == {route.strip(os.path.sep)}
  231. def test_add_page_invalid_api_route(app: App, index_page):
  232. """Test adding a page with an invalid route to an app.
  233. Args:
  234. app: The app to test.
  235. index_page: The index page.
  236. """
  237. with pytest.raises(ValueError):
  238. app.add_page(index_page, route="api")
  239. with pytest.raises(ValueError):
  240. app.add_page(index_page, route="/api")
  241. with pytest.raises(ValueError):
  242. app.add_page(index_page, route="/api/")
  243. with pytest.raises(ValueError):
  244. app.add_page(index_page, route="api/foo")
  245. with pytest.raises(ValueError):
  246. app.add_page(index_page, route="/api/foo")
  247. # These should be fine
  248. app.add_page(index_page, route="api2")
  249. app.add_page(index_page, route="/foo/api")
  250. def page1():
  251. return rx.fragment()
  252. def page2():
  253. return rx.fragment()
  254. def index():
  255. return rx.fragment()
  256. @pytest.mark.parametrize(
  257. ("first_page", "second_page", "route"),
  258. [
  259. (index, index, None),
  260. (page1, page1, None),
  261. ],
  262. )
  263. def test_add_the_same_page(
  264. mocker: MockerFixture, app: App, first_page, second_page, route
  265. ):
  266. app.add_page(first_page, route=route)
  267. mock_object = mocker.Mock()
  268. mocker.patch.object(
  269. console,
  270. "warn",
  271. mock_object,
  272. )
  273. app.add_page(second_page, route="/" + route.strip("/") if route else None)
  274. assert mock_object.call_count == 1
  275. @pytest.mark.parametrize(
  276. ("first_page", "second_page", "route"),
  277. [
  278. (lambda: rx.fragment(), lambda: rx.fragment(rx.text("second")), "/"),
  279. (rx.fragment(rx.text("first")), rx.fragment(rx.text("second")), "/page1"),
  280. (
  281. lambda: rx.fragment(rx.text("first")),
  282. rx.fragment(rx.text("second")),
  283. "page3",
  284. ),
  285. (page1, page2, "page1"),
  286. ],
  287. )
  288. def test_add_duplicate_page_route_error(app: App, first_page, second_page, route):
  289. app.add_page(first_page, route=route)
  290. with pytest.raises(ValueError):
  291. app.add_page(second_page, route="/" + route.strip("/") if route else None)
  292. def test_initialize_with_admin_dashboard(test_model):
  293. """Test setting the admin dashboard of an app.
  294. Args:
  295. test_model: The default model.
  296. """
  297. app = App(admin_dash=AdminDash(models=[test_model]))
  298. assert app.admin_dash is not None
  299. assert len(app.admin_dash.models) > 0
  300. assert app.admin_dash.models[0] == test_model
  301. def test_initialize_with_custom_admin_dashboard(
  302. test_get_engine,
  303. test_custom_auth_admin,
  304. test_model_auth,
  305. ):
  306. """Test setting the custom admin dashboard of an app.
  307. Args:
  308. test_get_engine: The default database engine.
  309. test_model_auth: The default model for an auth admin dashboard.
  310. test_custom_auth_admin: The custom auth provider.
  311. """
  312. custom_auth_provider = test_custom_auth_admin()
  313. custom_admin = Admin(engine=test_get_engine, auth_provider=custom_auth_provider)
  314. app = App(admin_dash=AdminDash(models=[test_model_auth], admin=custom_admin))
  315. assert app.admin_dash is not None
  316. assert app.admin_dash.admin is not None
  317. assert len(app.admin_dash.models) > 0
  318. assert app.admin_dash.models[0] == test_model_auth
  319. assert app.admin_dash.admin.auth_provider == custom_auth_provider
  320. def test_initialize_admin_dashboard_with_view_overrides(test_model):
  321. """Test setting the admin dashboard of an app with view class overridden.
  322. Args:
  323. test_model: The default model.
  324. """
  325. class TestModelView(ModelView):
  326. pass
  327. app = App(
  328. admin_dash=AdminDash(
  329. models=[test_model], view_overrides={test_model: TestModelView}
  330. )
  331. )
  332. assert app.admin_dash is not None
  333. assert app.admin_dash.models == [test_model]
  334. assert app.admin_dash.view_overrides[test_model] == TestModelView
  335. @pytest.mark.asyncio
  336. async def test_initialize_with_state(test_state: type[ATestState], token: str):
  337. """Test setting the state of an app.
  338. Args:
  339. test_state: The default state.
  340. token: a Token.
  341. """
  342. app = App(_state=test_state)
  343. assert app._state == test_state
  344. # Get a state for a given token.
  345. state = await app.state_manager.get_state(_substate_key(token, test_state))
  346. assert isinstance(state, test_state)
  347. assert state.var == 0
  348. if isinstance(app.state_manager, StateManagerRedis):
  349. await app.state_manager.close()
  350. @pytest.mark.asyncio
  351. async def test_set_and_get_state(test_state):
  352. """Test setting and getting the state of an app with different tokens.
  353. Args:
  354. test_state: The default state.
  355. """
  356. app = App(_state=test_state)
  357. # Create two tokens.
  358. token1 = str(uuid.uuid4()) + f"_{test_state.get_full_name()}"
  359. token2 = str(uuid.uuid4()) + f"_{test_state.get_full_name()}"
  360. # Get the default state for each token.
  361. state1 = await app.state_manager.get_state(token1)
  362. state2 = await app.state_manager.get_state(token2)
  363. assert state1.var == 0
  364. assert state2.var == 0
  365. # Set the vars to different values.
  366. state1.var = 1
  367. state2.var = 2
  368. await app.state_manager.set_state(token1, state1)
  369. await app.state_manager.set_state(token2, state2)
  370. # Get the states again and check the values.
  371. state1 = await app.state_manager.get_state(token1)
  372. state2 = await app.state_manager.get_state(token2)
  373. assert state1.var == 1
  374. assert state2.var == 2
  375. if isinstance(app.state_manager, StateManagerRedis):
  376. await app.state_manager.close()
  377. @pytest.mark.asyncio
  378. async def test_dynamic_var_event(test_state: type[ATestState], token: str):
  379. """Test that the default handler of a dynamic generated var
  380. works as expected.
  381. Args:
  382. test_state: State Fixture.
  383. token: a Token.
  384. """
  385. state = test_state() # pyright: ignore [reportCallIssue]
  386. state.add_var("int_val", int, 0)
  387. async for result in state._process(
  388. Event(
  389. token=token,
  390. name=f"{test_state.get_name()}.set_int_val",
  391. router_data={"pathname": "/", "query": {}},
  392. payload={"value": 50},
  393. )
  394. ):
  395. assert result.delta == {test_state.get_name(): {"int_val_rx_state_": 50}}
  396. @pytest.mark.asyncio
  397. @pytest.mark.parametrize(
  398. "event_tuples",
  399. [
  400. pytest.param(
  401. [
  402. (
  403. "make_friend",
  404. {"plain_friends_rx_state_": ["Tommy", "another-fd"]},
  405. ),
  406. (
  407. "change_first_friend",
  408. {"plain_friends_rx_state_": ["Jenny", "another-fd"]},
  409. ),
  410. ],
  411. id="append then __setitem__",
  412. ),
  413. pytest.param(
  414. [
  415. (
  416. "unfriend_first_friend",
  417. {"plain_friends_rx_state_": []},
  418. ),
  419. (
  420. "make_friend",
  421. {"plain_friends_rx_state_": ["another-fd"]},
  422. ),
  423. ],
  424. id="delitem then append",
  425. ),
  426. pytest.param(
  427. [
  428. (
  429. "make_friends_with_colleagues",
  430. {"plain_friends_rx_state_": ["Tommy", "Peter", "Jimmy"]},
  431. ),
  432. (
  433. "remove_tommy",
  434. {"plain_friends_rx_state_": ["Peter", "Jimmy"]},
  435. ),
  436. (
  437. "remove_last_friend",
  438. {"plain_friends_rx_state_": ["Peter"]},
  439. ),
  440. (
  441. "unfriend_all_friends",
  442. {"plain_friends_rx_state_": []},
  443. ),
  444. ],
  445. id="extend, remove, pop, clear",
  446. ),
  447. pytest.param(
  448. [
  449. (
  450. "add_jimmy_to_second_group",
  451. {
  452. "friends_in_nested_list_rx_state_": [
  453. ["Tommy"],
  454. ["Jenny", "Jimmy"],
  455. ]
  456. },
  457. ),
  458. (
  459. "remove_first_person_from_first_group",
  460. {"friends_in_nested_list_rx_state_": [[], ["Jenny", "Jimmy"]]},
  461. ),
  462. (
  463. "remove_first_group",
  464. {"friends_in_nested_list_rx_state_": [["Jenny", "Jimmy"]]},
  465. ),
  466. ],
  467. id="nested list",
  468. ),
  469. pytest.param(
  470. [
  471. (
  472. "add_jimmy_to_tommy_friends",
  473. {"friends_in_dict_rx_state_": {"Tommy": ["Jenny", "Jimmy"]}},
  474. ),
  475. (
  476. "remove_jenny_from_tommy",
  477. {"friends_in_dict_rx_state_": {"Tommy": ["Jimmy"]}},
  478. ),
  479. (
  480. "tommy_has_no_fds",
  481. {"friends_in_dict_rx_state_": {"Tommy": []}},
  482. ),
  483. ],
  484. id="list in dict",
  485. ),
  486. ],
  487. )
  488. async def test_list_mutation_detection__plain_list(
  489. event_tuples: list[tuple[str, list[str]]],
  490. list_mutation_state: State,
  491. token: str,
  492. ):
  493. """Test list mutation detection
  494. when reassignment is not explicitly included in the logic.
  495. Args:
  496. event_tuples: From parametrization.
  497. list_mutation_state: A state with list mutation features.
  498. token: a Token.
  499. """
  500. for event_name, expected_delta in event_tuples:
  501. async for result in list_mutation_state._process(
  502. Event(
  503. token=token,
  504. name=f"{list_mutation_state.get_name()}.{event_name}",
  505. router_data={"pathname": "/", "query": {}},
  506. payload={},
  507. )
  508. ):
  509. # prefix keys in expected_delta with the state name
  510. expected_delta = {list_mutation_state.get_name(): expected_delta}
  511. assert result.delta == expected_delta
  512. @pytest.mark.asyncio
  513. @pytest.mark.parametrize(
  514. "event_tuples",
  515. [
  516. pytest.param(
  517. [
  518. (
  519. "add_age",
  520. {"details_rx_state_": {"name": "Tommy", "age": 20}},
  521. ),
  522. (
  523. "change_name",
  524. {"details_rx_state_": {"name": "Jenny", "age": 20}},
  525. ),
  526. (
  527. "remove_last_detail",
  528. {"details_rx_state_": {"name": "Jenny"}},
  529. ),
  530. ],
  531. id="update then __setitem__",
  532. ),
  533. pytest.param(
  534. [
  535. (
  536. "clear_details",
  537. {"details_rx_state_": {}},
  538. ),
  539. (
  540. "add_age",
  541. {"details_rx_state_": {"age": 20}},
  542. ),
  543. ],
  544. id="delitem then update",
  545. ),
  546. pytest.param(
  547. [
  548. (
  549. "add_age",
  550. {"details_rx_state_": {"name": "Tommy", "age": 20}},
  551. ),
  552. (
  553. "remove_name",
  554. {"details_rx_state_": {"age": 20}},
  555. ),
  556. (
  557. "pop_out_age",
  558. {"details_rx_state_": {}},
  559. ),
  560. ],
  561. id="add, remove, pop",
  562. ),
  563. pytest.param(
  564. [
  565. (
  566. "remove_home_address",
  567. {"address_rx_state_": [{}, {"work": "work address"}]},
  568. ),
  569. (
  570. "add_street_to_home_address",
  571. {
  572. "address_rx_state_": [
  573. {"street": "street address"},
  574. {"work": "work address"},
  575. ]
  576. },
  577. ),
  578. ],
  579. id="dict in list",
  580. ),
  581. pytest.param(
  582. [
  583. (
  584. "change_friend_name",
  585. {
  586. "friend_in_nested_dict_rx_state_": {
  587. "name": "Nikhil",
  588. "friend": {"name": "Tommy"},
  589. }
  590. },
  591. ),
  592. (
  593. "add_friend_age",
  594. {
  595. "friend_in_nested_dict_rx_state_": {
  596. "name": "Nikhil",
  597. "friend": {"name": "Tommy", "age": 30},
  598. }
  599. },
  600. ),
  601. (
  602. "remove_friend",
  603. {"friend_in_nested_dict_rx_state_": {"name": "Nikhil"}},
  604. ),
  605. ],
  606. id="nested dict",
  607. ),
  608. ],
  609. )
  610. async def test_dict_mutation_detection__plain_list(
  611. event_tuples: list[tuple[str, list[str]]],
  612. dict_mutation_state: State,
  613. token: str,
  614. ):
  615. """Test dict mutation detection
  616. when reassignment is not explicitly included in the logic.
  617. Args:
  618. event_tuples: From parametrization.
  619. dict_mutation_state: A state with dict mutation features.
  620. token: a Token.
  621. """
  622. for event_name, expected_delta in event_tuples:
  623. async for result in dict_mutation_state._process(
  624. Event(
  625. token=token,
  626. name=f"{dict_mutation_state.get_name()}.{event_name}",
  627. router_data={"pathname": "/", "query": {}},
  628. payload={},
  629. )
  630. ):
  631. # prefix keys in expected_delta with the state name
  632. expected_delta = {dict_mutation_state.get_name(): expected_delta}
  633. assert result.delta == expected_delta
  634. @pytest.mark.asyncio
  635. @pytest.mark.parametrize(
  636. ("state", "delta"),
  637. [
  638. (
  639. FileUploadState,
  640. {
  641. FileUploadState.get_full_name(): {
  642. "img_list_rx_state_": ["image1.jpg", "image2.jpg"]
  643. }
  644. },
  645. ),
  646. (
  647. ChildFileUploadState,
  648. {
  649. ChildFileUploadState.get_full_name(): {
  650. "img_list_rx_state_": ["image1.jpg", "image2.jpg"]
  651. }
  652. },
  653. ),
  654. (
  655. GrandChildFileUploadState,
  656. {
  657. GrandChildFileUploadState.get_full_name(): {
  658. "img_list_rx_state_": ["image1.jpg", "image2.jpg"]
  659. }
  660. },
  661. ),
  662. ],
  663. )
  664. async def test_upload_file(tmp_path, state, delta, token: str, mocker):
  665. """Test that file upload works correctly.
  666. Args:
  667. tmp_path: Temporary path.
  668. state: The state class.
  669. delta: Expected delta
  670. token: a Token.
  671. mocker: pytest mocker object.
  672. """
  673. mocker.patch(
  674. "reflex.state.State.class_subclasses",
  675. {state if state is FileUploadState else FileStateBase1},
  676. )
  677. state._tmp_path = tmp_path
  678. # The App state must be the "root" of the state tree
  679. app = App()
  680. app._enable_state()
  681. app.event_namespace.emit = AsyncMock() # pyright: ignore [reportOptionalMemberAccess]
  682. current_state = await app.state_manager.get_state(_substate_key(token, state))
  683. data = b"This is binary data"
  684. # Create a binary IO object and write data to it
  685. bio = io.BytesIO()
  686. bio.write(data)
  687. request_mock = unittest.mock.Mock()
  688. request_mock.headers = {
  689. "reflex-client-token": token,
  690. "reflex-event-handler": f"{state.get_full_name()}.multi_handle_upload",
  691. }
  692. file1 = UploadFile(
  693. filename="image1.jpg",
  694. file=bio,
  695. )
  696. file2 = UploadFile(
  697. filename="image2.jpg",
  698. file=bio,
  699. )
  700. async def form():
  701. files_mock = unittest.mock.Mock()
  702. def getlist(key: str):
  703. assert key == "files"
  704. return [file1, file2]
  705. files_mock.getlist = getlist
  706. return files_mock
  707. request_mock.form = form
  708. upload_fn = upload(app)
  709. streaming_response = await upload_fn(request_mock)
  710. assert isinstance(streaming_response, StreamingResponse)
  711. async for state_update in streaming_response.body_iterator:
  712. assert (
  713. state_update
  714. == StateUpdate(delta=delta, events=[], final=True).json() + "\n"
  715. )
  716. current_state = await app.state_manager.get_state(_substate_key(token, state))
  717. state_dict = current_state.dict()[state.get_full_name()]
  718. assert state_dict["img_list_rx_state_"] == [
  719. "image1.jpg",
  720. "image2.jpg",
  721. ]
  722. if isinstance(app.state_manager, StateManagerRedis):
  723. await app.state_manager.close()
  724. @pytest.mark.asyncio
  725. @pytest.mark.parametrize(
  726. "state",
  727. [FileUploadState, ChildFileUploadState, GrandChildFileUploadState],
  728. )
  729. async def test_upload_file_without_annotation(state, tmp_path, token):
  730. """Test that an error is thrown when there's no param annotated with rx.UploadFile or list[UploadFile].
  731. Args:
  732. state: The state class.
  733. tmp_path: Temporary path.
  734. token: a Token.
  735. """
  736. state._tmp_path = tmp_path
  737. app = App(_state=State)
  738. request_mock = unittest.mock.Mock()
  739. request_mock.headers = {
  740. "reflex-client-token": token,
  741. "reflex-event-handler": f"{state.get_full_name()}.handle_upload2",
  742. }
  743. async def form():
  744. files_mock = unittest.mock.Mock()
  745. def getlist(key: str):
  746. assert key == "files"
  747. return [unittest.mock.Mock(filename="image1.jpg")]
  748. files_mock.getlist = getlist
  749. return files_mock
  750. request_mock.form = form
  751. fn = upload(app)
  752. with pytest.raises(ValueError) as err:
  753. await fn(request_mock)
  754. assert (
  755. err.value.args[0]
  756. == f"`{state.get_full_name()}.handle_upload2` handler should have a parameter annotated as list[rx.UploadFile]"
  757. )
  758. if isinstance(app.state_manager, StateManagerRedis):
  759. await app.state_manager.close()
  760. @pytest.mark.asyncio
  761. @pytest.mark.parametrize(
  762. "state",
  763. [FileUploadState, ChildFileUploadState, GrandChildFileUploadState],
  764. )
  765. async def test_upload_file_background(state, tmp_path, token):
  766. """Test that an error is thrown handler is a background task.
  767. Args:
  768. state: The state class.
  769. tmp_path: Temporary path.
  770. token: a Token.
  771. """
  772. state._tmp_path = tmp_path
  773. app = App(_state=State)
  774. request_mock = unittest.mock.Mock()
  775. request_mock.headers = {
  776. "reflex-client-token": token,
  777. "reflex-event-handler": f"{state.get_full_name()}.bg_upload",
  778. }
  779. async def form():
  780. files_mock = unittest.mock.Mock()
  781. def getlist(key: str):
  782. assert key == "files"
  783. return [unittest.mock.Mock(filename="image1.jpg")]
  784. files_mock.getlist = getlist
  785. return files_mock
  786. request_mock.form = form
  787. fn = upload(app)
  788. with pytest.raises(TypeError) as err:
  789. await fn(request_mock)
  790. assert (
  791. err.value.args[0]
  792. == f"@rx.event(background=True) is not supported for upload handler `{state.get_full_name()}.bg_upload`."
  793. )
  794. if isinstance(app.state_manager, StateManagerRedis):
  795. await app.state_manager.close()
  796. class DynamicState(BaseState):
  797. """State class for testing dynamic route var.
  798. This is defined at module level because event handlers cannot be addressed
  799. correctly when the class is defined as a local.
  800. There are several counters:
  801. * loaded: counts how many times `on_load` was triggered by the hydrate middleware
  802. * counter: counts how many times `on_counter` was triggered by a non-navigational event
  803. -> these events should NOT trigger reload or recalculation of router_data dependent vars
  804. * side_effect_counter: counts how many times a computed var was
  805. recalculated when the dynamic route var was dirty
  806. """
  807. is_hydrated: bool = False
  808. loaded: int = 0
  809. counter: int = 0
  810. @rx.event
  811. def on_load(self):
  812. """Event handler for page on_load, should trigger for all navigation events."""
  813. self.loaded = self.loaded + 1
  814. @rx.event
  815. def on_counter(self):
  816. """Increment the counter var."""
  817. self.counter = self.counter + 1
  818. @computed_var
  819. def comp_dynamic(self) -> str:
  820. """A computed var that depends on the dynamic var.
  821. Returns:
  822. same as self.dynamic
  823. """
  824. return self.dynamic
  825. on_load_internal = OnLoadInternalState.on_load_internal.fn # pyright: ignore [reportFunctionMemberAccess]
  826. def test_dynamic_arg_shadow(
  827. index_page: ComponentCallable,
  828. windows_platform: bool,
  829. token: str,
  830. app_module_mock: unittest.mock.Mock,
  831. mocker,
  832. ):
  833. """Create app with dynamic route var and try to add a page with a dynamic arg that shadows a state var.
  834. Args:
  835. index_page: The index page.
  836. windows_platform: Whether the system is windows.
  837. token: a Token.
  838. app_module_mock: Mocked app module.
  839. mocker: pytest mocker object.
  840. """
  841. arg_name = "counter"
  842. route = f"/test/[{arg_name}]"
  843. app = app_module_mock.app = App(_state=DynamicState)
  844. assert app._state is not None
  845. with pytest.raises(NameError):
  846. app.add_page(index_page, route=route, on_load=DynamicState.on_load)
  847. def test_multiple_dynamic_args(
  848. index_page: ComponentCallable,
  849. windows_platform: bool,
  850. token: str,
  851. app_module_mock: unittest.mock.Mock,
  852. mocker,
  853. ):
  854. """Create app with multiple dynamic route vars with the same name.
  855. Args:
  856. index_page: The index page.
  857. windows_platform: Whether the system is windows.
  858. token: a Token.
  859. app_module_mock: Mocked app module.
  860. mocker: pytest mocker object.
  861. """
  862. arg_name = "my_arg"
  863. route = f"/test/[{arg_name}]"
  864. route2 = f"/test2/[{arg_name}]"
  865. app = app_module_mock.app = App(_state=EmptyState)
  866. app.add_page(index_page, route=route)
  867. app.add_page(index_page, route=route2)
  868. @pytest.mark.asyncio
  869. async def test_dynamic_route_var_route_change_completed_on_load(
  870. index_page: ComponentCallable,
  871. windows_platform: bool,
  872. token: str,
  873. app_module_mock: unittest.mock.Mock,
  874. mocker,
  875. ):
  876. """Create app with dynamic route var, and simulate navigation.
  877. on_load should fire, allowing any additional vars to be updated before the
  878. initial page hydrate.
  879. Args:
  880. index_page: The index page.
  881. windows_platform: Whether the system is windows.
  882. token: a Token.
  883. app_module_mock: Mocked app module.
  884. mocker: pytest mocker object.
  885. """
  886. arg_name = "dynamic"
  887. route = f"/test/[{arg_name}]"
  888. app = app_module_mock.app = App(_state=DynamicState)
  889. assert app._state is not None
  890. assert arg_name not in app._state.vars
  891. app.add_page(index_page, route=route, on_load=DynamicState.on_load)
  892. assert arg_name in app._state.vars
  893. assert arg_name in app._state.computed_vars
  894. assert app._state.computed_vars[arg_name]._deps(objclass=DynamicState) == {
  895. DynamicState.get_full_name(): {constants.ROUTER},
  896. }
  897. assert constants.ROUTER in app._state()._var_dependencies
  898. substate_token = _substate_key(token, DynamicState)
  899. sid = "mock_sid"
  900. client_ip = "127.0.0.1"
  901. async with app.state_manager.modify_state(substate_token) as state:
  902. state.router_data = {"simulate": "hydrated"}
  903. assert state.dynamic == ""
  904. exp_vals = ["foo", "foobar", "baz"]
  905. def _event(name, val, **kwargs):
  906. return Event(
  907. token=kwargs.pop("token", token),
  908. name=name,
  909. router_data=kwargs.pop(
  910. "router_data", {"pathname": route, "query": {arg_name: val}}
  911. ),
  912. payload=kwargs.pop("payload", {}),
  913. **kwargs,
  914. )
  915. def _dynamic_state_event(name, val, **kwargs):
  916. return _event(
  917. name=format.format_event_handler(getattr(DynamicState, name)),
  918. val=val,
  919. **kwargs,
  920. )
  921. prev_exp_val = ""
  922. for exp_index, exp_val in enumerate(exp_vals):
  923. on_load_internal = _event(
  924. name=f"{state.get_full_name()}.{constants.CompileVars.ON_LOAD_INTERNAL.rpartition('.')[2]}",
  925. val=exp_val,
  926. )
  927. exp_router_data = {
  928. "headers": {},
  929. "ip": client_ip,
  930. "sid": sid,
  931. "token": token,
  932. **on_load_internal.router_data,
  933. }
  934. exp_router = RouterData(exp_router_data)
  935. process_coro = process(
  936. app,
  937. event=on_load_internal,
  938. sid=sid,
  939. headers={},
  940. client_ip=client_ip,
  941. )
  942. update = await process_coro.__anext__()
  943. # route change (on_load_internal) triggers: [call on_load events, call set_is_hydrated(True)]
  944. assert update == StateUpdate(
  945. delta={
  946. state.get_name(): {
  947. arg_name + "_rx_state_": exp_val,
  948. f"comp_{arg_name}_rx_state_": exp_val,
  949. constants.CompileVars.IS_HYDRATED + "_rx_state_": False,
  950. "router_rx_state_": exp_router,
  951. }
  952. },
  953. events=[
  954. _dynamic_state_event(
  955. name="on_load",
  956. val=exp_val,
  957. ),
  958. _event(
  959. name=f"{State.get_name()}.set_is_hydrated",
  960. payload={"value": True},
  961. val=exp_val,
  962. router_data={},
  963. ),
  964. ],
  965. )
  966. if isinstance(app.state_manager, StateManagerRedis):
  967. # When redis is used, the state is not updated until the processing is complete
  968. state = await app.state_manager.get_state(substate_token)
  969. assert state.dynamic == prev_exp_val
  970. # complete the processing
  971. with pytest.raises(StopAsyncIteration):
  972. await process_coro.__anext__()
  973. # check that router data was written to the state_manager store
  974. state = await app.state_manager.get_state(substate_token)
  975. assert state.dynamic == exp_val
  976. process_coro = process(
  977. app,
  978. event=_dynamic_state_event(name="on_load", val=exp_val),
  979. sid=sid,
  980. headers={},
  981. client_ip=client_ip,
  982. )
  983. on_load_update = await process_coro.__anext__()
  984. assert on_load_update == StateUpdate(
  985. delta={
  986. state.get_name(): {
  987. "loaded_rx_state_": exp_index + 1,
  988. },
  989. },
  990. events=[],
  991. )
  992. # complete the processing
  993. with pytest.raises(StopAsyncIteration):
  994. await process_coro.__anext__()
  995. process_coro = process(
  996. app,
  997. event=_dynamic_state_event(
  998. name="set_is_hydrated", payload={"value": True}, val=exp_val
  999. ),
  1000. sid=sid,
  1001. headers={},
  1002. client_ip=client_ip,
  1003. )
  1004. on_set_is_hydrated_update = await process_coro.__anext__()
  1005. assert on_set_is_hydrated_update == StateUpdate(
  1006. delta={
  1007. state.get_name(): {
  1008. "is_hydrated_rx_state_": True,
  1009. },
  1010. },
  1011. events=[],
  1012. )
  1013. # complete the processing
  1014. with pytest.raises(StopAsyncIteration):
  1015. await process_coro.__anext__()
  1016. # a simple state update event should NOT trigger on_load or route var side effects
  1017. process_coro = process(
  1018. app,
  1019. event=_dynamic_state_event(name="on_counter", val=exp_val),
  1020. sid=sid,
  1021. headers={},
  1022. client_ip=client_ip,
  1023. )
  1024. update = await process_coro.__anext__()
  1025. assert update == StateUpdate(
  1026. delta={
  1027. state.get_name(): {
  1028. "counter_rx_state_": exp_index + 1,
  1029. }
  1030. },
  1031. events=[],
  1032. )
  1033. # complete the processing
  1034. with pytest.raises(StopAsyncIteration):
  1035. await process_coro.__anext__()
  1036. prev_exp_val = exp_val
  1037. state = await app.state_manager.get_state(substate_token)
  1038. assert state.loaded == len(exp_vals)
  1039. assert state.counter == len(exp_vals)
  1040. if isinstance(app.state_manager, StateManagerRedis):
  1041. await app.state_manager.close()
  1042. @pytest.mark.asyncio
  1043. async def test_process_events(mocker, token: str):
  1044. """Test that an event is processed properly and that it is postprocessed
  1045. n+1 times. Also check that the processing flag of the last stateupdate is set to
  1046. False.
  1047. Args:
  1048. mocker: mocker object.
  1049. token: a Token.
  1050. """
  1051. router_data = {
  1052. "pathname": "/",
  1053. "query": {},
  1054. "token": token,
  1055. "sid": "mock_sid",
  1056. "headers": {},
  1057. "ip": "127.0.0.1",
  1058. }
  1059. app = App(_state=GenState)
  1060. mocker.patch.object(app, "_postprocess", AsyncMock())
  1061. event = Event(
  1062. token=token,
  1063. name=f"{GenState.get_name()}.go",
  1064. payload={"c": 5},
  1065. router_data=router_data,
  1066. )
  1067. async with app.state_manager.modify_state(event.substate_token) as state:
  1068. state.router_data = {"simulate": "hydrated"}
  1069. async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):
  1070. pass
  1071. assert (await app.state_manager.get_state(event.substate_token)).value == 5
  1072. assert app._postprocess.call_count == 6 # pyright: ignore [reportFunctionMemberAccess]
  1073. if isinstance(app.state_manager, StateManagerRedis):
  1074. await app.state_manager.close()
  1075. @pytest.mark.parametrize(
  1076. ("state", "overlay_component", "exp_page_child"),
  1077. [
  1078. (None, default_overlay_component, None),
  1079. (None, None, None),
  1080. (None, Text.create("foo"), Text),
  1081. (State, default_overlay_component, Fragment),
  1082. (State, None, None),
  1083. (State, Text.create("foo"), Text),
  1084. (State, lambda: Text.create("foo"), Text),
  1085. ],
  1086. )
  1087. def test_overlay_component(
  1088. state: type[State] | None,
  1089. overlay_component: Component | ComponentCallable | None,
  1090. exp_page_child: type[Component] | None,
  1091. ):
  1092. """Test that the overlay component is set correctly.
  1093. Args:
  1094. state: The state class to pass to App.
  1095. overlay_component: The overlay_component to pass to App.
  1096. exp_page_child: The type of the expected child in the page fragment.
  1097. """
  1098. app = App(_state=state, overlay_component=overlay_component)
  1099. app._setup_overlay_component()
  1100. if exp_page_child is None:
  1101. assert app.overlay_component is None
  1102. elif isinstance(exp_page_child, OverlayFragment):
  1103. assert app.overlay_component is not None
  1104. generated_component = app._generate_component(app.overlay_component)
  1105. assert isinstance(generated_component, OverlayFragment)
  1106. assert isinstance(
  1107. generated_component.children[0],
  1108. Cond, # ConnectionModal is a Cond under the hood
  1109. )
  1110. else:
  1111. assert app.overlay_component is not None
  1112. assert isinstance(
  1113. app._generate_component(app.overlay_component),
  1114. exp_page_child,
  1115. )
  1116. app.add_page(rx.box("Index"), route="/test")
  1117. # overlay components are wrapped during compile only
  1118. app._compile_page("test")
  1119. app._setup_overlay_component()
  1120. page = app._pages["test"]
  1121. if exp_page_child is not None:
  1122. assert len(page.children) == 3
  1123. children_types = (type(child) for child in page.children)
  1124. assert exp_page_child in children_types # pyright: ignore [reportOperatorIssue]
  1125. else:
  1126. assert len(page.children) == 2
  1127. @pytest.fixture
  1128. def compilable_app(tmp_path) -> Generator[tuple[App, Path], None, None]:
  1129. """Fixture for an app that can be compiled.
  1130. Args:
  1131. tmp_path: Temporary path.
  1132. Yields:
  1133. Tuple containing (app instance, Path to ".web" directory)
  1134. The working directory is set to the app dir (parent of .web),
  1135. allowing app.compile() to be called.
  1136. """
  1137. app_path = tmp_path / "app"
  1138. web_dir = app_path / ".web"
  1139. web_dir.mkdir(parents=True)
  1140. (web_dir / constants.PackageJson.PATH).touch()
  1141. app = App(theme=None)
  1142. app._get_frontend_packages = unittest.mock.Mock()
  1143. with chdir(app_path):
  1144. yield app, web_dir
  1145. @pytest.mark.parametrize(
  1146. "react_strict_mode",
  1147. [True, False],
  1148. )
  1149. def test_app_wrap_compile_theme(
  1150. react_strict_mode: bool, compilable_app: tuple[App, Path], mocker
  1151. ):
  1152. """Test that the radix theme component wraps the app.
  1153. Args:
  1154. react_strict_mode: Whether to use React Strict Mode.
  1155. compilable_app: compilable_app fixture.
  1156. mocker: pytest mocker object.
  1157. """
  1158. conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode)
  1159. mocker.patch("reflex.config._get_config", return_value=conf)
  1160. app, web_dir = compilable_app
  1161. app.theme = rx.theme(accent_color="plum")
  1162. app._compile()
  1163. app_js_contents = (web_dir / "pages" / "_app.js").read_text()
  1164. app_js_lines = [
  1165. line.strip() for line in app_js_contents.splitlines() if line.strip()
  1166. ]
  1167. lines = "".join(app_js_lines)
  1168. expected = (
  1169. "function AppWrap({children}) {"
  1170. "return ("
  1171. + ("jsx(StrictMode,{}," if react_strict_mode else "")
  1172. + "jsx(RadixThemesColorModeProvider,{},"
  1173. "jsx(RadixThemesTheme,{accentColor:\"plum\",css:{...theme.styles.global[':root'], ...theme.styles.global.body}},"
  1174. "jsx(Fragment,{},"
  1175. "jsx(MemoizedToastProvider,{},),"
  1176. "jsx(Fragment,{},"
  1177. "children,"
  1178. "),"
  1179. "),"
  1180. "),"
  1181. ")" + (",)" if react_strict_mode else "") + ")"
  1182. "}"
  1183. )
  1184. assert expected in lines
  1185. @pytest.mark.parametrize(
  1186. "react_strict_mode",
  1187. [True, False],
  1188. )
  1189. def test_app_wrap_priority(
  1190. react_strict_mode: bool, compilable_app: tuple[App, Path], mocker
  1191. ):
  1192. """Test that the app wrap components are wrapped in the correct order.
  1193. Args:
  1194. react_strict_mode: Whether to use React Strict Mode.
  1195. compilable_app: compilable_app fixture.
  1196. mocker: pytest mocker object.
  1197. """
  1198. conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode)
  1199. mocker.patch("reflex.config._get_config", return_value=conf)
  1200. app, web_dir = compilable_app
  1201. class Fragment1(Component):
  1202. tag = "Fragment1"
  1203. def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride]
  1204. return {(99, "Box"): rx.box()}
  1205. class Fragment2(Component):
  1206. tag = "Fragment2"
  1207. def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride]
  1208. return {(50, "Text"): rx.text()}
  1209. class Fragment3(Component):
  1210. tag = "Fragment3"
  1211. def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride]
  1212. return {(10, "Fragment2"): Fragment2.create()}
  1213. def page():
  1214. return Fragment1.create(Fragment3.create())
  1215. app.add_page(page)
  1216. app._compile()
  1217. app_js_contents = (web_dir / "pages" / "_app.js").read_text()
  1218. app_js_lines = [
  1219. line.strip() for line in app_js_contents.splitlines() if line.strip()
  1220. ]
  1221. lines = "".join(app_js_lines)
  1222. expected = (
  1223. "function AppWrap({children}) {"
  1224. "return ("
  1225. + ("jsx(StrictMode,{}," if react_strict_mode else "")
  1226. + "jsx(RadixThemesBox,{},"
  1227. 'jsx(RadixThemesText,{as:"p"},'
  1228. "jsx(RadixThemesColorModeProvider,{},"
  1229. "jsx(Fragment2,{},"
  1230. "jsx(Fragment,{},"
  1231. "jsx(MemoizedToastProvider,{},),"
  1232. "jsx(Fragment,{},"
  1233. "children"
  1234. ",),),),),)" + (",)" if react_strict_mode else "")
  1235. )
  1236. assert expected in lines
  1237. def test_app_state_determination():
  1238. """Test that the stateless status of an app is determined correctly."""
  1239. a1 = App()
  1240. assert a1._state is None
  1241. # No state, no router, no event handlers.
  1242. a1.add_page(rx.box("Index"), route="/")
  1243. assert a1._state is None
  1244. # Add a page with `on_load` enables state.
  1245. a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
  1246. a1._compile_page("about")
  1247. assert a1._state is not None
  1248. a2 = App()
  1249. assert a2._state is None
  1250. # Referencing a state Var enables state.
  1251. a2.add_page(rx.box(rx.text(GenState.value)), route="/")
  1252. a2._compile_page("index")
  1253. assert a2._state is not None
  1254. a3 = App()
  1255. assert a3._state is None
  1256. # Referencing router enables state.
  1257. a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
  1258. a3._compile_page("index")
  1259. assert a3._state is not None
  1260. a4 = App()
  1261. assert a4._state is None
  1262. a4.add_page(rx.box(rx.button("Click", on_click=rx.console_log(""))), route="/")
  1263. assert a4._state is None
  1264. a4.add_page(
  1265. rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
  1266. )
  1267. a4._compile_page("page2")
  1268. assert a4._state is not None
  1269. def test_raise_on_state():
  1270. """Test that the state is set."""
  1271. # state kwargs is deprecated, we just make sure the app is created anyway.
  1272. _app = App(_state=State)
  1273. assert _app._state is not None
  1274. assert issubclass(_app._state, State)
  1275. def test_call_app():
  1276. """Test that the app can be called."""
  1277. app = App()
  1278. app._compile = unittest.mock.Mock()
  1279. api = app()
  1280. assert isinstance(api, Starlette)
  1281. def test_app_with_optional_endpoints():
  1282. from reflex.components.core.upload import Upload
  1283. app = App()
  1284. Upload.is_used = True
  1285. app._add_optional_endpoints()
  1286. # TODO: verify the availability of the endpoints in app.api
  1287. def test_app_state_manager():
  1288. app = App()
  1289. with pytest.raises(ValueError):
  1290. app.state_manager
  1291. app._enable_state()
  1292. assert app.state_manager is not None
  1293. assert isinstance(
  1294. app.state_manager, (StateManagerMemory, StateManagerDisk, StateManagerRedis)
  1295. )
  1296. def test_generate_component():
  1297. def index():
  1298. return rx.box("Index")
  1299. def index_mismatch():
  1300. return rx.match(
  1301. 1,
  1302. (1, rx.box("Index")),
  1303. (2, "About"),
  1304. "Bar",
  1305. )
  1306. comp = App._generate_component(index)
  1307. assert isinstance(comp, Component)
  1308. with pytest.raises(exceptions.MatchTypeError):
  1309. App._generate_component(index_mismatch)
  1310. def test_add_page_component_returning_tuple():
  1311. """Test that a component or render method returning a
  1312. tuple is unpacked in a Fragment.
  1313. """
  1314. app = App()
  1315. def index():
  1316. return rx.text("first"), rx.text("second")
  1317. def page2():
  1318. return (rx.text("third"),)
  1319. app.add_page(index)
  1320. app.add_page(page2)
  1321. app._compile_page("index")
  1322. app._compile_page("page2")
  1323. fragment_wrapper = app._pages["index"].children[0]
  1324. assert isinstance(fragment_wrapper, Fragment)
  1325. first_text = fragment_wrapper.children[0]
  1326. assert isinstance(first_text, Text)
  1327. assert isinstance(first_text.children[0], Bare)
  1328. assert str(first_text.children[0].contents) == '"first"'
  1329. second_text = fragment_wrapper.children[1]
  1330. assert isinstance(second_text, Text)
  1331. assert isinstance(second_text.children[0], Bare)
  1332. assert str(second_text.children[0].contents) == '"second"'
  1333. # Test page with trailing comma.
  1334. page2_fragment_wrapper = app._pages["page2"].children[0]
  1335. assert isinstance(page2_fragment_wrapper, Fragment)
  1336. third_text = page2_fragment_wrapper.children[0]
  1337. assert isinstance(third_text, Text)
  1338. assert isinstance(third_text.children[0], Bare)
  1339. assert str(third_text.children[0].contents) == '"third"'
  1340. @pytest.mark.parametrize("export", (True, False))
  1341. def test_app_with_transpile_packages(compilable_app: tuple[App, Path], export: bool):
  1342. class C1(rx.Component):
  1343. library = "foo@1.2.3"
  1344. tag = "Foo"
  1345. transpile_packages: list[str] = ["foo"]
  1346. class C2(rx.Component):
  1347. library = "bar@4.5.6"
  1348. tag = "Bar"
  1349. transpile_packages: list[str] = ["bar@4.5.6"]
  1350. class C3(rx.NoSSRComponent):
  1351. library = "baz@7.8.10"
  1352. tag = "Baz"
  1353. transpile_packages: list[str] = ["baz@7.8.9"]
  1354. class C4(rx.NoSSRComponent):
  1355. library = "quuc@2.3.4"
  1356. tag = "Quuc"
  1357. transpile_packages: list[str] = ["quuc"]
  1358. class C5(rx.Component):
  1359. library = "quuc"
  1360. tag = "Quuc"
  1361. app, web_dir = compilable_app
  1362. page = Fragment.create(
  1363. C1.create(), C2.create(), C3.create(), C4.create(), C5.create()
  1364. )
  1365. app.add_page(page, route="/")
  1366. app._compile(export=export)
  1367. next_config = (web_dir / "next.config.js").read_text()
  1368. transpile_packages_match = re.search(r"transpilePackages: (\[.*?\])", next_config)
  1369. transpile_packages_json = transpile_packages_match.group(1) # pyright: ignore [reportOptionalMemberAccess]
  1370. transpile_packages = sorted(json.loads(transpile_packages_json))
  1371. assert transpile_packages == [
  1372. "bar",
  1373. "foo",
  1374. "quuc",
  1375. ]
  1376. if export:
  1377. assert 'output: "export"' in next_config
  1378. assert f'distDir: "{constants.Dirs.STATIC}"' in next_config
  1379. else:
  1380. assert 'output: "export"' not in next_config
  1381. assert f'distDir: "{constants.Dirs.STATIC}"' not in next_config
  1382. def test_app_with_valid_var_dependencies(compilable_app: tuple[App, Path]):
  1383. app, _ = compilable_app
  1384. class ValidDepState(BaseState):
  1385. base: int = 0
  1386. _backend: int = 0
  1387. @computed_var()
  1388. def foo(self) -> str:
  1389. return "foo"
  1390. @computed_var(deps=["_backend", "base", foo])
  1391. def bar(self) -> str:
  1392. return "bar"
  1393. class Child1(ValidDepState):
  1394. @computed_var(deps=["base", ValidDepState.bar])
  1395. def other(self) -> str:
  1396. return "other"
  1397. class Child2(ValidDepState):
  1398. @computed_var(deps=["base", Child1.other])
  1399. def other(self) -> str:
  1400. return "other"
  1401. app._state = ValidDepState
  1402. app._compile()
  1403. def test_app_with_invalid_var_dependencies(compilable_app: tuple[App, Path]):
  1404. app, _ = compilable_app
  1405. class InvalidDepState(BaseState):
  1406. @computed_var(deps=["foolksjdf"])
  1407. def bar(self) -> str:
  1408. return "bar"
  1409. app._state = InvalidDepState
  1410. with pytest.raises(exceptions.VarDependencyError):
  1411. app._compile()
  1412. # Test custom exception handlers
  1413. def valid_custom_handler(exception: Exception, logger: str = "test"):
  1414. print("Custom Backend Exception")
  1415. print(exception)
  1416. def custom_exception_handler_with_wrong_arg_order(
  1417. logger: str,
  1418. exception: Exception, # Should be first
  1419. ):
  1420. print("Custom Backend Exception")
  1421. print(exception)
  1422. def custom_exception_handler_with_wrong_argspec(
  1423. exception: str, # Should be Exception
  1424. ):
  1425. print("Custom Backend Exception")
  1426. print(exception)
  1427. class DummyExceptionHandler:
  1428. """Dummy exception handler class."""
  1429. def handle(self, exception: Exception):
  1430. """Handle the exception.
  1431. Args:
  1432. exception: The exception.
  1433. """
  1434. print("Custom Backend Exception")
  1435. print(exception)
  1436. custom_exception_handlers = {
  1437. "lambda": lambda exception: print("Custom Exception Handler", exception),
  1438. "wrong_argspec": custom_exception_handler_with_wrong_argspec,
  1439. "wrong_arg_order": custom_exception_handler_with_wrong_arg_order,
  1440. "valid": valid_custom_handler,
  1441. "partial": functools.partial(valid_custom_handler, logger="test"),
  1442. "method": DummyExceptionHandler().handle,
  1443. }
  1444. @pytest.mark.parametrize(
  1445. "handler_fn, expected",
  1446. [
  1447. pytest.param(
  1448. custom_exception_handlers["partial"],
  1449. pytest.raises(ValueError),
  1450. id="partial",
  1451. ),
  1452. pytest.param(
  1453. custom_exception_handlers["lambda"],
  1454. pytest.raises(ValueError),
  1455. id="lambda",
  1456. ),
  1457. pytest.param(
  1458. custom_exception_handlers["wrong_argspec"],
  1459. pytest.raises(ValueError),
  1460. id="wrong_argspec",
  1461. ),
  1462. pytest.param(
  1463. custom_exception_handlers["wrong_arg_order"],
  1464. pytest.raises(ValueError),
  1465. id="wrong_arg_order",
  1466. ),
  1467. pytest.param(
  1468. custom_exception_handlers["valid"],
  1469. does_not_raise(),
  1470. id="valid_handler",
  1471. ),
  1472. pytest.param(
  1473. custom_exception_handlers["method"],
  1474. does_not_raise(),
  1475. id="valid_class_method",
  1476. ),
  1477. ],
  1478. )
  1479. def test_frontend_exception_handler_validation(handler_fn, expected):
  1480. """Test that the custom frontend exception handler is properly validated.
  1481. Args:
  1482. handler_fn: The handler function.
  1483. expected: The expected result.
  1484. """
  1485. with expected:
  1486. rx.App(frontend_exception_handler=handler_fn)._validate_exception_handlers()
  1487. def backend_exception_handler_with_wrong_return_type(exception: Exception) -> int:
  1488. """Custom backend exception handler with wrong return type.
  1489. Args:
  1490. exception: The exception.
  1491. Returns:
  1492. int: The wrong return type.
  1493. """
  1494. print("Custom Backend Exception")
  1495. print(exception)
  1496. return 5
  1497. @pytest.mark.parametrize(
  1498. "handler_fn, expected",
  1499. [
  1500. pytest.param(
  1501. backend_exception_handler_with_wrong_return_type,
  1502. pytest.raises(ValueError),
  1503. id="wrong_return_type",
  1504. ),
  1505. pytest.param(
  1506. custom_exception_handlers["partial"],
  1507. pytest.raises(ValueError),
  1508. id="partial",
  1509. ),
  1510. pytest.param(
  1511. custom_exception_handlers["lambda"],
  1512. pytest.raises(ValueError),
  1513. id="lambda",
  1514. ),
  1515. pytest.param(
  1516. custom_exception_handlers["wrong_argspec"],
  1517. pytest.raises(ValueError),
  1518. id="wrong_argspec",
  1519. ),
  1520. pytest.param(
  1521. custom_exception_handlers["wrong_arg_order"],
  1522. pytest.raises(ValueError),
  1523. id="wrong_arg_order",
  1524. ),
  1525. pytest.param(
  1526. custom_exception_handlers["valid"],
  1527. does_not_raise(),
  1528. id="valid_handler",
  1529. ),
  1530. pytest.param(
  1531. custom_exception_handlers["method"],
  1532. does_not_raise(),
  1533. id="valid_class_method",
  1534. ),
  1535. ],
  1536. )
  1537. def test_backend_exception_handler_validation(handler_fn, expected):
  1538. """Test that the custom backend exception handler is properly validated.
  1539. Args:
  1540. handler_fn: The handler function.
  1541. expected: The expected result.
  1542. """
  1543. with expected:
  1544. rx.App(backend_exception_handler=handler_fn)._validate_exception_handlers()