test_dynamic_routes.py 9.1 KB

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