"""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 (```` 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 ```` (``rgb(15, 10, 10)`` = ``var(--bg)``) and ```` 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 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 ````, ```` 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 — 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()