Files
brain-of-reese/tests/e2e/test_background_no_motion.py
ducoterra 7fce6572d0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s
feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Single consolidated commit for four completed, validated phases (77, 78,
79, 80). The pipeline run left all work uncommitted because the harness
commits only with PHASE_COMMIT=1 while child executors are forbidden from
committing; the phases themselves all passed validation and moved to
.agents/phases/complete/.

Phase 77 — navbar view refresh
- router.js dispatches bor:view-refresh on re-show / active re-click /
  popstate (gated on wasMounted; first show and boot exempt)
- History / RAG / Sources / Tuning re-fetch on refresh (admin branch);
  Chat deliberately excluded (stream survival)
- History "Refresh" button (admin-only, in-flight disable + status line)
- New story suite tests/e2e/test_navbar_refresh.py (7 tests)

Phase 78 — static background
- Removed the animated glow layers; static 44px grid over the flat --bg
  canvas; default and reduced-motion renders byte-identical
- Updated background/theme E2E suites; removed bg-glow test pins

Phase 79 — API tokens
- api_tokens model + migration 0012; hash-only token service
- Admin tokens API + Tokens admin view; POST /api/token-auth;
  live-revoking require_user on chat / suggestions / document content
- Frontend token gate with localStorage cache; anonymous E2E suites
  migrated to token login
- New story suite tests/e2e/test_api_tokens.py (9 tests)

Phase 80 — history suggestion chips
- last_questions() endpoint with SEED fallback; startNewChat() refetch
- Seed-semantics docs (config.py, .env.example, README)
- Integration state matrix + E2E suite rewritten to the 4 chip states

