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
259 lines
11 KiB
Python
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()
|