"""Unit: document viewer (phase 10).
Python side:
* ``doc_format`` — format from the path suffix (incl. ``.markdown`` and the
no-suffix fallback);
* the content endpoint's 200/404 mapping — tested WITHOUT a database by
stubbing the session via FastAPI's dependency override (unknown pairs and
traversal-style paths map to 404 ``{detail: "document not found"}``;
known pairs map to the full ``DocContent`` shape).
Frontend side:
* the viewer URL builder — its real query-encoding behavior (paths with
spaces/slashes) executed under node when available, plus source pins that
run everywhere;
* the shared-renderer extraction — ``markdown.js`` holds the renderer,
loaded by the pages that use it via a relative ``', html), (
f"{page} must load markdown.js via a relative "
+ "\\n\\n**bold** and `code`'));"
)
html = out.strip()
assert "', html)
assert html.index('src="assets/markdown.js"') < html.index('type="module"'), (
"index.html: markdown.js must load before the module scripts"
)
def test_modal_close_contract_pins() -> None:
"""Phase 26: the modal closes on the close button, on backdrop
click, and on Escape (captured document-level, so it works from any
focus position); focus returns to the triggering control
(best-effort); the fetch goes to the stateless content endpoint with
the same percent-encoding the page uses, and success renders through
the shared renderDocument; the "Full page" link is rebuilt on open."""
js = _read(MODAL_JS)
assert 'e.key === "Escape"' in js
assert 'addEventListener("keydown"' in js
assert "backdropEl.addEventListener(\"click\", closeDocumentModal)" in js
assert "closeEl.addEventListener(\"click\", closeDocumentModal)" in js
assert "triggerEl.focus" in js # best-effort focus restore
assert '"/api/documents/content?source=" + encodeURIComponent(' in js
assert '"&path=" + encodeURIComponent(' in js
# The shared renderer (not a copy), called with the modal's own
# #doc-modal-title / #doc-modal-meta / #doc-modal-content elements.
assert "renderDocument(doc, { titleEl, metaEl, contentEl })" in js
assert 'openEl.href = fullPageUrl(source, path)' in js
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_modal_url_builders_encode_like_the_page() -> None:
"""Behavioral check (node) of the modal's own URL builders: the
content fetch and the "Full page" href must come out percent-encoded
exactly like the page's builders (slashes/spaces in real paths)."""
js = _read(MODAL_JS)
content_fn = _extract_function(js, "contentUrl")
full_fn = _extract_function(js, "fullPageUrl")
out = _run_node(
content_fn
+ full_fn
+ "\nconsole.log(contentUrl('Homelab', 'notes/my file.yaml'));\n"
+ "console.log(fullPageUrl('H omelab', 'a/b.md'));"
)
assert out.splitlines() == [
"/api/documents/content?source=Homelab&path=notes%2Fmy%20file.yaml",
"/document.html?source=H%20omelab&path=a%2Fb.md",
]
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, 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
# 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