fix(ui): background no longer moves — static grid, three glow spots fading in and out on their own slow cycles (owner 2026-08-25)

This commit is contained in:
2026-08-25 09:43:19 -04:00
parent 1e6ae360e0
commit 025f57beb5
14 changed files with 1454 additions and 223 deletions
+58 -31
View File
@@ -13,12 +13,14 @@ Test → story mapping (Playwright Mapping Rule):
(same contrast helper as Phase 07).
2. ``test_no_emoji_in_chrome`` — neither page's ``innerText`` nor raw
``outerHTML`` contains any emoji code point.
3. ``test_animated_background`` — ``body::before``/``::after`` carry
background images AND run their animations by default (fixed,
pointer-events none).
3. ``test_animated_background`` — the background layers (fixed,
pointer-events none, carrying images): the grid (``body::before``)
is STATIC (phase 25, owner 2026-08-25: no movement) and the three
glow spots run their opacity-only fades ``bg-glow-a/b/c`` at
26s/34s/42s (``body::after`` + the two ``html`` pseudo-layers).
4. ``test_reduced_motion_honored`` — a context with
``reduced_motion="reduce"`` → ``animation-name: none`` on both layers
(the static grid + glows remain).
``reduced_motion="reduce"`` → ``animation-name: none`` on all four
layers (the static grid + spot images remain).
5. ``test_behavior_unchanged_smoke`` — on-topic question streams an answer
+ a source chip + the send button recovers (state machine intact under
the new skin).
@@ -258,14 +260,19 @@ def test_no_emoji_in_chrome(page: Page, app_url: str, db_ready: None) -> None:
def test_animated_background(page: Page, app_url: str, db_ready: None) -> None:
"""AC3: the background is subtly animated, pure CSS, zero JS, and can
never block or dim content: both body pseudo-layers are fixed,
pointer-events:none, carry a background image and run their animation
by default (60s grid drift + 14s glow breathing)."""
never block or dim content. Phase-25 contract (owner 2026-08-25:
"not moving. Different bright spots should slowly fade in and out")
supersedes the phase-08 recipe: the grid (``body::before``) is a
STATIC texture (``animationName: none``, image still painted) and
the three glow spots each run their own opacity-only fade —
``body::after`` → ``bg-glow-a`` 26s, ``html::before`` →
``bg-glow-b`` 34s, ``html::after`` → ``bg-glow-c`` 42s — all fixed,
pointer-events:none, carrying an image."""
page.goto(app_url)
report = page.evaluate(
"""() => {
const pick = (pseudo) => {
const cs = getComputedStyle(document.body, pseudo);
const pick = (el, pseudo) => {
const cs = getComputedStyle(el, pseudo);
return {
image: cs.backgroundImage,
anim: cs.animationName,
@@ -274,27 +281,42 @@ def test_animated_background(page: Page, app_url: str, db_ready: None) -> None:
pointerEvents: cs.pointerEvents,
};
};
return { before: pick("::before"), after: pick("::after") };
return {
grid: pick(document.body, "::before"),
glowA: pick(document.body, "::after"),
glowB: pick(document.documentElement, "::before"),
glowC: pick(document.documentElement, "::after"),
};
}"""
)
for layer in ("before", "after"):
info = report[layer]
assert info["image"] != "none", f"body::{layer} must carry a background image"
assert info["anim"] not in ("", "none"), (
f"body::{layer} must animate by default (got {info['anim']!r})"
for key in ("grid", "glowA", "glowB", "glowC"):
info = report[key]
assert info["image"] != "none", f"{key} must carry a background image"
assert info["position"] == "fixed", f"{key} must be position:fixed"
assert info["pointerEvents"] == "none", f"{key} must not intercept input"
# The phase-25 recipe: a static grid + three out-of-phase spot fades.
assert report["grid"]["anim"] == "none", (
f"the grid must be static (got {report['grid']['anim']!r})"
)
for key, name, seconds in (
("glowA", "bg-glow-a", 26.0),
("glowB", "bg-glow-b", 34.0),
("glowC", "bg-glow-c", 42.0),
):
info = report[key]
assert info["anim"] == name, f"{key} must run {name} (got {info['anim']!r})"
assert _seconds(info["duration"]) == pytest.approx(seconds), (
f"{key} must run its {seconds:.0f}s fade (got {info['duration']!r})"
)
assert info["position"] == "fixed", f"body::{layer} must be position:fixed"
assert info["pointerEvents"] == "none", f"body::{layer} must not intercept input"
# The recipe: 60s seamless grid drift, 14s breathing glows.
assert _seconds(report["before"]["duration"]) == pytest.approx(60.0)
assert _seconds(report["after"]["duration"]) == pytest.approx(14.0)
def test_reduced_motion_honored(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""AC4: with prefers-reduced-motion: reduce both background layers stop
animating (animation-name: none) — the static grid + glows remain."""
"""AC4: with prefers-reduced-motion: reduce ALL FOUR background
layers stop animating (animation-name: none) — the static grid +
spot images remain (phase 25 stills the two html pseudo-layers as
well as the body pair)."""
context = browser.new_context(
reduced_motion="reduce", viewport={"width": 1280, "height": 800}
)
@@ -303,19 +325,24 @@ def test_reduced_motion_honored(
rpage.goto(app_url)
report = rpage.evaluate(
"""() => {
const pick = (pseudo) => {
const cs = getComputedStyle(document.body, pseudo);
const pick = (el, pseudo) => {
const cs = getComputedStyle(el, pseudo);
return { anim: cs.animationName, image: cs.backgroundImage };
};
return { before: pick("::before"), after: pick("::after") };
return {
grid: pick(document.body, "::before"),
glowA: pick(document.body, "::after"),
glowB: pick(document.documentElement, "::before"),
glowC: pick(document.documentElement, "::after"),
};
}"""
)
for layer in ("before", "after"):
assert report[layer]["anim"] == "none", (
f"body::{layer} must not animate under reduced motion"
for key in ("grid", "glowA", "glowB", "glowC"):
assert report[key]["anim"] == "none", (
f"{key} must not animate under reduced motion (got {report[key]['anim']!r})"
)
assert report[layer]["image"] != "none", (
f"body::{layer}: the static background must remain visible"
assert report[key]["image"] != "none", (
f"{key}: the static background image must remain visible"
)
finally:
context.close()