Files
rar-autopass/tests/test_preview_import.py
Claude Agent 283299ff20 feat(ux): import compact + preview format Trimiteri + navigatie + scoatere auto_send (5.11)
8 stories TDD (echipa Sonnet, lead orchestreaza). US-001 scoate hold-ul auto_send din mapare
(has_no_auto_send->False, simbol pastrat; cod rezolvat->queued). US-002 scoate bifa auto_send
din UI. US-003 preview pas 3 in format .tabel-trimiteri (STARI_PREVIEW + nota_umana_preview,
fara repr Python; view-model prez). US-004 filtre layout/stil ca referinta + buton Custom.
US-005 navigatie Trimiteri/Mapari sub contoare pe toate paginile. US-006 import <details> nativ
colapsabil. US-007 post-commit reveal (OOB _coada/_status + HX-Trigger). US-008 auto-refresh
dupa actiuni (nudge eliminat).

VERIFY context curat PASS (8/8). /code-review high: 3 buguri reparate (tab nav la self-refresh,
pill Custom valori stale, nota_umana_preview precedenta needs_mapping). 934 passed, 1 skipped.
Backend trimitere + schema NEATINSE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:16:28 +00:00

284 lines
10 KiB
Python

"""Teste US-003 (PRD 5.11) — Preview pas 3 in format identic cu tabelul Trimiteri.
Proces TDD: aceste teste sunt scrise INAINTE de implementare (RED) si verifica:
1. Coloana Note nu afiseaza repr Python brut (lista/dict Python) pentru needs_mapping.
2. Starea afisata in pill-ul randului este eticheta umana (nu codul brut).
3. Tabelul de preview foloseste clasa CSS .tabel-trimiteri.
Fixture real cu rand needs_mapping OBLIGATORIU pentru test_preview_nu_contine_repr_python
(altfel testul trece in gol pe un preview fara erori in Note).
"""
from __future__ import annotations
import csv
import io
import os
import re
import tempfile
import pytest
from fastapi.testclient import TestClient
# --------------------------------------------------------------------------- #
# Fixture client cu DB izolat #
# --------------------------------------------------------------------------- #
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "preview_us003.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.crypto import reset_cache
reset_cache()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
reset_cache()
# --------------------------------------------------------------------------- #
# Utilitare #
# --------------------------------------------------------------------------- #
def _csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep)
writer.writeheader()
writer.writerows(rows)
return buf.getvalue().encode("utf-8")
def _seed_nomenclator_si_mapare(
cod_prestatie: str = "R-FRANE", cod_op: str = "OP-FRANE"
) -> None:
"""Semeaza nomenclatorul si o mapare pentru a produce randuri ok in preview."""
from app.db import get_connection
conn = get_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?,?)",
(cod_prestatie, "Reparatie frane"),
)
conn.execute(
"INSERT OR IGNORE INTO operations_mapping "
"(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1,?,?,1)",
(cod_op, cod_prestatie),
)
conn.commit()
finally:
conn.close()
def _upload_and_preview(client: TestClient, rows: list[dict]) -> tuple[int, str]:
"""Upload CSV + salveaza mapare coloane (daca lipseste) + GET preview.
Returneaza (import_id, html_preview).
"""
data = _csv_bytes(rows)
r = client.post(
"/_import/upload",
files={"file": ("test.csv", io.BytesIO(data), "text/csv")},
)
assert r.status_code == 200, r.text
# Extrage import_id din URL-urile prezente in raspuns
m = re.search(r"/_import/(\d+)/", r.text)
assert m, f"import_id negasit in raspunsul de upload: {r.text[:400]}"
iid = int(m.group(1))
# Daca s-a returnat formularul de mapare coloane, il salvam
if f"/_import/{iid}/mapare-coloane" in r.text:
r2 = client.post(
f"/_import/{iid}/mapare-coloane",
data={
"colname": ["VIN", "Nr", "Data", "KM", "Operatie"],
"canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"],
"format_data": "YYYY-MM-DD",
},
)
assert r2.status_code == 200, r2.text
r3 = client.get(f"/_import/{iid}/preview")
assert r3.status_code == 200, r3.text
return iid, r3.text
# --------------------------------------------------------------------------- #
# Date fixture #
# --------------------------------------------------------------------------- #
# Un rand cu operatie nemapata — produce starea needs_mapping cu errors=[{"unmapped":[...]}]
_ROWS_UNMAPPED = [
{
"VIN": "WVWZZZ1KZAW000001",
"Nr": "B001TST",
"Data": "2026-06-15",
"KM": "123456",
"Operatie": "OP-FARA-COD",
},
]
# Un rand ok (mapare existenta) + un rand unmapped
_ROWS_OK_SI_UNMAPPED = [
{
"VIN": "WVWZZZ1KZAW000001",
"Nr": "B001TST",
"Data": "2026-06-15",
"KM": "123456",
"Operatie": "OP-FRANE",
},
{
"VIN": "WVWZZZ1KZAW000002",
"Nr": "B002TST",
"Data": "2026-06-15",
"KM": "100000",
"Operatie": "OP-FARA-COD",
},
]
# Doua randuri identice — produc starea duplicate_in_file
_ROWS_DUPLICATE = [
{
"VIN": "WVWZZZ1KZAW000001",
"Nr": "B001TST",
"Data": "2026-06-15",
"KM": "123456",
"Operatie": "OP-FRANE",
},
{
"VIN": "WVWZZZ1KZAW000001",
"Nr": "B001TST",
"Data": "2026-06-15",
"KM": "123456",
"Operatie": "OP-FRANE",
},
]
# --------------------------------------------------------------------------- #
# Teste RED → GREEN #
# --------------------------------------------------------------------------- #
def test_preview_nu_contine_repr_python(client):
"""Coloana Note nu afiseaza repr Python brut pentru randuri needs_mapping.
Fixture real cu rand needs_mapping OBLIGATORIU — altfel testul ar trece in gol
pe un preview fara erori (coloana Note goala nu produce repr).
Repr Python apare in codul curent cand Jinja2 randeaza
``{{ e.values() | list | first }}`` unde valoarea e lista ``unmapped``:
[{'cod_op_service': 'OP-FARA-COD', 'denumire': 'OP-FARA-COD'}]
"""
_, html = _upload_and_preview(client, _ROWS_UNMAPPED)
# Confirma ca preview-ul contine cel putin un rand needs_mapping (fixture corect)
has_nm = (
"s-needs_mapping" in html
or "needs_mapping" in html
or "Cod RAR lipsa" in html
)
assert has_nm, "Testul necesita cel putin un rand needs_mapping — fixture gresit"
# Repr Python brut: Jinja2 auto-escapa ghilimelele simple -> &#39;
# Deci [{'cod_op_service': ...}] devine [{&#39;cod_op_service&#39;: ...}] in HTML.
# Verificam secventa specifica a repr-ului HTML-escapata:
assert "&#39;cod_op_service&#39;" not in html, (
"Repr Python HTML-escapata (&#39;cod_op_service&#39;) gasita in HTML — "
"adaptorul trebuie sa formateze erorile uman INAINTE de randare in Note"
)
def test_preview_stare_eticheta_umana(client):
"""Starea din pill-ul fiecarui rand este eticheta umana, nu codul brut.
Testeaza starile ok si needs_mapping (cele mai comune in preview).
Celelalte stari (needs_review, already_sent, duplicate_in_file) sunt in testele ext.
"""
_seed_nomenclator_si_mapare()
_, html = _upload_and_preview(client, _ROWS_OK_SI_UNMAPPED)
# Etichetele umane trebuie sa apara in HTML (din pill-urile randurilor)
# "Gata de trimis" cu majuscula — nu confundam cu "gata de trimis" din
# summary pills (deja prezente in codul curent cu lowercase)
assert "Gata de trimis" in html, (
"Eticheta umana 'Gata de trimis' lipsa — "
"pill-ul randului ok afiseaza inca codul brut"
)
assert "Cod RAR lipsa" in html, (
"Eticheta umana 'Cod RAR lipsa' lipsa — "
"pill-ul randului needs_mapping afiseaza inca codul brut"
)
# Codurile brute NU trebuie sa apara ca text vizibil al pill-ului de rand.
# Cautam <span class="pill ...">ok</span> (codul brut ca text al pill-ului).
assert re.search(r'class="pill[^"]*">ok<', html) is None, (
"Pill cu text brut 'ok' gasit in randuri — trebuie 'Gata de trimis'"
)
assert re.search(r'class="pill[^"]*">needs_mapping<', html) is None, (
"Pill cu text brut 'needs_mapping' gasit in randuri — trebuie 'Cod RAR lipsa'"
)
def test_preview_stare_eticheta_umana_duplicate(client):
"""Starea duplicate_in_file afiseaza eticheta umana 'Duplicat in fisier'."""
_seed_nomenclator_si_mapare()
_, html = _upload_and_preview(client, _ROWS_DUPLICATE)
# Confirma ca exista randuri duplicate_in_file
has_dup = "duplicate_in_file" in html or "Duplicat in fisier" in html
assert has_dup, "Testul necesita randuri duplicate_in_file — fixture gresit"
assert "Duplicat in fisier" in html, (
"Eticheta umana 'Duplicat in fisier' lipsa — "
"pill-ul randului duplicate_in_file afiseaza inca codul brut"
)
assert re.search(r'class="pill[^"]*">duplicate_in_file<', html) is None, (
"Pill cu text brut 'duplicate_in_file' gasit — trebuie 'Duplicat in fisier'"
)
def test_nota_umana_preview_needs_mapping_cu_flag_prioritizeaza_unmapped():
"""needs_mapping + flag -> Note afiseaza 'Cod RAR lipsa', nu textul flag-ului.
BUG 3: nota_umana_preview verifica `if flags:` inaintea ramurei unmapped.
Un rand needs_mapping care are si un flag (ex. VIN numeric) afisa textul
flag-ului in loc de 'Cod RAR lipsa pentru: COD' — exact confuzia US-003
voia s-o evite (userul corecteaza data si ramane blocat pe cod).
Fix: cand status == 'needs_mapping', prioritizeaza ramura unmapped.
"""
from app.web.labels import nota_umana_preview
errors = [{"unmapped": [{"cod_op_service": "OP-TEST", "denumire": "Op Test"}]}]
flags = ["VIN numeric (12345) — verificati seria sasiului"]
result = nota_umana_preview("needs_mapping", errors, flags)
assert "Cod RAR lipsa" in result, (
f"needs_mapping cu flag trebuie sa afiseze 'Cod RAR lipsa', "
f"nu textul flag-ului — primit: {result!r}"
)
assert "VIN numeric" not in result, (
f"needs_mapping cu flag NU trebuie sa afiseze textul flag-ului — primit: {result!r}"
)
def test_preview_foloseste_clasa_tabel_trimiteri(client):
"""Tabelul de preview foloseste clasa CSS .tabel-trimiteri (nu doar .tablewrap).
Necesara pentru:
- table-layout:fixed (fara overflow orizontal la 1280px)
- carduri <768px: td::before citeste data-eticheta
- col-* latimi pentru cele 4 coloane extra (col-km, col-note, col-verificat, col-actiuni)
"""
_, html = _upload_and_preview(client, _ROWS_UNMAPPED)
assert "tabel-trimiteri" in html, (
"Clasa CSS 'tabel-trimiteri' lipsa din tabelul de preview — "
"necesara pentru table-layout:fixed si carduri mobile"
)