"""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 '
' in html, ( "the page's sticky header is the copy block's anchor" ) assert 'id="doc-copy-announcer"' in html, "the announcer node is missing" assert 'role="status"' in html, "the announcer must be a live region" assert 'id="doc-meta"' in html, "the meta row (the control's home) is missing" def test_copy_path_css() -> None: """The Copy-path styles (phase 153, task 02): * .doc-copy-path — the row-action language (inline-flex icon + label), neutral secondary fill, brand-soft hover pair, focus-visible outline; the ≤640px block gives it the ≥44px touch-target floor (PLAN §7.1); * .doc-copy-status — the visible status (ink-soft on surface — the meta row's 5.1:1 pair), serving BOTH surfaces; * .doc-copy-pop — the page's floating state block (position absolute in the sticky header — never grows the bar), z-index 30 inside the header's z-20 root context; * .doc-path-fallback — the .share-link-fallback pattern (input variant): mono, ellipsis, surface fill, --line border.""" css = (FRONTEND / "assets" / "styles.css").read_text(encoding="utf-8") bp = css.find(".doc-copy-path {") assert bp != -1 b = css[bp : css.find("}", bp)] assert "display: inline-flex" in b assert "gap: 0.3rem" in b assert "cursor: pointer" in b assert ".doc-copy-path svg" in css assert ".doc-copy-path:hover" in css assert ".doc-copy-path:focus-visible" in css assert ".doc-copy-path { min-height: 44px; min-width: 44px; }" in css # mobile floor sp = css.find(".doc-copy-status {") assert sp != -1 s = css[sp : css.find("}", sp)] assert "color: var(--ink-soft)" in s pp = css.find(".doc-copy-pop {") assert pp != -1 p = css[pp : css.find("}", pp)] assert "position: absolute" in p assert "z-index: 30" in p fp = css.find(".doc-path-fallback {") assert fp != -1 f = css[fp : css.find("}", fp)] assert "font-family: var(--mono)" in f assert "text-overflow: ellipsis" in f assert ".doc-path-fallback:hover" in css assert ".doc-path-fallback:focus-visible" in css def _copy_path_page_fixture() -> str: """The shared node fixture: a fake DOM (elements + query registry + tracked pop blocks) + a fake clipboard (resolves or rejects) + a fake timer (captures the fn + delay).""" return """ function makeEl(tag) { const el = { tag, children: [], attrs: {}, style: {}, className: "", type: "", readOnly: false, value: "", title: "", isConnected: true, focusCount: 0, selectCount: 0, listeners: {}, setAttribute(k, v) { this.attrs[k] = v; }, appendChild(c) { this.children.push(c); return c; }, append(...cs) { this.children.push(...cs); }, insertAdjacentElement(pos, node) { if (pos === "afterend") this.after.unshift(node); return node; }, addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); }, focus() { this.focusCount += 1; (this.listeners.focus || []).forEach((f) => f()); }, select() { this.selectCount += 1; }, remove() { this.isConnected = false; }, getBoundingClientRect() { return { top: 10, left: 20, bottom: 44, right: 90, width: 70, height: 34 }; }, }; el.after = []; return el; } const created = []; const registry = {}; let written = []; let reject = false; let timerFn = null; let timerDelay = null; globalThis.setTimeout = (fn, delay) => { timerFn = fn; timerDelay = delay; return 1; }; globalThis.clearTimeout = () => {}; // Node 24's globalThis.navigator is GETTER-ONLY (a plain assignment // silently fails in sloppy mode) — defineProperty is the only way to // stand up the fake clipboard. Object.defineProperty(globalThis, "navigator", { configurable: true, value: { clipboard: { writeText: async (s) => { written.push(s); if (reject) throw new Error("denied"); }, }, }, }); globalThis.document = { createElement: (t) => { const el = makeEl(t); created.push(el); return el; }, createElementNS: (ns, t) => makeEl(t), querySelector: (sel) => { if (!registry[sel]) registry[sel] = makeEl("root:" + sel); return registry[sel]; }, querySelectorAll: (sel) => sel === ".doc-copy-pop" ? created.filter((c) => c.className === "doc-copy-pop" && c.isConnected) : [], }; let copyStatusTimer = 0; """ @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_page_copy_path_behavior_under_node() -> None: """The page's copyPathWithFallback under node (a fake DOM + a fake clipboard): * success — the clipboard receives the EXACT `source + "/" + path` string (spaces + slashes intact — the read-tool form, ASSUMPTION A1), the visible status is "Path copied." (the aria-hidden node), the announcer reads the same text, the block is anchored under the button in the header (top/left computed from the rects), and the auto-clear timer is scheduled at 4000 ms and drops the block when it fires; * failure — no write, the pre-filled readOnly fallback field (doc-path-fallback, aria-labelled "Document path") is auto-focused and selects its content on focus, the status reads "Path ready — copy it from the field." (the owner-locked wording, path-adapted) and the announcer reads it too; * a re-click re-attempts (failure again → exactly one connected state block; then success → the field is replaced by the success status — never cleared into nothing).""" js = _copy_path_page_fixture() js += _fn(_read(DOCUMENT_JS), "makePathFallbackField") js += "\n" + _fn_async(_read(DOCUMENT_JS), "copyPathWithFallback") js += """ async function main() { let out = {}; const header = document.querySelector("header.doc-header"); const btn = document.createElement("button"); const ok1 = await copyPathWithFallback(btn, "H omelab/a/b c.md"); const pops = () => created.filter((c) => c.className === "doc-copy-pop"); const pop1 = pops()[pops().length - 1]; out = { ok1, written: written.slice(), popClass: pop1.className, inHeader: header.children.includes(pop1), top: pop1.style.top, left: pop1.style.left, statusText: pop1.children.find((c) => c.className === "doc-copy-status").textContent, statusAria: pop1.children.find((c) => c.className === "doc-copy-status").attrs["aria-hidden"], announcer: registry["#doc-copy-announcer"].textContent, timerDelay, connected: pops().filter((p) => p.isConnected).length, }; timerFn(); // the auto-clear fires out.cleared = !pop1.isConnected; reject = true; const ok2 = await copyPathWithFallback(btn, "H omelab/a/b c.md"); const pop2 = pops().filter((p) => p.isConnected)[0]; const field = pop2.children.find((c) => c.tag === "input"); out.ok2 = ok2; out.fieldValue = field.value; out.fieldType = field.type; out.fieldReadOnly = field.readOnly; out.fieldClass = field.className; out.fieldAria = field.attrs["aria-label"]; out.fieldTitle = field.title; out.fieldFocus = field.focusCount; out.fieldSelect = field.selectCount; out.failStatus = pop2.children.find((c) => c.className === "doc-copy-status").textContent; out.failAnnouncer = registry["#doc-copy-announcer"].textContent; out.connectedAfterFail = pops().filter((p) => p.isConnected).length; await copyPathWithFallback(btn, "H omelab/a/b c.md"); // re-attempt (fail again) out.connectedAfterRetry = pops().filter((p) => p.isConnected).length; reject = false; const ok3 = await copyPathWithFallback(btn, "x/y.md"); // re-attempt (success) out.ok3 = ok3; out.lastWritten = written[written.length - 1]; const pop3 = pops().filter((p) => p.isConnected)[0]; out.successReplacesField = !pop3.children.some((c) => c.tag === "input"); console.log(JSON.stringify(out)); } main().catch((e) => { console.error("FAIL " + e.message); process.exit(1); }); """ r = _run_node(js) out = json.loads(r) assert out["ok1"] is True assert out["written"] == ["H omelab/a/b c.md"] # the exact source/path form assert out["popClass"] == "doc-copy-pop" assert out["inHeader"] is True assert out["top"] == "40px" and out["left"] == "0px" # anchored under the button assert out["statusText"] == "Path copied." assert out["statusAria"] == "true" # visual only assert out["announcer"] == "Path copied." # the a11y region assert out["timerDelay"] == 4000 assert out["connected"] == 1 assert out["cleared"] is True # the auto-clear dropped the block assert out["ok2"] is False assert out["fieldValue"] == "H omelab/a/b c.md" # prefilled with the path assert out["fieldType"] == "text" assert out["fieldReadOnly"] is True assert out["fieldClass"] == "doc-path-fallback" assert out["fieldAria"] == "Document path" assert out["fieldTitle"] == "Document path — click, then copy (Ctrl/Cmd+C)" assert out["fieldFocus"] == 1 # auto-focused (selects the path) assert out["fieldSelect"] == 1 # the focus listener selected the content assert out["failStatus"] == "Path ready — copy it from the field." assert out["failAnnouncer"] == "Path ready — copy it from the field." assert out["connectedAfterFail"] == 1 assert out["connectedAfterRetry"] == 1 # the re-click replaced, never cleared assert out["ok3"] is True assert out["lastWritten"] == "x/y.md" assert out["successReplacesField"] is True # the field was swapped for the status @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_modal_copy_path_behavior_under_node() -> None: """The modal's copyPathWithFallback under node (a fake DOM + a fake clipboard + the setDesc announcer): * success — the clipboard receives the EXACT `source + "/" + path` string, the "Path copied." status lands right after the control (insertAdjacentElement("afterend")), the visible node is aria-hidden, #doc-modal-desc reads the outcome, and the auto-clear timer (4000 ms) drops the status when it fires; * failure — the pre-filled readOnly fallback field + the "Path ready — copy it from the field." status land in DOM order [field, status] right after the control (the meta block wraps), the field is auto-focused and selects its content on focus, and #doc-modal-desc reads the outcome; * a re-click re-attempts (the previous state is removed first — exactly one connected state pair after the retry — a failed copy never clears the previous state into nothing).""" js = """ """ + _copy_path_page_fixture() js += """ const desc = []; function setDesc(t) { desc.push(t); } let modalCopyTimer = 0; """ js += "\n" + _fn_async(DOCUMENT_MODAL_JS, "copyPathWithFallback") js += """ async function main() { let out = {}; const inserted = []; const btn = makeEl("button"); btn.className = "doc-copy-path"; btn.insertAdjacentElement = function (pos, node) { if (pos === "afterend") { this.after.unshift(node); inserted.push(node); } return node; }; const metaEl = { appendChild(c) { inserted.push(c); return c; }, append(...cs) { inserted.push(...cs); }, querySelector(sel) { return sel === ".doc-copy-path" ? btn : null; }, querySelectorAll() { return inserted; }, }; const ok1 = await copyPathWithFallback(metaEl, "H omelab/a/b c.md"); const live = () => btn.after.filter( (n) => (n.className === "doc-copy-status" || n.className === "doc-path-fallback") && n.isConnected, ); out = { ok1, written: written.slice(), after: live().map((n) => n.className), statusText: live().find((n) => n.className === "doc-copy-status").textContent, statusAria: live().find((n) => n.className === "doc-copy-status").attrs["aria-hidden"], desc: desc.slice(), timerDelay, }; timerFn(); // the auto-clear fires out.cleared = !btn.after[0].isConnected; reject = true; const ok2 = await copyPathWithFallback(metaEl, "H omelab/a/b c.md"); const lv = live(); const field = lv.find((n) => n.className === "doc-path-fallback"); out.ok2 = ok2; out.afterFail = lv.map((n) => n.className); out.fieldValue = field.value; out.fieldType = field.type; out.fieldReadOnly = field.readOnly; out.fieldAria = field.attrs["aria-label"]; out.fieldFocus = field.focusCount; out.fieldSelect = field.selectCount; out.failStatus = lv.find((n) => n.className === "doc-copy-status").textContent; out.descFail = desc[desc.length - 1]; out.connectedAfterFail = lv.length; await copyPathWithFallback(metaEl, "H omelab/a/b c.md"); // re-attempt (fail again) out.connectedAfterRetry = live().length; console.log(JSON.stringify(out)); } main().catch((e) => { console.error("FAIL " + e.message); process.exit(1); }); """ r = _run_node(js) out = json.loads(r) assert out["ok1"] is True assert out["written"] == ["H omelab/a/b c.md"] # the exact source/path form assert out["after"] == ["doc-copy-status"] # right after the control assert out["statusText"] == "Path copied." assert out["statusAria"] == "true" # visual only assert out["desc"] == ["Path copied."] # the a11y region (setDesc) assert out["timerDelay"] == 4000 assert out["cleared"] is True # the auto-clear dropped the status assert out["ok2"] is False assert out["afterFail"] == ["doc-path-fallback", "doc-copy-status"] # field first, status after assert out["fieldValue"] == "H omelab/a/b c.md" # prefilled with the path assert out["fieldType"] == "text" assert out["fieldReadOnly"] is True assert out["fieldAria"] == "Document path" assert out["fieldFocus"] == 1 # auto-focused (selects the path) assert out["fieldSelect"] == 1 # the focus listener selected the content assert out["failStatus"] == "Path ready — copy it from the field." assert out["descFail"] == "Path ready — copy it from the field." assert out["connectedAfterFail"] == 2 # one field + one status assert out["connectedAfterRetry"] == 2 # the re-click replaced, never cleared # --------------------------------------------------------------------------- # Phase 153 (task 03) — the copy-path composition, end-to-end under node # (the click handler + render() wiring → the EXACT composed bytes on the # (fake) clipboard; the pre-existing pins above call the helper with a # pre-composed string — these run the composition step itself) # --------------------------------------------------------------------------- PUMP = "async function pump() { for (let i = 0; i < 300; i++) await Promise.resolve(); }\n" @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_wire_copy_path_click_composes_the_exact_source_path_string() -> None: """The modal's ``wireCopyPath`` click handler, executed end-to-end under node (the REAL ``wireCopyPath`` + the REAL `` copyPathWithFallback`` from the live ``document-modal.js``, the DOM/clipboard/timers faked): for ``source = "H omelab"`` (space) + ``path = "a/b c.md"`` (slashes + a space), a click sends the EXACT ``"H omelab/a/b c.md"`` bytes to the clipboard (byte-for- byte — a refactor that reorders, re-encodes, or trims the composition breaks the exact string). The success beat announces the exact success string; a REJECTED clipboard then prefills the fallback field with that SAME composed string (the exact prefill, readOnly, focused + selected — the "never clears the previous state" re-click stands: the second status replaces the first).""" modal_js = _read(MODAL_JS) wire = _fn(modal_js, "wireCopyPath") helper = _fn_async(modal_js, "copyPathWithFallback") script = ( _copy_path_page_fixture() + "\nlet modalCopyTimer = 0;\n" + "const descCalls = [];\n" + "function setDesc(t) { descCalls.push(t); }\n" + "let createdInput = null;\n" + "document.createElement = (t) => { const el = makeEl(t);" + " if (t === 'input') createdInput = el; return el; };\n" + "const btn = makeEl('button');\n" + "const metaEl = { querySelector: (s) => (s === '.doc-copy-path' ? btn : null)," + " querySelectorAll: () => [], replaceChildren() {} };\n" + helper + "\n" + wire + "\n" + PUMP + "wireCopyPath(metaEl, { source: 'H omelab', path: 'a/b c.md' });\n" + "const newestStatus = () => (btn.after.find((e) => e.className === 'doc-copy-status')" + " || { textContent: undefined }).textContent;\n" + "const click = btn.listeners.click[0];\n" + "click(); await pump();\n" + "const okOut = { written: written.slice(), status: newestStatus()," + " descCalls: descCalls.slice() };\n" + "reject = true; click(); await pump();\n" + "const failOut = { input: createdInput && createdInput.value," + " readOnly: createdInput && createdInput.readOnly," + " focusCount: createdInput && createdInput.listeners.focus.length," + " selectCount: createdInput && createdInput.selectCount," + " status: newestStatus() };\n" + "console.log(JSON.stringify({ okOut, failOut }));\n" ) out = json.loads(_run_node(script)) # the exact composed bytes (spaces + slashes) on the clipboard assert out["okOut"]["written"] == ["H omelab/a/b c.md"], ( "the click handler must send the exact source + '/' + path bytes" ) assert out["okOut"]["status"] == "Path copied." assert out["okOut"]["descCalls"] == ["Path copied."] # the rejected-clipboard failure: the prefill is the SAME composed string assert out["failOut"]["input"] == "H omelab/a/b c.md", ( "the fallback field must prefill with the exact composed string" ) assert out["failOut"]["readOnly"] is True assert out["failOut"]["focusCount"] == 1 assert out["failOut"]["selectCount"] == 1 assert out["failOut"]["status"] == "Path ready — copy it from the field." @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_page_render_wires_the_copy_path_with_the_composed_string() -> None: """The page's ``render()`` wiring, executed end-to-end under node (the REAL ``render`` + the page-local ``copyPathWithFallback`` + ``makePathFallbackField`` from the live ``document.js``, the DOM/clipboard/timers faked): a click on the shared core's ``.doc-copy-path`` control sends the EXACT ``"H omelab/a/b c.md"`` bytes to the clipboard (the ``doc.source + "/" + doc.path`` composition, byte-for-byte). The success beat floats the status in the ``header.doc-header`` block (the meta row is never grown — the phase-12/34 contract); the REJECTED-clipboard failure floats the fallback field prefilled with that SAME composed string (readOnly, focused + selected) beside the exact failure status, and the role=status announcer mirror gets the same text.""" js = _read(DOCUMENT_JS) render_fn = _fn(js, "render") helper = _fn_async(js, "copyPathWithFallback") field_fn = _fn(js, "makePathFallbackField") script = ( _copy_path_page_fixture() + "const brand = () => 'Brain of Reese';\n" + "const rendered = [];\n" + "function renderDocument(d, els) { rendered.push(d); }\n" + "const btn = makeEl('button');\n" + "const titleEl = {};\n" + "const metaEl = { querySelector: (s) => (s === '.doc-copy-path' ? btn : null)," + " replaceChildren() {} };\n" + "const contentEl = {};\n" + "const header = document.querySelector('header.doc-header');\n" + field_fn + "\n" + helper + "\n" + render_fn + "\n" + PUMP + "render({ source: 'H omelab', path: 'a/b c.md', title: 'T' });\n" + "const click = btn.listeners.click[0];\n" + "click(); await pump();\n" + "const okBlock = header.children[0];\n" + "const okOut = { written: written.slice(), blockChildren: okBlock.children.length," + " status: okBlock.children[0].textContent," + " announcer: document.querySelector('#doc-copy-announcer').textContent };\n" + "reject = true; click(); await pump();\n" + "const failBlock = header.children[header.children.length - 1];\n" + "const field = failBlock.children[0]; const failStatus = failBlock.children[1];\n" + "const failOut = { value: field.value, readOnly: field.readOnly," + " focusCount: field.listeners.focus.length, selectCount: field.selectCount," + " status: failStatus.textContent, className: failBlock.className };\n" + "console.log(JSON.stringify({ okOut, failOut }));\n" ) out = json.loads(_run_node(script)) # the exact composed bytes (spaces + slashes) on the clipboard assert out["okOut"]["written"] == ["H omelab/a/b c.md"], ( "render() must wire the click to the exact source + '/' + path bytes" ) # the float lives in the header (never the meta row): one status child assert out["okOut"]["blockChildren"] == 1 assert out["okOut"]["status"] == "Path copied." assert out["okOut"]["announcer"] == "Path copied." # the a11y mirror # the rejected-clipboard failure: the prefill is the SAME composed string assert out["failOut"]["value"] == "H omelab/a/b c.md", ( "the page fallback field must prefill with the exact composed string" ) assert out["failOut"]["readOnly"] is True assert out["failOut"]["focusCount"] == 1 assert out["failOut"]["selectCount"] == 1 assert out["failOut"]["status"] == "Path ready — copy it from the field." assert out["failOut"]["className"] == "doc-copy-pop" # the floating block # --------------------------------------------------------------------------- # Phase 165 (task 01) — the prose content block: PROSE_FORMATS / # isProseDoc / renderDocument's third branch / the .doc-prose CSS # (TODO.md L3 — image descriptions and txt/text docs WRAP) # --------------------------------------------------------------------------- STYLES_CSS = FRONTEND / "assets" / "styles.css" def _content_branch(js: str) -> str: """The THREE-WAY content branch of renderDocument: from the markdown ``if`` to the close of the raw ``else`` (the exact end anchor: the raw arm's ``slot.appendChild(pre);`` + its closing brace — the _stmt_block brace-walk would swallow the trailing strip-mount statement, so this slice is literal).""" start = js.index("if (doc.format === \"md\" || doc.format === \"markdown\") {") end = js.index("slot.appendChild(pre);\n }") + len("slot.appendChild(pre);\n }") return js[start:end] def _prose_branch(js: str) -> str: """The prose arm alone: ``} else if (isProseDoc(doc)) {`` to its closing brace after ``slot.appendChild(prose);``.""" start = js.index("} else if (isProseDoc(doc)) {") end = js.index("slot.appendChild(prose);\n }") + len("slot.appendChild(prose);\n }") return js[start:end] def _prose_rule(js: str) -> str: """The phase-165 prose rule at module scope: the PROSE_FORMATS const through the close of isProseDoc (extracted together — the function references the const; the _fn brace-walk house pattern).""" start = js.index("const PROSE_FORMATS") end = js.index("}", js.index("function isProseDoc")) + 1 return js[start:end] def test_prose_set_is_one_named_constant() -> None: """LOCKED A1: the prose rule lives in ONE named constant pair — ``const PROSE_FORMATS = new Set(["txt", "text"])`` + ``isProseDoc(doc)`` with the image arm UNCONDITIONAL (phase 122 A3 — an image doc's content IS the vision description, prose by construction). Grep-verified single-site rule: the content-branch block itself names no format literal — it calls isProseDoc.""" js = _read(DOCUMENT_JS) rule = _prose_rule(js) assert 'const PROSE_FORMATS = new Set(["txt", "text"]);' in rule assert "doc.is_image === true || PROSE_FORMATS.has(doc.format)" in rule branch = _content_branch(js) assert "isProseDoc(doc)" in branch, "the prose arm gates on the ONE helper" assert '"txt"' not in branch and '"text"' not in branch, ( "no second format literal in the content branch (single-site rule)" ) def test_content_branch_is_three_way_md_prose_raw() -> None: """renderDocument's content branch is markdown → prose → raw, in that order, all three appending into the SAME ``slot`` (phase 122 image block FIRST, phase 36 summary between, phase 163 additive strip last — all unchanged). The markdown and raw arms stay byte-identical to pre-phase (locked A3); the prose arm is textContent-only — the XSS contract (never innerHTML for document content) is PINNED on the branch source.""" js = _read(DOCUMENT_JS) render = _fn(js, "renderDocument") md_i = render.find('if (doc.format === "md" || doc.format === "markdown") {') prose_i = render.find("} else if (isProseDoc(doc)) {", md_i) raw_i = render.find("} else {", prose_i) assert -1 < md_i < prose_i < raw_i, "the prose arm sits BETWEEN markdown and raw" # The untouched markdown / raw arms (byte-identical contracts). for stmt in ( 'wrap.className = "doc-md";', "wrap.innerHTML = renderMarkdown(doc.content);", "slot.appendChild(wrap);", 'pre.className = "doc-raw";', "pre.textContent = doc.content;", "slot.appendChild(pre);", ): assert stmt in render, f"the pre-phase arms must keep {stmt!r}" # The prose arm: div + class + text node + the same slot. prose_branch = _prose_branch(js) assert 'document.createElement("div")' in prose_branch assert 'prose.className = "doc-prose";' in prose_branch assert "prose.textContent = doc.content;" in prose_branch assert "slot.appendChild(prose);" in prose_branch assert "innerHTML" not in prose_branch, ( "the prose arm never touches innerHTML (the XSS contract)" ) #: (doc, expected isProseDoc) — LOCKED A1's exact set: #: {is_image: true} ∪ {format ∈ txt, text}; code, code-ish, pdf, #: unknown, extension-less (no format) and the string-``"true"`` #: lookalike all stay on the scrolling side (locked A3). _PROSE_CASES: list[tuple[dict[str, object], bool]] = [ ({"is_image": True, "format": "png"}, True), ({"is_image": True, "format": "webp"}, True), ({"is_image": True, "format": "md"}, True), # the image arm wins ({"is_image": True, "format": "pdf"}, True), # …regardless of format ({"is_image": True}, True), # …even without one ({"format": "txt"}, True), ({"format": "text"}, True), ({"is_image": False, "format": "txt"}, True), ({"is_image": False, "format": "text"}, True), ({"is_image": "true", "format": "py"}, False), # strict === true ({"is_image": False, "format": "py"}, False), ({"is_image": False, "format": "yaml"}, False), ({"is_image": False, "format": "yml"}, False), ({"is_image": False, "format": "json"}, False), ({"is_image": False, "format": "j2"}, False), ({"is_image": False, "format": "container"}, False), # quadlet family ({"is_image": False, "format": "network"}, False), ({"is_image": False, "format": "volume"}, False), ({"is_image": False, "format": "kube"}, False), ({"is_image": False, "format": "pdf"}, False), # phase 165 A4 (deferred) ({"is_image": False, "format": "md"}, False), ({"is_image": False, "format": "markdown"}, False), ({"is_image": False, "format": "dockerfile"}, False), ({"is_image": False, "format": "rst"}, False), # unknown suffix ({"is_image": False, "format": ""}, False), ({"is_image": False}, False), # extension-less (no format at all) ] @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_is_prose_doc_truth_table() -> None: """Behavioral (node): the REAL isProseDoc over the full truth table — a future format cannot drift into prose by accident (phase 165 D3's pin).""" js = _read(DOCUMENT_JS) script = ( _prose_rule(js) + "\nconst cases = " + json.dumps([c for c, _ in _PROSE_CASES]) + ";\nconsole.log(JSON.stringify(cases.map((d) => isProseDoc(d))));\n" ) assert json.loads(_run_node(script)) == [e for _, e in _PROSE_CASES] # The renderDocument content-branch harness: the minimal fake DOM of # test_viewer_additive_toggle.py (the house pattern), the real # docImageBlock standing in for the image arm's first block. _CONTENT_BRANCH_FAKE_DOM = """ function makeEl(tag) { return { tag, className: "", textContent: "", innerHTML: "", attrs: {}, children: [], setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }, appendChild(c) { this.children.push(c); return c; }, append(...cs) { this.children.push(...cs); }, replaceChildren(...cs) { this.children = cs; }, addEventListener() {}, }; } globalThis.document = { createElement: (t) => makeEl(t) }; function fmtDate(x) { return "DATE:" + x; } function metaBadge(cls, text, title) { const s = makeEl("span"); s.className = cls; s.textContent = text; if (title !== undefined) s.setAttribute("title", title); return s; } function copyPathButton() { return makeEl("button"); } function originAnchor() { return makeEl("a"); } async function docAdminReady() { return false; } function wireDateEdit() {} function wireSummaryEdit() {} function renderMarkdown(s) { return s; } function docPagesStrip() { return makeEl("div"); } """ @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_render_document_content_branch_format_table() -> None: """Behavioral (node, fake DOM): the REAL renderDocument per format — txt/text → .doc-prose (the TODO's wrapping ask), md / markdown → .doc-md (the escape-first renderer, unchanged), and py/yaml/json/j2/quadlet/pdf/""/unknown → .doc-raw (locked A1/A3: the code scroller unchanged). An is_image doc lands in .doc-prose whatever its format — never a doc-raw sibling.""" js = _read(DOCUMENT_JS) script = ( _CONTENT_BRANCH_FAKE_DOM + _prose_rule(js) + _fn(js, "renderDocument") + _fn(js, "docImageBlock") + """ const base = { title: "T", path: "p", source: "S", content: "BODY", summary: null, created_at: "c", indexed_at: "i", chunks: 1, }; function classesFor(doc) { const contentEl = makeEl("div"); renderDocument(doc, { titleEl: makeEl("h1"), metaEl: makeEl("div"), contentEl }); return contentEl.children.map((c) => c.className); } const out = {}; for (const format of ["md", "markdown", "txt", "text", "py", "yaml", "yml", "json", "j2", "conf", "pdf", "container", "network", "volume", "kube", "dockerfile", "rst", ""]) { out[format] = classesFor({ ...base, format }); } out["image:png"] = classesFor({ ...base, format: "png", is_image: true, image_url: "/api/documents/x/image" }); console.log(JSON.stringify(out)); """ ) got = json.loads(_run_node(script)) expected = { "md": ["doc-md"], "markdown": ["doc-md"], "txt": ["doc-prose"], "text": ["doc-prose"], "py": ["doc-raw"], "yaml": ["doc-raw"], "yml": ["doc-raw"], "json": ["doc-raw"], "j2": ["doc-raw"], "conf": ["doc-raw"], "pdf": ["doc-raw"], # phase 165 A4: PDF text stays raw (owner call) "container": ["doc-raw"], "network": ["doc-raw"], "volume": ["doc-raw"], "kube": ["doc-raw"], "dockerfile": ["doc-raw"], "rst": ["doc-raw"], "": ["doc-raw"], "image:png": ["doc-image", "doc-prose"], # image block first, prose below } assert got == expected def test_doc_prose_css_wraps_in_the_reading_column() -> None: """styles.css: the ``.doc-prose`` rule (immediately after ``.doc-raw``) is the .doc-md column with prose rhythm — ``white-space: pre-wrap`` + ``overflow-wrap: anywhere`` (the TODO's literal no-horizontal-scroll ask) inside the 72rem ``var(--chat-column)`` reading column, phase-08 TOKENS only (zero color literals — the :root-drift contract; --ink on --surface is the documented AA pair), no animation. The ``.doc-raw`` rule keeps the locked code contract (A3): ``white-space: pre`` + ``overflow-x: auto``.""" css = _read(STYLES_CSS) raw_i = css.find(".doc-raw {") prose_i = css.find(".doc-prose {") assert -1 < raw_i < prose_i, ".doc-prose must sit immediately after .doc-raw" # Provenance comment directly above the rule (house style) — it # cites the TODO, the defect and the locked decisions. comment_start = css.rfind("/*", 0, prose_i) provenance = css[comment_start:prose_i] assert -1 < comment_start < prose_i for citation in ("phase 165", "TODO.md L3", ".doc-md", ".doc-raw", "A1", "A2"): assert citation in provenance, f"the comment must cite {citation!r}" assert "no animation" in provenance rule = css[prose_i : css.find("}", prose_i)] for decl in ( "width: 100%", "max-width: var(--chat-column)", "margin-inline: auto", "background: var(--surface)", "border: 1px solid var(--line)", "border-radius: var(--radius)", "box-shadow: var(--shadow)", "padding: 1.5rem 1.75rem", "color: var(--ink)", "white-space: pre-wrap", "overflow-wrap: anywhere", "line-height: 1.55", ): assert decl in rule, f".doc-prose must carry {decl!r}" # Tokens only — zero color literals (the :root-drift test parses # built-ins from this file; no new hue — the monochrome invariant). assert not re.search(r"#[0-9a-fA-F]{3,8}\b", rule) assert "rgb(" not in rule and "hsl(" not in rule and "color-mix(" not in rule # The untouched code scroller (phase 116 locked A1, phase 165 A3). raw_rule = css[raw_i : css.find("}", raw_i)] assert "white-space: pre" in raw_rule assert "overflow-x: auto" in raw_rule