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>
454 lines
19 KiB
Python
454 lines
19 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 paleta dark exacta: --bg:#0f1218, --card:#181c24, --ink:#e6e9ef.
|
|
Valorile actualizate la US-013 (PRD 5.10) conform DESIGN.md (accent azur ROMFAST).
|
|
"""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
assert "--bg:#0f1218" in html, "Paleta dark --bg:#0f1218 a fost modificata sau stearsa"
|
|
assert "--card:#181c24" in html, "Paleta dark --card:#181c24 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)
|
|
)
|
|
|
|
|
|
# ── US-001 (PRD 5.15): Teme aditive + tokeni --card2/--line2 ──────────────────
|
|
|
|
def test_cele_4_teme_definite(client):
|
|
"""Cele 4 teme noi (grafit/cobalt/cupru/hartie) au blocuri CSS [data-theme="..."]
|
|
cu tokenul minim: --bg/--card/--ink/--muted/--line/--ok/--err/--accent."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
for tema in ("grafit", "cobalt", "cupru", "hartie"):
|
|
blk = re.search(
|
|
r'\[data-theme=["\']' + tema + r'["\']\]\s*\{([^}]+)\}',
|
|
html, re.DOTALL,
|
|
)
|
|
assert blk, f"Bloc CSS [data-theme=\"{tema}\"] negasit in HTML"
|
|
block = blk.group(1)
|
|
for var in ("--bg", "--card", "--ink", "--muted", "--line", "--ok", "--err", "--accent"):
|
|
assert var in block, (
|
|
f"Token {var} lipseste din blocul CSS [data-theme=\"{tema}\"]"
|
|
)
|
|
|
|
|
|
def test_tokeni_card2_line2_in_toate_temele(client):
|
|
"""--card2 si --line2 sunt definiti in TOATE cele 7 teme:
|
|
dark (:root), light, petrol, grafit, cobalt, cupru, hartie."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
# dark e in :root
|
|
root_blk = re.search(r':root\s*\{([^}]+)\}', html, re.DOTALL)
|
|
assert root_blk, ":root CSS block negasit"
|
|
root_block = root_blk.group(1)
|
|
for var in ("--card2", "--line2"):
|
|
assert var in root_block, f"{var} lipseste din :root (dark)"
|
|
|
|
for tema in ("light", "petrol", "grafit", "cobalt", "cupru", "hartie"):
|
|
blk = re.search(
|
|
r'\[data-theme=["\']' + tema + r'["\']\]\s*\{([^}]+)\}',
|
|
html, re.DOTALL,
|
|
)
|
|
assert blk, f"Bloc CSS [data-theme=\"{tema}\"] negasit"
|
|
block = blk.group(1)
|
|
for var in ("--card2", "--line2"):
|
|
assert var in block, f"{var} lipseste din blocul CSS [data-theme=\"{tema}\"]"
|
|
|
|
|
|
def test_anti_fouc_7_stari(client):
|
|
"""Anti-FOUC din <head> cunoaste TOATE cele 7+1 stari valide:
|
|
light/dark/petrol/grafit/cobalt/cupru/hartie + auto.
|
|
Valoare necunoscuta -> auto, fara blink.
|
|
Fostul test test_anti_fouc_4_stari acoperea doar light/dark/petrol/auto;
|
|
acum verifica toate 8 starile."""
|
|
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"
|
|
head = head_match.group(1)
|
|
style_pos = head.find('<style>')
|
|
assert style_pos >= 0, "<style> negasit in <head>"
|
|
head_before_style = head[:style_pos]
|
|
|
|
for tema in ("light", "dark", "petrol", "grafit", "cobalt", "cupru", "hartie", "auto"):
|
|
assert tema in head_before_style, (
|
|
f"Tema '{tema}' lipseste din scriptul anti-FOUC (section inainte de <style>). "
|
|
f"Utilizatorul cu localStorage.theme='{tema}' va vedea blink la prima incarcare."
|
|
)
|
|
|
|
|
|
def test_migrare_localStorage_legacy(client):
|
|
"""Valorile vechi (light/dark/petrol) din localStorage raman VALIDE dupa adaugarea
|
|
temelor noi. Fara migrare fortata; preferinta setata inainte de update e pastrata.
|
|
Valoare lipsa/necunoscuta -> auto (fallback sigur, fara blink)."""
|
|
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"
|
|
head = head_match.group(1)
|
|
style_pos = head.find('<style>')
|
|
head_before_style = head[:style_pos]
|
|
|
|
# Valorile vechi trebuie sa fie recunoscute ca valide in anti-FOUC
|
|
for tema_veche in ("light", "dark", "petrol"):
|
|
assert tema_veche in head_before_style, (
|
|
f"Tema legacy '{tema_veche}' a disparut din scriptul anti-FOUC. "
|
|
f"Userii cu localStorage.theme='{tema_veche}' vor vedea blink (tratati ca necunoscut)."
|
|
)
|
|
|
|
# Fallback la 'auto' trebuie sa fie prezent
|
|
assert "auto" in head_before_style, (
|
|
"'auto' (fallback pentru valori necunoscute) lipseste din anti-FOUC"
|
|
)
|
|
|
|
|
|
def test_themes_dry_single_source(client):
|
|
"""DRY (E2): config temelor traieste intr-o singura structura sursa-de-adevar
|
|
(var THEMES). ICONS/LABELS NU sunt literali separati (ar putea diverge de THEMES).
|
|
Un test prinde o intrare ICONS/LABELS lipsa, nu doar token CSS lipsa.
|
|
Adaugarea unei teme noi = O singura intrare in THEMES."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
# Structura THEMES trebuie sa existe
|
|
assert "THEMES" in html, (
|
|
"var THEMES (sursa de adevar unica pentru config teme) negasit in HTML. "
|
|
"E2: config trebuie consolidat intr-o singura structura."
|
|
)
|
|
|
|
themes_match = re.search(r'var THEMES\s*=\s*\[(.*?)\];', html, re.DOTALL)
|
|
assert themes_match, "var THEMES = [...]; nu a fost gasit (forma asteptata: var THEMES = [...])"
|
|
themes_body = themes_match.group(1)
|
|
|
|
# Fiecare tema (inclusiv cele 4 noi) trebuie sa fie in THEMES
|
|
for tema in ("light", "dark", "petrol", "grafit", "cobalt", "cupru", "hartie", "auto"):
|
|
assert (f"'{tema}'" in themes_body or f'"{tema}"' in themes_body), (
|
|
f"Tema '{tema}' lipseste din var THEMES. "
|
|
f"DRY (E2): adaugarea temei = O singura intrare in THEMES."
|
|
)
|
|
|
|
# ICONS si LABELS NU trebuie sa fie literali separati cu cheile hardcodate
|
|
# (daca sunt literali, o tema noua in THEMES nu apare automat in ICONS/LABELS)
|
|
icons_literal = re.search(r'var ICONS\s*=\s*\{', html)
|
|
labels_literal = re.search(r'var LABELS\s*=\s*\{', html)
|
|
assert not icons_literal, (
|
|
"var ICONS = {...} e inca un literal separat (nu derivat din THEMES). "
|
|
"O tema noua in THEMES nu va aparea automat in ICONS — rupe DRY (E2)."
|
|
)
|
|
assert not labels_literal, (
|
|
"var LABELS = {...} e inca un literal separat (nu derivat din THEMES). "
|
|
"O tema noua in THEMES nu va aparea automat in LABELS — rupe DRY (E2)."
|
|
)
|
|
|
|
|
|
# ── US-008: Test parametrizat robust — token critic in fiecare tema ─────────────
|
|
|
|
@pytest.mark.parametrize("token", ["--card2", "--line2", "--accent", "--ok", "--err"])
|
|
@pytest.mark.parametrize("tema", ["light", "dark", "petrol", "grafit", "cobalt", "cupru", "hartie"])
|
|
def test_token_critic_in_tema_parametrizat(client, tema, token):
|
|
"""US-008: test parametrizat robust — fiecare token critic e definit in fiecare tema.
|
|
|
|
Ancorare pe selectorul CSS [data-theme="X"] {...} (sau :root pentru dark),
|
|
NU pe felii fixe [idx:idx+N]. Evita false-green-ul din regresia 5.13:
|
|
testele care feliau cu [idx:idx+N] nu prindeau un token lipsa dintr-o tema specifica
|
|
(offset-ul era mascat de continut din alte teme).
|
|
|
|
La esec, pytest raporteaza EXACT combinatia (tema, token) care lipseste —
|
|
debugging rapid fara cautare manuala in CSS.
|
|
|
|
Auto (tema 8): rezolvat la dark/light de anti-FOUC, fara bloc CSS propriu;
|
|
acoperit de test_anti_fouc_7_stari. Verificam cele 7 teme concrete (cu bloc CSS).
|
|
"""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
if tema == "dark":
|
|
# Dark e tema implicita — traieste in :root {}
|
|
blk = re.search(r':root\s*\{([^}]+)\}', html, re.DOTALL)
|
|
assert blk, ":root CSS block negasit — dark tema nu are paleta definita"
|
|
block = blk.group(1)
|
|
else:
|
|
blk = re.search(
|
|
r'\[data-theme=["\']' + re.escape(tema) + r'["\']\]\s*\{([^}]+)\}',
|
|
html, re.DOTALL,
|
|
)
|
|
assert blk, (
|
|
f'Bloc CSS [data-theme="{tema}"] negasit in HTML. '
|
|
f'Tema "{tema}" nu are paleta definita — adauga blocul CSS.'
|
|
)
|
|
block = blk.group(1)
|
|
|
|
assert token in block, (
|
|
f"Token '{token}' lipseste din tema '{tema}' "
|
|
f"({'\":root\"' if tema == 'dark' else f'\"[data-theme={tema}]\"'}). "
|
|
f"Componentele cu var({token}) vor arata gresit pe aceasta tema. "
|
|
f"Adauga '{token}:<valoare>;' in blocul CSS al temei '{tema}'."
|
|
)
|
|
|
|
|
|
# ── US-001 PRD 5.16: Stiva font sistem standard web ───────────────────────────
|
|
|
|
def test_font_stack_system_in_base(client):
|
|
"""T-E2 (PRD 5.16): base.html DEFINESTE --font-ui si --font-mono in :root
|
|
si body foloseste var(--font-ui). Niciun @font-face IBM Plex nu mai exista."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
assert "--font-ui" in html, "Token --font-ui lipseste din :root (US-001 PRD 5.16)"
|
|
assert "--font-mono" in html, "Token --font-mono lipseste din :root (US-001 PRD 5.16)"
|
|
assert "var(--font-ui)" in html, "body nu foloseste var(--font-ui) (US-001 PRD 5.16)"
|
|
assert "@font-face" not in html, \
|
|
"@font-face inca prezent in base.html — sterge toate regulile IBM Plex (US-001 PRD 5.16)"
|
|
|
|
|
|
def test_zero_referinte_static_fonts(client):
|
|
"""T-E1 (PRD 5.16): nicio referinta /static/fonts/ in template-urile randate de app.
|
|
Toate literalele 'IBM Plex Sans' si 'IBM Plex Mono' sunt eliminate."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
assert "/static/fonts/" not in html, \
|
|
"Referinta /static/fonts/ inca prezenta in HTML randat — @font-face nestersi complet"
|
|
assert "IBM Plex Sans" not in html, \
|
|
"Literalul 'IBM Plex Sans' inca prezent in HTML — inlocuieste cu var(--font-ui)"
|
|
assert "IBM Plex Mono" not in html, \
|
|
"Literalul 'IBM Plex Mono' inca prezent in HTML — inlocuieste cu var(--font-mono)"
|
|
|
|
|
|
def test_landing_fara_font_face_ibm_plex():
|
|
"""T-E1 (PRD 5.16): landing.html nu contine @font-face IBM Plex si niciun
|
|
literal 'IBM Plex Sans' sau 'IBM Plex Mono' ca font primary."""
|
|
landing = Path(__file__).parent.parent / "app" / "web" / "templates" / "landing.html"
|
|
assert landing.exists(), f"landing.html negasit la {landing}"
|
|
content = landing.read_text(encoding="utf-8")
|
|
|
|
assert "@font-face" not in content, \
|
|
"@font-face inca in landing.html — sterge toate regulile IBM Plex (US-008 PRD 5.16)"
|
|
assert "IBM Plex Sans" not in content, \
|
|
"Literal 'IBM Plex Sans' inca in landing.html — inlocuieste cu var(--font-ui)"
|
|
assert "IBM Plex Mono" not in content, \
|
|
"Literal 'IBM Plex Mono' inca in landing.html — inlocuieste cu var(--font-mono)"
|
|
|
|
|
|
# ── US-002 PRD 5.16: Scala tipografica ────────────────────────────────────────
|
|
|
|
def test_tokeni_scala_fs_definiti(client):
|
|
"""US-002 (PRD 5.16): tokenurile de scala tipografica --fs-xs..--fs-3xl si
|
|
--lh-tight/--lh-body sunt definiti in :root."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
tokeni = [
|
|
"--fs-xs", "--fs-sm", "--fs-base", "--fs-md",
|
|
"--fs-lg", "--fs-xl", "--fs-2xl", "--fs-3xl",
|
|
"--lh-tight", "--lh-body",
|
|
]
|
|
for tok in tokeni:
|
|
assert tok in html, f"Token {tok} lipseste din :root (US-002 PRD 5.16)"
|
|
|
|
|
|
# ── US-011 PRD 5.16: Selector tema pill cu eticheta ───────────────────────────
|
|
|
|
def test_selector_tema_are_eticheta(client):
|
|
"""US-011 (PRD 5.16): butonul de tema este un pill cu clasa .tema-btn,
|
|
contine .tema-icon si #tema-label (eticheta vizibila a temei curente)."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
assert "tema-btn" in html, \
|
|
"Clasa .tema-btn lipseste din HTML — butonul de tema nu e pill (US-011 PRD 5.16)"
|
|
assert "tema-icon" in html, \
|
|
".tema-icon lipseste — iconita temei nu e separat de eticheta (US-011 PRD 5.16)"
|
|
assert 'id="tema-label"' in html, \
|
|
'#tema-label lipseste — eticheta temei nu e prezenta in pill (US-011 PRD 5.16)'
|