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`.
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
"""Unit: phase 165 task 02 — the word-wrap fix is PROVABLY SCOPED.
|
||||
|
||||
TODO.md L3 asks for word wrap on "image explanations and other long
|
||||
text". Phase 116's LOCKED A1 keeps horizontal scroll (NO line-wrap) for
|
||||
the code content — "wrapping breaks code readability" — so the fix must
|
||||
wrap exactly the prose set and nothing else. These pins make a future
|
||||
"we fixed wrapping" mean "we wrapped prose", never "we wrapped code" or
|
||||
"we wrapped wide tables":
|
||||
|
||||
* **the prose set is pinned as data** — ``PROSE_FORMATS`` holds exactly
|
||||
``"txt"`` and ``"text"`` (a set-membership assertion over the source
|
||||
line: adding a format is a deliberate, test-editing act), and
|
||||
``isProseDoc`` tests ``doc.is_image`` BEFORE the format arm (phase 122
|
||||
A3 — an image doc's ``content`` IS the vision description, prose by
|
||||
construction whatever its extension);
|
||||
* **the code scrollers are unchanged** — ``.doc-raw`` keeps
|
||||
``white-space: pre`` + ``overflow-x: auto``, ``.bubble pre`` keeps
|
||||
``overflow-x: auto`` with NO ``white-space`` added (the chat code
|
||||
block), and ``.md-table-wrap`` / ``.md-table`` still carry the
|
||||
phase-44 declaration (``overflow-x: auto`` on the wrapper,
|
||||
``min-width: max-content`` on the table) so wide tables still scroll
|
||||
sideways inside the reading column;
|
||||
* **phase 116's scrollbar theme still applies** — the four
|
||||
``.doc-modal .doc-raw`` rules (``scrollbar-width``,
|
||||
``scrollbar-color`` and the three ``::-webkit-scrollbar`` pseudos) are
|
||||
all present and still SCOPED to the modal (no global scrollbar
|
||||
restyle — the TODO's scope);
|
||||
* **``.doc-prose`` never reaches for the raw scroller** — the block has
|
||||
no ``overflow-x`` and no ``white-space: pre;``, only ``pre-wrap`` +
|
||||
``overflow-wrap: anywhere`` (the wrapping contract);
|
||||
* **the AA/contrast pin for the new block** — ``color: var(--ink)`` on
|
||||
``background: var(--surface)`` (the documented 14.5:1 pair, PLAN
|
||||
§7.2) and ZERO color literals in the block (the phase-92
|
||||
zero-literal / monochrome invariant — the ``:root``-drift test parses
|
||||
built-ins out of styles.css).
|
||||
|
||||
Repo-hygiene finding (task 02 step 3, ``grep -rn "doc-raw" frontend/``)
|
||||
— recorded here so the check is repeatable and its result on record:
|
||||
``doc-raw`` appears in exactly TWO writers, both expected. (1) The raw
|
||||
arm of ``app.renderDocument`` in ``frontend/assets/document.js`` (the
|
||||
ONE viewer core, ``pre.className = "doc-raw"``) plus its doc comments;
|
||||
(2) ``frontend/assets/styles.css`` (the base rule, the mobile squeeze,
|
||||
the phase-116 modal scrollbar rules and the ``[data-view="pages"]``
|
||||
hide list). Every other hit in ``frontend/`` is a comment.
|
||||
``frontend/doc-edit.html`` + ``assets/doc-edit.js`` do NOT render
|
||||
document content at all — they fill a ``<textarea>`` (``bodyInput.value
|
||||
= draft.body``, textContent for the status/error lines) for the admin
|
||||
draft editor, so the body never becomes markup and there is no scroller
|
||||
to wrap; ``frontend/shared.html`` + ``assets/shared.js`` render
|
||||
CHAT history through ``renderMarkdown`` into ``.bubble`` (the shared
|
||||
renderer's own ``.md-table-wrap`` / ``.bubble pre`` scrollers, asserted
|
||||
byte-identical above), never ``.doc-raw`` / ``.doc-md`` /
|
||||
``.doc-prose``. Neither is a second content renderer, so the prose rule
|
||||
correctly lives in the ONE place: ``isProseDoc`` + ``.doc-prose``.
|
||||
|
||||
ASSUMPTION (phase 165 A4, owner call — recorded in ``00_phase.md``
|
||||
"Deferred"): PDF-extracted text (``format == "pdf"``) is deliberately
|
||||
OUT of the prose set and stays in ``.doc-raw``. Three completed suites
|
||||
pin ``pre.doc-raw`` for PDFs (``test_pdf_pages_in_sources.py``,
|
||||
``test_pdf_strip_additive_selfheal.py``, ``test_garbage_pdf.py``) and
|
||||
the phase-163 strip contract depends on it; changing it is an owner
|
||||
call, one line (``PROSE_FORMATS.add("pdf")`` + those pins) once made —
|
||||
not an executor choice and not an oversight.
|
||||
|
||||
All assertions are static-source (the ``test_markdown_tables.py`` /
|
||||
``test_date_editor.py`` house pattern): no app, no browser. The
|
||||
measured wrapping proof is the phase's E2E suite
|
||||
(``tests/e2e/test_prose_word_wrap.py``, task 03).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
|
||||
DOCUMENT_JS = FRONTEND / "assets" / "document.js"
|
||||
STYLES_CSS = FRONTEND / "assets" / "styles.css"
|
||||
|
||||
|
||||
def _js() -> str:
|
||||
assert DOCUMENT_JS.is_file(), f"missing frontend file: {DOCUMENT_JS}"
|
||||
return DOCUMENT_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _css() -> str:
|
||||
assert STYLES_CSS.is_file(), f"missing frontend file: {STYLES_CSS}"
|
||||
return STYLES_CSS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _rule(selector: str, css: str | None = None) -> str:
|
||||
"""The FULL rule for ``selector`` (selector text + its ``{…}``
|
||||
block) matched at line start, so ``.doc-raw`` cannot accidentally
|
||||
match ``.doc-modal .doc-raw`` or ``.doc-views … .doc-raw`` (the
|
||||
``_rule_block`` brace-walk of ``test_wide_column_css.py``)."""
|
||||
text = _css() if css is None else css
|
||||
m = re.search(rf"^{re.escape(selector)} \{{", text, re.MULTILINE)
|
||||
assert m, f"styles.css must define a rule for {selector}"
|
||||
start = text.index("{", m.start())
|
||||
depth = 0
|
||||
for i in range(start, len(text)):
|
||||
if text[i] == "{":
|
||||
depth += 1
|
||||
elif text[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return text[m.start() : i + 1]
|
||||
raise AssertionError(f"unbalanced braces in the {selector} rule")
|
||||
|
||||
|
||||
def _decls(selector: str) -> str:
|
||||
"""Only the declaration block of ``selector`` (the braces stripped)."""
|
||||
rule = _rule(selector)
|
||||
return rule[rule.index("{") + 1 : rule.rindex("}")]
|
||||
|
||||
|
||||
def _rules() -> list[tuple[str, str]]:
|
||||
"""Every (selector, declarations) pair in styles.css, comments
|
||||
stripped (so a provenance comment naming a property never counts as
|
||||
a declaration) — the scope pins below walk this list."""
|
||||
text = re.sub(r"/\*.*?\*/", "", _css(), flags=re.DOTALL)
|
||||
pairs: list[tuple[str, str]] = []
|
||||
for m in re.finditer(r"([^{}]+)\{", text):
|
||||
selector = re.sub(r"\s+", " ", m.group(1)).strip()
|
||||
# An at-rule header (@media …) is a container, not a selector.
|
||||
if not selector or selector.startswith("@"):
|
||||
continue
|
||||
start = m.end()
|
||||
depth = 1
|
||||
i = start
|
||||
while i < len(text) and depth:
|
||||
if text[i] == "{":
|
||||
depth += 1
|
||||
elif text[i] == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
pairs.append((selector, text[start : i - 1]))
|
||||
return pairs
|
||||
|
||||
|
||||
def _hide_list_rule() -> str:
|
||||
"""The phase-163 ``[data-view="pages"]`` hide list — a MULTI-selector
|
||||
rule (the selectors are comma-separated lines, ``.doc-prose``
|
||||
joined it in task 01), so it is sliced from its first selector to
|
||||
the ``display: none; }`` that closes it rather than matched with
|
||||
the single-selector ``_rule`` helper."""
|
||||
css = _css()
|
||||
start = css.index('.doc-views[data-view="pages"] .doc-summary')
|
||||
end = css.index("display: none; }", start) + len("display: none; }")
|
||||
return css[start:end]
|
||||
|
||||
|
||||
def _code(text: str) -> str:
|
||||
"""Comment-free source: block comments (``/* … */``, the JSDoc
|
||||
headers that legitimately NAME the classes they describe) and
|
||||
whole-line ``//`` comments stripped — so a writer scan counts CODE
|
||||
only, not prose about the code."""
|
||||
no_block = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
||||
return "\n".join(
|
||||
line for line in no_block.splitlines() if not line.lstrip().startswith("//")
|
||||
)
|
||||
|
||||
|
||||
def _prose_rule_js(js: str) -> str:
|
||||
"""The phase-165 prose rule at module scope: ``const
|
||||
PROSE_FORMATS`` through the close of ``isProseDoc`` (the
|
||||
``test_document_viewer.py`` extraction — the function references
|
||||
the const, so they are read as one unit)."""
|
||||
start = js.index("const PROSE_FORMATS")
|
||||
end = js.index("}", js.index("function isProseDoc")) + 1
|
||||
return js[start:end]
|
||||
|
||||
|
||||
# ---------- the prose set is pinned as data (LOCKED A1) ----------
|
||||
|
||||
|
||||
def test_prose_formats_set_contains_exactly_txt_and_text() -> None:
|
||||
"""``PROSE_FORMATS`` contains EXACTLY {"txt", "text"} — parsed out
|
||||
of the source line as a set of quoted members, so adding (or
|
||||
renaming) a format fails this test and has to be an intentional,
|
||||
test-editing decision (phase 165 D3: "a future format cannot drift
|
||||
into prose by accident"). Code, code-ish and pdf are NOT here."""
|
||||
line = re.search(r"const PROSE_FORMATS = new Set\(\[(.*?)\]\);", _js())
|
||||
assert line, "PROSE_FORMATS must be a Set literal at module scope"
|
||||
members = set(re.findall(r'"([^"]*)"', line.group(1)))
|
||||
assert members == {"txt", "text"}, (
|
||||
f"the prose format set is {sorted(members)}, not exactly ['text', 'txt'] "
|
||||
"(locked A1: image descriptions + plain-text prose only)"
|
||||
)
|
||||
# The literal is the only place those strings gate the content arm.
|
||||
assert len(re.findall(r"const PROSE_FORMATS", _js())) == 1, (
|
||||
"the prose set is declared ONCE (the single-site rule)"
|
||||
)
|
||||
|
||||
|
||||
def test_is_prose_doc_reads_is_image_before_the_format_arm() -> None:
|
||||
"""``isProseDoc`` tests ``doc.is_image === true`` FIRST and the
|
||||
``PROSE_FORMATS`` arm second (locked A1 + phase 122 A3): the image
|
||||
arm is unconditional, so an image doc is prose whatever its
|
||||
extension — and the strict ``=== true`` keeps a string lookalike
|
||||
out (the truth table in test_document_viewer.py covers behavior)."""
|
||||
rule = _prose_rule_js(_js())
|
||||
returns = re.findall(r"return (.*?);", rule)
|
||||
assert len(returns) == 1, f"isProseDoc is a one-return predicate, got {returns}"
|
||||
body = returns[0]
|
||||
image_i = body.index("doc.is_image")
|
||||
format_i = body.index("PROSE_FORMATS.has(doc.format)")
|
||||
assert image_i < format_i, "the is_image arm must be evaluated first"
|
||||
assert body[image_i:].startswith("doc.is_image === true ||"), (
|
||||
"the image arm is strict (=== true) and ORs into the format arm"
|
||||
)
|
||||
|
||||
|
||||
# ---------- the code scrollers are present and unchanged (A3) ----------
|
||||
|
||||
|
||||
def test_doc_raw_keeps_the_locked_pre_and_horizontal_scroller() -> None:
|
||||
""".doc-raw — the code/code-ish/unknown/PDF content block — is
|
||||
byte-identical to the pre-phase declaration in the two properties
|
||||
that ARE the contract: white-space: pre (no wrapping) and
|
||||
overflow-x: auto (the scroller). Phase 116's locked A1, restated
|
||||
verbatim as phase 165's locked A3."""
|
||||
decls = _decls(".doc-raw")
|
||||
assert re.search(r"white-space:\s*pre;", decls), (
|
||||
".doc-raw must keep white-space: pre — code never wraps (locked A3)"
|
||||
)
|
||||
assert re.search(r"overflow-x:\s*auto", decls), (
|
||||
".doc-raw must keep overflow-x: auto — the horizontal scroller"
|
||||
)
|
||||
assert "pre-wrap" not in decls, ".doc-raw must not gain pre-wrap"
|
||||
|
||||
|
||||
def test_bubble_pre_scrolls_and_never_gains_a_white_space_rule() -> None:
|
||||
"""The chat/table-surface code block: .bubble pre still scrolls
|
||||
sideways (overflow-x: auto) and still declares NO white-space —
|
||||
the phase-165 fix touched the viewer's prose arm only, so a code
|
||||
span in a chat answer wraps exactly as it did before (wrapping
|
||||
breaks code readability — the reason behind phase 116's locked A1)."""
|
||||
decls = _decls(".bubble pre")
|
||||
assert re.search(r"overflow-x:\s*auto", decls), (
|
||||
".bubble pre must keep its horizontal scroller"
|
||||
)
|
||||
assert "white-space" not in decls, (
|
||||
".bubble pre must carry no white-space declaration (unchanged)"
|
||||
)
|
||||
|
||||
|
||||
def test_markdown_table_scrollers_match_the_phase_44_declaration() -> None:
|
||||
"""Wide GFM tables still scroll, never wrap: .md-table-wrap is
|
||||
STILL the one-declaration scroller (overflow-x: auto) and
|
||||
.md-table still carries the phase-44 task-03 set —
|
||||
border-collapse + width: 100% + min-width: max-content + 0.9rem —
|
||||
byte-for-byte, so the wrapper stays the real scroller and the
|
||||
72rem reading column never breaks."""
|
||||
assert _rule(".md-table-wrap") == ".md-table-wrap { overflow-x: auto; }", (
|
||||
"the table wrapper must stay the single-declaration horizontal scroller"
|
||||
)
|
||||
decls = _decls(".md-table")
|
||||
for declaration in (
|
||||
"border-collapse: collapse",
|
||||
"width: 100%",
|
||||
"min-width: max-content",
|
||||
"font-size: 0.9rem",
|
||||
):
|
||||
assert declaration in decls, f".md-table must keep {declaration!r}"
|
||||
|
||||
|
||||
# ---------- phase 116's scrollbar theme still applies (A1) ----------
|
||||
|
||||
|
||||
def test_phase_116_modal_scrollbar_rules_are_all_present_and_scoped() -> None:
|
||||
"""The four .doc-modal .doc-raw rules — scrollbar-width: thin,
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track),
|
||||
and the ::-webkit-scrollbar / -track / -thumb pseudos — survive
|
||||
this phase intact and stay SCOPED under .doc-modal (no global
|
||||
scrollbar restyle). What still lands in .doc-raw still gets the
|
||||
themed bar: the prose block has no scrollbar to theme because it
|
||||
never overflows horizontally."""
|
||||
css = _css()
|
||||
firefox = _rule(".doc-modal .doc-raw")
|
||||
assert re.search(r"scrollbar-width:\s*thin", firefox)
|
||||
assert re.search(
|
||||
r"scrollbar-color:\s*var\(--scrollbar-thumb\) var\(--scrollbar-track\)", firefox
|
||||
)
|
||||
for pseudo, needle in (
|
||||
(".doc-modal .doc-raw::-webkit-scrollbar", "height: 8px"),
|
||||
(".doc-modal .doc-raw::-webkit-scrollbar-track", "background: var(--scrollbar-track)"),
|
||||
(".doc-modal .doc-raw::-webkit-scrollbar-thumb", "background: var(--scrollbar-thumb)"),
|
||||
):
|
||||
rule = _rule(pseudo, css)
|
||||
assert needle in rule, f"{pseudo} must keep {needle!r}"
|
||||
# Scope pin (phase 116: "no global scrollbar restyle"): every rule
|
||||
# that themes a scrollbar — a scrollbar-color declaration or a
|
||||
# ::-webkit-scrollbar pseudo — is scoped under `.doc-modal .doc-raw`.
|
||||
themed = [
|
||||
sel
|
||||
for sel, decls in _rules()
|
||||
if "scrollbar-color" in decls or "scrollbar" in sel
|
||||
]
|
||||
assert len(themed) == 4, f"expected the 4 modal scrollbar rules, got {themed}"
|
||||
for selector in themed:
|
||||
assert selector.startswith(".doc-modal .doc-raw"), (
|
||||
f"scrollbar rule {selector!r} escaped the modal scope (phase 116 scope pin)"
|
||||
)
|
||||
# The prose block never asks for a themed bar (it has none to theme).
|
||||
assert not [s for s, d in _rules() if ".doc-prose" in s and "scrollbar" in d + s], (
|
||||
".doc-prose must carry no scrollbar theming"
|
||||
)
|
||||
|
||||
|
||||
# ---------- .doc-prose never reaches for the raw scroller ----------
|
||||
|
||||
|
||||
def test_doc_prose_declares_no_horizontal_scroller_and_no_pre() -> None:
|
||||
""".doc-prose is the OPPOSITE contract of .doc-raw: no overflow-x
|
||||
of any kind, and white-space is pre-WRAP (never the bare ``pre``
|
||||
that made every generated line unbreakable). The wrapping comes
|
||||
from white-space: pre-wrap + overflow-wrap: anywhere inside the
|
||||
72rem column — nothing else."""
|
||||
decls = _decls(".doc-prose")
|
||||
assert "overflow-x" not in decls, (
|
||||
".doc-prose must never declare a horizontal scroller (the TODO's ask)"
|
||||
)
|
||||
assert re.search(r"white-space:\s*pre-wrap", decls)
|
||||
assert not re.search(r"white-space:\s*pre;", decls), (
|
||||
".doc-prose must not carry the bare pre (the defect this phase fixes)"
|
||||
)
|
||||
assert re.search(r"overflow-wrap:\s*anywhere", decls)
|
||||
assert re.search(r"max-width:\s*var\(--chat-column\)", decls), (
|
||||
"the prose block rides the 72rem reading column"
|
||||
)
|
||||
|
||||
|
||||
# ---------- the AA/contrast pin for the new block ----------
|
||||
|
||||
|
||||
def test_doc_prose_uses_the_documented_aa_pair_and_zero_literals() -> None:
|
||||
""".doc-prose pairs color: var(--ink) with background:
|
||||
var(--surface) — the documented 14.5:1 pair (PLAN §7.2, WCAG AA) —
|
||||
and carries NO color literal at all (no hex, no rgb()/rgba(), no
|
||||
hsl(), no color-mix): the phase-92 zero-literal invariant that
|
||||
keeps the block correct under every saved theme, and what the
|
||||
:root-drift test relies on when it parses styles.css."""
|
||||
decls = _decls(".doc-prose")
|
||||
assert re.search(r"color:\s*var\(--ink\)", decls)
|
||||
assert re.search(r"background:\s*var\(--surface\)", decls)
|
||||
for pattern, name in (
|
||||
(r"#[0-9a-fA-F]{3,8}\b", "hex"),
|
||||
(r"\brgba?\(", "rgb()/rgba()"),
|
||||
(r"\bhsla?\(", "hsl()/hsla()"),
|
||||
(r"color-mix\(", "color-mix()"),
|
||||
):
|
||||
assert not re.search(pattern, decls), f".doc-prose must carry no {name} literal"
|
||||
|
||||
|
||||
# ---------- the raw writers are the two expected ones (step 3) ----------
|
||||
|
||||
|
||||
def test_doc_raw_writers_are_the_raw_branch_and_the_phase_116_css_only() -> None:
|
||||
"""The task-02 hygiene pass as an executable pin: the only FILE that
|
||||
writes the doc-raw element is document.js's raw arm (one
|
||||
``className`` assignment, inside renderDocument), and the only
|
||||
other doc-raw writers in frontend/ are styles.css rules (the base
|
||||
rule, the mobile squeeze, the phase-116 modal scrollbar rules and
|
||||
the [data-view="pages"] hide list). doc-edit.js renders a
|
||||
<textarea> and shared.js renders chat bubbles — neither is a second
|
||||
content renderer, so neither needs the prose rule (see the module
|
||||
docstring for the full finding)."""
|
||||
js = _js()
|
||||
assert js.count('className = "doc-raw"') == 1, (
|
||||
"document.js must assign doc-raw in exactly one place (the raw arm)"
|
||||
)
|
||||
# A WRITER is code that emits the class (JS className/innerHTML, an
|
||||
# HTML class attribute) — a doc comment naming it is not one.
|
||||
writer = re.compile(r'className\s*=\s*"doc-raw"|class="[^"]*\bdoc-raw\b')
|
||||
for path in sorted(FRONTEND.rglob("*")):
|
||||
if not path.is_file() or path.suffix not in {".js", ".html"}:
|
||||
continue
|
||||
writes = len(writer.findall(_code(path.read_text(encoding="utf-8"))))
|
||||
if path == DOCUMENT_JS:
|
||||
assert writes == 1, f"{path.name} must hold exactly the one raw arm"
|
||||
else:
|
||||
assert writes == 0, (
|
||||
f"{path.name} must not write doc-raw — the writers are "
|
||||
"renderDocument's raw arm and styles.css only"
|
||||
)
|
||||
# The two expected writers DO hold it (the pin is not vacuous).
|
||||
assert "doc-raw" in _css(), "styles.css is the second expected doc-raw writer"
|
||||
|
||||
|
||||
def test_prose_block_mounts_where_the_raw_block_did() -> None:
|
||||
"""The prose arm appends into the SAME ``slot`` the raw arm uses,
|
||||
so the phase-122 image block stays FIRST, the phase-36 summary
|
||||
panel between, and the phase-163 additive strip last — the ordering
|
||||
both surfaces rely on. The [data-view="pages"] hide list carries
|
||||
.doc-prose alongside .doc-raw (the additive/PDF case's text view),
|
||||
so Pages still hides the text view for a prose doc."""
|
||||
js = _js()
|
||||
for arm in ("wrap", "prose", "pre"):
|
||||
assert js.count(f"slot.appendChild({arm});") == 1, (
|
||||
f"the {arm} arm must mount into slot exactly once"
|
||||
)
|
||||
# One slot binding, three arms (the shared mount point), and the
|
||||
# strip still lands in the wrapper last.
|
||||
assert "let slot = contentEl;" in js, "the three arms share one slot binding"
|
||||
assert "views.appendChild(docPagesStrip(doc, pageUrls));" in js
|
||||
hide = _hide_list_rule()
|
||||
for cls in (".doc-summary", ".doc-md", ".doc-raw", ".doc-prose", ".doc-image"):
|
||||
assert cls in hide, f"the [data-view=\"pages\"] hide list must include {cls}"
|
||||
assert hide.count("display: none;") == 1, "the hide list stays ONE grouped declaration"
|
||||
Reference in New Issue
Block a user