Files
brain-of-reese/tests/unit/test_background_no_motion.py
ducoterra df91c6316c
Build and Push Containers / build-and-push-app (push) Successful in 1m47s
Build and Push Containers / build-and-push-db (push) Successful in 11s
phase: 92_theme_save_and_coverage
**Phase 92 final verification pass — all green.** This pass re-verified the completed tasks (all 5 task files already in `complete/`) against every completion criterion; no defects found, nothing to fix.

- Verified: 9th identity var `grid_line` end-to-end (migration `0015` at head, model/`theming.py`/schemas/API, 422 + built-in→NULL tests present); `styles.css` zero hardcoded literals outside `:root` + derived `--brand-*` vars; 9th picker in theme form; wordmark themed; `theme.js` save/reset/re-show/mount live-sync; dedicated E2E suite + phase-91 suite updated.
- `uv run pytest --cov=app --cov-report=term-missing` → **1845 passed, exit 0, TOTAL 99%** (>90%)
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings
- `uv run pytest tests/e2e/test_theme_save_and_coverage.py -v --no-cov` → **3 passed** (save-live, reset-live, whole-site)
- `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed**
- Criteria: (1) Save/Reset repaint open page, no nav, SPA-nav survives, pre-paint intact ✅; (2) both `rg` gates green (only `:root` + documented `#fff` Stop label; zero SVG hex attrs), grid/selection/hovers/wash/wordmark E2E-proven ✅; (3) no-op contract live-checked: row-less `/` = no tag + exact A1 CSP, grid-only row = 9-var tag in `COLOR_FIELDS` order + sha256 CSP, with-row ≡ row-less bytes ✅; (4) full suite/coverage/lint/both E2E ✅; (5) commit left to the harness per instructions.
- Deviations (previously made, probe-verified, kept): live repaint uses CSSOM `<html>` overrides because Chromium blocks `<style>` textContent mutations under the locked sha256-only CSP (tag text still mirrors the next load; `<html>` style exact-saved after Save, empty after Reset); wordmark themed via 3 `.brand-mark` CSS rules instead of inline styles (task 03's inline attrs were CSP-blocked — fixed during task 04).
- Next pending phase: none — `todo/` contains only `92_theme_save_and_coverage`.
2026-09-10 00:23:08 -04:00

195 lines
8.2 KiB
Python

"""Unit: the phase-78 static-background contract (source pins).
Owner direction (TODO.md L4, recorded per AGENTS.md rule 3): "Remove the
animated css background, it's too resource intensive" — the three
opacity-fading glow spots (``body::after`` / ``html::before`` /
``html::after``), their glow keyframes, and the
``prefers-reduced-motion`` rule whose only job was stilling those layers
are deleted from ``styles.css``. The 44px grid texture on
``body::before`` STAYS — it is static (zero animation cost).
Supersedes the phase-25 fading-glow contract (superseded chain
08 → 25 → 78); the phase-25 unit source-pin suite
(``tests/unit/test_background_animation.py``) is deleted with its
premise (three fading glows on named keyframe cycles).
Story: n/a (TODO-derived). Browser behavior (no background animation in
a real viewport, the grid still painted, no occlusion, no overflow) is
E2E-covered by ``tests/e2e/test_background_no_motion.py`` (task 02).
"""
from __future__ import annotations
import re
from pathlib import Path
STYLES_CSS = (
Path(__file__).resolve().parents[2] / "frontend" / "assets" / "styles.css"
)
GRID_LAYER = "body::before" # the static 44px grid — STAYS
# The phase-25 glow layers — all three deleted in phase 78 (they were
# the ONLY animated part of the background).
GLOW_LAYERS = ("body::after", "html::before", "html::after")
def _css() -> str:
return STYLES_CSS.read_text(encoding="utf-8")
def _css_no_comments() -> str:
"""styles.css with /* … */ comments stripped — for functional rule
checks that must not trip on explanatory prose."""
return re.sub(r"/\*[\s\S]*?\*/", "", _css())
def _find_rule(css: str, selector: str) -> re.Match[str] | None:
"""The first top-level ``selector { ... }`` rule, or None (the
deleted glow layers must be ABSENT, so this may not assert)."""
return re.search(r"(?m)^" + re.escape(selector) + r"\s*\{([\s\S]*?)\n\}", css)
def _rule_block(css: str, selector: str) -> str:
"""Body of the first ``selector { ... }`` rule (top-level, no
nesting)."""
rule = _find_rule(css, selector)
assert rule, f"styles.css must define a {selector} rule"
return rule.group(1)
# --------------------------------------------------------------------------
# The animated part is gone — no glow layers, no glow keyframes
# --------------------------------------------------------------------------
def test_no_bg_glow_keyframes_remain() -> None:
"""The three glow @keyframes blocks are deleted — the names must not
appear anywhere in the file (no declaration, no keyframe block, no
stale comment). Pinned by the ``bg-`` prefix: no token carrying the
background keyframe namespace may survive (the prefix is a plain
literal, so this pin itself carries none of the deleted names)."""
assert "bg-" not in _css(), (
"no token in the background keyframe namespace (bg-*) may remain "
"in styles.css — the glow keyframes and their declarations are deleted"
)
assert re.search(r"@keyframes bg-", _css_no_comments()) is None, (
"no background @keyframes may remain in styles.css"
)
def test_glow_layers_are_deleted() -> None:
"""body::after / html::before / html::after no longer exist as CSS
rules — the layers (their background-images, their animations, their
fixed/z-index:-1 boxes) are gone from the page entirely."""
rules = _css_no_comments()
for sel in GLOW_LAYERS:
assert _find_rule(rules, sel) is None, (
f"{sel} must be deleted (the phase-78 static contract)"
)
def test_no_background_layer_declares_animation() -> None:
"""None of the four background pseudo-element selectors carries an
``animation:`` declaration — the three glow selectors are ABSENT,
and the surviving grid layer is animation-free."""
rules = _css_no_comments()
for sel in (GRID_LAYER, *GLOW_LAYERS):
rule = _find_rule(rules, sel)
if rule is None:
continue # deleted layer — nothing to animate
assert "animation" not in rule.group(0), (
f"{sel} must not animate (static background contract)"
)
# --------------------------------------------------------------------------
# The static grid STAYS — byte-identical texture, no animation
# --------------------------------------------------------------------------
def test_grid_layer_is_static_and_unchanged() -> None:
"""The owner removed the animated part, not the grid: body::before
keeps 44px cells, 1px lines at 60% of the grid line color, and the
widened radial mask (both the -webkit- and standard mask
properties) — and carries NO animation. Phase 92 (task 02): the
line color is the 9th identity variable --grid-line at 60% (the
built-in #4a2626 reproduces the old warm tone exactly — and the
tab's Grid lines picker now repaints this texture)."""
block = _rule_block(_css(), GRID_LAYER)
grid = "color-mix(in srgb, var(--grid-line) 60%, transparent)"
assert "background-size: 44px 44px" in block
assert f"linear-gradient(to right, {grid} 1px, transparent 1px)" in block, (
"grid must keep horizontal 1px lines at 60% --grid-line"
)
assert f"linear-gradient(to bottom, {grid} 1px, transparent 1px)" in block, (
"grid must keep vertical 1px lines at 60% --grid-line"
)
mask = "radial-gradient(140% 110% at 50% 0%, black 40%, transparent 90%)"
assert f"-webkit-mask-image: {mask};" in block
assert f"mask-image: {mask};" in block
assert "animation" not in block, "the grid must stay static (no animation)"
def test_grid_layer_plumbing() -> None:
"""The surviving grid layer stays behind the content and can never
intercept input: fixed, full-viewport, z-index -1, pointer-events
none, with pseudo content (UI Structure Check: the fixed; inset: 0
layer adds no width — no 360px overflow)."""
block = _rule_block(_css(), GRID_LAYER)
assert "position: fixed" in block
assert "inset: 0" in block
assert "z-index: -1" in block
assert "pointer-events: none" in block
assert 'content: ""' in block
def test_html_owns_canvas_and_body_stays_transparent() -> None:
"""The no-occlusion contract survives the deletion: <html> keeps the
var(--bg) canvas; <body> stays transparent and non-stacking — or the
z-index:-1 grid layer would be painted over."""
html_block = _rule_block(_css(), "html")
assert "background: var(--bg)" in html_block, (
"html must keep background: var(--bg) (the page canvas)"
)
body_block = _rule_block(_css(), "body")
assert "background: transparent" in body_block, (
"body must keep background: transparent so the grid shows"
)
for prop in ("z-index", "transform", "opacity", "filter"):
assert prop + ":" not in body_block, (
f"body must not create a stacking context (found {prop})"
)
# --------------------------------------------------------------------------
# Reduced motion + phase-08 anchors
# --------------------------------------------------------------------------
def test_background_reduced_motion_block_is_deleted() -> None:
"""The prefers-reduced-motion rule whose only job was stilling the
background layers (body::before, body::after, html::before,
html::after → animation: none) is deleted WITH the layers. The
unrelated reduced-motion blocks (typing dots, spinner, toasts, nav
slide, …) stay untouched."""
blocks = re.findall(
r"@media \(prefers-reduced-motion: reduce\)\s*\{([\s\S]*?)\n\}", _css()
)
assert blocks, "the unrelated reduced-motion blocks must survive"
for i, block in enumerate(blocks):
for sel in (GRID_LAYER, *GLOW_LAYERS):
assert sel not in block, (
f"reduced-motion block {i} still references background layer {sel}"
)
def test_no_filter_no_blur() -> None:
"""The phase-08 performance anchor survives the deletion: no
``filter`` in the grid rule and no ``blur`` anywhere in styles.css
(comments stripped)."""
assert "filter" not in _rule_block(_css(), GRID_LAYER), (
"the grid layer must not use any filter"
)
assert "blur" not in _css_no_comments(), (
"no filter: blur anywhere in styles.css (phase-08 perf anchor)"
)