Files
brain-of-reese/tests/unit/test_git_sources_card_rhythm.py
ducoterra 938d6cf09d
Build and Push Containers / build-and-push-app (push) Successful in 1m51s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 174_git_sources_card_rhythm
All checks pass. Both docs verified consistent. Final state — no live-infrastructure changes were needed (pure CSS fix; verification ran against the already-running local dev DB).

**Phase 174 final verification pass — report**

- **Verified (no new work needed; both tasks already complete in `complete/`):** the single `#git-sources-content { display:flex; flex-direction:column; gap:1.25rem }` rule in `frontend/assets/styles.css` (with the phase-174 root-cause comment), `.git-sources-shell` untouched, the 4-test unit pin module, and the 4-test Playwright suite.
- **Full suite:** `uv run pytest --cov=app --cov-report=term-missing` → **4116 passed, 99% `app/` coverage** (gate >90% ✓).
- **Lint/types:** `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings.
- **Dedicated E2E in isolation:** `uv run pytest tests/e2e/test_git_sources_card_rhythm.py -v --no-cov` → **4 passed** (1440px + 375px bounding-box rhythm pins 20±2px, hidden-children + revealed-joins-rhythm, anonymous gate, a11y/no-CDN).
- **Regression set, each in isolation:** `test_git_sources_admin.py` **6 passed**, `test_archive_upload_sources.py` **5 passed**, `test_git_source_dates.py` **4 passed**.
- **Completion criteria:** cards stack with uniform ≈20px gap at both viewports ✓; hidden children stay hidden / gate+content flip intact ✓; ≤640px forms column-stack + table scrolls inside its card (no page-level h-scroll) ✓; full suite + coverage + ruff + pyright ✓; dedicated + regression E2E suites ✓.
- **Documentation:** README.md reviewed in full (every `.env.example` var documented; all documented routes/pages/endpoints exist; every config default cross-checked against `app/config.py`; payload-limit table, ports 8123/8901, Containerfile/compose/entrypoint claims all match) — **verified consistent, no changes needed**. AGENTS.md verified (all referenced files/sections exist: PLAN §2/§7.1/§7.4/§9, `response_budget.py`, `debugging.py` conditional import, `.agents/` tracked + gitignore rules, `commit.gpgsign=false`, phase-pinned CSS classes) — **verified consistent, no changes needed**.
- **Notable:** commit + phase-dir move are handled by the harness per pipeline rules (no git history/index touched by this pass); no live-infrastructure changes were required.
- **Next pending phase:** `175_history_pagination` (in `todo/`).
2026-10-11 01:49:49 -04:00

175 lines
7.8 KiB
Python

"""Unit: the Sources (git sources) page card rhythm (phase 174, task 01).
Owner bug report 2026-10-21: "the bubbles on the sources page are
really smooshed together" — the "Git sources" header card, the
"Add a git source" card, the "Upload a source archive" card, and the
sources table card stacked flush (only their 1px borders separated
them). Root cause: the `.git-sources-shell` gap (1.25rem) only
separates the sign-in gate from the single ``#git-sources-content``
wrapper; every visible card is a child of THAT div, which was a plain
block. The fix is the phase-171 ``#settings-content`` idiom applied to
the one remaining admin view whose cards live inside a wrapper div:
``#git-sources-content`` itself becomes ``display: flex;
flex-direction: column; gap: 1.25rem;`` — the shell's OWN existing
token (no new spacing value).
This module pins the source-level contract a silent regression would
break; the browser behavior (bounding-box gaps at 1440px and 375px,
hidden-children pin, gate reveal) is E2E-gated by the phase-174 story
suite (``tests/e2e/test_git_sources_card_rhythm.py``, task 02).
Pinned:
* the ``#git-sources-content`` rule exists in ``styles.css`` and
carries ``display: flex``, ``flex-direction: column``, and
``gap: 1.25rem`` EXACTLY (a regression to any other gap value or a
missing flex line fails);
* the ``.git-sources-shell`` rule still carries ``gap: 1.25rem`` and
``flex: 1`` (the fix must not have "fixed" the shell — D3);
* the structural pin: in ``index.html``, ``#git-sources-content`` sits
inside ``.container.git-sources-shell`` and the ``.sources-gate``
(``#git-sources-gate``) is a sibling of it — if a future refactor
moves the cards to the shell's direct children, this rule becomes
dead weight and this pin names it;
* ``#git-sources-load-error`` (role=alert) and the env note
(id ``git-sources-env-note``, class ``git-source-env-note`` — the
phase docstring's ``#git-source-env-note`` names the class) still
carry ``hidden`` in ``index.html`` (the D2 premise the E2E pin
proves in a browser: the UA ``[hidden] → display:none`` holds inside
a flex parent, so the reveal lifecycle is untouched).
"""
from __future__ import annotations
import re
from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
SHELL_HTML = FRONTEND / "index.html"
STYLES_CSS = FRONTEND / "assets" / "styles.css"
GAP_TOKEN = "1.25rem"
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _css_rule(css: str, selector: str) -> str:
"""The body of one CSS rule: ``selector { … }`` (no nested
braces in these rules), whitespace-normalized. Raises if the
selector's rule is absent (a missing rule IS the regression)."""
m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css)
assert m is not None, f"rule for {selector!r} not found in styles.css"
return " ".join(m.group(1).split())
def _div_span(html: str, open_tag: str) -> str:
"""The substring from an opening <div …> to its matching </div>
(balanced div-tag counting — the test_git_source_branch_ui.py
brace-counting helper, applied to HTML)."""
start = html.index(open_tag)
depth = 0
for m in re.finditer(r"<div\b|</div>", html[start:]):
if m.group(0) == "</div>":
depth -= 1
if depth == 0:
return html[start : start + m.end()]
else:
depth += 1
raise AssertionError(f"unbalanced divs from {open_tag!r}")
def _direct_children_tags(html: str) -> list[str]:
"""The element tags that open at DEPTH 1 (direct children) of the
substring (the leading <div> is depth 0 — the counted element).
Void elements (input/br/img/… — this markup uses non-self-closing
<input …>) change no depth."""
VOID = {"input", "br", "img", "hr", "meta", "link", "source", "wbr"}
out: list[str] = []
depth = 1 # we are inside the shell div
for m in re.finditer(r"</?[a-zA-Z][^>]*?>", html):
tag = m.group(0)
mname = re.match(r"</?([a-zA-Z]+)", tag)
name = mname.group(1).lower() if mname else ""
if tag.startswith("</"):
depth -= 1
if depth == 0:
break
elif name not in VOID and not tag.endswith("/>"):
if depth == 1:
out.append(tag)
depth += 1
return out
def test_content_wrapper_is_the_flex_rhythm_column() -> None:
"""D1: #git-sources-content carries the exact flex-column rhythm —
display: flex, flex-direction: column, gap: 1.25rem (the shell's
own token; a different gap value or a missing flex line fails)."""
body = _css_rule(_read(STYLES_CSS), "#git-sources-content")
assert "display: flex" in body, body
assert "flex-direction: column" in body, body
# The EXACT token — ``gap: 1.25rem`` and no other gap declaration:
gaps = re.findall(r"gap\s*:\s*([^;]+)", body)
assert gaps == [GAP_TOKEN], f"gap regression: {gaps}"
def test_shell_rule_unchanged() -> None:
"""D3: the fix must not have 'fixed' the shell — .git-sources-shell
keeps flex-direction: column, gap: 1.25rem, and flex: 1 (the
gate↔content separation at anonymous)."""
body = _css_rule(_read(STYLES_CSS), ".git-sources-shell")
assert "display: flex" in body, body
assert "flex-direction: column" in body, body
gaps = re.findall(r"gap\s*:\s*([^;]+)", body)
assert gaps == [GAP_TOKEN], f"shell gap regression: {gaps}"
assert "flex: 1" in body, body
def test_content_div_sits_inside_shell_with_gate_sibling() -> None:
"""The structural pin: #git-sources-content is a direct child of
.container.git-sources-shell, and the .sources-gate
(#git-sources-gate) is its sibling. If a future refactor moves the
cards back to the shell's direct children, the phase-174 rule
becomes dead weight — this pin names that."""
html = _read(SHELL_HTML)
shell_open = '<div class="container git-sources-shell">'
assert shell_open in html, "the .git-sources-shell container is gone"
shell = _div_span(html, shell_open)[len(shell_open):]
children = _direct_children_tags(shell)
gates = [t for t in children if 'id="git-sources-gate"' in t]
contents = [t for t in children if 'id="git-sources-content"' in t]
assert len(gates) == 1, (
"the #git-sources-gate section is no longer a DIRECT child of "
f"the shell (children: {children})"
)
assert len(contents) == 1, (
"the #git-sources-content div is no longer a DIRECT child of "
f"the shell (children: {children}) — if the cards moved back to "
"the shell's direct children, the phase-174 wrapper rule is "
"dead weight: move the rule or the cards, and update this pin"
)
# The gate precedes the content (the reveal hides the gate and
# flips the content):
assert children.index(gates[0]) < children.index(contents[0])
def test_hidden_children_keep_hidden_in_markup() -> None:
"""The D2 premise: #git-sources-load-error (role=alert) and the env
note (id git-sources-env-note, role=note) still carry ``hidden``
in the markup — inside the flex wrapper the UA [hidden]
display:none holds (the parent's display never overrides a child's
own), so git-sources.js's reveal lifecycle is untouched."""
html = _read(SHELL_HTML)
# The phase docstring's #git-source-env-note is the element's CLASS;
# its id is git-sources-env-note — pin the id, fall back to the
# class (both name the same element).
for tag_id in ("git-sources-load-error", "git-sources-env-note"):
m = re.search(rf'<[a-z]+ [^>]*id="{tag_id}"[^>]*>', html)
assert m is not None, f"the {tag_id} element is gone"
tag = m.group(0)
assert " hidden" in tag or tag.rstrip(">").endswith("hidden"), (
f"{tag_id} lost its hidden attribute — the E2E hidden pin "
f"would break: {tag}"
)