Also included: phase-76 report artifacts and the repo restore-test-db
skill (previously untracked), scripts/* ruff fixes from phase 77.

Final gate state (phase 80 final pass, covers everything above):
- uv run pytest --cov=app → 1637 passed, 0 failed, app/ coverage 99%
- uv run ruff check . && uv run pyright → clean, 0 errors
- Per-phase story E2E suites green in isolation
2026-09-07 12:39:01 -04:00

259 lines
11 KiB
Python

"""Phase 78 E2E (Playwright): the background is fully static.
Source: ``TODO.md`` L4 — owner direction: "Remove the animated css
background, it's too resource intensive" (supersedes the phase-25
fading-glow contract of the ``background-no-motion`` story; the
superseded chain is 08 → 25 → 78).
Run in isolation (DB must be up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_background_no_motion.py -v --no-cov
The three opacity-fading glow spots (``body::after`` / ``html::before``
/ ``html::after``), their glow keyframes, and the
``prefers-reduced-motion`` rule that stilled them are deleted from
``styles.css`` — the infinite CSS animations ran continuously on every
page, in every tab. The 44px grid texture (``body::before``) STAYS: it
is static (zero animation cost). All other UI animations (typing dots,
spinner, toasts, nav slide, …) are untouched.
This suite proves the *behavior* the unit source pins
(``tests/unit/test_background_no_motion.py``) only describe, in a real
Chromium viewport: the three glow pseudo-elements report no
background-image, ``animation-name: none``, and no box at all (the
rules are gone), no ``bg-*`` keyframes or running ``bg-*`` animations
exist anywhere, the grid is still painted and static, the
no-occlusion canvas contract survives (``<html>`` owns ``var(--bg)``),
reduced motion changes nothing (already static), and there is no
360px overflow.
Test → story mapping (Playwright Mapping Rule):
1. ``test_background_layers_computed_styles`` — computed styles:
``body::before`` keeps the grid (``backgroundImage`` non-empty,
``animationName: none``); ``body::after`` / ``html::before`` /
``html::after`` report ``backgroundImage: none`` +
``animationName: none`` + ``position: static`` + ``content: none``
(the pseudo-elements have no box — the rules are deleted, not just
stilled); the page canvas stays on ``<html>`` (``rgb(15, 10, 10)`` =
``var(--bg)``) and ``<body>`` stays transparent (no occlusion).
2. ``test_no_background_keyframes_or_animations`` — deterministic
static proof: no ``@keyframes`` rule with a ``bg-`` name in any
live stylesheet, and no running ``bg-*`` entry in
``document.getAnimations()`` (pseudo-element CSS animations are
enumerated by the document-level list, not the element-level one —
verified on Chromium 151).
3. ``test_grid_layer_is_fixed_behind_content`` — the surviving grid
layer keeps the UI-structure contract: ``position: fixed``,
``z-index: -1``, ``pointer-events: none``, ``inset: 0`` (behind
content, click-through, full-viewport).
4. ``test_reduced_motion_background_static`` — a
``reduced_motion="reduce"`` context: the grid is still painted and
static, the deleted glow layers still report no image/animation —
nothing is resurrected under reduced motion.
5. ``test_no_horizontal_overflow_with_layers`` — 360px viewport:
``documentElement.scrollWidth <= clientWidth`` (the phase-07 pin —
the background adds no width).
"""
from __future__ import annotations
from playwright.sync_api import Browser, Page
PAGE_BG = "rgb(15, 10, 10)" # var(--bg) — the <html> canvas (dark-red rebrand #0f0a0a)
# Computed styles of the surviving grid layer + the three deleted glow
# pseudo-layers + the html/body background contract (single evaluate —
# one round-trip per test).
JS_LAYER_REPORT = """() => {
const pick = (el, pseudo) => {
const cs = getComputedStyle(el, pseudo);
return {
anim: cs.animationName,
position: cs.position,
zIndex: cs.zIndex,
pointerEvents: cs.pointerEvents,
content: cs.content,
edges: [cs.top, cs.right, cs.bottom, cs.left],
image: cs.backgroundImage,
};
};
return {
grid: pick(document.body, "::before"),
glowA: pick(document.body, "::after"),
glowB: pick(document.documentElement, "::before"),
glowC: pick(document.documentElement, "::after"),
htmlBg: getComputedStyle(document.documentElement).backgroundColor,
bodyBg: getComputedStyle(document.body).backgroundColor,
};
}"""
# Deterministic static audit: walk every same-origin stylesheet and
# collect the names of @keyframes rules starting with "bg-" (must be
# empty), plus the animationNames running in document.getAnimations()
# that start with "bg-" (must be empty).
JS_BG_ANIMATION_AUDIT = """() => {
const keyframeNames = [];
for (const sheet of document.styleSheets) {
let rules;
try {
rules = sheet.cssRules;
} catch (e) {
continue; // cross-origin sheet — not expected (no-CDN rule)
}
for (const rule of rules) {
if (rule.type === CSSRule.KEYFRAMES_RULE && rule.name.startsWith("bg-")) {
keyframeNames.push(rule.name);
}
}
}
const running = document.getAnimations()
.map((a) => a.animationName)
.filter((n) => n && n.startsWith("bg-"));
return { keyframeNames, running };
}"""
# --------------------------------------------------------------------------
# Tests (story → test mapping, see module docstring)
# --------------------------------------------------------------------------
def test_background_layers_computed_styles(
page: Page, app_url: str, db_ready: None
) -> None:
"""AC1: the animated background no longer exists in a real viewport —
the three glow pseudo-elements report no background-image and
``animation-name: none``, and they have no box at all
(``position: static`` / ``content: none`` — the CSS rules are
deleted, not merely stilled). The static grid (``body::before``)
stays: its texture is still painted and it carries no animation.
The no-occlusion contract survives: the canvas is on ``<html>``,
``<body>`` transparent."""
page.goto(app_url)
report = page.evaluate(JS_LAYER_REPORT)
grid = report["grid"]
assert grid["image"] != "none", (
f"the static grid texture must still be painted (image {grid['image']!r})"
)
assert grid["anim"] == "none", (
f"body::before must stay static (animationName, got {grid['anim']!r})"
)
for key in ("glowA", "glowB", "glowC"):
info = report[key]
assert info["image"] == "none", (
f"{key}: the deleted glow layer must carry no background-image "
f"(got {info['image']!r})"
)
assert info["anim"] == "none", (
f"{key}: the deleted glow layer must not animate "
f"(got {info['anim']!r})"
)
assert info["content"] == "none" and info["position"] == "static", (
f"{key}: the glow pseudo-element must have no box "
f"(content {info['content']!r}, position {info['position']!r})"
)
assert report["htmlBg"] == PAGE_BG, (
f"the page canvas must stay on <html> — var(--bg) (got {report['htmlBg']!r})"
)
assert report["bodyBg"] == "rgba(0, 0, 0, 0)", (
f"body must stay transparent so the grid shows (got {report['bodyBg']!r})"
)
def test_no_background_keyframes_or_animations(
page: Page, app_url: str, db_ready: None
) -> None:
"""AC1: the deterministic static proof — no ``@keyframes`` rule in
the background keyframe namespace (the ``bg-`` prefix) exists in any
live stylesheet, and no background-namespaced animation is running
in ``document.getAnimations()`` (the owner removed the infinite CSS
animations entirely — they ran on every page, in every tab)."""
page.goto(app_url)
audit = page.evaluate(JS_BG_ANIMATION_AUDIT)
assert not audit["keyframeNames"], (
f"no bg-* @keyframes may remain (found {audit['keyframeNames']!r})"
)
assert not audit["running"], (
f"no bg-* animation may be running (found {audit['running']!r})"
)
def test_grid_layer_is_fixed_behind_content(
page: Page, app_url: str, db_ready: None
) -> None:
"""AC2: UI Structure Check — the surviving grid layer stays behind
the content and can never intercept input: ``position: fixed``,
``z-index: -1``, ``pointer-events: none``, full-viewport
(``inset: 0``)."""
page.goto(app_url)
grid = page.evaluate(JS_LAYER_REPORT)["grid"]
assert grid["position"] == "fixed", "body::before must stay position:fixed"
assert grid["zIndex"] == "-1", (
f"the grid must stay behind content (z-index -1, got {grid['zIndex']!r})"
)
assert grid["pointerEvents"] == "none", (
"the grid must stay click-through (pointer-events none)"
)
assert grid["edges"] == ["0px", "0px", "0px", "0px"], (
f"the grid must stay full-viewport (inset: 0, got {grid['edges']!r})"
)
def test_reduced_motion_background_static(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""AC3: with ``prefers-reduced-motion: reduce`` the background is
already fully static — the grid is still painted and reports
``animation-name: none``, and the deleted glow layers still report
no image/animation (nothing is resurrected; the reduced-motion
blocks that survive in styles.css cover UI animations only)."""
context = browser.new_context(
reduced_motion="reduce", viewport={"width": 1280, "height": 800}
)
try:
rpage = context.new_page()
rpage.goto(app_url)
report = rpage.evaluate(JS_LAYER_REPORT)
grid = report["grid"]
assert grid["image"] != "none", (
f"the static grid must remain visible under reduced motion "
f"(image {grid['image']!r})"
)
assert grid["anim"] == "none", (
f"the grid must stay static under reduced motion (got {grid['anim']!r})"
)
for key in ("glowA", "glowB", "glowC"):
info = report[key]
assert info["image"] == "none", (
f"{key}: no glow background may exist under reduced motion "
f"(got {info['image']!r})"
)
assert info["anim"] == "none", (
f"{key}: no glow animation may exist under reduced motion "
f"(got {info['anim']!r})"
)
finally:
context.close()
def test_no_horizontal_overflow_with_layers(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""AC4: the background adds no width — the phase-07 overflow pin
(``documentElement.scrollWidth <= clientWidth``) holds at the 360px
floor with the single fixed; inset: 0 grid layer."""
phone = browser.new_page(viewport={"width": 360, "height": 740})
try:
phone.goto(f"{app_url}/")
scroll, client = phone.evaluate(
"() => [document.documentElement.scrollWidth, document.documentElement.clientWidth]"
)
assert scroll <= client, (
f"horizontal overflow at 360px with the background layers: "
f"{scroll} > {client}"
)
finally:
phone.close()