test_app.py 45 KB

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