"""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"'), ( "sources.html: markdown.js must load before the module script" ) 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