Files
brain-of-reese/tests/unit/test_viewer_additive_toggle.py
ducoterra b0c7aed613
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 11s
phase: 165_prose_word_wrap
All verification complete. Final report:

**Phase 165 — Prose word wrap (final verification pass)**
- Verified: `PROSE_FORMATS={txt,text}` + `isProseDoc` (single-site, image arm wins); `renderDocument` three-way branch (md → `.doc-md`, prose → `.doc-prose` text node, else `pre.doc-raw` byte-identical); `.doc-prose` CSS = 72rem column, `pre-wrap` + `overflow-wrap: anywhere`, tokens only, zero literals; `[data-view="pages"]` hide list includes `.doc-prose` as one grouped declaration; phase-116 scrollbar rules intact and modal-scoped; no `innerHTML` in the prose path.
- Tests/lint (all 100% green):
  - `uv run pytest` → **3854 passed** (unit + integration)
  - `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90%)
  - `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**
  - `uv run pytest tests/e2e/test_prose_word_wrap.py -v --no-cov` → **5 passed** (isolation)
  - Isolation regressions: document_viewer 8, image_documents 4, pdf_pages_in_sources 4, pdf_strip_additive_selfheal 5, edit_summaries 4, summary_in_viewer 4, chat_answer_clusters 4, garbage_pdf 3, pdf_support 6, sticky_navbar 3, smoke 3 — all passed
- Completion criteria: image description wraps, full text, both surfaces ✔; `.txt` wraps same way ✔; `py`/`json`/code keep `pre.doc-raw` + horizontal overflow + themed modal scrollbar (E2E negative control + pins) ✔; no innerHTML, AA `--ink`/`--surface` pair ✔; full gates green ✔; commit + phase-dir move are harness-owned (not executed per pipeline rules).
- Documentation: README already updated by task 03 (viewer section + image-docs section); full README/AGENTS.md review found no further inconsistencies — **verified consistent, no changes needed**.
- Notable deviations: `.doc-prose` omits `font-family: inherit` from 00_phase D2's snippet — task 01's authoritative "exact property set" excludes it and `div` inherits fonts anyway (behavior identical). PDF deferral (A4) intact: `format == "pdf"` stays in `.doc-raw`, owner call recorded.
- Next pending phase: `166_hide_empty_answer_bubble`.
2026-10-07 22:57:40 -04:00

450 lines
19 KiB
Python

