"""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 ```` 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. """ 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"(? 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: """The fix preserves the phase-07 scroll contract: the card keeps overflow-x:auto — at any viewport narrower than the tables' 640px min-width the table scrolls INSIDE the card (AGENTS.md rule 5: full-width tables stay) while the document itself no longer pans.""" block = _rule_block(_css(), ".table-wrap") assert "overflow-x: auto" in block, ( "the .table-wrap card must keep overflow-x:auto (the in-card " "scroll the fix protects)" ) def test_the_fix_touched_only_the_shared_card() -> None: """The one-rule scope (A3): the fix lives on .table-wrap ONLY — .md-table-wrap (chat answer tables — different class, different context) and the #git-sources-table-wrap id card keep their pre-phase-86 rules (no position declaration acquired).""" css = _css() for sel in (".md-table-wrap", "#git-sources-table-wrap"): block = _rule_block(css, sel) assert not re.search(r"position\s*:", block), ( f"{sel} must not acquire a position declaration (the fix is " "the ONE .table-wrap rule)" )