Files
brain-of-reese/tests/unit/test_table_wrap_stacking.py
T
ducoterra 1513981c1c
Build and Push Containers / build-and-push-app (push) Successful in 2m15s
Build and Push Containers / build-and-push-db (push) Successful in 11s
phase: 156_table_ux_polish
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`
2026-10-04 20:46:57 -04:00

139 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)"
)