"""Teste US-004 (PRD 5.11): Rand filtre Trimiteri — layout + stil ca referinta. TDD — testele sunt scrise INAINTE de implementare (RED), apoi devin GREEN. Verifica: - Quick-pills de data (Azi/7 zile/30 zile) in STANGA, inainte de campul vehicul si de pills-stare - Pill-urile folosesc stil uniform (color-mix la hover, nu filter:brightness) - Quick-pills seteaza data_de/data_pana si reincarca lista pastrand starea activa """ from __future__ import annotations import json import os import re import tempfile import pytest from starlette.testclient import TestClient # ============================================================ # Helpers # ============================================================ def _create_account_user(email: str, password: str = "parolasecreta10"): 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, f"Service {email}", active=True) create_user(conn, acct_id, email, password) return acct_id finally: conn.close() def _login(client, email: str, password: str = "parolasecreta10") -> 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 pe /login" resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)}) assert resp.status_code == 303, f"Login esuat: {resp.status_code}" def _ins(acct: int, *, status: str = "needs_mapping") -> int: from app.db import get_connection conn = get_connection() try: cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json) VALUES (?, ?, ?, ?)", ( f"k-us004-{os.urandom(4).hex()}", acct, status, json.dumps({ "vin": "WVWZZZ1KZAW009999", "nr_inmatriculare": "B001TST", "data_prestatie": "2026-06-20", "odometru_final": "100000", "prestatii": [{"cod_prestatie": "OE-1"}], }), ), ) conn.commit() return int(cur.lastrowid) finally: conn.close() # ============================================================ # Fixture # ============================================================ @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "filtre_us004.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true") from app.config import get_settings get_settings.cache_clear() from app.web import ratelimit ratelimit._hits.clear() from app.main import app with TestClient(app, follow_redirects=False) as c: yield c ratelimit._hits.clear() get_settings.cache_clear() # ============================================================ # test_pill_uri_in_stanga_controalelor # ============================================================ def test_pill_uri_in_stanga_controalelor(client): """Quick-pills de data (Azi/7 zile/30 zile) apar in STANGA formularului de filtre. Ordinea in DOM: quick-pills → camp cautare vehicul → pills stare. Pill-urile de stare NU mai stau izolate la dreapta butonului Filtreaza cu margin-left:auto pe un span separat — layout-ul e controlat explicit prin pozitia in form. """ acct = _create_account_user("stanga@test.com") _ins(acct, status="needs_mapping") _login(client, "stanga@test.com") resp = client.get("/?tab=acasa") assert resp.status_code == 200 html = resp.text # Quick-pills trebuie sa fie prezente in forma assert "Azi" in html, "Quick-pill 'Azi' trebuie sa fie prezent in bara de filtre" assert "7 zile" in html, "Quick-pill '7 zile' trebuie sa fie prezent in bara de filtre" assert "30 zile" in html, "Quick-pill '30 zile' trebuie sa fie prezent in bara de filtre" # DOM order: quick-pills STANGA (index mai mic in HTML) fata de campul vehicul idx_azi = html.find("Azi") idx_vehicul = html.find('id="f-vehicul"') assert idx_azi != -1, "'Azi' nu s-a gasit in HTML" assert idx_vehicul != -1, "'f-vehicul' nu s-a gasit in HTML" assert idx_azi < idx_vehicul, ( "Quick-pill 'Azi' trebuie sa apara INAINTE de campul f-vehicul in DOM (stanga)" ) # Pills stare (pills-categorii) la DREAPTA (dupa vehicul) idx_pills_cat = html.find('id="pills-categorii"') assert idx_pills_cat != -1, "pills-categorii nu s-a gasit in HTML" assert idx_vehicul < idx_pills_cat, ( "Campul vehicul trebuie sa apara INAINTE de pills-categorii in DOM (pills-stare la dreapta)" ) # Quick-pills apar si inainte de pills-categorii (stanga totala) assert idx_azi < idx_pills_cat, ( "Quick-pills de data trebuie sa apara INAINTE de pills-categorii" ) # ============================================================ # test_pill_categorie_stil_uniform # ============================================================ def test_pill_categorie_stil_uniform(client): """Pill-urile au un singur stil uniform: hover cu color-mix, activ suprima hover. - Hover trebuie sa foloseasca color-mix(in srgb, currentColor 12%, transparent) si NU filter:brightness (care devenea rosu plin si ilizibil). - Focus :focus-visible pastrat pe pill-cat. - Pill-cat-reset activ = --accent; pill-cat activ = culoarea categoriei (nu toate accent). """ acct = _create_account_user("stil@test.com") _login(client, "stil@test.com") resp = client.get("/") assert resp.status_code == 200 html = resp.text # CSS-ul trebuie sa contina color-mix pentru hover pe pill-cat assert "color-mix" in html, ( "pill-cat:hover trebuie sa foloseasca color-mix, nu filter:brightness" ) # CSS-ul NU trebuie sa foloseasca filter:brightness pe .pill-cat:hover m = re.search(r'\.pill-cat:hover\s*\{([^}]*)\}', html) if m: hover_rule = m.group(1) assert "brightness" not in hover_rule, ( f"pill-cat:hover NU trebuie sa contina filter:brightness — regula gasita: {hover_rule}" ) # focus-visible pastrat pe pill-cat assert "pill-cat:focus-visible" in html, ( "pill-cat trebuie sa pastreze regula :focus-visible cu outline" ) # Pill-cat-reset activ foloseste --accent (nu culoarea categoriei) assert "pill-cat-reset" in html, "Clasa pill-cat-reset trebuie sa existe pentru butonul Toate" assert "var(--accent)" in html, ( "Pill Toate activ trebuie sa foloseasca var(--accent)" ) # ============================================================ # test_quick_pills_data_seteaza_interval # ============================================================ def test_quick_pills_data_seteaza_interval(client): """Quick-pills de data seteaza data_de/data_pana (preset) si reincarca lista HTMX. Pastrand pill-ul de stare activ: setDataRange NU schimba campul #f-status. """ acct = _create_account_user("datepill@test.com") _ins(acct, status="needs_mapping") _login(client, "datepill@test.com") resp = client.get("/?tab=acasa") assert resp.status_code == 200 html = resp.text # Trebuie sa existe un mecanism JS care seteaza data_de si data_pana assert "setDataRange" in html, ( "Quick-pills trebuie sa apeleze setDataRange (functie JS pentru setarea intervalului de date)" ) # setDataRange trebuie sa seteze campul data_de/data_pana (prin id sau name) assert "f-data-de" in html or "data_de" in html, ( "setDataRange trebuie sa seteze campul data_de (prin id f-data-de sau name data_de)" ) assert "f-data-pana" in html or "data_pana" in html, ( "setDataRange trebuie sa seteze campul data_pana (prin id f-data-pana sau name data_pana)" ) # Lista trebuie sa se reincarce prin form (HTMX) la click pe quick-pill assert "/_fragments/submissions" in html, ( "Formularul de filtre trebuie sa trimita catre /_fragments/submissions" ) # setDataRange NU trebuie sa schimbe campul de status (pastreaza pill-ul de stare activ) # Verificam ca in JS-ul setDataRange nu se face `hs.value = ` (schimbare status) # prin cautarea functiei in HTML idx_fn = html.find("setDataRange") assert idx_fn != -1 # Extrage corpul functiei (pana la urmatoarea definitie de functie mare) fn_body = html[idx_fn:idx_fn + 800] assert "f-status" not in fn_body or "hs.value" not in fn_body[:fn_body.find("f-data")], ( "setDataRange NU trebuie sa modifice campul f-status (pastreaza filtrul de stare)" ) # ============================================================ # test_custom_pill_prezent_si_dezvaluie_campuri # ============================================================ def test_custom_pill_prezent_si_dezvaluie_campuri(client): """Butonul Custom este al 4-lea quick-pill si dezvaluie campurile de data manuala. AC US-004: Azi / 7 zile / 30 zile / Custom (4 quick-pills). Custom NU seteaza un preset; dezvaluie #custom-date-fields cu focus pe #f-data-de. Campurile #f-data-de/#f-data-pana sunt de tip 'date' (nu hidden) pentru interactiune. """ acct = _create_account_user("custom@test.com") _ins(acct, status="needs_mapping") _login(client, "custom@test.com") resp = client.get("/?tab=acasa") assert resp.status_code == 200 html = resp.text # Butonul Custom trebuie sa fie prezent in quick-pills assert "Custom" in html, "Butonul 'Custom' trebuie sa fie prezent in bara de filtre (4 quick-pills)" # Butonul Custom apeleaza setDataRange cu 'custom' assert "setDataRange" in html and "'custom'" in html, ( "Butonul Custom trebuie sa apeleze setDataRange(this,'custom')" ) # In ramura 'custom' din JS, NU se apeleaza requestSubmit/form.submit # (se dezvaluie campurile; utilizatorul introduce datele si form-ul submite la change) # Cautam in JS (range === 'custom'), nu in atributul onclick al butonului idx_fn = html.find("range === 'custom'") assert idx_fn != -1, "Conditia `range === 'custom'` trebuie sa existe in JS (setDataRange)" # Cautam in blocul imediat urmator conditiei: trebuie sa apara 'return' # INAINTE de 'requestSubmit' (dovada ca nu submite automat in ramura custom) block_custom = html[idx_fn:idx_fn + 500] idx_return = block_custom.find("return") idx_submit = block_custom.find("requestSubmit") assert idx_return != -1, ( "Ramura 'custom' din setDataRange trebuie sa contina 'return' pentru a nu submite automat" ) assert idx_submit == -1 or idx_return < idx_submit, ( "Ramura 'custom' NU trebuie sa apeleze requestSubmit inainte de 'return'" ) # Campurile de data trebuie sa existe si sa fie de tip 'date' (nu hidden) # pentru ca utilizatorul sa le poata interactiona in modul Custom import re m_de = re.search(r']+id="f-data-de"[^>]*>', html) assert m_de, "Input #f-data-de negasit in HTML" tag_de = m_de.group(0) assert 'type="hidden"' not in tag_de, ( "Input #f-data-de NU trebuie sa fie type='hidden' — trebuie sa fie tip 'date' " "pentru interactiune in modul Custom" )