test_dynamic_routes.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. """Integration tests for dynamic route page behavior."""
  2. from __future__ import annotations
  3. from typing import Callable, Coroutine, Generator, Type
  4. from urllib.parse import urlsplit
  5. import pytest
  6. from selenium.webdriver.common.by import By
  7. from reflex.testing import AppHarness, AppHarnessProd, WebDriver
  8. from .utils import poll_for_navigation
  9. def DynamicRoute():
  10. """App for testing dynamic routes."""
  11. from typing import List
  12. import reflex_chakra as rc
  13. import reflex as rx
  14. class DynamicState(rx.State):
  15. order: List[str] = []
  16. page_id: str = ""
  17. def on_load(self):
  18. self.order.append(f"{self.router.page.path}-{self.page_id or 'no page id'}")
  19. def on_load_redir(self):
  20. query_params = self.router.page.params
  21. self.order.append(f"on_load_redir-{query_params}")
  22. return rx.redirect(f"/page/{query_params['page_id']}")
  23. @rx.var
  24. def next_page(self) -> str:
  25. try:
  26. return str(int(self.page_id) + 1)
  27. except ValueError:
  28. return "0"
  29. def index():
  30. return rx.fragment(
  31. rc.input(
  32. value=DynamicState.router.session.client_token,
  33. is_read_only=True,
  34. id="token",
  35. ),
  36. rc.input(value=DynamicState.page_id, is_read_only=True, id="page_id"),
  37. rc.input(
  38. value=DynamicState.router.page.raw_path,
  39. is_read_only=True,
  40. id="raw_path",
  41. ),
  42. rx.link("index", href="/", id="link_index"),
  43. rx.link("page_X", href="/static/x", id="link_page_x"),
  44. rx.link(
  45. "next",
  46. href="/page/" + DynamicState.next_page,
  47. id="link_page_next", # type: ignore
  48. ),
  49. rx.link("missing", href="/missing", id="link_missing"),
  50. rc.list(
  51. rx.foreach(
  52. DynamicState.order, # type: ignore
  53. lambda i: rc.list_item(rx.text(i)),
  54. ),
  55. ),
  56. )
  57. @rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir) # type: ignore
  58. def redirect_page():
  59. return rx.fragment(rx.text("redirecting..."))
  60. app = rx.App(state=rx.State)
  61. app.add_page(index)
  62. app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) # type: ignore
  63. app.add_page(index, route="/static/x", on_load=DynamicState.on_load) # type: ignore
  64. app.add_custom_404_page(on_load=DynamicState.on_load) # type: ignore
  65. @pytest.fixture(scope="module")
  66. def dynamic_route(
  67. app_harness_env: Type[AppHarness], tmp_path_factory
  68. ) -> Generator[AppHarness, None, None]:
  69. """Start DynamicRoute app at tmp_path via AppHarness.
  70. Args:
  71. app_harness_env: either AppHarness (dev) or AppHarnessProd (prod)
  72. tmp_path_factory: pytest tmp_path_factory fixture
  73. Yields:
  74. running AppHarness instance
  75. """
  76. with app_harness_env.create(
  77. root=tmp_path_factory.mktemp(f"dynamic_route"),
  78. app_name=f"dynamicroute_{app_harness_env.__name__.lower()}",
  79. app_source=DynamicRoute, # type: ignore
  80. ) as harness:
  81. yield harness
  82. @pytest.fixture
  83. def driver(dynamic_route: AppHarness) -> Generator[WebDriver, None, None]:
  84. """Get an instance of the browser open to the dynamic_route app.
  85. Args:
  86. dynamic_route: harness for DynamicRoute app
  87. Yields:
  88. WebDriver instance.
  89. """
  90. assert dynamic_route.app_instance is not None, "app is not running"
  91. driver = dynamic_route.frontend()
  92. try:
  93. yield driver
  94. finally:
  95. driver.quit()
  96. @pytest.fixture()
  97. def token(dynamic_route: AppHarness, driver: WebDriver) -> str:
  98. """Get the token associated with backend state.
  99. Args:
  100. dynamic_route: harness for DynamicRoute app.
  101. driver: WebDriver instance.
  102. Returns:
  103. The token visible in the driver browser.
  104. """
  105. assert dynamic_route.app_instance is not None
  106. token_input = driver.find_element(By.ID, "token")
  107. assert token_input
  108. # wait for the backend connection to send the token
  109. token = dynamic_route.poll_for_value(token_input)
  110. assert token is not None
  111. return token
  112. @pytest.fixture()
  113. def poll_for_order(
  114. dynamic_route: AppHarness, token: str
  115. ) -> Callable[[list[str]], Coroutine[None, None, None]]:
  116. """Poll for the order list to match the expected order.
  117. Args:
  118. dynamic_route: harness for DynamicRoute app.
  119. token: The token visible in the driver browser.
  120. Returns:
  121. An async function that polls for the order list to match the expected order.
  122. """
  123. dynamic_state_name = dynamic_route.get_state_name("_dynamic_state")
  124. dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
  125. async def _poll_for_order(exp_order: list[str]):
  126. async def _backend_state():
  127. return await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
  128. async def _check():
  129. return (await _backend_state()).substates[
  130. dynamic_state_name
  131. ].order == exp_order
  132. await AppHarness._poll_for_async(_check)
  133. assert (await _backend_state()).substates[dynamic_state_name].order == exp_order
  134. return _poll_for_order
  135. @pytest.mark.asyncio
  136. async def test_on_load_navigate(
  137. dynamic_route: AppHarness,
  138. driver: WebDriver,
  139. token: str,
  140. poll_for_order: Callable[[list[str]], Coroutine[None, None, None]],
  141. ):
  142. """Click links to navigate between dynamic pages with on_load event.
  143. Args:
  144. dynamic_route: harness for DynamicRoute app.
  145. driver: WebDriver instance.
  146. token: The token visible in the driver browser.
  147. poll_for_order: function that polls for the order list to match the expected order.
  148. """
  149. dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
  150. assert dynamic_route.app_instance is not None
  151. is_prod = isinstance(dynamic_route, AppHarnessProd)
  152. link = driver.find_element(By.ID, "link_page_next")
  153. assert link
  154. exp_order = [f"/page/[page_id]-{ix}" for ix in range(10)]
  155. # click the link a few times
  156. for ix in range(10):
  157. # wait for navigation, then assert on url
  158. with poll_for_navigation(driver):
  159. link.click()
  160. assert urlsplit(driver.current_url).path == f"/page/{ix}/"
  161. link = driver.find_element(By.ID, "link_page_next")
  162. page_id_input = driver.find_element(By.ID, "page_id")
  163. raw_path_input = driver.find_element(By.ID, "raw_path")
  164. assert link
  165. assert page_id_input
  166. assert dynamic_route.poll_for_value(
  167. page_id_input, exp_not_equal=str(ix - 1)
  168. ) == str(ix)
  169. assert dynamic_route.poll_for_value(raw_path_input) == f"/page/{ix}/"
  170. await poll_for_order(exp_order)
  171. # manually load the next page to trigger client side routing in prod mode
  172. if is_prod:
  173. exp_order += ["/404-no page id"]
  174. exp_order += ["/page/[page_id]-10"]
  175. with poll_for_navigation(driver):
  176. driver.get(f"{dynamic_route.frontend_url}/page/10/")
  177. await poll_for_order(exp_order)
  178. # make sure internal nav still hydrates after redirect
  179. exp_order += ["/page/[page_id]-11"]
  180. link = driver.find_element(By.ID, "link_page_next")
  181. with poll_for_navigation(driver):
  182. link.click()
  183. await poll_for_order(exp_order)
  184. # load same page with a query param and make sure it passes through
  185. if is_prod:
  186. exp_order += ["/404-no page id"]
  187. exp_order += ["/page/[page_id]-11"]
  188. with poll_for_navigation(driver):
  189. driver.get(f"{driver.current_url}?foo=bar")
  190. await poll_for_order(exp_order)
  191. assert (
  192. await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
  193. ).router.page.params["foo"] == "bar"
  194. # hit a 404 and ensure we still hydrate
  195. exp_order += ["/404-no page id"]
  196. with poll_for_navigation(driver):
  197. driver.get(f"{dynamic_route.frontend_url}/missing")
  198. await poll_for_order(exp_order)
  199. # browser nav should still trigger hydration
  200. if is_prod:
  201. exp_order += ["/404-no page id"]
  202. exp_order += ["/page/[page_id]-11"]
  203. with poll_for_navigation(driver):
  204. driver.back()
  205. await poll_for_order(exp_order)
  206. # next/link to a 404 and ensure we still hydrate
  207. exp_order += ["/404-no page id"]
  208. link = driver.find_element(By.ID, "link_missing")
  209. with poll_for_navigation(driver):
  210. link.click()
  211. await poll_for_order(exp_order)
  212. # hit a page that redirects back to dynamic page
  213. if is_prod:
  214. exp_order += ["/404-no page id"]
  215. exp_order += ["on_load_redir-{'foo': 'bar', 'page_id': '0'}", "/page/[page_id]-0"]
  216. with poll_for_navigation(driver):
  217. driver.get(f"{dynamic_route.frontend_url}/redirect-page/0/?foo=bar")
  218. await poll_for_order(exp_order)
  219. # should have redirected back to page 0
  220. assert urlsplit(driver.current_url).path == "/page/0/"
  221. @pytest.mark.asyncio
  222. async def test_on_load_navigate_non_dynamic(
  223. dynamic_route: AppHarness,
  224. driver: WebDriver,
  225. poll_for_order: Callable[[list[str]], Coroutine[None, None, None]],
  226. ):
  227. """Click links to navigate between static pages with on_load event.
  228. Args:
  229. dynamic_route: harness for DynamicRoute app.
  230. driver: WebDriver instance.
  231. poll_for_order: function that polls for the order list to match the expected order.
  232. """
  233. assert dynamic_route.app_instance is not None
  234. link = driver.find_element(By.ID, "link_page_x")
  235. assert link
  236. with poll_for_navigation(driver):
  237. link.click()
  238. assert urlsplit(driver.current_url).path == "/static/x/"
  239. await poll_for_order(["/static/x-no page id"])
  240. # go back to the index and navigate back to the static route
  241. link = driver.find_element(By.ID, "link_index")
  242. with poll_for_navigation(driver):
  243. link.click()
  244. assert urlsplit(driver.current_url).path == "/"
  245. link = driver.find_element(By.ID, "link_page_x")
  246. with poll_for_navigation(driver):
  247. link.click()
  248. assert urlsplit(driver.current_url).path == "/static/x/"
  249. await poll_for_order(["/static/x-no page id", "/static/x-no page id"])