|
| 1 | +"""Tests for the docs navbar links. |
| 2 | +
|
| 3 | +The docs app is served under ``frontend_path="/docs"``. ``rx.el.a`` compiles to |
| 4 | +React Router's ``Link``, which resolves its destination against that basename; |
| 5 | +``rx.el.elements.a`` stays a raw HTML anchor, which does not. In-app links must |
| 6 | +therefore use the former, and links that already carry a full path (other |
| 7 | +deployments, the marketing site) must use the latter. |
| 8 | +""" |
| 9 | + |
| 10 | +import pytest |
| 11 | +import reflex as rx |
| 12 | + |
| 13 | + |
| 14 | +@pytest.fixture |
| 15 | +def navbar(): |
| 16 | + """Import the navbar module through the pages package. |
| 17 | +
|
| 18 | + Importing ``reflex_docs.views.docs_navbar`` first in a fresh interpreter |
| 19 | + trips a pre-existing circular import via ``reflex_docs.pages``. |
| 20 | +
|
| 21 | + Yields: |
| 22 | + The ``reflex_docs.views.docs_navbar`` module. |
| 23 | + """ |
| 24 | + import reflex_docs.pages # noqa: F401 |
| 25 | + from reflex_docs.views import docs_navbar |
| 26 | + |
| 27 | + yield docs_navbar |
| 28 | + |
| 29 | + |
| 30 | +def _collect_links(component) -> list[tuple[str, str]]: |
| 31 | + """Walk a component tree and collect every anchor destination. |
| 32 | +
|
| 33 | + Walks the tree rather than matching against ``str(component)``, whose repr |
| 34 | + is truncated for a tree this size. |
| 35 | +
|
| 36 | + Args: |
| 37 | + component: The component to walk. |
| 38 | +
|
| 39 | + Returns: |
| 40 | + A list of ``(kind, destination)`` pairs, where kind is ``"router"`` for |
| 41 | + a React Router link and ``"anchor"`` for a raw HTML anchor. |
| 42 | + """ |
| 43 | + links = [] |
| 44 | + name = type(component).__name__ |
| 45 | + if name == "ReactRouterLink": |
| 46 | + links.append(("router", str(component.to).strip('"'))) |
| 47 | + elif name == "A": |
| 48 | + links.append(("anchor", str(component.href).strip('"'))) |
| 49 | + for child in getattr(component, "children", ()): |
| 50 | + links.extend(_collect_links(child)) |
| 51 | + return links |
| 52 | + |
| 53 | + |
| 54 | +def test_internal_menu_items_use_router_links(navbar): |
| 55 | + """In-app navbar links must compile to React Router links. |
| 56 | +
|
| 57 | + Regression test: as a raw anchor, "Build with AI" sent users to |
| 58 | + ``reflex.dev/ai/overview/best-practices/`` instead of |
| 59 | + ``reflex.dev/docs/ai/overview/best-practices/``. |
| 60 | + """ |
| 61 | + links = _collect_links( |
| 62 | + navbar.menu_item("Build with AI", "/ai/overview/best-practices/", "ai") |
| 63 | + ) |
| 64 | + |
| 65 | + assert links == [("router", "/ai/overview/best-practices/")] |
| 66 | + |
| 67 | + |
| 68 | +def test_external_menu_items_use_plain_anchors(navbar): |
| 69 | + """Cross-app navbar links must stay raw anchors that own their full path.""" |
| 70 | + links = _collect_links(navbar.menu_item("XY", "/docs/xy/", "xy", external=True)) |
| 71 | + |
| 72 | + assert links == [("anchor", "/docs/xy/")] |
| 73 | + |
| 74 | + |
| 75 | +def test_navigation_menu_routes_in_app_destinations(navbar): |
| 76 | + """Every in-app navbar destination compiles to a router link.""" |
| 77 | + from reflex_docs.pages.docs import ai_builder, getting_started, hosting |
| 78 | + |
| 79 | + router_targets = { |
| 80 | + dest |
| 81 | + for kind, dest in _collect_links(navbar.navigation_menu()) |
| 82 | + if kind == "router" |
| 83 | + } |
| 84 | + |
| 85 | + for path in ( |
| 86 | + "/", |
| 87 | + ai_builder.overview.best_practices.path, |
| 88 | + getting_started.introduction.path, |
| 89 | + hosting.deploy_quick_start.path, |
| 90 | + ): |
| 91 | + assert path in router_targets, f"{path} is not a router link" |
| 92 | + |
| 93 | + # Router links resolve against frontend_path, so a literal /docs prefix |
| 94 | + # here would compile to /docs/docs/... |
| 95 | + double_prefixed = [dest for dest in router_targets if dest.startswith("/docs")] |
| 96 | + assert not double_prefixed, f"double-prefixed router links: {double_prefixed}" |
| 97 | + |
| 98 | + |
| 99 | +def test_navigation_menu_keeps_cross_app_destinations_raw(navbar): |
| 100 | + """Destinations outside this app render as raw anchors.""" |
| 101 | + anchor_targets = { |
| 102 | + dest |
| 103 | + for kind, dest in _collect_links(navbar.navigation_menu()) |
| 104 | + if kind == "anchor" |
| 105 | + } |
| 106 | + |
| 107 | + assert "/docs/xy/" in anchor_targets |
| 108 | + |
| 109 | + |
| 110 | +def test_external_links_bypass_the_router(navbar): |
| 111 | + """Absolute off-site URLs render as raw anchors, not router links.""" |
| 112 | + from reflex_site_shared.constants import GITHUB_URL, REFLEX_URL |
| 113 | + |
| 114 | + assert _collect_links(navbar.github_button()) == [("anchor", GITHUB_URL)] |
| 115 | + assert _collect_links(navbar.logo()) == [("anchor", REFLEX_URL)] |
| 116 | + |
| 117 | + |
| 118 | +def test_reflex_el_a_and_elements_a_are_not_interchangeable(): |
| 119 | + """Guard the distinction the navbar relies on. |
| 120 | +
|
| 121 | + ``rx.el.a`` is aliased to React Router's ``Link``; ``rx.el.elements.a`` is |
| 122 | + the raw HTML anchor. If those ever converge, the navbar's internal vs. |
| 123 | + external split becomes meaningless and this test should be revisited. |
| 124 | + """ |
| 125 | + assert _collect_links(rx.el.a(href="/x/")) == [("router", "/x/")] |
| 126 | + assert _collect_links(rx.el.elements.a(href="/x/")) == [("anchor", "/x/")] |
0 commit comments