fix(ui): document viewer back button returns to the page you came from (chat or sources)

This commit is contained in:
2026-08-22 15:26:43 -04:00
parent 8ca564cd83
commit 2485b50af0
6 changed files with 287 additions and 31 deletions
+33 -11
View File
@@ -160,18 +160,26 @@ def test_content_requires_both_params() -> None:
def test_viewer_url_builder_present_in_chat_and_sources() -> None:
"""Both entry points (chat chips, Sources rows) build the same
encoded viewer URL and open it in a new tab with rel=noopener."""
encoded viewer URL and open it in a new tab with rel=noopener.
Phase 13: the chat builder additionally carries ``back=/`` (encoded
%2F) so the viewer's back button returns to the chat; Sources links
intentionally omit the param (the viewer's /sources.html default)."""
for name, js in (("app.js", _read(APP_JS)), ("sources.js", _read(SOURCES_JS))):
assert "function documentUrl(source, path)" in js, name
assert '"/document.html?source=" + encodeURIComponent(' in js, name
assert '"&path=" + encodeURIComponent(' in js, name
app_js = _read(APP_JS)
assert "chip.href = documentUrl(s.source, s.path)" in app_js
# Chat: 3-arg builder with back defaulting to the chat page.
assert 'function documentUrl(source, path, back = "/")' in app_js
assert '"&back=" + encodeURIComponent(back)' in app_js
assert 'chip.href = documentUrl(s.source, s.path, "/")' in app_js
assert 'chip.target = "_blank"' in app_js
assert 'chip.rel = "noopener"' in app_js
sources_js = _read(SOURCES_JS)
# Sources: unchanged 2-arg builder — no back param in the URL.
assert "function documentUrl(source, path)" in sources_js
assert 'link.className = "doc-link"' in sources_js
assert "link.href = documentUrl(d.source, d.path)" in sources_js
assert 'link.target = "_blank"' in sources_js
@@ -188,7 +196,9 @@ def _run_node(script: str) -> str:
def _extract_function(js: str, name: str) -> str:
match = re.search(rf"(?:export )?function {name}\(source, path\) \{{.*?\n\}}", js, re.S)
match = re.search(
rf"(?:export )?function {name}\(source, path(?:, back = \"/\")?\) \{{.*?\n\}}", js, re.S
)
assert match, f"{name}(source, path) not found"
return match.group(0).replace("export ", "", 1)
@@ -196,18 +206,21 @@ def _extract_function(js: str, name: str) -> str:
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_viewer_url_builder_encodes_spaces_and_slashes() -> None:
"""Behavioral check of the real builder (app.js) under node: slashes
and spaces in source/path values must come out percent-encoded."""
and spaces in source/path values must come out percent-encoded, and
the back target is appended + encoded (phase 13)."""
fn = _extract_function(_read(APP_JS), "documentUrl")
out = _run_node(
f"{fn}\n"
"console.log(documentUrl('Homelab', 'kubernetes.md'));\n"
"console.log(documentUrl('Homelab', 'notes/my file.yaml'));\n"
"console.log(documentUrl('H omelab', 'a/b.md'));"
"console.log(documentUrl('H omelab', 'a/b.md'));\n"
"console.log(documentUrl('Homelab', 'kubernetes.md', '/sources.html'));"
)
assert out.splitlines() == [
"/document.html?source=Homelab&path=kubernetes.md",
"/document.html?source=Homelab&path=notes%2Fmy%20file.yaml",
"/document.html?source=H%20omelab&path=a%2Fb.md",
"/document.html?source=Homelab&path=kubernetes.md&back=%2F",
"/document.html?source=Homelab&path=notes%2Fmy%20file.yaml&back=%2F",
"/document.html?source=H%20omelab&path=a%2Fb.md&back=%2F",
"/document.html?source=Homelab&path=kubernetes.md&back=%2Fsources.html",
]
@@ -259,10 +272,19 @@ def test_markdown_renderer_stays_xss_safe_and_unchanged() -> None:
def test_viewer_js_rendering_contracts() -> None:
"""document.js: raw formats go in via textContent (never parsed as
HTML), markdown via the shared renderer, 404 → designed not-found
state, back link prefers browser history when there is one."""
state, and (phase 13) the back link resolves the ``back`` param —
same-origin relative URLs only, /sources.html default, no browser
history heuristics (both entry points are fresh tabs)."""
js = _read(DOCUMENT_JS)
assert "pre.textContent = doc.content" in js # raw formats: text node
assert "renderMarkdown(doc.content)" in js # md/markdown: shared renderer
assert "showNotFound" in js
assert "history.length > 1" in js
# Phase 13: deterministic back-target resolution, no history heuristics.
assert "history.length" not in js
assert "window.history.back" not in js
assert 'backParam.startsWith("/")' in js # same-origin relative only…
assert 'backParam.startsWith("//")' in js # …and not protocol-relative
assert 'backLink.href = backTarget' in js # deterministic anchor navigation
assert '"/sources.html"' in js # default target + no-JS fallback value
assert '"Chat"' in js and '"Sources"' in js # labels for the two entry points
assert "encodeURIComponent" in js # content fetch uses the same encoding