Tema light ca bloc [data-theme="light"] peste variabilele :root (dark nemodificat la octet). Comutator soare/luna in header pe toate paginile, default OS-aware (prefers-color-scheme, fallback dark), persistenta in localStorage doar la comutare explicita, script anti-FOUC in <head> pre-paint. Suprafetele de stare hardcodate convertite la color-mix in base.html + 7 fragmente _*.html (light lizibil, contrast WCAG AA). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
187 lines
7.5 KiB
Python
187 lines
7.5 KiB
Python
"""Teste US-001 + US-002 (PRD 5.3): Light/Dark mode comutator tema.
|
|
|
|
TDD: testele se scriu INAINTE de implementare (RED), dupa implementare trec (GREEN).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from starlette.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "tema.db"))
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.main import app
|
|
with TestClient(app) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def _create_user(email: str = "tema@test.com", password: str = "parolasecreta"):
|
|
from app.accounts import create_account
|
|
from app.users import create_user
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
acct_id = create_account(conn, "Service Tema", active=True)
|
|
create_user(conn, acct_id, email, password)
|
|
return acct_id
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _login(client, email: str, password: str = "parolasecreta") -> None:
|
|
resp = client.get("/login")
|
|
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
|
|
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
|
assert m, "csrf_token negasit in /login"
|
|
resp = client.post(
|
|
"/login",
|
|
data={"email": email, "parola": password, "csrf_token": m.group(1)},
|
|
follow_redirects=False,
|
|
)
|
|
assert resp.status_code == 303, f"Login esuat: {resp.status_code}"
|
|
|
|
|
|
# ── US-001: Tema light ─────────────────────────────────────────────────────────
|
|
|
|
def test_paleta_light_definita(client):
|
|
"""HTML de la GET /login contine un selector [data-theme="light"] care redefineste
|
|
cel putin --bg, --card, --ink, --muted, --line."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
assert '[data-theme="light"]' in html, 'Lipseste blocul [data-theme="light"] in HTML'
|
|
|
|
light_block = re.search(r'\[data-theme=["\']light["\']\]\s*\{([^}]+)\}', html, re.DOTALL)
|
|
assert light_block, 'Nu am gasit blocul CSS [data-theme="light"] { ... }'
|
|
block = light_block.group(1)
|
|
for var in ("--bg", "--card", "--ink", "--muted", "--line"):
|
|
assert var in block, f"Variabila {var} lipseste din blocul [data-theme=\"light\"]"
|
|
|
|
|
|
def test_dark_ramane_default(client):
|
|
""":root contine inca paleta dark exacta: --bg:#0f1115, --card:#181b22, --ink:#e6e9ef."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
assert "--bg:#0f1115" in html, "Paleta dark --bg:#0f1115 a fost modificata sau stearsa"
|
|
assert "--card:#181b22" in html, "Paleta dark --card:#181b22 a fost modificata sau stearsa"
|
|
assert "--ink:#e6e9ef" in html, "Paleta dark --ink:#e6e9ef a fost modificata sau stearsa"
|
|
|
|
|
|
def test_suprafete_fara_fundal_hardcodat(client):
|
|
"""<style> NU mai contine literalii hex dark-fix #241a1a, #201c0f, #16241c
|
|
(banner eroare / banner warn / flash facute theme-aware)."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
|
|
style_match = re.search(r'<style>(.*?)</style>', resp.text, re.DOTALL)
|
|
assert style_match, "<style> negasit in HTML"
|
|
style = style_match.group(1)
|
|
|
|
assert "#241a1a" not in style, "Fundalul hardcodat #241a1a (banner eroare) inca in <style>"
|
|
assert "#201c0f" not in style, "Fundalul hardcodat #201c0f (banner warn) inca in <style>"
|
|
assert "#16241c" not in style, "Fundalul hardcodat #16241c (flash) inca in <style>"
|
|
|
|
|
|
# ── US-002: Comutator tema + anti-FOUC ────────────────────────────────────────
|
|
|
|
def test_script_antifouc_in_head_inainte_de_style(client):
|
|
"""<head> contine un <script> care citeste localStorage (cheia 'theme') si seteaza
|
|
data-theme pe document.documentElement, pozitionat INAINTE de <style>."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
head_match = re.search(r'<head>(.*?)</head>', html, re.DOTALL | re.IGNORECASE)
|
|
assert head_match, "<head> negasit in HTML"
|
|
head = head_match.group(1)
|
|
|
|
style_pos = head.find('<style>')
|
|
assert style_pos >= 0, "<style> negasit in <head>"
|
|
|
|
head_before_style = head[:style_pos]
|
|
assert 'localStorage' in head_before_style, \
|
|
"Scriptul anti-FOUC (cu localStorage) trebuie sa fie in <head> INAINTE de <style>"
|
|
assert 'theme' in head_before_style, \
|
|
"Scriptul anti-FOUC trebuie sa citeasca cheia 'theme' din localStorage"
|
|
assert ('data-theme' in head_before_style or 'dataset.theme' in head_before_style), \
|
|
"Scriptul anti-FOUC trebuie sa seteze data-theme pe documentElement"
|
|
|
|
|
|
def test_buton_toggle_in_header_cu_eticheta(client):
|
|
"""<header> contine un <button> de comutare cu aria-label descriptiv (contine 'tema')."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
header_match = re.search(r'<header>(.*?)</header>', html, re.DOTALL | re.IGNORECASE)
|
|
assert header_match, "<header> negasit in HTML"
|
|
header = header_match.group(1)
|
|
|
|
labels = re.findall(r'<button[^>]*aria-label=["\']([^"\']+)["\']', header, re.IGNORECASE)
|
|
assert labels, "<button> cu aria-label negasit in <header>"
|
|
assert any('tema' in lbl.lower() for lbl in labels), \
|
|
f"Niciun <button> in <header> cu aria-label care contine 'tema'. Gasit: {labels}"
|
|
|
|
|
|
def test_toggle_pe_login_si_dashboard(client):
|
|
"""Butonul toggle apare atat pe /login (neautentificat) cat si pe dashboard (autentificat)."""
|
|
# Pe /login
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
header_match = re.search(r'<header>(.*?)</header>', resp.text, re.DOTALL | re.IGNORECASE)
|
|
assert header_match, "<header> negasit pe /login"
|
|
assert re.search(
|
|
r'<button[^>]*aria-label=["\'][^"\']*tema[^"\']*["\']',
|
|
header_match.group(1),
|
|
re.IGNORECASE,
|
|
), "Butonul toggle lipseste pe /login"
|
|
|
|
# Pe dashboard (autentificat)
|
|
_create_user("tema_dash@test.com", "parolasecreta")
|
|
_login(client, "tema_dash@test.com", "parolasecreta")
|
|
resp = client.get("/")
|
|
assert resp.status_code == 200
|
|
header_match = re.search(r'<header>(.*?)</header>', resp.text, re.DOTALL | re.IGNORECASE)
|
|
assert header_match, "<header> negasit pe dashboard"
|
|
assert re.search(
|
|
r'<button[^>]*aria-label=["\'][^"\']*tema[^"\']*["\']',
|
|
header_match.group(1),
|
|
re.IGNORECASE,
|
|
), "Butonul toggle lipseste pe dashboard"
|
|
|
|
|
|
# ── US-003: Fragmente HTMX fara fundal hardcodat ──────────────────────────────
|
|
|
|
def test_fragmente_fara_fundal_hardcodat():
|
|
"""Niciun fisier _*.html din app/web/templates/ nu contine literalii hex dark-fix
|
|
#241a1a, #201c0f, #16241c (suprafete banner eroare / warn / flash)."""
|
|
templates_dir = Path(__file__).parent.parent / "app" / "web" / "templates"
|
|
fragmente = sorted(templates_dir.glob("_*.html"))
|
|
assert fragmente, f"Nu am gasit fragmente _*.html in {templates_dir}"
|
|
|
|
vinovate = []
|
|
for f in fragmente:
|
|
continut = f.read_text(encoding="utf-8")
|
|
for literal in ("#241a1a", "#201c0f", "#16241c"):
|
|
if literal in continut:
|
|
vinovate.append(f"{f.name}: {literal}")
|
|
|
|
assert not vinovate, (
|
|
"Fragmente cu fundal hardcodat dark (nu adapteaza la tema light):\n"
|
|
+ "\n".join(vinovate)
|
|
)
|