"""Unit: the additive PDF strip view (phase 163, task 04 — D4, LOCKED A1/A6).
Frontend source-contract pins + node behavioral runs (the
``test_document_viewer.py`` house pattern) over ``document.js``'s
``renderDocument`` — the ONE shared core (Sources modal AND
/document.html):
* the MODE branch — ``page_urls`` + ``page_strip_mode === "additive"``
falls through to the text-doc rendering (Summary panel + content,
the pre-148 branch reused) wrapped in a ``.doc-views`` container
with the ``.doc-view-toggle`` group above it (Text pressed by
default, LOCKED A6); ``page_urls`` with the mode ABSENT or
``"primary"`` keeps the phase-148 strip-only branch VERBATIM (append
the strip to ``contentEl`` + return — no toggle, no wrapper, no
Summary panel); falsy ``page_urls`` is untouched;
* the toggle semantics — ``role="group"`` + ``aria-label="Document
view"``, two ``<button type="button">`` labeled ``Text`` / ``Pages``
(fixed strings, no doc-derived text), ``aria-pressed`` true/false
initial state, and a click mutating ONLY the two ``aria-pressed``
states + the wrapper's ``data-view`` attribute (CSS-driven show/
hide — the strip node stays mounted, no re-render, no refetch);
* the never-build-HTML rule — createElement + properties/textContent
only in the new code path (the module's ONE innerHTML stays the
escape-first markdown render); the strip imgs keep
``loading = "lazy"`` (the hidden strip never fetches);
* the ``.doc-view-toggle`` CSS — the house ghost-pill idiom, the
pressed state as text + style (never color alone, B5), ≥44px
targets, and the ``data-view`` show/hide contract.
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
DOCUMENT_JS = FRONTEND / "assets" / "document.js"
STYLES_CSS = FRONTEND / "assets" / "styles.css"
HAVE_NODE = shutil.which("node") is not None
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _fn(js: str, name: str) -> str:
"""The source of a top-level function via balanced-brace counting
(the ``test_document_viewer.py`` pattern)."""
start = js.find(f"function {name}(")
assert start != -1, f"{name}() must exist in document.js"
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 _code(js: str) -> str:
"""The source with comments stripped (the never-build-HTML pins)."""
return re.sub(r"//.*?$|/\*.*?\*/", "", js, flags=re.S | re.M)
def _prose_rule(js: str) -> str:
"""The phase-165 prose rule (the PROSE_FORMATS const + isProseDoc,
module scope) — renderDocument's prose gate, needed beside the
extracted functions for the node runs."""
start = js.index("const PROSE_FORMATS")
end = js.index("}", js.index("function isProseDoc")) + 1
return js[start:end]
def _run_node(script: str) -> str:
proc = subprocess.run(
["node", "-e", script], capture_output=True, text=True, timeout=60
)
assert proc.returncode == 0, f"node failed: {proc.stderr}"
return proc.stdout
# ---------------------------------------------------------------------------
# Source pins — the renderDocument mode branch
# ---------------------------------------------------------------------------
def test_render_document_branches_on_strip_mode() -> None:
"""The mode branch (LOCKED A2 — the tolerant read): the additive
flag keys on the EXACT ``doc.page_strip_mode === "additive"``
string (absent / "primary" / anything else ⇒ primary), and the
primary/absent strip branch stays the phase-148 shape — append
``docPagesStrip`` to ``contentEl`` and RETURN — evaluated BEFORE
any wrapper/toggle construction."""
render = _fn(_read(DOCUMENT_JS), "renderDocument")
assert 'doc.page_strip_mode === "additive"' in render, (
"only the exact additive string switches on the toggle"
)
gate_i = render.find('const additive = stripEligible && doc.page_strip_mode === "additive";')
primary_i = render.find("if (stripEligible && !additive) {")
strip_i = render.find("contentEl.appendChild(docPagesStrip(doc, pageUrls));")
return_i = render.find("return;", strip_i)
wrapper_i = render.find('views.className = "doc-views";')
assert -1 < gate_i < primary_i < strip_i < return_i < wrapper_i, (
"the primary/absent branch (strip into contentEl + return) precedes "
"the additive wrapper — its DOM stays the phase-148 shape verbatim"
)
# The wrapper + toggle: ONE .doc-views container, the toggle BEFORE
# it, both into contentEl; the text view lands in the wrapper.
append_i = render.find("contentEl.append(docViewToggle(views), views);")
assert wrapper_i < append_i, "the wrapper is built, then appended after its toggle"
assert 'views.setAttribute("data-view", "text");' in render, (
"LOCKED A6 — Text is the default view"
)
# The strip mounts ONCE inside the wrapper, after the text view —
# never rebuilt on a flip (no docPagesStrip call outside the two
# branches).
assert render.count("docPagesStrip(doc, pageUrls)") == 2
mount_i = render.rfind("views.appendChild(docPagesStrip(doc, pageUrls));")
assert mount_i > render.find('slot.appendChild(pre);') or mount_i > render.rfind(
"slot.appendChild(wrap);"
), "the strip mounts AFTER the text view inside the wrapper"
# The non-additive path is untouched: slot === contentEl (the
# pre-148 appends keep their exact shape through the slot var).
assert "let slot = contentEl;" in render
def test_primary_absent_branch_builds_no_toggle() -> None:
"""The primary/absent branch is toggle-free by construction: the
strip-branch block (from the ``pageUrls`` read to its ``return``)
contains NO ``doc-view-toggle`` / ``doc-views`` construction — the
strip-only DOM stays byte-identical to pre-phase-163."""
render = _fn(_read(DOCUMENT_JS), "renderDocument")
primary_start = render.find("if (stripEligible && !additive)")
branch = render[render.find("const pageUrls =") : render.find("return;", primary_start)]
assert "doc-view-toggle" not in branch
assert "doc-views" not in branch
assert branch.count("appendChild") == 1, (
"the primary branch is exactly the phase-148 append + return"
)
def test_toggle_never_builds_html_and_labels_are_fixed() -> None:
"""The never-build-HTML rule: the toggle builders use
createElement + properties/textContent only — no innerHTML /
outerHTML — and the labels are the FIXED strings ``Text`` /
``Pages`` (no doc-derived text inside the toggle). The module's
ONE innerHTML (the escape-first markdown render) survives."""
js = _read(DOCUMENT_JS)
toggle = _fn(js, "docViewToggle")
btn = _fn(js, "viewToggleButton")
for body in (toggle, btn):
code = _code(body)
assert "innerHTML" not in code and "outerHTML" not in code
assert 'document.createElement("div")' in toggle
assert 'group.setAttribute("role", "group")' in toggle
assert 'group.setAttribute("aria-label", "Document view")' in toggle
assert 'document.createElement("button")' in btn
assert 'btn.type = "button"' in btn
assert 'btn.className = "doc-view-btn"' in btn
assert 'viewToggleButton("Text")' in toggle
assert 'viewToggleButton("Pages")' in toggle
# The module-wide XSS contract: exactly one innerHTML, the
# renderMarkdown line.
whole = _code(js)
assert whole.count("innerHTML") == 1
assert "outerHTML" not in whole
def test_strip_imgs_stay_lazy() -> None:
"""The phase-148 lazy-img pin re-runs green: the strip builder is
UNMODIFIED — every page ``<img>`` keeps ``loading = "lazy"`` (the
additive default view never fetches the strip)."""
strip = _fn(_read(DOCUMENT_JS), "docPagesStrip")
assert 'img.loading = "lazy";' in strip
def test_view_toggle_css() -> None:
"""styles.css: the ``.doc-view-toggle`` group (the chat-column
reading width, the pill pair adjacent) + the ``.doc-view-btn``
ghost pill (≥44px target, ink-soft text — AA) + the PRESSED state
(``[aria-pressed="true"]`` — brand fill + font-weight 700: text +
style, never color alone, B5) + the CSS-driven view swap
(``.doc-views .doc-pages`` hidden by default; ``[data-view=
"pages"]`` hides the Summary panel / .doc-md / .doc-raw /
.doc-prose (phase 165, task 01) / .doc-image and shows the strip as
the grid — one grouped declaration). Provenance comment above the
block (house style)."""
css = _read(STYLES_CSS)
toggle_i = css.find(".doc-view-toggle {")
assert toggle_i != -1, "styles.css must style .doc-view-toggle"
comment_start = css.rfind("/*", 0, toggle_i)
assert -1 < comment_start < toggle_i and "phase 163" in css[comment_start:toggle_i]
btn_i = css.find(".doc-view-btn {")
pressed_i = css.find('.doc-view-btn[aria-pressed="true"]')
assert toggle_i < btn_i < pressed_i
btn_rule = css[btn_i : css.find("}", btn_i)]
for decl in ("min-height: 44px", "color: var(--ink-soft)", "border: 1px solid var(--line)"):
assert decl in btn_rule
pressed_rule = css[pressed_i : css.find("}", pressed_i)]
assert "background: var(--brand)" in pressed_rule
assert "font-weight: 700" in pressed_rule, (
"the pressed state is text weight + fill — never color alone (B5)"
)
# The CSS-driven swap: hidden strip by default; pages hides the
# text view and shows the strip. The hide list is ONE grouped
# declaration (phase 165: .doc-prose joins it — defensive today,
# load-bearing if the owner ever adds "pdf" to PROSE_FORMATS).
assert '.doc-views .doc-pages { display: none; }' in css
expected = ",\n".join(
f'.doc-views[data-view="pages"] {sel}'
for sel in (".doc-summary", ".doc-md", ".doc-raw", ".doc-prose", ".doc-image")
) + " { display: none; }"
assert css.count(expected) == 1, (
"the text-view hide list is exactly ONE grouped declaration "
"over .doc-summary/.doc-md/.doc-raw/.doc-prose/.doc-image"
)
assert '.doc-views[data-view="pages"] .doc-pages { display: grid; }' in css
# ---------------------------------------------------------------------------
# Node behavioral runs — the real renderDocument over a minimal fake DOM
# ---------------------------------------------------------------------------
_FAKE_DOM = """
function makeEl(tag) {
return {
tag, className: "", textContent: "", attrs: {}, children: [], handlers: {},
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(ev, fn) { this.handlers[ev] = fn; },
click() { this.handlers.click(); },
};
}
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 docImageBlock() { return makeEl("div"); }
function describe(el) {
return {
tag: el.tag,
className: el.className,
attrs: el.attrs,
text: el.textContent,
children: el.children.map(describe),
};
}
"""
def _render_script() -> str:
js = _read(DOCUMENT_JS)
parts = [_prose_rule(js)] + [
_fn(js, n)
for n in ("renderDocument", "docViewToggle", "viewToggleButton", "docPagesStrip")
]
return _FAKE_DOM + "\n".join(parts)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_additive_doc_renders_text_views_and_toggle() -> None:
"""Behavioral (node, fake DOM): an ADDITIVE doc renders the TEXT
doc exactly as a non-qualifying one — Summary panel + content —
inside ``.doc-views`` (data-view="text"), with the toggle group
FIRST in the content slot: role=group, aria-label="Document
view", the two ``doc-view-btn`` buttons labeled Text/Pages, Text
aria-pressed="true" (LOCKED A6), and the strip MOUNTED inside the
wrapper (figures + lazy imgs) after the text view."""
out = _run_node(
_render_script()
+ """
const doc = {
title: "Letumdei", path: "book.pdf", source: "Library", format: "pdf",
summary: "A book.", content: "BODY TEXT", created_at: "c", indexed_at: "i",
chunks: 9, page_urls: ["/api/documents/x/pages/1", "/api/documents/x/pages/2"],
page_strip_mode: "additive",
};
const contentEl = makeEl("div");
renderDocument(doc, { titleEl: makeEl("h1"), metaEl: makeEl("div"), contentEl });
console.log(JSON.stringify(describe(contentEl)));
"""
)
content = json.loads(out)
assert [c["className"] for c in content["children"]] == [
"doc-view-toggle",
"doc-views",
], "the toggle group sits above the .doc-views wrapper"
toggle, views = content["children"]
assert toggle["attrs"]["role"] == "group"
assert toggle["attrs"]["aria-label"] == "Document view"
labels = [c["text"] for c in toggle["children"]]
assert labels == ["Text", "Pages"]
assert [c["attrs"]["aria-pressed"] for c in toggle["children"]] == ["true", "false"], (
"Text pressed by default (LOCKED A6)"
)
for c in toggle["children"]:
assert c["tag"] == "button"
assert views["attrs"]["data-view"] == "text"
assert [c["className"] for c in views["children"]] == [
"doc-summary",
"doc-raw",
"doc-pages",
], "additive = Summary panel + text content + the mounted strip"
# pre.textContent — a text node, never parsed as HTML
assert views["children"][1]["text"] == "BODY TEXT"
strip = views["children"][2]
assert len(strip["children"]) == 2, "one figure per page_urls entry"
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_primary_and_absent_modes_keep_the_strip_only_dom() -> None:
"""Behavioral: mode "primary" AND an absent mode (the pre-163
stamped row — the live Letumdei pre-sync shape) keep the phase-148
DOM byte-identically: the content slot holds EXACTLY the
``.doc-pages`` strip — no wrapper, no toggle, no Summary panel,
no text block."""
out = _run_node(
_render_script()
+ """
const results = [];
for (const mode of ["primary", undefined]) {
const doc = {
title: "Manual", path: "m.pdf", source: "Library", format: "pdf",
summary: "A manual.", content: "SHOULD NOT SHOW", created_at: "c",
indexed_at: "i", chunks: 4,
page_urls: ["/api/documents/y/pages/1"],
};
if (mode !== undefined) doc.page_strip_mode = mode;
const contentEl = makeEl("div");
renderDocument(doc, { titleEl: makeEl("h1"), metaEl: makeEl("div"), contentEl });
results.push(describe(contentEl));
}
console.log(JSON.stringify(results));
"""
)
primary, absent = json.loads(out)
for content in (primary, absent):
assert len(content["children"]) == 1
assert content["children"][0]["className"] == "doc-pages"
classes = json.dumps(content)
assert "doc-view-toggle" not in classes
assert "doc-views" not in classes
assert "doc-summary" not in classes
assert "doc-raw" not in classes, "the strip replaces the text block (primary class)"
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_text_doc_dom_is_untouched() -> None:
"""Behavioral: a doc with no page_urls is UNTOUCHED — Summary
panel + content appended straight to the content slot, no
wrapper, no toggle (the byte-identity property for every
non-PDF)."""
out = _run_node(
_render_script()
+ """
const doc = {
title: "Note", path: "n.pdf", source: "Library", format: "pdf",
summary: "S.", content: "TEXT", created_at: "c", indexed_at: "i", chunks: 1,
};
const contentEl = makeEl("div");
renderDocument(doc, { titleEl: makeEl("h1"), metaEl: makeEl("div"), contentEl });
console.log(JSON.stringify(describe(contentEl)));
"""
)
content = json.loads(out)
assert [c["className"] for c in content["children"]] == ["doc-summary", "doc-raw"]
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_toggle_flips_view_without_rerender_or_refetch() -> None:
"""Behavioral: the toggle handler mutates ONLY the two
aria-pressed states + the wrapper's data-view. Clicking Pages
flips data-view to "pages"; clicking back to Text reverses; the
strip node STAYS MOUNTED the whole time (same object, no
re-append — the flip-flop fetches nothing) and the wrapper's
children never change count."""
out = _run_node(
_render_script()
+ """
const doc = {
title: "Book", path: "b.pdf", source: "Library", format: "pdf",
summary: "S.", content: "TEXT", created_at: "c", indexed_at: "i", chunks: 2,
page_urls: ["/api/documents/z/pages/1", "/api/documents/z/pages/2"],
page_strip_mode: "additive",
};
const contentEl = makeEl("div");
renderDocument(doc, { titleEl: makeEl("h1"), metaEl: makeEl("div"), contentEl });
const toggle = contentEl.children[0];
const views = contentEl.children[1];
const [textBtn, pagesBtn] = toggle.children;
const stripBefore = views.children[2];
const countBefore = views.children.length;
pagesBtn.click();
const afterPages = {
view: views.getAttribute("data-view"),
pressed: toggle.children.map((b) => b.getAttribute("aria-pressed")),
};
textBtn.click();
const afterText = {
view: views.getAttribute("data-view"),
pressed: toggle.children.map((b) => b.getAttribute("aria-pressed")),
stripSame: views.children[2] === stripBefore,
countSame: views.children.length === countBefore,
};
console.log(JSON.stringify({ afterPages, afterText }));
"""
)
res = json.loads(out)
assert res["afterPages"] == {"view": "pages", "pressed": ["false", "true"]}
assert res["afterText"] == {
"view": "text",
"pressed": ["true", "false"],
"stripSame": True,
"countSame": True,
}, "the strip node persists across flips — no re-render, no refetch"