PRD 5.16 — propagare design finalizata (system font stack, fara IBM Plex self-hostat): - US-001/002/008: tokeni --font-ui/--font-mono (system stack) + scala --fs-*; zero @font-face si zero /static/fonts/; landing aliniat la acelasi stack - US-003: RAR online = dot compact in antet + meniu burger; banda rosie DOAR pe blocat (invariant zero-silent-failures pastrat) - US-010: antet "ROMFAST AUTOPASS" + nume service + /login brandeit 2 coloane + badge plan; meniu burger cu separatoare; gate strict pe is_authenticated - US-011: selector tema pill icon+eticheta (reuse THEMES) - US-004/005/006/007: bug-fix editor prestatii (picker cod+denumire, add_extra in mod operatii, cod ales se salveaza fara "+", Renunta inchide via closest) - US-012/013: landing Autentificare->/login; wizard import colapsat + 4 pasi pe tokeni - fix VERIFY E2E: contoare duplicate pe 390px (inline display:flex batea @media) -> CSS + test-lock PRD 5.17 — tipuri de cont + trial Pro 30z + enforcement DUR: - US-001/002/008: accounts.tier + trial_until (migrare aditiva defensiva); app/plans.py sursa unica (PLANS, FREE_MONTHLY_LIMIT=60, effective_tier(now injectabil), monthly_usage, CONSUMED_STATUSES); create_account trial Pro 30z; CLI set-tier (protejat id=1, audit) - US-003/004/005: enforce volum 60/luna INAINTE de build_key pe ambele canale (PLAN_LIMITA_LUNARA, 3 niveluri + log_event); gate API Pro+ (PLAN_FARA_API 403 actionabil); valideaza/nomenclator raman permise; downgrade lazy; flag AUTOPASS_ENFORCE_PLANS (kill-switch) - US-006: badge plan antet + linie burger + consum N/60 + warn>=80% + 6 stari + copy RO pluralizat + banner one-time trial->Gratuit + pagina Cont Regresie: 1380 passed, 0 failed, 1 deselected (live). E2E browser pe 390/1280 confirmat. Backend trimitere (worker/masina stari/idempotenta/contract RAR) NEATINS. Lucrul 5.18 (corpus kNN) ramane separat, necomis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
254 lines
9.3 KiB
Python
254 lines
9.3 KiB
Python
"""Teste US-013 (PRD 5.10): Tema de culori ROMFAST (accent azur) + tipografie IBM Plex.
|
|
|
|
TDD: testele se scriu INAINTE de implementare (RED), dupa implementare trec (GREEN).
|
|
|
|
Testeaza:
|
|
- test_paleta_accent_azur_definita: accentul azur ROMFAST definit corect in :root si [data-theme="light"]
|
|
- test_font_ibm_plex_aplicat: IBM Plex Sans + Mono declarate in font-family si @font-face
|
|
- test_contrast_aa_pe_text_principal: contrast text principal >= 4.5:1 in dark si light
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import tempfile
|
|
|
|
import pytest
|
|
from starlette.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "culori.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 _get_style_block(html: str) -> str:
|
|
"""Extrage continutul primului <style> din HTML."""
|
|
m = re.search(r"<style>(.*?)</style>", html, re.DOTALL)
|
|
assert m, "<style> negasit in HTML"
|
|
return m.group(1)
|
|
|
|
|
|
def _hex_to_srgb(hex_color: str) -> tuple[float, float, float]:
|
|
"""Converteste hex (#rrggbb) la tuple (r, g, b) in [0,1]."""
|
|
h = hex_color.lstrip("#")
|
|
assert len(h) == 6, f"Hex invalid: {hex_color}"
|
|
r = int(h[0:2], 16) / 255.0
|
|
g = int(h[2:4], 16) / 255.0
|
|
b = int(h[4:6], 16) / 255.0
|
|
return r, g, b
|
|
|
|
|
|
def _linearize(c: float) -> float:
|
|
"""Liniarizeaza o componenta sRGB pentru calcul luminanta WCAG."""
|
|
if c <= 0.04045:
|
|
return c / 12.92
|
|
return ((c + 0.055) / 1.055) ** 2.4
|
|
|
|
|
|
def _luminance(hex_color: str) -> float:
|
|
"""Calculeaza luminanta relativa WCAG 2.1 pentru o culoare hex."""
|
|
r, g, b = _hex_to_srgb(hex_color)
|
|
rl = _linearize(r)
|
|
gl = _linearize(g)
|
|
bl = _linearize(b)
|
|
return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl
|
|
|
|
|
|
def _contrast_ratio(c1: str, c2: str) -> float:
|
|
"""Calculeaza raportul de contrast WCAG 2.1 intre doua culori hex."""
|
|
l1 = _luminance(c1)
|
|
l2 = _luminance(c2)
|
|
lighter = max(l1, l2)
|
|
darker = min(l1, l2)
|
|
return (lighter + 0.05) / (darker + 0.05)
|
|
|
|
|
|
# ── test_paleta_accent_azur_definita ─────────────────────────────────────────
|
|
|
|
def test_paleta_accent_azur_definita(client):
|
|
"""Accentul azur ROMFAST definit corect si neutrele actualizate conform DESIGN.md.
|
|
|
|
:root (dark default):
|
|
--accent:#2E74D6
|
|
--bg:#0f1218 --card:#181c24 --ink:#e6e9ef --muted:#8b93a7 --line:#262b36
|
|
--ok:#2FBF8F --warn:#E0A93B --err:#E05D5D
|
|
|
|
[data-theme="light"]:
|
|
--accent:#1F66C9
|
|
--bg:#f5f7fa --card:#ffffff --ink:#1a1d24 --muted:#5c6473 --line:#e2e5ea
|
|
--ok:#15803d --warn:#b45309 --err:#dc2626
|
|
"""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
style = _get_style_block(resp.text)
|
|
|
|
# Paleta dark (:root)
|
|
dark_vars = {
|
|
"--accent": "#2E74D6",
|
|
"--bg": "#0f1218",
|
|
"--card": "#181c24",
|
|
"--ink": "#e6e9ef",
|
|
"--muted": "#8b93a7",
|
|
"--line": "#262b36",
|
|
"--ok": "#2FBF8F",
|
|
"--warn": "#E0A93B",
|
|
"--err": "#E05D5D",
|
|
}
|
|
# Extrage blocul :root
|
|
root_m = re.search(r":root\s*\{([^}]+)\}", style, re.DOTALL)
|
|
assert root_m, "Blocul :root negasit in <style>"
|
|
root_block = root_m.group(1)
|
|
|
|
for var, val in dark_vars.items():
|
|
assert val.lower() in root_block.lower(), (
|
|
f"Variabila {var}:{val} lipseste sau are valoare gresita in :root (dark). "
|
|
f"Continut :root: {root_block.strip()}"
|
|
)
|
|
|
|
# Paleta light ([data-theme="light"])
|
|
light_vars = {
|
|
"--accent": "#1F66C9",
|
|
"--bg": "#f5f7fa",
|
|
"--card": "#ffffff",
|
|
"--ink": "#1a1d24",
|
|
"--muted": "#5c6473",
|
|
"--line": "#e2e5ea",
|
|
"--ok": "#15803d",
|
|
"--warn": "#b45309",
|
|
"--err": "#dc2626",
|
|
}
|
|
light_m = re.search(r'\[data-theme=["\']light["\']\]\s*\{([^}]+)\}', style, re.DOTALL)
|
|
assert light_m, 'Blocul [data-theme="light"] negasit in <style>'
|
|
light_block = light_m.group(1)
|
|
|
|
for var, val in light_vars.items():
|
|
assert val.lower() in light_block.lower(), (
|
|
f"Variabila {var}:{val} lipseste sau are valoare gresita in [data-theme=\"light\"]. "
|
|
f"Continut light: {light_block.strip()}"
|
|
)
|
|
|
|
|
|
# ── test_font_system_stack_aplicat ───────────────────────────────────────────
|
|
|
|
def test_font_system_stack_aplicat(client):
|
|
"""US-001 (PRD 5.16): IBM Plex eliminat; body foloseste stiva de fonturi sistem.
|
|
|
|
Verifica:
|
|
- body font-family foloseste var(--font-ui) (CSS custom property)
|
|
- --font-ui este definit in :root si contine un system font stack (system-ui / -apple-system)
|
|
- ZERO @font-face cu 'IBM Plex' in <style> (IBM Plex eliminat complet)
|
|
- ZERO referinte catre /static/fonts/ in HTML (nu se mai servesc fisiere woff2)
|
|
"""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
style = _get_style_block(resp.text)
|
|
|
|
# 1. body font-family refera var(--font-ui) (nu IBM Plex inline)
|
|
body_m = re.search(r"body\s*\{([^}]+)\}", style, re.DOTALL)
|
|
assert body_m, "Regula 'body { ... }' negasita in <style>"
|
|
body_block = body_m.group(1)
|
|
assert "var(--font-ui)" in body_block, (
|
|
f"body font-family trebuie sa foloseasca var(--font-ui) (sistem font stack). "
|
|
f"body block: {body_block.strip()}"
|
|
)
|
|
|
|
# 2. --font-ui definit in :root si contine un system font stack
|
|
root_m = re.search(r":root\s*\{([^}]+)\}", style, re.DOTALL)
|
|
assert root_m, "Blocul :root negasit in <style>"
|
|
root_block = root_m.group(1)
|
|
assert "--font-ui" in root_block, (
|
|
f"--font-ui lipseste din :root. Continut :root: {root_block.strip()}"
|
|
)
|
|
font_ui_m = re.search(r"--font-ui\s*:\s*([^;]+)", root_block)
|
|
assert font_ui_m, "--font-ui negasit in blocul :root"
|
|
font_ui_val = font_ui_m.group(1).lower()
|
|
assert "system-ui" in font_ui_val or "-apple-system" in font_ui_val, (
|
|
f"--font-ui trebuie sa contina system-ui sau -apple-system (stiva sistem). "
|
|
f"Valoare gasita: {font_ui_m.group(1).strip()}"
|
|
)
|
|
|
|
# 3. ZERO @font-face cu IBM Plex (eliminat in US-001)
|
|
font_face_blocks = re.findall(r"@font-face\s*\{([^}]+)\}", style, re.DOTALL)
|
|
ibm_face = [b for b in font_face_blocks if "IBM Plex" in b or "ibm-plex" in b.lower()]
|
|
assert not ibm_face, (
|
|
f"@font-face cu IBM Plex trebuia eliminat (US-001 PRD 5.16). "
|
|
f"Blocat gasit: {ibm_face}"
|
|
)
|
|
|
|
# 4. ZERO referinte /static/fonts/ in HTML randat (nu mai servim woff2)
|
|
html = resp.text
|
|
assert "/static/fonts/" not in html, (
|
|
"Referinte catre /static/fonts/ gasite in HTML — trebuie eliminate (US-001 PRD 5.16)."
|
|
)
|
|
|
|
|
|
# ── test_contrast_aa_pe_text_principal ───────────────────────────────────────
|
|
|
|
def test_contrast_aa_pe_text_principal(client):
|
|
"""Contrastul text principal este >= 4.5:1 in dark si light (WCAG 2.1 AA).
|
|
|
|
Dark: --ink:#e6e9ef pe --bg:#0f1218
|
|
Light: --ink:#1a1d24 pe --bg:#f5f7fa
|
|
Accent ca text pe alb: #1F66C9 pe #ffffff (WCAG AA pentru text normal)
|
|
"""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
style = _get_style_block(resp.text)
|
|
|
|
# Extrage valorile de culoare din :root si [data-theme="light"]
|
|
def _extract_var(block: str, var_name: str) -> str | None:
|
|
m = re.search(
|
|
re.escape(var_name) + r"\s*:\s*(#[0-9a-fA-F]{6})",
|
|
block,
|
|
re.IGNORECASE,
|
|
)
|
|
return m.group(1) if m else None
|
|
|
|
root_m = re.search(r":root\s*\{([^}]+)\}", style, re.DOTALL)
|
|
assert root_m, "Blocul :root negasit"
|
|
root_block = root_m.group(1)
|
|
|
|
light_m = re.search(r'\[data-theme=["\']light["\']\]\s*\{([^}]+)\}', style, re.DOTALL)
|
|
assert light_m, 'Blocul [data-theme="light"] negasit'
|
|
light_block = light_m.group(1)
|
|
|
|
# --- Dark: ink pe bg ---
|
|
dark_ink = _extract_var(root_block, "--ink")
|
|
dark_bg = _extract_var(root_block, "--bg")
|
|
assert dark_ink and dark_bg, (
|
|
f"Nu am putut extrage --ink/{dark_ink} sau --bg/{dark_bg} din :root"
|
|
)
|
|
cr_dark = _contrast_ratio(dark_ink, dark_bg)
|
|
assert cr_dark >= 4.5, (
|
|
f"Contrast dark insuficient: {dark_ink} pe {dark_bg} = {cr_dark:.2f}:1 (minim 4.5:1 AA)"
|
|
)
|
|
|
|
# --- Light: ink pe bg ---
|
|
light_ink = _extract_var(light_block, "--ink")
|
|
light_bg = _extract_var(light_block, "--bg")
|
|
assert light_ink and light_bg, (
|
|
f"Nu am putut extrage --ink/{light_ink} sau --bg/{light_bg} din [data-theme=light]"
|
|
)
|
|
cr_light = _contrast_ratio(light_ink, light_bg)
|
|
assert cr_light >= 4.5, (
|
|
f"Contrast light insuficient: {light_ink} pe {light_bg} = {cr_light:.2f}:1 (minim 4.5:1 AA)"
|
|
)
|
|
|
|
# --- Accent ca text pe alb (tema light) ---
|
|
light_accent = _extract_var(light_block, "--accent")
|
|
assert light_accent, f"--accent negasit in [data-theme=light]: {light_block.strip()}"
|
|
cr_accent_white = _contrast_ratio(light_accent, "#ffffff")
|
|
assert cr_accent_white >= 4.5, (
|
|
f"Accent light ({light_accent}) pe alb: contrast {cr_accent_white:.2f}:1 < 4.5:1 AA. "
|
|
f"Foloseste o varianta mai inchisa (ex. #1F66C9)."
|
|
)
|