"""Unit: document viewer (phase 10).
Python side:
* ``doc_format`` — format from the path suffix (incl. ``.markdown`` and the
no-suffix fallback) + the phase-102 extensionless name-token rule
(``Dockerfile`` → ``dockerfile`` with a configured token, suffixes still
unconditional, no-arg calls byte-identical to the suffix-only rule);
* 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, currentBack)' 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).
Phase 153 (task 01): the "Full page" builder's optional ``back``
(default null) — a null/absent ``back`` keeps the URL byte-identical
to pre-phase (no ``&back=`` at all — the Sources path), and a truthy
``back`` is appended + percent-encoded (``"/"`` → ``%2F``)."""
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'));\n"
+ "console.log(fullPageUrl('s', 'p'));\n"
+ "console.log(fullPageUrl('s', 'p', '/'));\n"
+ "console.log(fullPageUrl('H omelab', 'a/b.md', '/sources.html'));"
)
assert out.splitlines() == [
"/api/documents/content?source=Homelab&path=notes%2Fmy%20file.yaml",
# pre-phase byte-identical: no ``back`` arg → no ``&back=``
"/document.html?source=H%20omelab&path=a%2Fb.md",
"/document.html?source=s&path=p",
"/document.html?source=s&path=p&back=%2F",
"/document.html?source=H%20omelab&path=a%2Fb.md&back=%2Fsources.html",
]
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
def _stmt_block(js: str, start_marker: str, last_stmt: str) -> str:
"""Extract the source of a statement group: from ``start_marker``
to the balanced close of the ``{…}`` opened by ``last_stmt``
(the brace walk is the house style — no third-party parsers)."""
start = js.find(start_marker)
assert start != -1, f"marker not found: {start_marker!r}"
stmt_i = js.find(last_stmt, start)
assert stmt_i != -1, f"statement not found: {last_stmt!r}"
brace_i = js.find("{", stmt_i)
depth = 0
for j in range(brace_i, len(js)):
if js[j] == "{":
depth += 1
elif js[j] == "}":
depth -= 1
if depth == 0:
return js[start : j + 1]
raise AssertionError(f"unbalanced braces from {last_stmt!r}")
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_phase13_back_resolution_guard() -> None:
"""The phase-13 back-resolution contract, guarded in BOTH layers
(task 03; the phase-13 pins in ``test_viewer_js_rendering_
contracts`` are untouched — this runs beside them):
1. SOURCE — the resolution block itself, extracted from the live
``document.js`` and pinned byte-for-byte (the exact statements,
including the two-label ternary);
2. BEHAVIOR — that exact extracted block (not a re-implementation)
run under node over a fake ``params``/``backLink``:
* missing / empty / garbage (absolute cross-origin) /
protocol-relative / pseudo-protocol ``back`` → the
``/sources.html`` default + the "Sources" label;
* ``/`` → the same-origin chat root WINS + the "Chat" label;
* any other same-origin relative → wins verbatim + the
"Back" label (and an explicit ``/sources.html`` keeps the
"Sources" label).
A refactor that re-introduces a browser-history heuristic, an
origin check, or a different label mapping fails the behavior run
(and a reformat of the block fails the byte pin)."""
js = _read(DOCUMENT_JS)
block = _stmt_block(
js,
'const backParam = params.get("back") || "";',
"if (backLabel) {",
)
# 1. the byte-for-byte source pin (the exact statements)
assert 'const backParam = params.get("back") || "";' in block
assert 'backParam.startsWith("/") && !backParam.startsWith("//")' in block
assert "? backParam" in block and ': "/sources.html";' in block
assert "backLink.href = backTarget;" in block
assert 'backTarget === "/"' in block # the label ternary…
assert '"Chat"' in block
assert 'backTarget === "/sources.html"' in block
assert '"Sources"' in block
assert '"Back"' in block # …and the three labels
# 2. the behavior — the EXACT extracted block, run under node
script = (
"const block = " + json.dumps(block) + ";\n"
"function run(v) {\n"
" const params = { get: (k) => (k === 'back' ? v : null) };\n"
" const label = { textContent: '' };\n"
" const backLink = { href: '',"
' querySelector: (s) => (s === "span" ? label : null) };\n'
' const f = new Function("params", "backLink",'
' block + "\\nreturn backTarget;");\n'
" const target = f(params, backLink);\n"
" return [target, label.textContent];\n"
"}\n"
"const cases = [null, '', 'https://evil.com/x', '//evil.com',"
" 'javascript:alert(1)', '/', '/sources.html', '/chat?x=1'];\n"
"for (const v of cases) console.log(JSON.stringify([v, ...run(v)]));\n"
)
out = [json.loads(line) for line in _run_node(script).splitlines()]
assert out == [
[None, "/sources.html", "Sources"], # missing
["", "/sources.html", "Sources"], # empty
["https://evil.com/x", "/sources.html", "Sources"], # garbage (cross-origin)
["//evil.com", "/sources.html", "Sources"], # protocol-relative
["javascript:alert(1)", "/sources.html", "Sources"], # pseudo-protocol
["/", "/", "Chat"], # the chat root wins
["/sources.html", "/sources.html", "Sources"], # explicit, same label
["/chat?x=1", "/chat?x=1", "Back"], # other same-origin relative wins
]
# ---------------------------------------------------------------------------
# Phase 153 (task 02) — "Copy path" (the standalone titlebar meta row +
# the modal meta block — one shared-core control, per-surface wiring)
# ---------------------------------------------------------------------------
DOCUMENT_MODAL_JS = _read(MODAL_JS) # the modal owner (the live asset)
COPY_PATH_STR = 'doc.source + "/" + doc.path' # the copied string (ASSUMPTION A1)
STATUS_TERNARY = (
'status.textContent = ok ? "Path copied." : "Path ready — copy it from the field."'
) # the status wording pair (owner-locked fallback wording, path-adapted)
def _fn_async(js: str, name: str) -> str:
"""Like _fn, but includes a leading ``async `` when the function is
async (the phase-153 helpers are async)."""
start = js.find(f"function {name}(")
assert start != -1, f"{name}() must exist"
if js[max(0, start - 6) : start] == "async ":
start -= 6
i = js.find("(", start)
depth = 0
close = i
while i < len(js):
if js[i] == "(":
depth += 1
elif js[i] == ")":
depth -= 1
if depth == 0:
close = i
break
i += 1
brace = js.find("{", close)
depth = 0
for j in range(brace, len(js)):
if js[j] == "{":
depth += 1
elif js[j] == "}":
depth -= 1
if depth == 0:
return js[start : j + 1]
raise AssertionError(f"unbalanced braces in {name}")
def test_copy_path_button_built_in_the_shared_core() -> None:
"""renderDocument (the ONE shared core — modal AND /document.html):
the Copy-path control joins the meta row AFTER the path code and
BEFORE the origin anchor (one build site — the surfaces can never
drift). The button is a labeled icon+text control: type=button,
class doc-copy-path, an aria-hidden SVG (createElementNS — the
never-build-HTML rule stands: the module's ONE innerHTML stays the
escape-first markdown render) and the VISIBLE "Copy path" text as
the accessible name (no redundant aria-label)."""
fn = _fn(_read(DOCUMENT_JS), "copyPathButton")
assert 'document.createElement("button")' in fn
assert "b.type = \"button\"" in fn
assert 'b.className = "doc-copy-path"' in fn
assert "document.createElementNS(SVG_NS, \"svg\")" in fn
assert 'icon.setAttribute("aria-hidden", "true")' in fn
assert 'label.textContent = "Copy path"' in fn
assert "innerHTML" not in re.sub(r"//.*?$|/\*.*?\*/", "", fn, flags=re.S | re.M)
rd = _fn(_read(DOCUMENT_JS), "renderDocument")
assert "copyPathButton()" in rd
# meta row order: path code → copy button → origin anchor (spread) →
# created badge (the phase-145 pin's slot, unchanged).
code_i = rd.find("pathCode,")
btn_i = rd.find("copyBtn,")
spread_i = rd.find("...originItems,")
created_i = rd.find("metaBadge(\"doc-created\"")
assert 0 <= code_i < btn_i < spread_i < created_i
def test_page_copy_path_helper_pins() -> None:
"""document.js's page-local helper (the per-page duplication house
style — the modal module keeps its own copy; no shared module):
* the copied string is `doc.source + "/" + doc.path` (the read-tool
form — the same two values the meta row renders);
* navigator.clipboard.writeText in a try (a non-secure http origin
rejecting the clipboard is the fallback trigger);
* the status ternary: "Path copied." (success, visual-only —
aria-hidden, the toast idiom) / "Path ready — copy it from the
field." (failure — the owner-locked fallback wording,
path-adapted);
* a re-click re-attempts (the previous .doc-copy-pop block is
removed first — a failed copy never clears the previous state
into nothing);
* the success status auto-clears ~4s (the block is dropped if it
is still connected);
* the page's #doc-copy-announcer (role=status) reads the outcome
(one a11y announcer — the visible node is aria-hidden);
* the fallback field: input-like (type text, readOnly, class
doc-path-fallback, prefilled with the path, selects its content
on focus, auto-focused with preventScroll — the
.share-link-fallback pattern);
* the page's state floats in a .doc-copy-pop block anchored under
the button (in the sticky header — the meta row never wraps or
grows the bar, the phase-12/34 contract)."""
js = _read(DOCUMENT_JS)
assert "async function copyPathWithFallback(btn, path)" in js
assert "function makePathFallbackField(path)" in js
assert 'document.querySelectorAll(".doc-copy-pop").forEach((el) => el.remove());' in js
assert "await navigator.clipboard.writeText(path);" in js
try_i = js.find("try {\n await navigator.clipboard.writeText(path);\n } catch")
assert try_i != -1, "the writeText call must sit in a try (the rejection fallback)"
assert js.count(STATUS_TERNARY) == 1
assert "status.setAttribute(\"aria-hidden\", \"true\")" in js # visual-only status
assert 'document.querySelector("#doc-copy-announcer")' in js
assert "if (block.isConnected) block.remove();" in js
assert "}, 4000);" in js # the auto-clear beat
assert js.count(COPY_PATH_STR) == 1 # the copied string, wired in render()
assert 'metaEl.querySelector(".doc-copy-path")' in js # the render() wiring
# the fallback field (makePathFallbackField)
f = _fn(js, "makePathFallbackField")
assert 'document.createElement("input")' in f
assert "field.type = \"text\"" in f
assert "field.readOnly = true" in f
assert 'field.className = "doc-path-fallback"' in f
assert "field.value = path" in f
assert 'field.setAttribute("aria-label", "Document path")' in f
assert "field.addEventListener(\"focus\", () => field.select())" in f
# the floating block (page surface)
p = _fn_async(js, "copyPathWithFallback")
assert 'block.className = "doc-copy-pop"' in p
assert 'document.querySelector("header.doc-header")' in p
assert "br.bottom - hr.top + 6" in p # anchored under the button
assert "field.focus({ preventScroll: true })" in p
def test_modal_copy_path_helper_pins() -> None:
"""document-modal.js's page-local helper (the per-page duplication
house style):
* the copied string is `doc.source + "/" + doc.path` (the read-tool
form — the same two values the meta row renders);
* writeText in a try; the status ternary (the same owner-locked
wording as the page);
* the nodes land right after the "Copy path" control
(insertAdjacentElement("afterend") — the meta block wraps, so
they may take a new row);
* a re-click re-attempts (the previous .doc-copy-status /
.doc-path-fallback state is removed first — a failed copy never
clears the previous state into nothing);
* success auto-clears ~4s; #doc-modal-desc (setDesc) reads the
outcome (one a11y announcer — the visible node is aria-hidden);
* the renderDocument call is followed by the wireCopyPath wiring
(the control is live on every open)."""
js = DOCUMENT_MODAL_JS
assert "async function copyPathWithFallback(metaEl, path)" in js
assert "function wireCopyPath(metaEl, doc)" in js
assert "let modalCopyTimer = 0" in js
assert "await navigator.clipboard.writeText(path);" in js
assert js.count(STATUS_TERNARY) == 1
assert 'status.setAttribute("aria-hidden", "true")' in js
assert js.count('".doc-copy-status, .doc-path-fallback"') == 1 # the re-attempt cleanup
assert js.count('insertAdjacentElement("afterend"') == 3 # 1 success + 2 failure
assert 'setDesc("Path copied.")' in js
assert 'setDesc("Path ready — copy it from the field.")' in js
assert "if (status.isConnected) status.remove();" in js
assert "}, 4000);" in js
assert js.count(COPY_PATH_STR) == 1 # wired in wireCopyPath
assert 'metaEl.querySelector(".doc-copy-path")' in js
# renderDocument → wireCopyPath, in openDocumentModal's fetch .then
# (the LAST occurrence — the docstring names the render earlier):
rd_i = js.rfind("renderDocument(doc, { titleEl, metaEl, contentEl });")
wire_i = js.rfind("wireCopyPath(metaEl, doc)")
assert rd_i != -1 and wire_i != -1
assert 0 < wire_i - rd_i < 300 # right after the render
# the shared core builds the button for BOTH surfaces — the modal
# module never builds one itself (no per-surface copy):
assert "copyPathButton" not in js
def test_document_html_carries_the_copy_announcer() -> None:
"""The /document.html page's role=status live region for the copy
outcome — visually hidden (the visible status line is the
aria-hidden floating node; this node is the sole a11y announcer,
the share pattern)."""
html = (FRONTEND / "document.html").read_text(encoding="utf-8")
i = html.find('id="doc-copy-announcer"')
assert i != -1
node = html[i - 200 : i + 200]
assert "visually-hidden" in node
assert "role=\"status\"" in node
assert "aria-live" in node
def test_document_html_carries_the_copy_path_static_nodes() -> None:
"""The ``document.html`` Copy-path static nodes (task 03 — the
Copy-path node on the full page): the floating ``.doc-copy-pop``
block is anchored in the page's ``header.doc-header`` (absent,
the page helper's ``if (header)`` guard would drop the block and
the status would never float — the meta row is never grown, the
phase-12/34 contract); the ``#doc-copy-announcer`` (role=status,
the sole a11y announcer) and the ``#doc-meta`` row (where the
shared core builds the control) must all be present for the
page surface to work."""
html = (FRONTEND / "document.html").read_text(encoding="utf-8")
assert '