Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 43 additions & 14 deletions tests/test_app/router/components.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from uuid import uuid4
from uuid import UUID, uuid4

from reactpy import component, html, use_location, use_state
from reactpy_router import link, route, use_params, use_search_params
Expand All @@ -7,6 +7,22 @@
from reactpy_django.router import django_router


class _TokenStore:
"""Process-wide holder for a stable navigation-state token."""

def __init__(self) -> None:
self._value: UUID | None = None

def get(self) -> UUID:
"""Return the token, creating one on first access."""
if self._value is None:
self._value = uuid4()
return self._value


_NEXT_PAGE_TOKEN = _TokenStore()


@component
def display_params(string: str):
location = use_location()
Expand All @@ -32,7 +48,13 @@ def show_route(path: str, *children: Route) -> Route:
@component
def next_page():
url_params = use_params()
state, _set_state = use_state(uuid4)
# ReactPy preserves `use_state` across SPA navigation, but the WebSocket can
# reconnect under load, which causes ReactPy to re-mount the component and
# reset hook state. Keep the token in a module-level store so it survives both
# navigation and transient reconnects, otherwise this state-preservation test
# becomes flaky on slow/loaded CI runners.
token_hex = _NEXT_PAGE_TOKEN.get()
state, _set_state = use_state(token_hex)
page = url_params.get("page", 0)
next_page = page + 1
return html.fragment(
Expand All @@ -44,17 +66,24 @@ def next_page():
)


# Routes are defined at module scope so that the route elements (including the
# stateful `next_page`) have a stable identity across renders. Recreating them
# inside `main()` on each render would cause ReactPy to treat them as new
# components, remounting them and resetting `use_state` during SPA navigation.
ROUTES: tuple[Route, ...] = (
show_route("/router/", show_route("subroute/")),
show_route("/router/unspecified/<value>/"),
show_route("/router/integer/<int:value>/"),
show_route("/router/path/<path:value>/"),
show_route("/router/slug/<slug:value>/"),
show_route("/router/string/<str:value>/"),
show_route("/router/uuid/<uuid:value>/"),
show_route("/router/any/<any:name>"),
show_route("/router/two/<int:value>/<str:value2>/"),
route("/router/next/<int:page>/", next_page()),
)


@component
def main():
return django_router(
show_route("/router/", show_route("subroute/")),
show_route("/router/unspecified/<value>/"),
show_route("/router/integer/<int:value>/"),
show_route("/router/path/<path:value>/"),
show_route("/router/slug/<slug:value>/"),
show_route("/router/string/<str:value>/"),
show_route("/router/uuid/<uuid:value>/"),
show_route("/router/any/<any:name>"),
show_route("/router/two/<int:value>/<str:value2>/"),
route("/router/next/<int:page>/", next_page()),
)
return django_router(*ROUTES)
7 changes: 5 additions & 2 deletions tests/test_app/tests/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,9 +594,12 @@ def test_channel_layer_components(self):

@navigate_to_page("/pyscript/")
def test_pyscript_0_hello_world(self):
# Use this test to wait for PyScript to fully load on the page
# This is the FIRST test to load the PyScript page, so it bears the cold-start
# cost of booting Pyodide and installing PyScript packages (micropip fetch).
# Give the rendered component an equally generous timeout, otherwise slow CI
# runners intermittently exceed the default 10s timeout and flake.
self.page.wait_for_selector("#hello-world-loading", timeout=30000)
self.page.wait_for_selector("#hello-world")
self.page.wait_for_selector("#hello-world", timeout=30000)

@navigate_to_page("/pyscript/")
def test_pyscript_1_custom_root(self):
Expand Down
7 changes: 6 additions & 1 deletion tests/test_app/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ def start_playwright_client(cls):
headless = str_to_bool(os.environ.get("PLAYWRIGHT_HEADLESS", GITHUB_ACTIONS))
cls.browser = cls.playwright.chromium.launch(headless=bool(headless))
cls.page = cls.browser.new_page()
cls.page.set_default_timeout(10000)
# A generous default timeout is required because the suite runs the full
# component set under one shared browser page and server. Under load (e.g.
# CI), async state updates, form mutations, and page loads can legitimately
# take longer than a short 10s window, causing intermittent TimeoutErrors.
# This matches ReactPy core's 30s test fixture timeout.
cls.page.set_default_timeout(30000)
cls.page.on("console", lambda msg: print(f"CLIENT {msg.type.upper()}: {msg.text}"))
cls.page.on("pageerror", lambda err: print(f"CLIENT EXCEPTION: {err.name}: {err.message}\n{err.stack}"))

Expand Down
Loading