|
@@ -0,0 +1,237 @@
|
|
|
|
+"""Integration test for dynamic routes."""
|
|
|
|
+
|
|
|
|
+from __future__ import annotations
|
|
|
|
+
|
|
|
|
+from typing import Generator, Type
|
|
|
|
+
|
|
|
|
+import pytest
|
|
|
|
+from playwright.sync_api import Page, expect
|
|
|
|
+
|
|
|
|
+from reflex.testing import AppHarness, AppHarnessProd
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+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"), # type: ignore
|
|
|
|
+ rx.input(
|
|
|
|
+ value=DynamicState.router.page.raw_path, read_only=True, id="raw_path"
|
|
|
|
+ ),
|
|
|
|
+ rx.vstack(
|
|
|
|
+ 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", # type: ignore
|
|
|
|
+ ),
|
|
|
|
+ rx.link("missing", href="/missing", id="link_missing"),
|
|
|
|
+ ),
|
|
|
|
+ rx.list( # type: ignore
|
|
|
|
+ rx.foreach(
|
|
|
|
+ DynamicState.order, # type: ignore
|
|
|
|
+ lambda i: rx.list_item(rx.text(i)),
|
|
|
|
+ ),
|
|
|
|
+ id="order",
|
|
|
|
+ ),
|
|
|
|
+ rx.list( # type: ignore
|
|
|
|
+ rx.foreach(
|
|
|
|
+ DynamicState.router.page.params,
|
|
|
|
+ lambda i: rx.list_item(
|
|
|
|
+ rx.text(f"{i[0]}: {i[1]}"), # type: ignore
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ id="params",
|
|
|
|
+ ),
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ class ArgState(rx.State):
|
|
|
|
+ """The app state."""
|
|
|
|
+
|
|
|
|
+ @rx.var
|
|
|
|
+ def arg(self) -> int:
|
|
|
|
+ return int(self.arg_str or 0)
|
|
|
|
+
|
|
|
|
+ class ArgSubState(ArgState):
|
|
|
|
+ @rx.var(cache=True)
|
|
|
|
+ def cached_arg(self) -> int:
|
|
|
|
+ return self.arg
|
|
|
|
+
|
|
|
|
+ @rx.var(cache=True)
|
|
|
|
+ def cached_arg_str(self) -> str:
|
|
|
|
+ return self.arg_str
|
|
|
|
+
|
|
|
|
+ @rx.page(route="/arg/[arg_str]")
|
|
|
|
+ def arg() -> rx.Component:
|
|
|
|
+ return rx.vstack(
|
|
|
|
+ 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"), # type: ignore
|
|
|
|
+ ),
|
|
|
|
+ rx.data_list.item(
|
|
|
|
+ rx.data_list.label("ArgState.arg_str (dynamic) (inherited)"),
|
|
|
|
+ rx.data_list.value(ArgState.arg_str, id="argstate-arg_str"), # type: ignore
|
|
|
|
+ ),
|
|
|
|
+ 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"), # type: ignore
|
|
|
|
+ ),
|
|
|
|
+ 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) # type: ignore
|
|
|
|
+ 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) # type: ignore
|
|
|
|
+ app.add_page(index, route="/static/x", on_load=DynamicState.on_load) # type: ignore
|
|
|
|
+ app.add_page(index)
|
|
|
|
+ app.add_custom_404_page(index, on_load=DynamicState.on_load) # type: ignore
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+@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
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def test_on_load_navigate(dynamic_route: AppHarness, page: Page):
|
|
|
|
+ assert dynamic_route.frontend_url is not None
|
|
|
|
+ is_prod = isinstance(dynamic_route, AppHarnessProd)
|
|
|
|
+
|
|
|
|
+ page.goto(dynamic_route.frontend_url)
|
|
|
|
+
|
|
|
|
+ # click the next link 10 times
|
|
|
|
+ exp_order = [f"/page/[page_id]-{ix}" for ix in range(10)]
|
|
|
|
+ link = page.locator("#link_page_next")
|
|
|
|
+ for ix in range(10):
|
|
|
|
+ link.click()
|
|
|
|
+ expect(page.locator("#page_id")).to_have_value(str(ix))
|
|
|
|
+ expect(page).to_have_url(dynamic_route.frontend_url + f"/page/{ix}/")
|
|
|
|
+
|
|
|
|
+ order = page.locator("#order")
|
|
|
|
+ expect(order).to_have_text("".join(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"]
|
|
|
|
+ page.goto(dynamic_route.frontend_url + "/page/10/")
|
|
|
|
+ expect(order).to_have_text("".join(exp_order))
|
|
|
|
+
|
|
|
|
+ # make sure internal nav still hydrates after redirect
|
|
|
|
+ exp_order += ["/page/[page_id]-11"]
|
|
|
|
+ link.click()
|
|
|
|
+ expect(order).to_have_text("".join(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"]
|
|
|
|
+ page.goto(f"{page.url}?foo=bar")
|
|
|
|
+ expect(order).to_have_text("".join(exp_order))
|
|
|
|
+
|
|
|
|
+ params = page.locator("#params")
|
|
|
|
+ params_str = params.text_content()
|
|
|
|
+ assert params_str and "foo: bar" in params_str
|
|
|
|
+
|
|
|
|
+ # test 404 page and make sure we hydrate
|
|
|
|
+ exp_order += ["/404-no page id"]
|
|
|
|
+ page.goto(dynamic_route.frontend_url + "/missing")
|
|
|
|
+ expect(page).to_have_url(dynamic_route.frontend_url + "/missing/")
|
|
|
|
+ # At that point we're on the 404 page, so #order is not rendered
|
|
|
|
+ expect(order).to_have_text("".join(exp_order))
|
|
|
|
+
|
|
|
|
+ # browser nav should still trigger hydration
|
|
|
|
+ if is_prod:
|
|
|
|
+ exp_order += ["/404-no page id"]
|
|
|
|
+ exp_order += ["/page/[page_id]-11"]
|
|
|
|
+ page.go_back()
|
|
|
|
+ expect(order).to_have_text("".join(exp_order))
|
|
|
|
+
|
|
|
|
+ # next/link to a 404 and ensure we still hydrate
|
|
|
|
+ exp_order += ["/404-no page id"]
|
|
|
|
+ missing_link = page.locator("#link_missing")
|
|
|
|
+ missing_link.click()
|
|
|
|
+ expect(order).to_have_text("".join(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"]
|
|
|
|
+ page.goto(dynamic_route.frontend_url + "/redirect-page/0/")
|
|
|
|
+
|
|
|
|
+ # # should have redirected to page 0
|
|
|
|
+ # expect(page).to_have_url(dynamic_route.frontend_url + "/page/0/")
|