Files
rar-autopass/tests/test_web_tema_culori.py
Claude Agent 5a964a1a8d feat(5.10): UX trimiteri (pill filtre, paginare, editare) + Mapari in meniu + branding ROMFAST
14 stories TDD prin echipa de workeri (lead orchestreaza, 3 teammates pe valuri cu fisiere disjuncte; routes.py + base.html serializate ca fisiere fierbinti).

- US-001 fix filtrare data (_iso_date_prefix pe garda+comparatie, prinde timestamp cu ora)
- US-002/007 operatie service distincta in payload_view + afisare in detaliu
- US-003 pill-uri categorii (button/aria-pressed; needs_mapping --warn, needs_data/error --err); fara lista ID-uri/dropdown
- US-004 paginare numerotata 25/pag (total ramificat SQL-COUNT vs fetch-all+slice, clamp page, poll pastreaza pagina)
- US-005 VIN block-level sub nr
- US-006/006b editare cod RAR + validare nomenclator + recalcul idempotency (needs_data/needs_mapping via /corecteaza, error via /repune)
- US-008 card eroare 3-niveluri doar pe read-only + rezumat top-of-form
- US-009 Mapari in meniu hamburger; scoatere tab-bar + role=tablist orfan
- US-010/011 pagina Mapari consolidata + butoane icon SVG + dirty-state (fara kebab/emoji)
- US-012/012b header centrat + logo ROMFAST (/static/romfast_logo.png) in header
- US-013 paleta azur ROMFAST (#2E74D6/#1F66C9) + IBM Plex Sans/Mono self-host (woff2 reale)
- US-014 selector tema ciclic Light/Dark/Petrol/Auto + anti-FOUC pe 4 stari

Backend trimitere (worker/masina stari/idempotenta/mapping) + schema NEATINSE (UI/UX pur + 1 fix de filtrare).
VERIFY context curat PASS; /code-review high: 1 finding material reparat (US-006b). Regresie 896 passed, 1 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 20:20:58 +00:00

257 lines
9.4 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_ibm_plex_aplicat ────────────────────────────────────────────────
def test_font_ibm_plex_aplicat(client):
"""IBM Plex Sans si IBM Plex Mono declarate in font-family si @font-face cu font-display:swap.
Verifica:
- body font-family contine 'IBM Plex Sans' (sau alias ibm-plex-sans)
- exista cel putin un @font-face cu 'IBM Plex Sans' sau 'IBM Plex Mono'
- @font-face include font-display:swap
- @font-face pointeaza spre /static/fonts/
"""
resp = client.get("/login")
assert resp.status_code == 200
style = _get_style_block(resp.text)
# 1. body font-family contine IBM Plex Sans
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 "IBM Plex Sans" in body_block or "ibm-plex-sans" in body_block.lower(), (
f"'IBM Plex Sans' lipseste din font-family al body. body block: {body_block.strip()}"
)
# 2. Exista cel putin un @font-face cu IBM Plex
font_face_blocks = re.findall(r"@font-face\s*\{([^}]+)\}", style, re.DOTALL)
assert font_face_blocks, "@font-face negasit in <style>"
ibm_face = [b for b in font_face_blocks if "IBM Plex" in b or "ibm-plex" in b.lower()]
assert ibm_face, (
"@font-face cu 'IBM Plex Sans' sau 'IBM Plex Mono' negasit. "
f"Blocuri @font-face gasite: {font_face_blocks}"
)
# 3. font-display:swap prezent in cel putin un bloc IBM Plex @font-face
swap_present = any("swap" in b.lower() for b in ibm_face)
assert swap_present, (
"font-display:swap lipseste din @font-face IBM Plex. "
f"Blocuri @font-face IBM Plex: {ibm_face}"
)
# 4. @font-face pointeaza spre /static/fonts/
fonts_src = any("/static/fonts/" in b for b in ibm_face)
assert fonts_src, (
"@font-face IBM Plex nu pointeaza spre /static/fonts/. "
f"Blocuri: {ibm_face}"
)
# 5. IBM Plex Mono pentru monospace: exista un context monospace cu IBM Plex Mono
# (fie @font-face, fie o regula font-family cu monospace)
has_mono = any("IBM Plex Mono" in b or "ibm-plex-mono" in b.lower() for b in font_face_blocks)
if not has_mono:
# Acceptam si daca e in o regula CSS (nu neaparat @font-face)
has_mono = "IBM Plex Mono" in style
assert has_mono, (
"'IBM Plex Mono' lipseste din <style> (trebuie pentru coduri RAR/VIN/nr)."
)
# ── 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)."
)