Allow underscores in routes (#1713)
This commit is contained in:
parent
9987e18eef
commit
891e6a4736
@ -169,8 +169,7 @@ def test_on_load_navigate(
|
|||||||
link = driver.find_element(By.ID, "link_page_next")
|
link = driver.find_element(By.ID, "link_page_next")
|
||||||
assert link
|
assert link
|
||||||
|
|
||||||
exp_order = [f"/page/[page-id]-{ix}" for ix in range(10)]
|
exp_order = [f"/page/[page_id]-{ix}" for ix in range(10)]
|
||||||
|
|
||||||
# click the link a few times
|
# click the link a few times
|
||||||
for ix in range(10):
|
for ix in range(10):
|
||||||
# wait for navigation, then assert on url
|
# wait for navigation, then assert on url
|
||||||
@ -190,13 +189,13 @@ def test_on_load_navigate(
|
|||||||
# manually load the next page to trigger client side routing in prod mode
|
# manually load the next page to trigger client side routing in prod mode
|
||||||
if is_prod:
|
if is_prod:
|
||||||
exp_order += ["/404-no page id"]
|
exp_order += ["/404-no page id"]
|
||||||
exp_order += ["/page/[page-id]-10"]
|
exp_order += ["/page/[page_id]-10"]
|
||||||
with poll_for_navigation(driver):
|
with poll_for_navigation(driver):
|
||||||
driver.get(f"{dynamic_route.frontend_url}/page/10/")
|
driver.get(f"{dynamic_route.frontend_url}/page/10/")
|
||||||
poll_for_order(exp_order)
|
poll_for_order(exp_order)
|
||||||
|
|
||||||
# make sure internal nav still hydrates after redirect
|
# make sure internal nav still hydrates after redirect
|
||||||
exp_order += ["/page/[page-id]-11"]
|
exp_order += ["/page/[page_id]-11"]
|
||||||
link = driver.find_element(By.ID, "link_page_next")
|
link = driver.find_element(By.ID, "link_page_next")
|
||||||
with poll_for_navigation(driver):
|
with poll_for_navigation(driver):
|
||||||
link.click()
|
link.click()
|
||||||
@ -205,7 +204,7 @@ def test_on_load_navigate(
|
|||||||
# load same page with a query param and make sure it passes through
|
# load same page with a query param and make sure it passes through
|
||||||
if is_prod:
|
if is_prod:
|
||||||
exp_order += ["/404-no page id"]
|
exp_order += ["/404-no page id"]
|
||||||
exp_order += ["/page/[page-id]-11"]
|
exp_order += ["/page/[page_id]-11"]
|
||||||
with poll_for_navigation(driver):
|
with poll_for_navigation(driver):
|
||||||
driver.get(f"{driver.current_url}?foo=bar")
|
driver.get(f"{driver.current_url}?foo=bar")
|
||||||
poll_for_order(exp_order)
|
poll_for_order(exp_order)
|
||||||
@ -220,7 +219,7 @@ def test_on_load_navigate(
|
|||||||
# browser nav should still trigger hydration
|
# browser nav should still trigger hydration
|
||||||
if is_prod:
|
if is_prod:
|
||||||
exp_order += ["/404-no page id"]
|
exp_order += ["/404-no page id"]
|
||||||
exp_order += ["/page/[page-id]-11"]
|
exp_order += ["/page/[page_id]-11"]
|
||||||
with poll_for_navigation(driver):
|
with poll_for_navigation(driver):
|
||||||
driver.back()
|
driver.back()
|
||||||
poll_for_order(exp_order)
|
poll_for_order(exp_order)
|
||||||
@ -235,7 +234,7 @@ def test_on_load_navigate(
|
|||||||
# hit a page that redirects back to dynamic page
|
# hit a page that redirects back to dynamic page
|
||||||
if is_prod:
|
if is_prod:
|
||||||
exp_order += ["/404-no page id"]
|
exp_order += ["/404-no page id"]
|
||||||
exp_order += ["on_load_redir-{'foo': 'bar', 'page_id': '0'}", "/page/[page-id]-0"]
|
exp_order += ["on_load_redir-{'foo': 'bar', 'page_id': '0'}", "/page/[page_id]-0"]
|
||||||
with poll_for_navigation(driver):
|
with poll_for_navigation(driver):
|
||||||
driver.get(f"{dynamic_route.frontend_url}/redirect-page/0/?foo=bar")
|
driver.get(f"{dynamic_route.frontend_url}/redirect-page/0/?foo=bar")
|
||||||
poll_for_order(exp_order)
|
poll_for_order(exp_order)
|
||||||
|
@ -355,7 +355,10 @@ class App(Base):
|
|||||||
assert isinstance(
|
assert isinstance(
|
||||||
component, Callable
|
component, Callable
|
||||||
), "Route must be set if component is not a callable."
|
), "Route must be set if component is not a callable."
|
||||||
route = component.__name__
|
# Format the route.
|
||||||
|
route = format.format_route(component.__name__)
|
||||||
|
else:
|
||||||
|
route = format.format_route(route, format_case=False)
|
||||||
|
|
||||||
# Check if the route given is valid
|
# Check if the route given is valid
|
||||||
verify_route_validity(route)
|
verify_route_validity(route)
|
||||||
@ -388,9 +391,6 @@ class App(Base):
|
|||||||
if script_tags:
|
if script_tags:
|
||||||
component.children.extend(script_tags)
|
component.children.extend(script_tags)
|
||||||
|
|
||||||
# Format the route.
|
|
||||||
route = format.format_route(route)
|
|
||||||
|
|
||||||
# Add the page.
|
# Add the page.
|
||||||
self._check_routes_conflict(route)
|
self._check_routes_conflict(route)
|
||||||
self.pages[route] = component
|
self.pages[route] = component
|
||||||
|
@ -164,6 +164,21 @@ def to_title_case(text: str) -> str:
|
|||||||
return "".join(word.capitalize() for word in text.split("_"))
|
return "".join(word.capitalize() for word in text.split("_"))
|
||||||
|
|
||||||
|
|
||||||
|
def to_kebab_case(text: str) -> str:
|
||||||
|
"""Convert a string to kebab case.
|
||||||
|
|
||||||
|
The words in the text are converted to lowercase and
|
||||||
|
separated by hyphens.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The string to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The title case string.
|
||||||
|
"""
|
||||||
|
return to_snake_case(text).replace("_", "-")
|
||||||
|
|
||||||
|
|
||||||
def format_string(string: str) -> str:
|
def format_string(string: str) -> str:
|
||||||
"""Format the given string as a JS string literal..
|
"""Format the given string as a JS string literal..
|
||||||
|
|
||||||
@ -202,18 +217,20 @@ def format_var(var: Var) -> str:
|
|||||||
return json_dumps(var.full_name)
|
return json_dumps(var.full_name)
|
||||||
|
|
||||||
|
|
||||||
def format_route(route: str) -> str:
|
def format_route(route: str, format_case=True) -> str:
|
||||||
"""Format the given route.
|
"""Format the given route.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
route: The route to format.
|
route: The route to format.
|
||||||
|
format_case: whether to format case to kebab case.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The formatted route.
|
The formatted route.
|
||||||
"""
|
"""
|
||||||
# Strip the route.
|
|
||||||
route = route.strip("/")
|
route = route.strip("/")
|
||||||
route = to_snake_case(route).replace("_", "-")
|
# Strip the route and format casing.
|
||||||
|
if format_case:
|
||||||
|
route = to_kebab_case(route)
|
||||||
|
|
||||||
# If the route is empty, return the index route.
|
# If the route is empty, return the index route.
|
||||||
if route == "":
|
if route == "":
|
||||||
|
71
tests/test_route.py
Normal file
71
tests/test_route.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from reflex import constants
|
||||||
|
from reflex.route import catchall_in_route, get_route_args, verify_route_validity
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"route_name, expected",
|
||||||
|
[
|
||||||
|
("/users/[id]", {"id": constants.RouteArgType.SINGLE}),
|
||||||
|
(
|
||||||
|
"/posts/[postId]/comments/[commentId]",
|
||||||
|
{
|
||||||
|
"postId": constants.RouteArgType.SINGLE,
|
||||||
|
"commentId": constants.RouteArgType.SINGLE,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_route_args(route_name, expected):
|
||||||
|
assert get_route_args(route_name) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"route_name",
|
||||||
|
[
|
||||||
|
"/products/[id]/[id]",
|
||||||
|
"/posts/[postId]/comments/[postId]",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_invalid_route_args(route_name):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
get_route_args(route_name)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"route_name,expected",
|
||||||
|
[
|
||||||
|
("/events/[year]/[month]/[...slug]", "[...slug]"),
|
||||||
|
("pages/shop/[[...slug]]", "[[...slug]]"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_catchall_in_route(route_name, expected):
|
||||||
|
assert catchall_in_route(route_name) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"route_name",
|
||||||
|
[
|
||||||
|
"/products",
|
||||||
|
"/products/[category]/[...]/details/[version]",
|
||||||
|
"[...]",
|
||||||
|
"/products/details",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_verify_valid_routes(route_name):
|
||||||
|
verify_route_validity(route_name)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"route_name",
|
||||||
|
[
|
||||||
|
"/products/[...]/details/[category]/latest",
|
||||||
|
"/blog/[...]/post/[year]/latest",
|
||||||
|
"/products/[...]/details/[...]/[category]/[...]/latest",
|
||||||
|
"/products/[...]/details/category",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_verify_invalid_routes(route_name):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
verify_route_validity(route_name)
|
@ -250,23 +250,31 @@ def test_is_generic_alias(cls: type, expected: bool):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"route,expected",
|
"route,format_case,expected",
|
||||||
[
|
[
|
||||||
("", "index"),
|
("", True, "index"),
|
||||||
("/", "index"),
|
("/", True, "index"),
|
||||||
("custom-route", "custom-route"),
|
("custom-route", True, "custom-route"),
|
||||||
("custom-route/", "custom-route"),
|
("custom-route", False, "custom-route"),
|
||||||
("/custom-route", "custom-route"),
|
("custom-route/", True, "custom-route"),
|
||||||
|
("custom-route/", False, "custom-route"),
|
||||||
|
("/custom-route", True, "custom-route"),
|
||||||
|
("/custom-route", False, "custom-route"),
|
||||||
|
("/custom_route", True, "custom-route"),
|
||||||
|
("/custom_route", False, "custom_route"),
|
||||||
|
("/CUSTOM_route", True, "custom-route"),
|
||||||
|
("/CUSTOM_route", False, "CUSTOM_route"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_format_route(route: str, expected: bool):
|
def test_format_route(route: str, format_case: bool, expected: bool):
|
||||||
"""Test formatting a route.
|
"""Test formatting a route.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
route: The route to format.
|
route: The route to format.
|
||||||
|
format_case: Whether to change casing to snake_case.
|
||||||
expected: The expected formatted route.
|
expected: The expected formatted route.
|
||||||
"""
|
"""
|
||||||
assert format.format_route(route) == expected
|
assert format.format_route(route, format_case=format_case) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
Loading…
Reference in New Issue
Block a user