From c03d32c75f9ad80dcd157bd309ffd1ca647af934 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Wed, 27 Nov 2024 20:34:34 +0100 Subject: [PATCH] playwright.goto doesn't handle redirect well --- .../tests_playwright/test_dynamic_routes.py | 237 ++++++++++++++++++ .../tests_playwright/test_redirect.py | 66 +++++ 2 files changed, 303 insertions(+) create mode 100644 tests/integration/tests_playwright/test_dynamic_routes.py create mode 100644 tests/integration/tests_playwright/test_redirect.py diff --git a/tests/integration/tests_playwright/test_dynamic_routes.py b/tests/integration/tests_playwright/test_dynamic_routes.py new file mode 100644 index 000000000..9d8692de9 --- /dev/null +++ b/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/") diff --git a/tests/integration/tests_playwright/test_redirect.py b/tests/integration/tests_playwright/test_redirect.py new file mode 100644 index 000000000..7a882fa7f --- /dev/null +++ b/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/")