From 303ea3efbce95eedd3de1f386c479b65153d473f Mon Sep 17 00:00:00 2001 From: Omar Padron Date: Sat, 18 Apr 2026 00:08:43 -1000 Subject: [PATCH] Use full_raw_path for heading anchor hrefs HeadingLink built its anchor href from router.page.full_path, which is the normalized (registered-route) path. When frontend_path is set, that path is stripped of the mount prefix; the generated href drops the prefix too and clicking jumps to a route that doesn't exist on the ingress. Switch to full_raw_path, which is the URL the client actually requested and preserves the frontend_path prefix. When frontend_path is unset, full_path equals full_raw_path, so this is a no-op for that case. The paired rx.set_clipboard(href) in on_click reads the same local, so the "copy link" action and the rendered href stay consistent. --- .../components/blocks/headings.py | 2 +- tests/units/reflex_ui_shared/__init__.py | 0 .../reflex_ui_shared/components/__init__.py | 0 .../components/blocks/__init__.py | 0 .../components/blocks/test_headings.py | 42 +++++++++++++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/units/reflex_ui_shared/__init__.py create mode 100644 tests/units/reflex_ui_shared/components/__init__.py create mode 100644 tests/units/reflex_ui_shared/components/blocks/__init__.py create mode 100644 tests/units/reflex_ui_shared/components/blocks/test_headings.py diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/headings.py b/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/headings.py index c75b145b940..455c430792a 100644 --- a/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/headings.py +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/headings.py @@ -112,7 +112,7 @@ def create( The component. """ id_ = cls.slugify(text) - href = rx.State.router.page.full_path + "#" + id_ + href = rx.State.router.page.full_raw_path + "#" + id_ scroll_margin = rx.cond( HostingBannerState.is_banner_visible, "scroll-mt-[113px]", diff --git a/tests/units/reflex_ui_shared/__init__.py b/tests/units/reflex_ui_shared/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/units/reflex_ui_shared/components/__init__.py b/tests/units/reflex_ui_shared/components/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/units/reflex_ui_shared/components/blocks/__init__.py b/tests/units/reflex_ui_shared/components/blocks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/units/reflex_ui_shared/components/blocks/test_headings.py b/tests/units/reflex_ui_shared/components/blocks/test_headings.py new file mode 100644 index 00000000000..f235218aca1 --- /dev/null +++ b/tests/units/reflex_ui_shared/components/blocks/test_headings.py @@ -0,0 +1,42 @@ +"""Unit tests for reflex_ui_shared.components.blocks.headings.""" + +from reflex_ui_shared.components.blocks.headings import HeadingLink + + +def _all_rendered_prop_text(component) -> str: + """Collect the string form of every prop across a component tree. + + Args: + component: A Reflex component. + + Returns: + A single string containing the concatenated props of the component and + all of its descendants. + """ + texts = [str(component._render().props)] + for child in component.children: + texts.append(_all_rendered_prop_text(child)) + return " ".join(texts) + + +def test_heading_link_uses_full_raw_path(): + """Heading anchor href must preserve the frontend_path prefix. + + The HeadingLink anchor's href is built from ``router.page``. Using + ``full_path`` loses the ``frontend_path`` prefix because ``path`` is the + registered (normalized) route; ``full_raw_path`` preserves the URL the + client actually requested, which is what a public-facing anchor needs. + """ + component = HeadingLink.create(text="Sample Section", heading="h2") + rendered = _all_rendered_prop_text(component) + + assert "full_raw_path" in rendered, ( + "heading anchor href should reference router.page.full_raw_path to " + "preserve frontend_path prefix" + ) + # "full_path" is a substring of "full_raw_path", so strip matches of the + # correct name before checking for the stale one. + assert "full_path" not in rendered.replace("full_raw_path", ""), ( + "heading anchor href still references router.page.full_path; this " + "drops the frontend_path prefix when frontend_path is set" + )