123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- """Integration tests for dynamic route page behavior."""
- from __future__ import annotations
- import time
- from typing import Callable, Coroutine, Generator, Type
- from urllib.parse import urlsplit
- import pytest
- from selenium.webdriver.common.by import By
- from reflex.testing import AppHarness, AppHarnessProd, WebDriver
- from .utils import poll_for_navigation
- def DynamicRoute():
- """App for testing dynamic routes."""
- from typing import List
- import reflex as rx
- class DynamicState(rx.State):
- order: List[str] = []
- def on_load(self):
- page_data = f"{self.router.page.path}-{self.page_id or 'no page id'}"
- print(f"on_load: {page_data}")
- self.order.append(page_data)
- def on_load_redir(self):
- query_params = self.router.page.params
- page_data = f"on_load_redir-{query_params}"
- print(f"on_load_redir: {page_data}")
- self.order.append(page_data)
- return rx.redirect(f"/page/{query_params['page_id']}")
- @rx.var
- def next_page(self) -> str:
- try:
- return str(int(self.page_id) + 1)
- except ValueError:
- return "0"
- def index():
- return rx.fragment(
- rx.input(
- value=DynamicState.router.session.client_token,
- read_only=True,
- id="token",
- ),
- rx.input(value=rx.State.page_id, read_only=True, id="page_id"), # pyright: ignore [reportAttributeAccessIssue]
- rx.input(
- value=DynamicState.router.page.raw_path,
- read_only=True,
- id="raw_path",
- ),
- rx.link("index", href="/", id="link_index"),
- rx.link("page_X", href="/static/x", id="link_page_x"),
- rx.link(
- "next",
- href="/page/" + DynamicState.next_page,
- id="link_page_next",
- ),
- rx.link("missing", href="/missing", id="link_missing"),
- rx.list( # pyright: ignore [reportAttributeAccessIssue]
- rx.foreach(
- DynamicState.order, # pyright: ignore [reportAttributeAccessIssue]
- lambda i: rx.list_item(rx.text(i)),
- ),
- ),
- )
- class ArgState(rx.State):
- """The app state."""
- @rx.var(cache=False)
- def arg(self) -> int:
- return int(self.arg_str or 0)
- class ArgSubState(ArgState):
- @rx.var
- def cached_arg(self) -> int:
- return self.arg
- @rx.var
- def cached_arg_str(self) -> str:
- return self.arg_str
- @rx.page(route="/arg/[arg_str]")
- def arg() -> rx.Component:
- return rx.vstack(
- rx.input(
- value=DynamicState.router.session.client_token,
- read_only=True,
- id="token",
- ),
- rx.data_list.root(
- rx.data_list.item(
- rx.data_list.label("rx.State.arg_str (dynamic)"),
- rx.data_list.value(rx.State.arg_str, id="state-arg_str"), # pyright: ignore [reportAttributeAccessIssue]
- ),
- rx.data_list.item(
- rx.data_list.label("ArgState.arg_str (dynamic) (inherited)"),
- rx.data_list.value(ArgState.arg_str, id="argstate-arg_str"), # pyright: ignore [reportAttributeAccessIssue]
- ),
- rx.data_list.item(
- rx.data_list.label("ArgState.arg"),
- rx.data_list.value(ArgState.arg, id="argstate-arg"),
- ),
- rx.data_list.item(
- rx.data_list.label("ArgSubState.arg_str (dynamic) (inherited)"),
- rx.data_list.value(ArgSubState.arg_str, id="argsubstate-arg_str"), # pyright: ignore [reportAttributeAccessIssue]
- ),
- rx.data_list.item(
- rx.data_list.label("ArgSubState.arg (inherited)"),
- rx.data_list.value(ArgSubState.arg, id="argsubstate-arg"),
- ),
- rx.data_list.item(
- rx.data_list.label("ArgSubState.cached_arg"),
- rx.data_list.value(
- ArgSubState.cached_arg, id="argsubstate-cached_arg"
- ),
- ),
- rx.data_list.item(
- rx.data_list.label("ArgSubState.cached_arg_str"),
- rx.data_list.value(
- ArgSubState.cached_arg_str, id="argsubstate-cached_arg_str"
- ),
- ),
- ),
- rx.link("+", href=f"/arg/{ArgState.arg + 1}", id="next-page"),
- align="center",
- height="100vh",
- )
- @rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir)
- def redirect_page():
- return rx.fragment(rx.text("redirecting..."))
- app = rx.App(_state=rx.State)
- app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load)
- app.add_page(index, route="/static/x", on_load=DynamicState.on_load)
- app.add_page(index)
- app.add_custom_404_page(on_load=DynamicState.on_load)
- @pytest.fixture(scope="module")
- def dynamic_route(
- app_harness_env: Type[AppHarness], tmp_path_factory
- ) -> Generator[AppHarness, None, None]:
- """Start DynamicRoute app at tmp_path via AppHarness.
- Args:
- app_harness_env: either AppHarness (dev) or AppHarnessProd (prod)
- tmp_path_factory: pytest tmp_path_factory fixture
- Yields:
- running AppHarness instance
- """
- with app_harness_env.create(
- root=tmp_path_factory.mktemp("dynamic_route"),
- app_name=f"dynamicroute_{app_harness_env.__name__.lower()}",
- app_source=DynamicRoute,
- ) as harness:
- yield harness
- @pytest.fixture
- def driver(dynamic_route: AppHarness) -> Generator[WebDriver, None, None]:
- """Get an instance of the browser open to the dynamic_route app.
- Args:
- dynamic_route: harness for DynamicRoute app
- Yields:
- WebDriver instance.
- """
- assert dynamic_route.app_instance is not None, "app is not running"
- driver = dynamic_route.frontend()
- # TODO: drop after flakiness is resolved
- driver.implicitly_wait(30)
- try:
- yield driver
- finally:
- driver.quit()
- @pytest.fixture()
- def token(dynamic_route: AppHarness, driver: WebDriver) -> str:
- """Get the token associated with backend state.
- Args:
- dynamic_route: harness for DynamicRoute app.
- driver: WebDriver instance.
- Returns:
- The token visible in the driver browser.
- """
- assert dynamic_route.app_instance is not None
- token_input = driver.find_element(By.ID, "token")
- assert token_input
- # wait for the backend connection to send the token
- token = dynamic_route.poll_for_value(token_input)
- assert token is not None
- return token
- @pytest.fixture()
- def poll_for_order(
- dynamic_route: AppHarness, token: str
- ) -> Callable[[list[str]], Coroutine[None, None, None]]:
- """Poll for the order list to match the expected order.
- Args:
- dynamic_route: harness for DynamicRoute app.
- token: The token visible in the driver browser.
- Returns:
- An async function that polls for the order list to match the expected order.
- """
- dynamic_state_name = dynamic_route.get_state_name("_dynamic_state")
- dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
- async def _poll_for_order(exp_order: list[str]):
- async def _backend_state():
- return await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
- async def _check():
- return (await _backend_state()).substates[
- dynamic_state_name
- ].order == exp_order
- await AppHarness._poll_for_async(_check, timeout=60)
- assert (
- list((await _backend_state()).substates[dynamic_state_name].order)
- == exp_order
- )
- return _poll_for_order
- @pytest.mark.asyncio
- async def test_on_load_navigate(
- dynamic_route: AppHarness,
- driver: WebDriver,
- token: str,
- poll_for_order: Callable[[list[str]], Coroutine[None, None, None]],
- ):
- """Click links to navigate between dynamic pages with on_load event.
- Args:
- dynamic_route: harness for DynamicRoute app.
- driver: WebDriver instance.
- token: The token visible in the driver browser.
- poll_for_order: function that polls for the order list to match the expected order.
- """
- dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
- assert dynamic_route.app_instance is not None
- is_prod = isinstance(dynamic_route, AppHarnessProd)
- link = driver.find_element(By.ID, "link_page_next")
- assert link
- exp_order = [f"/page/[page_id]-{ix}" for ix in range(10)]
- # click the link a few times
- for ix in range(10):
- # wait for navigation, then assert on url
- with poll_for_navigation(driver):
- link.click()
- assert urlsplit(driver.current_url).path == f"/page/{ix}/"
- link = driver.find_element(By.ID, "link_page_next")
- page_id_input = driver.find_element(By.ID, "page_id")
- raw_path_input = driver.find_element(By.ID, "raw_path")
- assert link
- assert page_id_input
- assert dynamic_route.poll_for_value(
- page_id_input, exp_not_equal=str(ix - 1)
- ) == str(ix)
- assert dynamic_route.poll_for_value(raw_path_input) == f"/page/{ix}/"
- await poll_for_order(exp_order)
- # manually load the next page to trigger client side routing in prod mode
- if is_prod:
- exp_order += ["/404-no page id"]
- exp_order += ["/page/[page_id]-10"]
- with poll_for_navigation(driver):
- driver.get(f"{dynamic_route.frontend_url}/page/10/")
- await poll_for_order(exp_order)
- # make sure internal nav still hydrates after redirect
- exp_order += ["/page/[page_id]-11"]
- link = driver.find_element(By.ID, "link_page_next")
- with poll_for_navigation(driver):
- link.click()
- await poll_for_order(exp_order)
- # load same page with a query param and make sure it passes through
- if is_prod:
- exp_order += ["/404-no page id"]
- exp_order += ["/page/[page_id]-11"]
- with poll_for_navigation(driver):
- driver.get(f"{driver.current_url}?foo=bar")
- await poll_for_order(exp_order)
- assert (
- await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
- ).router.page.params["foo"] == "bar"
- # hit a 404 and ensure we still hydrate
- exp_order += ["/404-no page id"]
- with poll_for_navigation(driver):
- driver.get(f"{dynamic_route.frontend_url}/missing")
- await poll_for_order(exp_order)
- # browser nav should still trigger hydration
- if is_prod:
- exp_order += ["/404-no page id"]
- exp_order += ["/page/[page_id]-11"]
- with poll_for_navigation(driver):
- driver.back()
- await poll_for_order(exp_order)
- # next/link to a 404 and ensure we still hydrate
- exp_order += ["/404-no page id"]
- link = driver.find_element(By.ID, "link_missing")
- with poll_for_navigation(driver):
- link.click()
- await poll_for_order(exp_order)
- # hit a page that redirects back to dynamic page
- if is_prod:
- exp_order += ["/404-no page id"]
- exp_order += ["on_load_redir-{'foo': 'bar', 'page_id': '0'}", "/page/[page_id]-0"]
- with poll_for_navigation(driver):
- driver.get(f"{dynamic_route.frontend_url}/redirect-page/0/?foo=bar")
- await poll_for_order(exp_order)
- # should have redirected back to page 0
- assert urlsplit(driver.current_url).path == "/page/0/"
- @pytest.mark.asyncio
- async def test_on_load_navigate_non_dynamic(
- dynamic_route: AppHarness,
- driver: WebDriver,
- poll_for_order: Callable[[list[str]], Coroutine[None, None, None]],
- ):
- """Click links to navigate between static pages with on_load event.
- Args:
- dynamic_route: harness for DynamicRoute app.
- driver: WebDriver instance.
- poll_for_order: function that polls for the order list to match the expected order.
- """
- assert dynamic_route.app_instance is not None
- link = driver.find_element(By.ID, "link_page_x")
- assert link
- with poll_for_navigation(driver):
- link.click()
- assert urlsplit(driver.current_url).path == "/static/x/"
- await poll_for_order(["/static/x-no page id"])
- # go back to the index and navigate back to the static route
- link = driver.find_element(By.ID, "link_index")
- with poll_for_navigation(driver):
- link.click()
- assert urlsplit(driver.current_url).path == "/"
- link = driver.find_element(By.ID, "link_page_x")
- with poll_for_navigation(driver):
- link.click()
- assert urlsplit(driver.current_url).path == "/static/x/"
- await poll_for_order(["/static/x-no page id", "/static/x-no page id"])
- @pytest.mark.asyncio
- async def test_render_dynamic_arg(
- dynamic_route: AppHarness,
- driver: WebDriver,
- token: str,
- ):
- """Assert that dynamic arg var is rendered correctly in different contexts.
- Args:
- dynamic_route: harness for DynamicRoute app.
- driver: WebDriver instance.
- token: The token visible in the driver browser.
- """
- assert dynamic_route.app_instance is not None
- with poll_for_navigation(driver):
- driver.get(f"{dynamic_route.frontend_url}/arg/0")
- # TODO: drop after flakiness is resolved
- time.sleep(3)
- def assert_content(expected: str, expect_not: str):
- ids = [
- "state-arg_str",
- "argstate-arg",
- "argstate-arg_str",
- "argsubstate-arg_str",
- "argsubstate-arg",
- "argsubstate-cached_arg",
- "argsubstate-cached_arg_str",
- ]
- for id in ids:
- el = driver.find_element(By.ID, id)
- assert el
- assert (
- dynamic_route.poll_for_content(el, timeout=30, exp_not_equal=expect_not)
- == expected
- )
- assert_content("0", "")
- next_page_link = driver.find_element(By.ID, "next-page")
- assert next_page_link
- with poll_for_navigation(driver):
- next_page_link.click()
- assert driver.current_url == f"{dynamic_route.frontend_url}/arg/1/"
- assert_content("1", "0")
- next_page_link = driver.find_element(By.ID, "next-page")
- assert next_page_link
- with poll_for_navigation(driver):
- next_page_link.click()
- assert driver.current_url == f"{dynamic_route.frontend_url}/arg/2/"
- assert_content("2", "1")
|