"""Teste US-004: wizard import cu stepper vizual (4 pasi numerotati). TDD — testele sunt scrise INAINTE de implementare (RED), apoi se face GREEN. Verifica: - Pasul 1 activ (aria-current="step") in fragmentul de upload - Pasul 2 activ in fragmentul mapare-coloane - Pasul 3 activ in preview - Pasii 1 si 2 marcati ca "facuti" in preview (clasa/marcaj) - hx-target="#import-section" pastrat in fragmentele de import - csrf_token prezent in formularele de import """ from __future__ import annotations import io import os import re import tempfile import pytest @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false") from app.config import get_settings get_settings.cache_clear() from app.web import ratelimit ratelimit._hits.clear() # izolare: limiterul login e global in-proces from app.main import app from fastapi.testclient import TestClient with TestClient(app) as c: yield c ratelimit._hits.clear() get_settings.cache_clear() # --------------------------------------------------------------------------- # Helpere # --------------------------------------------------------------------------- def _make_csv_bytes(rows: list[dict], sep: str = ";") -> bytes: import csv buf = io.StringIO() if not rows: return b"" writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep) writer.writeheader() writer.writerows(rows) return buf.getvalue().encode("utf-8") def _make_xlsx_bytes(rows: list[dict]) -> bytes: openpyxl = pytest.importorskip("openpyxl") wb = openpyxl.Workbook() ws = wb.active if not rows: return b"" headers = list(rows[0].keys()) ws.append(headers) for row in rows: ws.append([row.get(h) for h in headers]) buf = io.BytesIO() wb.save(buf) return buf.getvalue() _SAMPLE_ROWS = [ { "VIN": "WVWZZZ1KZAW000123", "Nr inmatriculare": "B001TST", "Data prestatie": "15.06.2026", "Odometru final": "123456", "Operatie": "Revizie", }, { "VIN": "WVWZZZ1KZAW000456", "Nr inmatriculare": "B002TST", "Data prestatie": "16.06.2026", "Odometru final": "200000", "Operatie": "Revizie", }, ] def _seed_op_mapping(client, cod_op: str = "Revizie", cod_prest: str = "OE-1") -> None: client.post("/v1/mapari", json={ "cod_op_service": cod_op, "cod_prestatie": cod_prest, "auto_send": True, }) def _upload_and_get_import_id(client, rows=None) -> int: xlsx = _make_xlsx_bytes(rows or _SAMPLE_ROWS) r = client.post( "/_import/upload", files={"file": ("test.xlsx", xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}, ) assert r.status_code == 200 m = re.search(r"/_import/(\d+)/mapare-coloane", r.text) assert m, f"Nu s-a gasit import_id in raspuns: {r.text[:500]}" return int(m.group(1)) def _get_preview_via_mapare(client, import_id: int) -> str: """Salveaza maparea de coloane si returneaza textul raspunsului preview.""" r = client.post( f"/_import/{import_id}/mapare-coloane", data={ "colname": ["VIN", "Nr inmatriculare", "Data prestatie", "Odometru final", "Operatie"], "canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"], "format_data": "DD.MM.YYYY", }, ) assert r.status_code == 200 return r.text # --------------------------------------------------------------------------- # US-004 Teste stepper # --------------------------------------------------------------------------- def test_stepper_pas1_la_upload(client): """Fragmentul de upload contine stepper-ul cu pasul 1 activ. Verifica prezenta marcajului aria-current='step' pe pasul 'Incarca fisier' sau clasa activa asociata pasului 1. """ r = client.get("/_import/reset") assert r.status_code == 200 text = r.text # Stepper-ul trebuie sa fie prezent assert "stepper" in text or "pasi-import" in text or "step" in text.lower(), \ "Stepper-ul nu a fost gasit in fragmentul de upload" # Pasul 1 trebuie sa aiba aria-current="step" assert 'aria-current="step"' in text, \ "aria-current='step' nu a fost gasit in fragmentul de upload (pasul 1)" # Textul pasului 1 trebuie sa fie prezent assert "Incarca" in text, "Textul pasului 1 'Incarca' nu a fost gasit" def test_stepper_pas1_via_tab_import(client): """Accesand /?tab=import, panoul contine stepper cu pasul 1 activ.""" r = client.get("/?tab=import") assert r.status_code == 200 text = r.text assert 'aria-current="step"' in text, \ "aria-current='step' nu a fost gasit in panoul Import (/?tab=import)" assert "Incarca" in text, "Textul pasului 1 'Incarca' nu a fost gasit in panoul Import" def test_stepper_pas2_la_mapare(client): """Fragmentul mapare-coloane contine stepper cu pasul 2 activ. Declanseaza un upload cu coloane NEMAPATE ca sa primesti _mapcoloane.html. """ # Upload fara mapare salvata → trebuie sa vina _mapcoloane.html csv_bytes = _make_csv_bytes(_SAMPLE_ROWS) r = client.post( "/_import/upload", files={"file": ("test.csv", csv_bytes, "text/csv")}, ) assert r.status_code == 200 text = r.text # Trebuie sa fie formularul de mapare coloane assert "mapare-coloane" in text, "Nu s-a primit fragmentul de mapare coloane" # Stepper prezent assert "stepper" in text or "step" in text.lower(), \ "Stepper-ul nu a fost gasit in fragmentul mapare-coloane" # Pasul 2 trebuie sa aiba aria-current="step" cu textul "Potriveste" # (pasul 1 e facut, pasul 2 e activ) assert 'aria-current="step"' in text, \ "aria-current='step' nu a fost gasit in fragmentul mapare-coloane (pasul 2)" assert "Potriveste" in text, "Textul pasului 2 'Potriveste' nu a fost gasit" def test_stepper_pas3_la_preview(client): """Preview contine stepper cu pasul 3 activ. Declanseaza upload + salvare mapare → se ajunge la preview. """ _seed_op_mapping(client) import_id = _upload_and_get_import_id(client) text = _get_preview_via_mapare(client, import_id) # Preview trebuie sa fie prezent assert "Preview" in text or "confirm-form" in text, \ "Nu s-a primit fragmentul de preview" # Stepper prezent assert "stepper" in text or "step" in text.lower(), \ "Stepper-ul nu a fost gasit in preview" # Pasul 3 activ assert 'aria-current="step"' in text, \ "aria-current='step' nu a fost gasit in preview (pasul 3)" assert "Verifica" in text, "Textul pasului 3 'Verifica' nu a fost gasit in preview" def test_stepper_pas3_la_preview_direct_mapare_retinuta(client): """Upload cu mapare retinuta sare direct la preview cu pasul 3 activ. Primul upload + mapare memoreaza configuratia. Al doilea upload cu acelasi antet sare direct la preview (pas 3). Pasii 1 si 2 sunt implicit facuti (comportament stepper la pas=3). """ _seed_op_mapping(client) import_id1 = _upload_and_get_import_id(client) _get_preview_via_mapare(client, import_id1) # Al doilea upload — mapare retinuta → preview direct xlsx = _make_xlsx_bytes(_SAMPLE_ROWS) r = client.post( "/_import/upload", files={"file": ("test2.xlsx", xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}, ) assert r.status_code == 200 text = r.text # Preview direct cu mesaj "Mapare retinuta" assert "Mapare retinuta" in text, "Preview direct (mapare retinuta) nu a fost randat" # Stepper prezent cu pasul 3 activ assert 'aria-current="step"' in text, \ "aria-current='step' nu a fost gasit in preview direct (mapare retinuta)" assert "Verifica" in text, "Textul pasului 3 'Verifica' nu a fost gasit in preview direct" def test_stepper_marcheaza_pasii_facuti(client): """In preview (pas 3), pasii 1 si 2 sunt marcati ca facuti (clasa is-done). Verifica prin prezenta clasei CSS is-done (doua aparitii: pasii 1 si 2). """ _seed_op_mapping(client) import_id = _upload_and_get_import_id(client) text = _get_preview_via_mapare(client, import_id) # Clasa "is-done" trebuie sa apara pentru pasii 1 si 2 (index < pas curent) assert "is-done" in text, \ "Clasa 'is-done' nu a fost gasita in preview (pasii 1 si 2 ar trebui marcati ca is-done)" # Numarul de aparitii: cel putin 2 pasi marcati ca is-done count_done = text.count("is-done") assert count_done >= 2, \ f"Asteptat cel putin 2 pasi marcati ca 'is-done' in preview, gasit {count_done}" def test_import_hx_target_in_tab(client): """Fragmentele de import pastreaza hx-target='#import-section'. Fragmentul de upload (/_import/reset) trebuie sa contina hx-target='#import-section' pentru ca HTMX sa actualizeze corect containerul din panoul de tab, nu din alta parte. """ r = client.get("/_import/reset") assert r.status_code == 200 text = r.text assert 'hx-target="#import-section"' in text, \ "hx-target='#import-section' nu a fost gasit in fragmentul de upload" # Wrapper-ul extern trebuie sa aiba id="import-section" assert 'id="import-section"' in text, \ "id='import-section' nu a fost gasit in fragmentul de upload" def test_import_forms_pastreaza_csrf(client): """Formularele de import contin csrf_token (input hidden cu valoare). Testeaza atat fragmentul de upload cat si cel de mapare coloane. """ # Fragment upload r_upload = client.get("/_import/reset") assert r_upload.status_code == 200 text_upload = r_upload.text # Trebuie sa contina campul csrf_token (poate fi gol in modul dev fara sesiune, # dar campul trebuie sa existe) assert 'name="csrf_token"' in text_upload, \ "name='csrf_token' nu a fost gasit in formularul de upload" # Fragment mapare coloane csv_bytes = _make_csv_bytes(_SAMPLE_ROWS) r_map = client.post( "/_import/upload", files={"file": ("test.csv", csv_bytes, "text/csv")}, ) assert r_map.status_code == 200 text_map = r_map.text if "mapare-coloane" in text_map: # s-a primit fragmentul de mapare assert 'name="csrf_token"' in text_map, \ "name='csrf_token' nu a fost gasit in formularul mapare-coloane" # --------------------------------------------------------------------------- # US-013 Teste: import colapsat + tokeni scala + pill-uri cu dot (PRD 5.16) # --------------------------------------------------------------------------- def test_import_colapsat_implicit(client): """Pe Acasa (first-run, fara trimiteri), sectiunea de import e deschisa implicit. La first-run (are_trimiteri=False),
trebuie sa aiba atributul `open`. Summary-ul trebuie sa contina textul slim 'Importa fisier' (bara colapsabila). Verifica si ca
este prezent pe pagina principala. """ r = client.get("/") assert r.status_code == 200 text = r.text # Elementul
trebuie sa fie prezent assert 'id="import-details"' in text, \ "Elementul
lipseste de pe pagina principala" # La first-run (nu exista trimiteri), details trebuie sa fie deschis (atribut open) assert 'id="import-details" open' in text, \ "La first-run,
trebuie sa aiba atributul 'open'" # Textul summary trebuie sa contina 'Importa fisier' (bara slim colapsabila) assert "Importa fisier" in text, \ "Textul 'Importa fisier' nu a fost gasit in summary-ul sectiunii de import" def test_wizard_foloseste_scala_tokeni(client): """Fragmentele wizard-ului de import folosesc tokeni var(--fs-*) in loc de px hardcodat. Verifica ca fragmentul de mapare coloane (_mapcoloane.html) si cel de upload (_upload.html) contin referinte la tokenii de scala --fs-* in inline styles, nu font-size hardcodat in px sub 12px. """ # Fragment upload (/_import/reset) → _upload.html r_upload = client.get("/_import/reset") assert r_upload.status_code == 200 upload_text = r_upload.text # Tokenii trebuie sa apara in inline styles assert "var(--fs-" in upload_text, \ "Tokenii var(--fs-*) nu au fost gasiti in fragmentul de upload (_upload.html)" # Fragment mapare coloane → _mapcoloane.html csv_bytes = _make_csv_bytes(_SAMPLE_ROWS) r_map = client.post( "/_import/upload", files={"file": ("test.csv", csv_bytes, "text/csv")}, ) assert r_map.status_code == 200 map_text = r_map.text # Mapcoloane trebuie sa contina tokeni assert "var(--fs-" in map_text, \ "Tokenii var(--fs-*) nu au fost gasiti in fragmentul mapare coloane (_mapcoloane.html)" # Verifica ca nu exista font-size sub 12px hardcodat in fragmentele wizard import re for fragment_text, fragment_name in [(upload_text, "upload"), (map_text, "mapcoloane")]: for size_str in re.findall(r'font-size:\s*(\d+)px', fragment_text): size = int(size_str) assert size >= 12, \ f"font-size:{size}px sub 12px gasit in fragmentul {fragment_name} — trebuie var(--fs-*)" def test_preview_stari_pill_dot(client): """Pill-urile de stare din preview contin un dot consistent cu designul 5.16. Verifica ca pill-urile din tabelul de preview si din rezumatul de stari contin un element dot (span cu border-radius:99px ca inline style), consistent cu stripul slim si cu designul 5.16 (dot + text, nu text gol). Eticheta umana: din STARI_PREVIEW ('Gata de trimis', 'Cod RAR lipsa' etc.) — nicio eticheta noua. """ _seed_op_mapping(client) import_id = _upload_and_get_import_id(client) text = _get_preview_via_mapare(client, import_id) # Preview trebuie sa fie prezent assert "confirm-form" in text or "Preview" in text, \ "Fragmentul de preview nu a fost randat" # Pill-urile de stare trebuie sa contina un dot (span cu border-radius:99px) assert "border-radius:99px" in text, \ "Dot-ul (border-radius:99px) nu a fost gasit in pill-urile de stare din preview" # Etichetele umane din STARI_PREVIEW trebuie sa fie prezente (nicio eticheta noua) # 'Gata de trimis' apare in rezumatul de stari (pill) sau in tabelul de randuri assert "Gata de trimis" in text or "Cod RAR lipsa" in text or "Verifica valori" in text, \ "Etichetele umane din STARI_PREVIEW nu au fost gasite in preview"