reflex/integration/test_dynamic_routes.py
Khaleel Al-Adhami a5c73ad8e5
Use old serializer system in LiteralVar (#3875)
* use serializer system

* add checks for unsupported operands

* and and or are now supported

* format

* remove unnecessary call to JSON

* put base before rest

* fix failing testcase

* add hinting to get static analysis to complain

* damn

* big changes

* get typeguard from extensions

* please darglint

* dangit darglint

* remove one from vars

* add without data and use it in plotly

* DARGLINT

* change format for special props

* add pyi

* delete instances of Var.create

* modify client state to work

* fixed so much

* remove every Var.create

* delete all basevar stuff

* checkpoint

* fix pyi

* get older python to work

* dangit darglint

* add simple fix to last failing testcase

* remove var name unwrapped and put client state on immutable var

* fix older python

* fox event issues

* change forms pyi

* make test less strict

* use rx state directly

* add typeignore to page_id

* implement foreach

* delete .web states folder silly

* update reflex chakra

* fix issue when on mount or on unmount is not set

* nuke Var

* run pyi

* import immutablevar in critical location

* delete unwrap vars

* bring back array ref

* fix style props in app

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* Update find_replace (#3886)

* [REF-3592]Promote `rx.progress` from radix themes (#3878)

* Promote `rx.progress` from radix themes

* fix pyi

* add warning when accessing `rx._x.progress`

* Use correct flexgen backend URL (#3891)

* Remove demo template (#3888)

* gitignore .web (#3885)

* update overflowY in AUTO_HEIGHT_JS from hidden to scroll (#3882)

* Retain mutability inside `async with self` block (#3884)

When emitting a state update, restore `_self_mutable` to the value it had
previously so that `yield` in the middle of `async with self` does not result
in an immutable StateProxy.

Fix #3869

* Include child imports in markdown component_map (#3883)

If a component in the markdown component_map contains children components, use
`_get_all_imports` to recursively enumerate them.

Fix #3880

* [REF-3570] Remove deprecated REDIS_URL syntax (#3892)

* mixin computed vars should only be applied to highest level state (#3833)

* improve state hierarchy validation, drop old testing special case (#3894)

* fix var dependency dicts (#3842)

* Adding array to array pluck operation. (#3868)

* fix initial state without cv fallback (#3670)

* add fragment to foreach (#3877)

* Update docker-example (#3324)

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* Merge branch 'main' into use-old-serializer-in-literalvar

* [REF-3570] Remove deprecated REDIS_URL syntax (#3892)

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* [REF-3570] Remove deprecated REDIS_URL syntax (#3892)

* remove extra var

Co-authored-by: Masen Furer <m_github@0x26.net>

* resolve typo

* write better doc for var.create

* return var value when we know it's literal var

* fix unit test

* less bloat for ToOperations

* simplify ImmutableComputedVar.__get__ (#3902)

* simplify ImmutableComputedVar.__get__

* ruff it

---------

Co-authored-by: Samarth Bhadane <samarthbhadane119@gmail.com>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>
Co-authored-by: Masen Furer <m_github@0x26.net>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Vishnu Deva <vishnu.deva12@gmail.com>
Co-authored-by: abulvenz <a.eismann@senbax.de>
2024-09-10 11:43:37 -07:00

307 lines
10 KiB
Python

"""Integration tests for dynamic route page behavior."""
from __future__ import annotations
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_chakra as rc
import reflex as rx
class DynamicState(rx.State):
order: List[str] = []
def on_load(self):
self.order.append(f"{self.router.page.path}-{self.page_id or 'no page id'}")
def on_load_redir(self):
query_params = self.router.page.params
self.order.append(f"on_load_redir-{query_params}")
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(
rc.input(
value=DynamicState.router.session.client_token,
is_read_only=True,
id="token",
),
rc.input(value=rx.State.page_id, is_read_only=True, id="page_id"), # type: ignore
rc.input(
value=DynamicState.router.page.raw_path,
is_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", # type: ignore
),
rx.link("missing", href="/missing", id="link_missing"),
rc.list(
rx.foreach(
DynamicState.order, # type: ignore
lambda i: rc.list_item(rx.text(i)),
),
),
)
@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(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(f"dynamic_route"),
app_name=f"dynamicroute_{app_harness_env.__name__.lower()}",
app_source=DynamicRoute, # type: ignore
) 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()
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)
assert (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"])