phase: 86_history_page_width
All criteria verified. Final report: **Phase 86 final verification pass — all green, no defects found.** - **Verified (previously implemented):** `position: relative` on shared `.table-wrap` (the one-rule A3 fix, +12 lines in `frontend/assets/styles.css`); unit pin `tests/unit/test_table_wrap_stacking.py` (3 tests); E2E story `tests/e2e/test_history_page_width.py` (5 tests: History 626→≤375, Tokens 618→≤375, SPA switch with window-sentinel, RAG regression, desktop 1280×800) - **Tests/lint (this pass):** `uv run pytest` → 1720 passed · `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90% floor) · E2E in isolation: `test_history_page_width.py` 5 passed, `test_chat_history.py` 5 passed, `test_api_tokens.py` 9 passed, `test_smoke.py` 3 passed · `uv run ruff check . && uv run pyright` → clean, 0 errors - **Completion criteria:** ① 375px direct+SPA `scrollWidth ≤ innerWidth` on History/Tokens — PASS (E2E pins) ② in-card scroll preserved + full-width + a11y spans in DOM — PASS ③ Sources + desktop regression — PASS ④ full suite/coverage/E2E/lint gates — PASS ⑤ `git diff --stat` limited to `styles.css` (+12) + new test files + phase files, no `app/`/markup/JS — PASS ⑥ commit/move — left to harness per executor rules (working tree intact, `todo/` dir removal already reflected) - **Notable:** red→green (CSS reverted → 626px failure) and manual live check already recorded in `.agents/reports/86_history_page_width/`; pre-existing untracked `.agents/remediation_plan.md` (Sep 7 security audit) untouched - **Next pending phase:** `87_big_read_progress`
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""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.
|
||||
"""
|
||||
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:
|
||||
"""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)"
|
||||
)
|
||||
Reference in New Issue
Block a user