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>
This commit is contained in:
Claude Agent
2026-06-26 15:16:28 +00:00
parent 412102b9b1
commit 283299ff20
34 changed files with 3079 additions and 389 deletions

250
tests/test_import_commit.py Normal file
View File

@@ -0,0 +1,250 @@
"""Teste US-007 (PRD 5.11) — Post-commit: lista Trimiteri apare + refresh auto.
TDD RED: testele sunt scrise inainte de implementare.
Verifica:
1. Raspunsul confirma emite header HX-Trigger: trimiteriChanged.
2. Raspunsul confirma include OOB swap al #trimiteri-section cu submissions-wrap.
3. Prima vizita (first-run, zero trimiteri) randeaza placeholder #trimiteri-section in DOM.
4. Mesajul de succes este onest: contine numarul de prezentari puse in coada.
"""
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, "us007.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(client: TestClient, cod_prestatie: str = "R-FRANE", cod_op: str = "OP-FRANE") -> None:
"""Semeaza nomenclatorul si o mapare operatie->cod RAR (pentru randuri ok)."""
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_preview_si_commit(client: TestClient, rows: list[dict]) -> tuple[int, object]:
"""Parcurge fluxul web complet: upload -> mapare coloane -> confirma.
Intoarce (import_id, raspuns_confirma).
Presupune: nomenclatorul si maparea operatiei sunt deja semanate.
"""
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
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))
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
# GET preview pentru a afla n_ok — citit din atributul value al inputului #n-confirmat.
# (Regex generic pe "ok" ar putea prinde valori din CSS like min-height:36px -> 36.)
rp = client.get(f"/_import/{iid}/preview")
assert rp.status_code == 200, rp.text
m_ok = re.search(r'id="n-confirmat"[^>]*?value="(\d+)"', rp.text)
n_ok = int(m_ok.group(1)) if m_ok else len(rows)
r_conf = client.post(
f"/_import/{iid}/confirma",
data={
"csrf_token": "",
"n_confirmat": str(n_ok),
"confirmed_by": "test@us007.ro",
},
)
return iid, r_conf
# Date fixture: un singur rand ok
_ROWS_OK = [
{
"VIN": "WVWZZZ1KZAW007001",
"Nr": "B007TST",
"Data": "2026-06-15",
"KM": "77000",
"Operatie": "OP-FRANE",
},
]
# --------------------------------------------------------------------------- #
# Teste RED → GREEN #
# --------------------------------------------------------------------------- #
def test_confirma_emite_hx_trigger(client):
"""Raspunsul confirma include header HX-Trigger: trimiteriChanged.
Necesar pentru ca HTMX sa emita evenimentul pe <body>, pe care alte
elemente abonate (submissions-wrap) sa il prinda si sa se reimprospateze.
"""
_seed_nomenclator(client)
_, r = _upload_preview_si_commit(client, _ROWS_OK)
assert r.status_code == 200, r.text
hx_trigger = r.headers.get("HX-Trigger", "")
assert "trimiteriChanged" in hx_trigger, (
f"Header HX-Trigger lipseste sau nu contine 'trimiteriChanged'. "
f"Primit: {hx_trigger!r}"
)
def test_confirma_oob_trimiteri_section(client):
"""Raspunsul confirma include OOB swap al #trimiteri-section.
Elementul <section id='trimiteri-section'> apare in raspuns cu atributul
hx-swap-oob, astfel incat HTMX sa il injecteze in DOM fara reload complet.
La first-run, #trimiteri-section era absent din DOM (zero trimiteri anterior);
OOB-ul il populeaza si-l face vizibil.
"""
_seed_nomenclator(client)
_, r = _upload_preview_si_commit(client, _ROWS_OK)
assert r.status_code == 200, r.text
html = r.text
assert "hx-swap-oob" in html, (
"Atributul hx-swap-oob lipseste din raspuns — OOB swap nu e emis"
)
assert 'id="trimiteri-section"' in html or "id='trimiteri-section'" in html, (
"#trimiteri-section lipseste din raspuns OOB"
)
assert "submissions-wrap" in html, (
"#submissions-wrap lipseste din OOB — lista Trimiteri nu va aparea"
)
def test_confirma_mesaj_succes_onest(client):
"""Mesajul de succes mentioneaza numarul de prezentari puse in coada.
'Onest' inseamna ca mesajul reflecta exact numarul de randuri enqueue-uite,
nu o formulare vaga (ex. 'succes'). Permite utilizatorului sa verifice.
"""
_seed_nomenclator(client)
_, r = _upload_preview_si_commit(client, _ROWS_OK)
assert r.status_code == 200, r.text
html = r.text
# Mesajul trebuie sa contina cel putin numarul '1' (un rand ok in fixture)
# si sa indice ca randurile sunt "in coada" sau "prezentari".
assert re.search(r"\b1\b", html), "Numarul de prezentari (1) lipseste din raspuns"
# Cel putin unul din cuvintele cheie care indica succes de incarcare in coada
assert any(kw in html.lower() for kw in ("coada", "prezenta", "trimiter")), (
"Mesajul de succes nu contine cuvinte cheie despre enqueue (coada/prezentari/trimiteri)"
)
def test_commit_actualizeaza_status_bar(client):
"""Raspunsul confirma include OOB swap al #status-bar cu trigger trimiteriChanged.
Verifica doua lucruri:
1. #status-bar apare in raspunsul confirma cu hx-swap-oob (actualizare imediata
a contorului 'In asteptare' fara a astepta poll-ul de 15s).
2. Markup-ul #status-bar contine 'trimiteriChanged' in hx-trigger, deci la
urmatoarele evenimente trimiteriChanged (mapeaza, corectie etc.) bara se
re-incarca imediat, nu abia la 15s.
"""
_seed_nomenclator(client)
_, r = _upload_preview_si_commit(client, _ROWS_OK)
assert r.status_code == 200, r.text
html = r.text
assert 'id="status-bar"' in html or "id='status-bar'" in html, (
"#status-bar lipseste din raspuns — OOB swap nu va actualiza contoarele"
)
assert "hx-swap-oob" in html, (
"Atributul hx-swap-oob lipseste — cel putin un OOB swap trebuie emis"
)
assert "trimiteriChanged" in html, (
"trimiteriChanged lipseste din raspuns — #status-bar nu va reactiona "
"la eveniment si se va actualiza abia la urmatorul poll de 15s"
)
# Verifica direct ca fragmentul /_fragments/status contine trigger-ul in markup.
r_status = client.get("/_fragments/status")
assert r_status.status_code == 200, r_status.text
assert "trimiteriChanged" in r_status.text, (
"/_fragments/status nu contine 'trimiteriChanged' in hx-trigger — "
"bara nu va reactiona la evenimentul emis de confirma"
)
def test_acasa_placeholder_trimiteri_first_run(client):
"""GET / (zero trimiteri) randeaza elementul #trimiteri-section in DOM.
La first-run (niciun submission anterior), #trimiteri-section trebuia sa
existe in HTML ca placeholder gol/ascuns, astfel incat OOB swap-ul de la
confirma sa aiba tinta valida. Fara placeholder, HTMX ignora silentios OOB-ul
si lista Trimiteri nu apare dupa commit.
"""
r = client.get("/")
assert r.status_code == 200, r.text
html = r.text
assert 'id="trimiteri-section"' in html or "id='trimiteri-section'" in html, (
"#trimiteri-section lipseste din DOM la first-run — "
"OOB swap-ul de la confirma nu va gasi tinta si lista nu va aparea"
)