Everything is verified. All gates green on the current working tree. Final report: **Phase 156 — final verification pass (all 4 task files present in `complete/` — no outstanding work)** - **Verified (not re-implemented):** bounded-panel `.table-wrap` (`max-height: min(70vh, 64rem)`, `overflow: auto`, opaque sticky `th` via `var(--brand-soft)`), `.cell-clip` ellipsis rule + the five label families in the three renderers, the added aria-labels, unit pins (incl. `.md-table-wrap` byte-unchanged scope guard, `#git-sources-table-wrap` harmony — no divergent rule), dedicated E2E suite (sticky ×3 tables, horizontal contract, computed-style ellipsis + reachable full text, mobile hamburger + scroll + pin) - **Tests/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **3570 passed, 99% on `app/`** (gate >90%); `uv run pytest tests/e2e/test_table_ux_polish.py -v --no-cov` (DB up) → **6/6 passed in isolation**; `uv run ruff check . && uv run pyright` → **clean** (0 errors) - **Completion criteria:** sticky header all 3 tables (exact-0 offset while `scrollTop > 0`) ✅ · horizontal scroll unchanged ✅ · ellipsis computed style + actual clip + title/aria reachable ✅ · mobile ≤640px contract ✅ · full suite/E2E/lint/coverage ✅ · atomic commit: **harness commits this phase after the pass** (per pipeline protocol — no git history commands run by me) - **Documentation:** AGENTS.md phase-156 wording (bounded panels, sticky headers, `.cell-clip` + aria-label idiom) matches the implementation exactly — verified consistent; README.md reviewed in full (pages/URLs vs registered routes, all `.env.example` vars documented, Quick Start, eval/llm scripts, compose prod profile, E2E examples) — verified consistent, no changes needed (README describes no table layout/scroll, so no phase-156 mention required) - **Deviations:** none — all work was already in the working tree; this pass re-verified every gate and the docs - **Next pending phase:** `157_chat_chip_cap`
139 lines
6.7 KiB
Python
139 lines
6.7 KiB
Python
"""Unit: the .table-wrap containing-block contract (phase 86, task 01).
|
||
|
||
TODO.md L4 (owner bug report, 2026-09-07): "The history page appears to
|
||
be the width of the table despite the table being scrollable. On mobile
|
||
this results in half the page being blank and awkwardly scrollable."
|
||
|
||
Bug basis (confirmed by live reproduction, 2026-09-07 — headless
|
||
Chromium, 375×812, signed-in admin): the History and Tokens tables
|
||
carry ``class="visually-hidden"`` on their ``<caption>`` and their
|
||
Actions column header — a ``position: absolute`` 1px clipped box. No
|
||
ancestor in the chain (th → table → .table-wrap → the view shell →
|
||
#main → body) was positioned, so the span's containing block was the
|
||
INITIAL containing block: its 1px box sat at the 640px table's right
|
||
edge and contributed to the DOCUMENT's scrollable overflow, bypassing
|
||
the card's own scroll clipping. Measured ``documentElement.scrollWidth``
|
||
at a 375px viewport: /history.html **626** (the page panned ~250px
|
||
into a blank region — the owner's "half the page being blank"),
|
||
/tokens.html **618** (the byte-identical defect, folded in per the
|
||
owner-confirmed A3 scope), /sources.html **375** (the RAG view is
|
||
clean — its table headers are visible text, no positioned hidden
|
||
spans). Hiding the span, OR making the card ``position: relative``,
|
||
both bring the scrollWidth back to 375.
|
||
|
||
The fix is ONE CSS property (owner decision A3): ``position: relative``
|
||
on the shared ``.table-wrap`` card — it becomes the containing block
|
||
for the ``.visually-hidden`` elements it hosts, so History AND Tokens
|
||
are fixed by the same rule with no markup change (the accessible
|
||
column name / caption stay) and no JS change. Zero-offset positioning
|
||
changes no layout: the spans stay clipped by their own
|
||
``clip: rect(0 0 0 0)`` + 1px box, and cards without such spans (the
|
||
RAG ``.docs-table``, the git-sources table) render unchanged.
|
||
|
||
This module pins the source-level contract (the house pattern of
|
||
``tests/unit/test_hamburger_nav.py``: read ``styles.css`` as text, no
|
||
browser) so the regression that omitted the containing block cannot
|
||
return.
|
||
|
||
Phase 156 (task 01) superseded two of these pins: the card's
|
||
``overflow-x: auto`` became ``overflow: auto`` (both axes — the
|
||
tables' min-width still triggers the in-card horizontal scroll) plus
|
||
the bounded-panel ``max-height: min(70vh, 64rem)`` (owner-locked A1:
|
||
the card is the vertical scroll region the sticky ``th`` pins to),
|
||
and the ``#git-sources-table-wrap`` id override was harmonized into
|
||
the base ``.table-wrap`` (the wrapper carries ``class="table-wrap"`,
|
||
so its duplicate declarations were removed — one-rule contract).
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
|
||
STYLES_CSS = FRONTEND / "assets" / "styles.css"
|
||
|
||
|
||
def _text(path: Path) -> str:
|
||
assert path.is_file(), f"missing frontend file: {path}"
|
||
return path.read_text(encoding="utf-8")
|
||
|
||
|
||
def _css() -> str:
|
||
"""styles.css with comments stripped (a comment may legally carry
|
||
braces — the brace-matching helper below must never see them)."""
|
||
return re.sub(r"/\*.*?\*/", "", _text(STYLES_CSS), flags=re.S)
|
||
|
||
|
||
def _rule_block(css: str, selector: str) -> str:
|
||
"""The first rule body for the EXACT selector ``selector`` (e.g.
|
||
``.table-wrap``). The lookbehind before the selector rejects
|
||
decorated variants: ``.table-wrap`` must NOT match ``.md-table-wrap`` (the
|
||
chat-answer markdown tables) nor ``.history-table-wrap`` /
|
||
``.tokens-table-wrap`` (the cards' extra classes), and an id rule
|
||
like ``#git-sources-table-wrap`` is never a match for the class
|
||
selector."""
|
||
m = re.search(r"(?<![\w-])" + re.escape(selector) + r"\s*\{([^}]*)\}", css)
|
||
assert m, f"missing rule for {selector!r}"
|
||
return m.group(1)
|
||
|
||
|
||
def test_table_wrap_is_a_positioned_containing_block() -> None:
|
||
"""THE regression pin: the shared .table-wrap card carries
|
||
position:relative — as its FIRST declaration — making it the
|
||
containing block for the position:absolute .visually-hidden
|
||
elements it hosts (the table caption + the Actions header span in
|
||
#view-history / #view-tokens). Without a positioned ancestor their
|
||
1px boxes are laid out against the initial containing block at the
|
||
640px table's right edge and leak into the document's scrollable
|
||
overflow (TODO.md L4 — the page appeared "the width of the
|
||
table": 626px on History, 618px on Tokens, at 375px)."""
|
||
block = _rule_block(_css(), ".table-wrap")
|
||
assert re.search(r"position:\s*relative", block), (
|
||
"the .table-wrap card must be position:relative (the "
|
||
"containing block for the .visually-hidden spans)"
|
||
)
|
||
assert block.lstrip().startswith("position: relative"), (
|
||
"position:relative must be the FIRST declaration of .table-wrap "
|
||
"(the containing-block contract precedes the card look)"
|
||
)
|
||
|
||
|
||
def test_table_wrap_keeps_the_horizontal_scroll_contract() -> None:
|
||
"""Phase 156 (task 01) supersedes the phase-86 overflow-x pin:
|
||
the card carries overflow:auto (BOTH axes — at any viewport
|
||
narrower than the tables' min-width the table still scrolls
|
||
INSIDE the card, AGENTS.md rule 5: full-width tables stay) plus
|
||
the bounded-panel max-height: min(70vh, 64rem) (owner-locked A1:
|
||
the card is the vertical scroll region — a 15-row table scrolls,
|
||
a 3-row one fits without a scrollbar) so the sticky th pins to
|
||
the card instead of scrolling away with the page."""
|
||
block = _rule_block(_css(), ".table-wrap")
|
||
assert "overflow: auto" in block, (
|
||
"the .table-wrap card must carry overflow: auto (both axes — "
|
||
"the in-card horizontal scroll + the bounded vertical scroll)"
|
||
)
|
||
assert "max-height: min(70vh, 64rem)" in block, (
|
||
"the .table-wrap card must be bounded: max-height: "
|
||
"min(70vh, 64rem) (the A1 bounded-panel design)"
|
||
)
|
||
|
||
|
||
def test_the_fix_touched_only_the_shared_card() -> None:
|
||
"""The one-rule scope (A3, harmonized by phase 156 task 01): the
|
||
positioning fix lives on .table-wrap ONLY — .md-table-wrap (chat
|
||
answer tables — different class, different context) keeps its
|
||
pre-phase-86 rules (no position declaration), and the
|
||
#git-sources-table-wrap id override is GONE: the wrapper carries
|
||
class="table-wrap" and inherits the base card, so a divergent
|
||
unbounded id rule cannot return."""
|
||
css = _css()
|
||
block = _rule_block(css, ".md-table-wrap")
|
||
assert not re.search(r"position\s*:", block), (
|
||
".md-table-wrap must not acquire a position declaration (the "
|
||
"fix is the ONE .table-wrap rule)"
|
||
)
|
||
assert not re.search(r"(?<![\w-])#git-sources-table-wrap\s*\{", css), (
|
||
"the #git-sources-table-wrap id override must be harmonized "
|
||
"into the base .table-wrap (phase 156 — no divergent rule)"
|
||
)
|