Files
brain-of-reese/tests/unit/test_background_animation.py
T

209 lines
8.9 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 phase-22 animated-background contract (source pins).
The owner report (2026-08-24, roadmap A3): the phase-08 background "just
blinks". The diagnosis (`.agent/reports/22_background_animation/`) found
both layers *were* animating with no occlusion — the grid simply wasn't
perceptible: 1px lines at 35% `--line` alpha (≈10-18/255 over the page
bg), masked to the top ~25% of the viewport, drifting 0.73px/s. Only the
glow's 0.65↔1 opacity swing was visible, and it read as a blink.
The fix (styles.css, pure CSS, zero JS, no `filter: blur`):
- grid lines 35% → 60% `--line` alpha;
- mask widened: `120% 90% … black 25%, transparent 78%` →
`140% 110% … black 40%, transparent 90%` (grid now readable across
most of the viewport, fading to the corners);
- glow breathe narrowed 0.65↔1 → 0.85↔1 (breathing, not pulsing).
Durations are the owner-confirmed phase-08 design and stay pinned at
60s (one-cell seamless drift) and 14s — changing them would also break
the phase-08 story gate (`tests/e2e/test_dark_tech_theme.py` pins the
live durations). This file pins the FINAL values so a silent regression
(weaker alpha, shrunken mask, wider opacity swing, re-occluded layer) is
caught without a browser. Browser behavior (visible motion, no jank) is
E2E-covered by tests/e2e/test_background_animation.py (task 02).
"""
from __future__ import annotations
import re
from pathlib import Path
STYLES_CSS = (
Path(__file__).resolve().parents[2] / "frontend" / "assets" / "styles.css"
)
def _css() -> str:
return STYLES_CSS.read_text(encoding="utf-8")
def _css_no_comments() -> str:
"""styles.css with /* … */ comments stripped — for functional anchor
checks (filter/blur) that must not trip on explanatory comments."""
return re.sub(r"/\*[\s\S]*?\*/", "", _css())
def _rule_block(css: str, selector: str) -> str:
"""Body of the first `selector { ... }` rule (top-level, no nesting)."""
rule = re.search(
r"(?m)^" + re.escape(selector) + r"\s*\{([\s\S]*?)\n\}", css
)
assert rule, f"styles.css must define a {selector} rule"
return rule.group(1)
def _grid_rule(css: str) -> str:
return _rule_block(css, "body::before")
def _glow_rule(css: str) -> str:
return _rule_block(css, "body::after")
# --------------------------------------------------------------------------
# Layer plumbing — the no-occlusion contract (phase 08) must survive
# --------------------------------------------------------------------------
def test_both_layers_are_fixed_zminus1_noninteractive() -> None:
"""Both background layers stay behind the content and can never
intercept input: fixed, full-viewport, z-index -1, pointer-events
none (UI Structure Check: layers behind content, no 360px overflow)."""
for name, block in (("body::before", _grid_rule(_css())),
("body::after", _glow_rule(_css()))):
assert "position: fixed" in block, f"{name} must stay position:fixed"
assert "inset: 0" in block, f"{name} must stay full-viewport (inset: 0)"
assert "z-index: -1" in block, f"{name} must stay z-index:-1"
assert "pointer-events: none" in block, f"{name} must stay click-through"
assert "content: \"\"" in block, f"{name} must keep its pseudo content"
def test_html_owns_bg_and_body_stays_transparent() -> None:
"""The no-occlusion contract: the visible page background lives on
<html>; <body> must remain transparent and non-stacking, or the
z-index:-1 layers are painted over (the phase-08 recipe)."""
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 layers show"
)
# body must not gain a z-index/transform/opacity that would turn it
# into a stacking context trapping the negative-z-index layers.
for prop in ("z-index", "transform", "opacity", "filter"):
assert prop + ":" not in body_block, (
f"body must not create a stacking context (found {prop})"
)
# --------------------------------------------------------------------------
# Grid layer — the phase-22 final values
# --------------------------------------------------------------------------
def test_grid_animates_seamless_one_cell_drift() -> None:
"""body::before runs bg-grid-drift 60s linear infinite — the
owner-confirmed 60s one-cell loop (seamless, delta == 44px)."""
block = _grid_rule(_css())
assert "animation: bg-grid-drift 60s linear infinite" in block
def test_grid_keyframes_move_exactly_one_cell() -> None:
"""The drift delta must equal one 44px cell (0 0 → 44px 44px) for a
seamless loop — if the speed ever changes, only the duration may move."""
keyframes = re.search(
r"@keyframes bg-grid-drift\s*\{([\s\S]*?)\n\}", _css()
)
assert keyframes, "styles.css must define @keyframes bg-grid-drift"
body = keyframes.group(1)
assert "background-position: 0 0, 0 0" in body
assert "background-position: 44px 44px, 44px 44px" in body
def test_grid_cells_and_line_contrast() -> None:
"""44px cells with 1px lines at the phase-22 fixed 60% --line alpha
(phase-08's 35% measured imperceptible at 0.73px/s — see module
docstring)."""
block = _grid_rule(_css())
assert "background-size: 44px 44px" in block
line = "linear-gradient(to right, rgb(38 48 74 / 0.6) 1px, transparent 1px)"
assert line in block, "grid must keep horizontal 1px lines at 60% --line"
assert (
"linear-gradient(to bottom, rgb(38 48 74 / 0.6) 1px, transparent 1px)"
in block
), "grid must keep vertical 1px lines at 60% --line"
assert "0.35" not in block, "the too-faint 35% line alpha must not return"
def test_grid_mask_widened_and_prefixed() -> None:
"""The phase-22 mask: 140%×110% ellipse, fully visible to 40% of the
radius, faded out by 90% — the grid must read across most of the
viewport (phase-08's 120%×90%/25%/78% masked it to the top ~25%).
The -webkit- and standard mask-image must stay in lockstep."""
block = _grid_rule(_css())
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
# --------------------------------------------------------------------------
# Glow layer — the phase-22 final values
# --------------------------------------------------------------------------
def test_glow_animates_breathe_not_blink() -> None:
"""body::after runs bg-glow-breathe 14s ease-in-out infinite alternate
— the owner-confirmed 14s breathing period (untouched)."""
block = _glow_rule(_css())
assert "animation: bg-glow-breathe 14s ease-in-out infinite alternate" in block
def test_glow_keyframes_narrowed_opacity_swing() -> None:
"""The opacity swing is narrowed to 0.85↔1 (phase-08's 0.65↔1 was the
only visible motion and read as a blink). The gentle scale (1↔1.05)
stays."""
keyframes = re.search(
r"@keyframes bg-glow-breathe\s*\{([\s\S]*?)\n\}", _css()
)
assert keyframes, "styles.css must define @keyframes bg-glow-breathe"
body = keyframes.group(1)
assert re.search(r"opacity:\s*0\.85", body), "glow low must be 0.85"
assert re.search(r"opacity:\s*1;?", body), "glow high must be 1"
assert "0.65" not in body, "the blinking 0.65 low must not return"
assert re.search(r"scale\(1\)", body)
assert re.search(r"scale\(1\.05\)", body)
def test_glow_colors_and_radii_untouched() -> None:
"""Phase-22 is a perception fix, not a redesign: the two glow
gradients (indigo top-left, cyan bottom-right) keep phase-08's colors
and radii."""
block = _glow_rule(_css())
assert (
"radial-gradient(circle 56rem at 12% 8%, rgb(109 120 242 / 0.14), "
"transparent 62%)" in block
)
assert (
"radial-gradient(circle 60rem at 88% 92%, rgb(34 211 238 / 0.10), "
"transparent 62%)" in block
)
# --------------------------------------------------------------------------
# Phase-08 anchor: pure CSS, zero JS, no blur
# --------------------------------------------------------------------------
def test_no_blur_no_js_in_background_layers() -> None:
"""The phase-08 performance anchor: no `filter: blur` (or any filter)
on either layer, and the animation is CSS-only (both layers carry an
`animation:` shorthand; nothing in styles.css references a script)."""
for name, block in (("body::before", _grid_rule(_css())),
("body::after", _glow_rule(_css()))):
assert "filter" not in block, f"{name} must not use any filter"
assert "animation:" in block, f"{name} must be CSS-animated"
assert "blur" not in _css_no_comments(), (
"no filter: blur anywhere in styles.css (phase-08 perf anchor)"
)