Bläddra i källkod

playwright.goto doesn't handle redirect well

Lendemor 5 månader sedan
förälder
incheckning
c03d32c75f

+ 237 - 0
tests/integration/tests_playwright/test_dynamic_routes.py

@@ -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/")

+ 66 - 0
tests/integration/tests_playwright/test_redirect.py

@@ -0,0 +1,66 @@
+from typing import Generator
+
+import pytest
+from playwright.sync_api import Page, expect
+
+from reflex.testing import AppHarness
+
+
+def RedirectRoute():
+    """App for testing redirects."""
+    import reflex as rx
+
+    class RedirectState(rx.State):
+        redirected: bool = False
+        order: list[str] = []
+
+        def on_load(self):
+            self.redirected = True
+            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):
+            page_id = self.router.page.params["page_id"]
+            return rx.redirect(f"/redirected/{page_id}")
+
+    app = rx.App(state=rx.State)
+
+    def index():
+        return rx.fragment(
+            rx.link("redirect", href="/redirect-page/3", id="link_redirect"),
+            rx.ordered_list(
+                rx.foreach(RedirectState.order, rx.text),
+            ),
+        )
+
+    # reuse the same page, only the URL interests us
+    app.add_page(index, "index")
+    app.add_page(index, "redirect-page/[page_id]", on_load=RedirectState.on_load_redir)
+    app.add_page(index, "redirected/[page_id]", on_load=RedirectState.on_load)
+
+
+@pytest.fixture(scope="module")
+def redirect_repro(tmp_path_factory) -> Generator[AppHarness, None, None]:
+    with AppHarness.create(
+        app_source=RedirectRoute,
+        root=tmp_path_factory.mktemp("redirect_repro"),
+    ) as harness:
+        assert harness.app_instance is not None, "app is not running"
+        yield harness
+
+
+def test_redirect(redirect_repro: AppHarness, page: Page):
+    assert redirect_repro.frontend_url is not None
+    page.goto(redirect_repro.frontend_url)
+
+    # by clicking the link, we should be redirected to /redirected
+    page.click("#link_redirect")
+    expect(page).to_have_url(redirect_repro.frontend_url + "/redirected/3/")
+
+    # return to index
+    page.goto(redirect_repro.frontend_url)
+    expect(page).to_have_url(redirect_repro.frontend_url + "/")
+
+    page.goto(redirect_repro.frontend_url + "/redirect/3")
+    expect(page).to_have_url(redirect_repro.frontend_url + "/redirected/3/")