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>
284 lines
10 KiB
Python
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 -> '
|
|
# Deci [{'cod_op_service': ...}] devine [{'cod_op_service': ...}] in HTML.
|
|
# Verificam secventa specifica a repr-ului HTML-escapata:
|
|
assert "'cod_op_service'" not in html, (
|
|
"Repr Python HTML-escapata ('cod_op_service') 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"
|
|
)
|