feat(ui): documents open in an almost-fullscreen modal instead of a new page — same-page overlay on chat + Sources, /document.html kept as the no-JS/direct-link fallback

This commit is contained in:
2026-08-25 13:45:57 -04:00
parent 476aa0e066
commit fcde1fd37b
18 changed files with 1307 additions and 258 deletions
+40 -31
View File
@@ -6,21 +6,26 @@ Run in isolation (DB must be up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_document_back_navigation.py -v --no-cov
Both entry points (chat source chips, Sources table links) open the viewer
in a NEW tab, where there is no browser history — so the return target is
carried in the viewer URL: chat chips append ``&back=%2F`` (resolves to
"Chat"), Sources links omit the param (the viewer's default
``/sources.html`` applies → "Sources"). The viewer only honors
same-origin relative ``back`` values; everything else falls back to
``/sources.html``.
The viewer can be reached directly (no browser history to go back to),
so the return target is carried in the viewer URL: chat chips append
``&back=%2F`` (resolves to "Chat"), Sources links omit the param (the
viewer's default ``/sources.html`` applies → "Sources"). The viewer only
honors same-origin relative ``back`` values; everything else falls back
to ``/sources.html``.
Phase 26 adaptation: the chip/row-link LEFT click now opens the
document in the same-page modal — no new tab is spawned. The encoded
viewer URL survives as each link's ``href`` (the no-JS / context-menu
"open in new tab" escape hatch), so the back contract is asserted on
that exact href and verified by navigating to it directly.
Test → story mapping (Playwright Mapping Rule):
1. ``test_back_from_chat_returns_to_chat`` — question → source chip →
new tab with ``&back=%2F`` → back link href ``/`` labeled "Chat" →
click → the chat page.
2. ``test_back_from_sources_returns_to_sources`` — Sources table link →
new tab without a ``back`` param → back link href ``/sources.html``
labeled "Sources" → click → the Sources page.
1. ``test_back_from_chat_returns_to_chat`` — question → source chip href
(carries ``&back=%2F``) → viewer back link href ``/`` labeled "Chat"
→ click → the chat page.
2. ``test_back_from_sources_returns_to_sources`` — Sources table link
href (no ``back`` param) → back link href ``/sources.html`` labeled
"Sources" → click → the Sources page.
3. ``test_malicious_back_param_is_rejected`` — absolute,
protocol-relative, and ``javascript:`` ``back`` values all fall back
to ``/sources.html`` (labeled "Sources", navigable).
@@ -106,30 +111,33 @@ def test_back_from_chat_returns_to_chat(
chip = page.locator(".msg.brain .source-chip", has_text="kubernetes.md")
expect(chip).to_have_count(1, timeout=30_000)
# Chat chips carry back=/ (encoded %2F) so the viewer knows where home is.
# Chat chips carry back=/ (encoded %2F) so the viewer knows where
# home is. Phase 26: the left click opens the same-page modal (no
# target=_blank); this href is what the no-JS / context-menu "open
# in a new tab" path reaches, so the back contract rides on it.
expect(chip.first).to_have_attribute(
"href", f"/document.html?source={DOC_SOURCE}&path={DOC_PATH}&back=%2F"
)
expect(chip.first).not_to_have_attribute("target") # phase 26: modal, not a new tab
with page.expect_popup() as popup_info:
chip.first.click()
viewer = popup_info.value
expect(viewer).to_have_url(
# The exact href asserted above (the no-JS / new-tab escape hatch).
page.goto(f"{app_url}/document.html?source={DOC_SOURCE}&path={DOC_PATH}&back=%2F")
expect(page).to_have_url(
re.compile(
re.escape(f"{app_url}/document.html?source={DOC_SOURCE}&path={DOC_PATH}&back=%2F")
)
)
# The cited document actually rendered (this is the viewer, not an error).
expect(viewer.locator("#doc-title")).to_have_text(DOC_TITLE)
expect(page.locator("#doc-title")).to_have_text(DOC_TITLE)
# Back link resolved to the chat page, labeled "Chat".
back = viewer.locator("#doc-back")
back = page.locator("#doc-back")
expect(back).to_have_attribute("href", "/")
expect(back).to_have_text("Chat")
# Click: deterministic anchor navigation back to the chat page.
back.click()
expect(viewer).to_have_url(f"{app_url}/")
expect(viewer.locator("#composer")).to_be_visible()
expect(page).to_have_url(f"{app_url}/")
expect(page.locator("#composer")).to_be_visible()
# ---------------------------------------------------------------------------
@@ -148,24 +156,25 @@ def test_back_from_sources_returns_to_sources(
link = row.locator("td:nth-child(2) a.doc-link")
expect(link).to_have_count(1)
# Sources links carry NO back param — the viewer's default target
# (/sources.html) applies.
# (/sources.html) applies. Phase 26: left click opens the modal;
# the href (no back param) is the no-JS / new-tab escape hatch.
expect(link).to_have_attribute(
"href", f"/document.html?source={DOC_SOURCE}&path={DOC_PATH}"
)
expect(link).not_to_have_attribute("target") # phase 26: modal, not a new tab
with page.expect_popup() as popup_info:
link.click()
viewer = popup_info.value
assert "back=" not in viewer.url, f"unexpected back param: {viewer.url}"
expect(viewer.locator("#doc-title")).to_have_text(DOC_TITLE)
# The exact href asserted above — no back param in the URL.
page.goto(f"{app_url}/document.html?source={DOC_SOURCE}&path={DOC_PATH}")
assert "back=" not in page.url, f"unexpected back param: {page.url}"
expect(page.locator("#doc-title")).to_have_text(DOC_TITLE)
# Back link kept the default target, labeled "Sources".
back = viewer.locator("#doc-back")
back = page.locator("#doc-back")
expect(back).to_have_attribute("href", "/sources.html")
expect(back).to_have_text("Sources")
back.click()
expect(viewer).to_have_url(f"{app_url}/sources.html")
expect(viewer.locator("#docs-table")).to_be_visible()
expect(page).to_have_url(f"{app_url}/sources.html")
expect(page.locator("#docs-table")).to_be_visible()
# ---------------------------------------------------------------------------