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

View File

@@ -129,9 +129,10 @@ def test_badge_trimiteri_scoped_pe_acasa(client):
def test_trimiteri_fara_poll_periodic_pe_tabel(client):
"""Tabelul de trimiteri NU se mai reimprospateaza periodic: #submissions-wrap se
incarca la load / actiunile utilizatorului / Reincarca (nudge), fara `every Ns`.
Reimprospatarea live se face prin nudge-ul "Date noi" + endpointul de versiune."""
"""Tabelul de trimiteri NU se reimprospateaza periodic: #submissions-wrap se
incarca la load / actiunile utilizatorului (trimiteriChanged) / reincarcaTrimiteri.
Reimprospatarea automata la date noi externe se face prin pollerul de versiune
care cheama reincarcaTrimiteri() — fara nudge manual (US-008)."""
_seed_submission("sent")
r = client.get("/?tab=acasa")
html = r.text
@@ -141,6 +142,6 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
assert "reincarcaTrimiteri" in wrap
assert "trimiteriChanged" in wrap
# Mecanismul de nudge exista (banner + endpoint versiune).
assert 'id="nudge-trimiteri"' in html
# Pollerul de versiune exista (auto-refresh la date noi externe). Nudge eliminat (US-008).
assert "/_fragments/trimiteri-versiune" in html
assert 'id="nudge-trimiteri"' not in html

View File

@@ -1,11 +1,8 @@
"""Teste US-007 (PRD 3.6): bifa "auto-send" devine un comutator cu doua stari,
etichetat pe COADA (nu pe trimitere).
"""Teste US-007 (PRD 3.6) actualizate dupa US-002 (PRD 5.11).
Framing decis la poarta autoplan (UC-A): "Pune automat in coada" / "Tine pentru
verificare". NU "Automat/Manual" (risc de send-safety peste declaratii ireversibile).
`name="auto_send"` pastrat cu semantica de prezenta (checkbox value="true"):
bifat -> auto_send True, nebifat -> absent -> False. Zero atingere backend, identic
cu ambele parsere existente (`Form(bool)` la /mapari si `bool(form.get())` la preview).
US-002: macro autosend_toggle neutralizat (intoarce string gol).
Checkbox-ul name=auto_send a fost scos din UI. Coloanele DB raman.
Testele de UI verifica ABSENTA toggle-ului; testele de backend (stocare DB) raman.
"""
from __future__ import annotations
@@ -94,38 +91,34 @@ def client(monkeypatch):
def _macro_html(checked: bool = True, form_id: str = "") -> str:
"""Randeaza direct macro-ul comutatorului, izolat de restul paginii."""
"""Randeaza direct macro-ul, izolat de restul paginii."""
from app.web.routes import templates
mod = templates.env.get_template("_macros.html").make_module({})
return str(mod.autosend_toggle(form_id=form_id, checked=checked))
# --- markup / copy ---
# --- markup: macro neutralizat dupa US-002 ---
def test_comutator_coada_prezent():
"""5.5 (supersede framing 3.6): comutator etichetat Auto/Manual, compact.
Send-safety pastrata prin tooltip/Ajutor (Manual = tine pentru verificare; nimic nu
pleaca la RAR pana confirmi). Semantica de prezenta name=auto_send nealterata."""
"""US-002: macro autosend_toggle neutralizat -> output gol (fara checkbox)."""
html = _macro_html()
assert "name=\"auto_send\"" in html and 'value="true"' in html
assert "Auto" in html and "Manual" in html, "ambele stari etichetate"
assert "verificare" in html, "sensul de verificare manuala trebuie pastrat (tooltip/ajutor)"
assert "trimite" not in html.lower(), "fara cuvantul 'trimite' izolat in eticheta"
assert "auto-send" not in html, "jargonul 'auto-send' trebuie inlocuit"
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din UI"
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
def test_eticheta_scoped_pe_operatie():
"""Microcopy scoped pe operatie (NU global)."""
"""US-002: macro neutralizat -> nicio eticheta scoped."""
html = _macro_html()
assert "aceasta operatie" in html
assert "aceasta operatie" not in html
assert html.strip() == ""
def test_default_pune_automat():
"""Default = "Pune automat in coada" (mirror la checkbox-ul `checked` de azi)."""
"""US-002: macro neutralizat intoarce gol indiferent de parametrul checked."""
html_default = _macro_html(checked=True)
assert "checked" in html_default
assert html_default.strip() == ""
html_off = _macro_html(checked=False)
assert "checked" not in html_off, "starea stocata False nu trebuie bifata (H4)"
assert html_off.strip() == ""
# --- comportament (zero atingere backend) ---
@@ -161,7 +154,7 @@ def test_tine_pentru_verificare_mapeaza_auto_send_false(client):
# --- prezent in AMBELE locuri (mapari tab + panou preview) ---
def test_comutator_in_tab_mapari(client):
"""Tabul Mapari (de-rezolvat) foloseste comutatorul de coada, nu jargonul vechi."""
"""US-002: tabul Mapari nu mai contine checkbox auto_send (macro neutralizat)."""
from app.db import get_connection
import json
acct = _create_account_user("tm@test.com")
@@ -179,13 +172,12 @@ def test_comutator_in_tab_mapari(client):
_login(client, "tm@test.com")
resp = client.get("/?tab=mapari")
assert resp.status_code == 200
# 5.5: comutatorul compact Auto/Manual e prezent in tabul Mapari
assert 'name="auto_send"' in resp.text
assert "Manual" in resp.text and "Auto" in resp.text
assert 'name="auto_send"' not in resp.text, "US-002: checkbox auto_send scos din UI"
assert "In coada" not in resp.text, "US-002: coloana 'In coada' scoasa"
def test_comutator_in_panou_preview(client):
"""Panoul de mapare din preview are si el comutatorul + caption (azi lipsea caption)."""
"""US-002: panoul de mapare din preview nu mai contine checkbox auto_send."""
_create_account_user("pp@test.com")
_seed_nomenclator("R-FRANE")
_login(client, "pp@test.com")
@@ -211,6 +203,5 @@ def test_comutator_in_panou_preview(client):
})
assert r.status_code == 200
assert "OP-NEMAPAT" in r.text, "operatia nemapata trebuie sa apara in panoul de mapare"
# 5.5: comutatorul compact Auto/Manual e prezent si in panoul de mapare din preview
assert 'name="auto_send"' in r.text
assert "Manual" in r.text and "Auto" in r.text
assert 'name="auto_send"' not in r.text, "US-002: checkbox auto_send scos din preview"
assert "In coada automat" not in r.text, "US-002: eticheta 'In coada automat' scoasa"

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"
)

View File

@@ -150,7 +150,7 @@ def test_mapari_de_rezolvat_in_tabel(client):
def test_mapari_salvate_in_tabel(client):
"""Sectiunea "Operatii salvate" randata ca tabel; H4: auto-send reflecta valoarea STOCATA."""
"""Sectiunea "Operatii salvate" randata ca tabel. US-002: fara coloana auto_send."""
acct = _create_account_user("salv@test.com")
_seed_nomenclator("R-FRANE", "Reparatie frane")
_seed_nomenclator("R-MOTOR", "Reparatie motor")
@@ -165,9 +165,9 @@ def test_mapari_salvate_in_tabel(client):
assert "tablewrap" in sec, "tabelul Operatii salvate trebuie sa foloseasca .tablewrap"
assert "<table" in sec and "<th" in sec, "Operatii salvate trebuie randat ca tabel cu antet"
assert "OP-AUTO" in sec and "OP-MANUAL" in sec
assert 'name="auto_send"' in sec
# H4: exact maparile cu auto_send STOCAT True sunt bifate (aici: o singura)
assert sec.count("checked") == 1, "comutatorul auto-send trebuie sa reflecte valoarea stocata, nu un default"
# US-002: checkbox auto_send si coloana In coada scoase din UI
assert 'name="auto_send"' not in sec, "US-002: checkbox auto_send scos din tabelul salvate"
assert "In coada" not in sec, "US-002: coloana In coada scoasa din tabelul salvate"
# POST-urile neschimbate
assert 'hx-post="/mapari/salvate"' in sec
assert 'hx-post="/mapari/salvate/sterge"' in sec

View File

@@ -380,9 +380,12 @@ def test_unmapped_are_3niveluri(client):
assert err["fix"]
def test_auto_send_oprit_3niveluri(client):
"""Mapare cu auto_send=0 -> needs_mapping; rar_error are cheie 'auto_send'
PASTRATA + campurile AUTO_SEND_OPRIT (cod/problema/cauza/fix)."""
def test_auto_send_oprit_3niveluri_noul_comportament(client):
"""Mapare cu auto_send=0 -> queued (auto_send ignorat dupa US-001).
Dupa US-001: classify_prezentare nu mai produce ramura AUTO_SEND_OPRIT.
O operatie cu cod rezolvat (indiferent de auto_send) -> queued direct.
"""
import json
from app.mapping import classify_prezentare
@@ -396,15 +399,9 @@ def test_auto_send_oprit_3niveluri(client):
mapping = {"OP_REVIEW": "OE-1"}
mapping_meta = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
res = classify_prezentare(content, mapping, mapping_meta)
assert res["status"] == "needs_mapping"
err = json.loads(res["rar_error"])
# Cheia originala pastrata
assert "auto_send" in err
# 3 niveluri prezente
assert err["cod"] == "AUTO_SEND_OPRIT"
assert err["problema"]
assert err["cauza"]
assert err["fix"]
assert res["status"] == "queued", (
f"dupa US-001 auto_send=0 -> queued (nu needs_mapping), got {res['status']}"
)
def test_needs_data_pass_through(client):
@@ -431,3 +428,153 @@ def test_needs_data_pass_through(client):
assert "cod" in e, f"lipseste 'cod' in {e}"
assert "problema" in e, f"lipseste 'problema' in {e}"
assert "fix" in e, f"lipseste 'fix' in {e}"
# =========================================================================== #
# US-001: Scoate hold auto_send din mapare — teste RED (inainte de implementare)
# =========================================================================== #
@pytest.fixture()
def db_conn(monkeypatch, tmp_path):
"""Conexiune directa la o DB temporara, fara client HTTP."""
db_path = str(tmp_path / "us001.db")
monkeypatch.setenv("AUTOPASS_DB_PATH", db_path)
from app.config import get_settings
get_settings.cache_clear()
from app.db import init_db
init_db()
from app.db import get_connection
c = get_connection()
yield c
c.close()
get_settings.cache_clear()
def test_operatie_mapata_intra_in_queued_indiferent_de_autosend():
"""classify_prezentare cu mapare auto_send=0 -> queued (nu needs_mapping).
Dupa US-001: has_no_auto_send nu mai blocheaza; un cod rezolvat e direct queued.
"""
from app.mapping import classify_prezentare
content = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_op_service": "OP_REVIEW", "denumire": "Operatie cu review"}],
}
mapping = {"OP_REVIEW": "OE-1"}
mapping_meta = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
valid_codes = {"OE-1"}
result = classify_prezentare(content, mapping, mapping_meta, valid_codes)
assert result["status"] == "queued", (
f"asteptat queued (auto_send ignorat), got {result['status']}: {result.get('rar_error')}"
)
def test_regula_text_rezolvata_nu_mai_tine_randul():
"""Regula text cu auto_send=0 rezolva codul -> queued (nu needs_mapping held).
Dupa US-001: regula_fara_autosend nu se mai seteaza; codul rezolvat = queued direct.
"""
from app.mapping import classify_prezentare
content = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_op_service": "X99", "denumire": "Verificare faruri"}],
}
text_rules = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}]
valid_codes = {"OE-2"}
result = classify_prezentare(content, {}, {}, valid_codes, text_rules)
assert result["status"] == "queued", (
f"asteptat queued (regula text, auto_send ignorat), got {result['status']}: {result.get('rar_error')}"
)
def test_fara_stare_needs_mapping_pe_auto_send_oprit():
"""has_no_auto_send intotdeauna False dupa US-001; nu mai produce AUTO_SEND_OPRIT."""
from app.mapping import has_no_auto_send
mapping_meta_false = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
resolved = [{"cod_op_service": "OP_REVIEW", "cod_prestatie": "OE-1"}]
assert has_no_auto_send(resolved, mapping_meta_false) is False, (
"has_no_auto_send trebuie sa intoarca mereu False dupa US-001"
)
resolved_cu_flag = [{"cod_op_service": "X", "cod_prestatie": "OE-1", "regula_fara_autosend": True}]
assert has_no_auto_send(resolved_cu_flag, {}) is False, (
"has_no_auto_send ignora regula_fara_autosend dupa US-001"
)
def test_niciun_rand_existent_nu_se_dezgheata(db_conn):
"""Randuri legacy needs_mapping-din-auto_send: fara afordanta UI (cod prezent),
dezghetabile via reresolve_account explicit (nu automat).
_nemapate_pentru_submission -> [] (fara panou mapare in UI).
reresolve_account cu mapare activa -> requeued=1 (dezghet via actiune explicita).
"""
import json as _json
payload = {
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1",
"data_prestatie": "2026-06-15", "odometru_final": "123456",
"prestatii": [{"cod_op_service": "X1", "cod_prestatie": "OE-1"}],
}
rar_error = _json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual"})
db_conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error) "
"VALUES (?, ?, ?, ?, ?)",
("k-legacy-us001", 1, "needs_mapping", _json.dumps(payload), rar_error),
)
db_conn.execute(
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES ('OE-1', 'Test')"
)
db_conn.execute(
"INSERT OR REPLACE INTO operations_mapping "
"(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1, 'X1', 'OE-1', 0)"
)
db_conn.commit()
row = db_conn.execute(
"SELECT * FROM submissions WHERE idempotency_key='k-legacy-us001'"
).fetchone()
nomenclator = [{"cod_prestatie": "OE-1", "nume_prestatie": "Test"}]
# Nicio afordanta UI (cod deja prezent -> nu se arata panoul de mapare)
from app.web.routes import _nemapate_pentru_submission
assert _nemapate_pentru_submission(row, nomenclator) == [], (
"_nemapate_pentru_submission trebuie sa intoarca [] (cod deja prezent)"
)
# Dezghetare via reresolve_account explicit (actiune admin la deploy)
from app.mapping import reresolve_account
stats = reresolve_account(db_conn, 1)
assert stats["requeued"] == 1, (
f"reresolve_account trebuie sa requeueze randul legacy: {stats}"
)
row2 = db_conn.execute(
"SELECT status FROM submissions WHERE idempotency_key='k-legacy-us001'"
).fetchone()
assert row2["status"] == "queued"
def test_canal_api_auto_send_ignorat_intra_queued():
"""classify_prezentare (canal API) cu mapping_meta auto_send=0 -> queued.
Campul auto_send din mapping_meta nu mai afecteaza decizia de clasificare.
"""
from app.mapping import classify_prezentare
content = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_op_service": "ITP-CHECK", "denumire": "Inspectie"}],
}
mapping = {"ITP-CHECK": "OE-1"}
mapping_meta = {"ITP-CHECK": {"cod_prestatie": "OE-1", "auto_send": False}}
valid_codes = {"OE-1"}
result = classify_prezentare(content, mapping, mapping_meta, valid_codes)
assert result["status"] == "queued", (
f"canal API: auto_send ignorat -> asteptat queued, got {result['status']}"
)

View File

@@ -0,0 +1,283 @@
"""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"
)

View File

@@ -1,8 +1,18 @@
"""Teste T6: gate auto_send pe coduri nou-mapate (OV-1).
"""Teste T6: comportament dupa US-001 (PRD 5.11) — auto_send nu mai tine randuri.
Verify:
(a) cod nou-mapat cu auto_send=0 -> nu auto-send, review manual.
(b) REGRESIE: mapare existenta cu auto_send=1 tot se requeue ca azi.
US-001: has_no_auto_send neutralizat (return False); un cod rezolvat (mapare exacta
sau regula text) -> queued direct, indiferent de auto_send=0/1 in mapping_meta.
Coloanele DB operations_mapping.auto_send si operation_text_rules.auto_send RAMAN
(default 1, ne-citite pentru hold). Functia has_no_auto_send RAMANE DEFINITA (importata
in routes.py + import_router.py) dar intoarce mereu False.
Inainte de US-001:
(a) cod nou-mapat cu auto_send=0 -> nu auto-send, review manual (needs_mapping).
(b) mapare existenta cu auto_send=1 -> queued.
Dupa US-001:
(a) cod nou-mapat cu auto_send=0 -> queued (ca si (b)).
(b) mapare existenta cu auto_send=1 -> queued (neschimbat).
"""
from __future__ import annotations
@@ -70,6 +80,7 @@ def _add_mapping(conn, account_id=1, cod_op="ITP-CHECK", cod_prestatie="OE-1", a
# --- load_mapping_meta ---
def test_load_mapping_meta_returns_auto_send(conn):
"""load_mapping_meta expune coloana auto_send din DB (ramane, default 1)."""
from app.mapping import load_mapping_meta
_add_mapping(conn, cod_op="ITP-1", cod_prestatie="OE-1", auto_send=True)
_add_mapping(conn, cod_op="ITP-2", cod_prestatie="OE-2", auto_send=False)
@@ -79,18 +90,20 @@ def test_load_mapping_meta_returns_auto_send(conn):
assert meta["ITP-2"]["auto_send"] is False
# --- has_no_auto_send ---
# --- has_no_auto_send (neutralizat, mereu False) ---
def test_has_no_auto_send_detecteaza_false(conn):
def test_has_no_auto_send_mereu_false_cu_auto_send_fals(conn):
"""has_no_auto_send intoarce False chiar si cand auto_send=False in mapping_meta (US-001)."""
from app.mapping import has_no_auto_send
mapping_meta = {
"ITP-1": {"cod_prestatie": "OE-1", "auto_send": False},
}
resolved = [{"cod_op_service": "ITP-1", "cod_prestatie": "OE-1"}]
assert has_no_auto_send(resolved, mapping_meta) is True
assert has_no_auto_send(resolved, mapping_meta) is False
def test_has_no_auto_send_trece_cu_true(conn):
def test_has_no_auto_send_mereu_false_cu_auto_send_true(conn):
"""has_no_auto_send intoarce False si cand auto_send=True (neschimbat, mereu False)."""
from app.mapping import has_no_auto_send
mapping_meta = {
"ITP-1": {"cod_prestatie": "OE-1", "auto_send": True},
@@ -100,46 +113,44 @@ def test_has_no_auto_send_trece_cu_true(conn):
def test_has_no_auto_send_direct_cod_prestatie(conn):
"""Item cu cod_prestatie direct (fara cod_op_service) nu e afectat de auto_send."""
"""Item cu cod_prestatie direct (fara cod_op_service) -> False (neschimbat)."""
from app.mapping import has_no_auto_send
mapping_meta = {}
resolved = [{"cod_prestatie": "OE-1"}]
assert has_no_auto_send(resolved, mapping_meta) is False
# --- reresolve_account cu auto_send=0 ---
# --- reresolve_account cu auto_send=0 (US-001: acum requeue) ---
def test_reresolve_auto_send_zero_nu_requeue(conn):
"""(a) cod nou-mapat cu auto_send=0 -> ramane needs_mapping (nu trece pe queued)."""
def test_reresolve_auto_send_zero_acum_requeue(conn):
"""(US-001) cod nou-mapat cu auto_send=0 -> queued (nu mai review_manual)."""
from app.mapping import reresolve_account
sid = _insert_needs_mapping(conn, cod_op="ITP-CHECK")
_add_mapping(conn, cod_op="ITP-CHECK", cod_prestatie="OE-1", auto_send=False)
stats = reresolve_account(conn, 1)
assert stats["review_manual"] == 1
assert stats["requeued"] == 0
assert stats["requeued"] == 1, f"asteptat requeued=1, got {stats}"
assert stats.get("review_manual", 0) == 0
row = conn.execute("SELECT status, rar_error FROM submissions WHERE id=?", (sid,)).fetchone()
assert row["status"] == "needs_mapping"
err = json.loads(row["rar_error"])
assert "auto_send" in err
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
assert row["status"] == "queued"
def test_reresolve_auto_send_unu_requeue(conn):
"""(b) REGRESIE: mapare cu auto_send=1 tot se requeue ca azi."""
"""REGRESIE: mapare cu auto_send=1 -> queued (neschimbat)."""
from app.mapping import reresolve_account
sid = _insert_needs_mapping(conn, cod_op="ITP-CHECK")
_add_mapping(conn, cod_op="ITP-CHECK", cod_prestatie="OE-1", auto_send=True)
stats = reresolve_account(conn, 1)
assert stats["requeued"] == 1
assert stats["review_manual"] == 0
assert stats.get("review_manual", 0) == 0
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
assert row["status"] == "queued"
# --- POST /v1/prezentari cu auto_send=0 ---
# --- POST /v1/prezentari cu auto_send=0 (US-001: acum queued) ---
def _body_with_op(cod_op="ITP-CHECK"):
return {
@@ -154,8 +165,8 @@ def _body_with_op(cod_op="ITP-CHECK"):
}
def test_post_auto_send_zero_nu_queued(client, env):
"""(a) Via API: cod nou-mapat cu auto_send=0 -> nu 'queued', review manual."""
def test_post_auto_send_zero_acum_queued(client, env):
"""(US-001) Via API: cod nou-mapat cu auto_send=0 -> queued (nu mai needs_mapping)."""
from app.db import get_connection
conn2 = get_connection()
try:
@@ -166,12 +177,11 @@ def test_post_auto_send_zero_nu_queued(client, env):
r = client.post("/v1/prezentari", json=_body_with_op("ITP-X"))
assert r.status_code == 200
status = r.json()["results"][0]["status"]
assert status != "queued", f"auto_send=0 nu trebuie sa fie queued, e: {status}"
assert status == "needs_mapping"
assert status == "queued", f"auto_send=0 dupa US-001 -> queued, e: {status}"
def test_post_auto_send_unu_queued(client, env):
"""(b) REGRESIE: mapare existenta cu auto_send=1 -> queued ca azi."""
"""REGRESIE: mapare existenta cu auto_send=1 -> queued ca inainte."""
from app.db import get_connection
conn2 = get_connection()
try:

View File

@@ -1,9 +1,10 @@
"""FIX (code-review 5.8): o regula text cu auto_send=0 (DEFAULT, decizia CEO) trebuie
sa TINA randul pentru verificare umana (needs_mapping/review), NU sa-l trimita automat.
"""Teste reguli text + auto_send dupa US-001 (PRD 5.11).
`has_no_auto_send` trebuie sa prinda si itemii rezolvati-prin-regula-text cu auto_send=0,
nu doar maparile exacte din operations_mapping. Adnotarile (cod_sursa/regula_fara_autosend)
trebuie curatate la fiecare rezolvare (anti-staleness).
Inainte de US-001: o regula text cu auto_send=0 tinea randul in needs_mapping
(regula_fara_autosend=True + has_no_auto_send -> True).
Dupa US-001: has_no_auto_send mereu False; o regula text cu auto_send=0 rezolva
codul RAR si randul intra in queued direct. regula_fara_autosend nu se mai seteaza.
Functii pure -> teste fara DB.
"""
@@ -26,34 +27,37 @@ def _content_cu(op_denumire="Verificare faruri"):
return {**_CONTENT, "prestatii": [{"cod_op_service": "X99", "denumire": op_denumire}]}
def test_regula_auto_send_0_tine_randul():
"""Regula text auto_send=0 + continut valid -> needs_mapping (review), NU queued."""
def test_regula_auto_send_0_acum_queued():
"""Regula text auto_send=0 + continut valid -> queued (nu mai needs_mapping) dupa US-001."""
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}]
cl = classify_prezentare(_content_cu(), {}, {}, VALID, tr)
assert cl["status"] == "needs_mapping", f"asteptat needs_mapping (held), got {cl['status']}"
assert cl["status"] == "queued", f"asteptat queued (auto_send ignorat), got {cl['status']}"
def test_regula_auto_send_1_trece_in_coada():
"""Regula text auto_send=1 + continut valid -> queued (trimite automat)."""
"""Regula text auto_send=1 + continut valid -> queued (neschimbat)."""
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 1, "priority": 0}]
cl = classify_prezentare(_content_cu(), {}, {}, VALID, tr)
assert cl["status"] == "queued", f"asteptat queued, got {cl['status']}"
def test_has_no_auto_send_prinde_flagul_regula():
"""has_no_auto_send=True cand un item poarta regula_fara_autosend; codul e tot rezolvat."""
def test_has_no_auto_send_mereu_false_cu_flag_regula():
"""has_no_auto_send=False chiar daca regula_fara_autosend e prezent (US-001)."""
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}]
resolved, unmapped = resolve_prestatii(
[{"cod_op_service": "X99", "denumire": "Verificare faruri"}], {}, VALID, tr
)
assert unmapped == []
assert resolved[0]["cod_prestatie"] == "OE-2"
assert resolved[0].get("regula_fara_autosend") is True
assert has_no_auto_send(resolved, {}) is True
# Flagul regula_fara_autosend nu se mai seteaza (US-001)
assert resolved[0].get("regula_fara_autosend") is None, (
"regula_fara_autosend nu trebuie sa mai fie setat dupa US-001"
)
assert has_no_auto_send(resolved, {}) is False
def test_has_no_auto_send_fals_cand_regula_auto_send_1():
"""Regula auto_send=1 -> fara flag -> has_no_auto_send False."""
"""Regula auto_send=1 -> fara flag -> has_no_auto_send False (neschimbat)."""
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 1, "priority": 0}]
resolved, _ = resolve_prestatii(
[{"cod_op_service": "X99", "denumire": "Verificare faruri"}], {}, VALID, tr
@@ -64,14 +68,13 @@ def test_has_no_auto_send_fals_cand_regula_auto_send_1():
def test_adnotari_stale_curatate_la_mapare_exacta():
"""Un item venit cu cod_sursa/regula_fara_autosend stale dar re-rezolvat acum prin
mapare EXACTA cu auto_send=1 -> adnotarile sunt curatate; randul NU mai e tinut."""
mapare EXACTA -> adnotarile sunt curatate; randul NU mai e tinut (neschimbat)."""
item_stale = {
"cod_op_service": "X99",
"denumire": "Verificare faruri",
"cod_sursa": "text_rule:verificare",
"regula_fara_autosend": True,
}
# Acum X99 are mapare exacta cu auto_send=1.
mapping = {"X99": "OE-2"}
mapping_meta = {"X99": {"cod_prestatie": "OE-2", "auto_send": True}}
resolved, _ = resolve_prestatii([item_stale], mapping, VALID, text_rules=None)

130
tests/test_web_acasa.py Normal file
View File

@@ -0,0 +1,130 @@
"""Teste US-006 (PRD 5.11): Import = container compact colapsabil.
TDD — testele sunt scrise INAINTE de implementare (RED), apoi devin GREEN.
Verifica:
- Containerul de import e colapsat implicit cand exista trimiteri (are_trimiteri=True)
- Containerul de import e deschis la first-run (are_trimiteri=False)
- Implementare cu <details> nativ (CSS-only disclosure)
"""
from __future__ import annotations
import json
import os
import re
import tempfile
import pytest
# ============================================================
# Fixture
# ============================================================
@pytest.fixture()
def client(monkeypatch):
"""Client cu BD izolata; auth dezactivat (cont implicit id=1)."""
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "acasa_test.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()
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 _add_submission(acct_id: int = 1) -> None:
"""Adauga un submission minimal pentru cont (simuleaza un import efectuat)."""
from app.db import get_connection
conn = get_connection()
try:
conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
"VALUES (?, ?, 'queued', ?)",
(f"test_key_us006_{acct_id}", acct_id, json.dumps({"test": True})),
)
finally:
conn.close()
def _detalii_import(html: str) -> re.Match | None:
"""Returneaza match-ul pentru tagul <details> care contine containerul de import."""
# Cauta <details cu id="import-details" (sau variante)
m = re.search(r'<details([^>]*)id=["\']import-details["\']([^>]*)>', html)
if m:
return m
# Fallback: primul <details> care nu e .kebab (e containerul de import)
m2 = re.search(r'<details(?!.*class=["\'][^"\']*kebab)([^>]*)>', html)
return m2
# ============================================================
# test_import_colapsat_cand_are_trimiteri
# ============================================================
def test_import_colapsat_cand_are_trimiteri(client):
"""Cand contul are deja trimiteri, containerul de import e colapsat (fara atribut open).
Serverul NU seteaza atributul `open` pe <details> cand are_trimiteri=True,
deci browserul il randeaza colapsat implicit.
"""
_add_submission(acct_id=1)
resp = client.get("/_fragments/acasa")
assert resp.status_code == 200
html = resp.text
# Containerul de import trebuie sa existe ca element <details>
m = _detalii_import(html)
assert m is not None, (
"Nu s-a gasit un element <details> pentru containerul de import. "
"Implementati containerul cu <details id='import-details'>."
)
# Atributul `open` NU trebuie sa fie prezent pe <details> cand exista trimiteri
tag = m.group(0)
assert "open" not in tag.lower(), (
f"<details> are atributul 'open' cand sunt trimiteri existente — trebuie colapsat: {tag}"
)
# ============================================================
# test_import_deschis_la_first_run
# ============================================================
def test_import_deschis_la_first_run(client):
"""La first-run (zero trimiteri), containerul de import e deschis (atribut open prezent).
Serverul seteaza `open` pe <details> cand are_trimiteri=False, deci importul
e vizibil imediat fara JS.
"""
# Nu adaugam niciun submission — cont proaspat
resp = client.get("/_fragments/acasa")
assert resp.status_code == 200
html = resp.text
# Containerul de import trebuie sa existe ca element <details>
m = _detalii_import(html)
assert m is not None, (
"Nu s-a gasit un element <details> pentru containerul de import. "
"Implementati containerul cu <details id='import-details'>."
)
# Atributul `open` TREBUIE sa fie prezent pe <details> la first-run
tag = m.group(0)
assert "open" in tag.lower(), (
f"<details> NU are atributul 'open' la first-run — trebuie deschis: {tag}"
)

286
tests/test_web_filtre.py Normal file
View File

@@ -0,0 +1,286 @@
"""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'<input[^>]+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"
)

160
tests/test_web_mapari.py Normal file
View File

@@ -0,0 +1,160 @@
"""Teste US-002 (PRD 5.11) — scoate bifa auto_send din UI.
Verifica ca bifa "In coada automat" (macro autosend_toggle) nu mai apare in:
- tab-ul Mapari (/_fragments/mapari)
- panoul de mapare din preview (pas 3 import)
- panoul de detaliu trimitere (_trimitere_detaliu.html)
"""
from __future__ import annotations
import csv
import io
import json
import os
import re
import tempfile
import pytest
from fastapi.testclient import TestClient
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us002.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
@pytest.fixture()
def conn(client):
from app.db import get_connection
c = get_connection()
yield c
c.close()
def _csv_bytes(rows: list[dict]) -> bytes:
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=";")
writer.writeheader()
writer.writerows(rows)
return buf.getvalue().encode("utf-8")
def _seed_nomenclator(conn, cod="OE-1"):
conn.execute(
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
(cod, f"Operatie {cod}"),
)
conn.execute(
"INSERT OR IGNORE INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) "
"VALUES (1, 'OP-EXISTENT', ?, 1)",
(cod,),
)
conn.commit()
def test_mapari_fara_toggle_autosend(client, conn):
"""GET /_fragments/mapari nu contine checkbox auto_send sau coloana 'In coada'.
Dupa US-002: macro autosend_toggle golit; coloana In coada scoasa din toate tabelele.
"""
_seed_nomenclator(conn)
r = client.get("/_fragments/mapari")
assert r.status_code == 200
body = r.text
assert 'name="auto_send"' not in body, (
"tab-ul Mapari nu trebuie sa contina input auto_send dupa US-002"
)
assert "autosend-toggle" not in body, (
"tab-ul Mapari nu trebuie sa contina clasa autosend-toggle dupa US-002"
)
assert "In coada" not in body, (
"tab-ul Mapari nu trebuie sa contina coloana 'In coada' dupa US-002"
)
def test_preview_panou_mapare_fara_autosend(client):
"""Panoul de mapare din preview (pas 3) nu contine checkbox auto_send.
Dupa US-002: macro autosend_toggle golit din _preview_import.html.
"""
# Upload CSV cu operatie nemapata -> map coloane -> ajunge la preview
rows = [
{"VIN": "WVWZZZ1KZAW000123", "Nr inmatriculare": "B001TST",
"Data prestatie": "15.06.2026", "Odometru final": "123456",
"Operatie": "OP-NEMAPATA-US002"},
]
data = _csv_bytes(rows)
r = client.post("/_import/upload", files={"file": ("test.csv", data, "text/csv")})
assert r.status_code == 200
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
assert m, f"Nu am gasit import_id in raspuns: {r.text[:300]}"
import_id = int(m.group(1))
# Salveaza maparea coloanelor -> genereaza 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
# Obtine preview-ul HTML (ar trebui sa aiba panoul de mapare cu OP-NEMAPATA-US002)
r2 = client.get(f"/_import/{import_id}/preview")
assert r2.status_code == 200
body = r2.text
# Panoul de mapare apare (operatia nemapata e prezenta)
assert "OP-NEMAPATA-US002" in body, "preview-ul trebuie sa contina operatia nemapata"
# Fara bifa auto_send
assert 'name="auto_send"' not in body, (
"preview-ul nu trebuie sa contina input auto_send dupa US-002"
)
assert "autosend-toggle" not in body, (
"preview-ul nu trebuie sa contina clasa autosend-toggle dupa US-002"
)
assert "In coada automat" not in body, (
"preview-ul nu trebuie sa contina 'In coada automat' dupa US-002"
)
def test_detaliu_mapare_inline_fara_autosend(client, conn):
"""GET /_fragments/trimitere/{id} cu needs_mapping nu contine checkbox auto_send.
Dupa US-002: macro autosend_toggle golit din _trimitere_detaliu.html.
"""
conn.execute(
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES ('OE-1', 'Test')"
)
payload = {
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1",
"data_prestatie": "2026-06-15", "odometru_final": "123456",
"prestatii": [{"cod_op_service": "ITP-US002", "denumire": "Inspectie",
"cod_prestatie": None}],
}
sid = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error) "
"VALUES (?, ?, ?, ?, ?)",
("k-us002-det", 1, "needs_mapping", json.dumps(payload),
json.dumps({"unmapped": [{"cod_op_service": "ITP-US002"}]})),
).lastrowid
conn.commit()
r = client.get(f"/_fragments/trimitere/{sid}")
assert r.status_code == 200
body = r.text
# Operatia nemapata e afisata
assert "ITP-US002" in body, "detaliul trebuie sa contina operatia nemapata"
assert 'name="auto_send"' not in body, (
"detaliul nu trebuie sa contina input auto_send dupa US-002"
)
assert "autosend-toggle" not in body, (
"detaliul nu trebuie sa contina clasa autosend-toggle dupa US-002"
)

129
tests/test_web_mapeaza.py Normal file
View File

@@ -0,0 +1,129 @@
"""Teste POST /trimitere/{id}/mapeaza — US-001 auto_send ignorat.
Dupa US-001: auto_send nu mai tine randul in needs_mapping; un cod valid mapat
-> queued direct, indiferent de valoarea auto_send din form sau din mapping_meta.
"""
from __future__ import annotations
import json
import os
import tempfile
import pytest
from starlette.testclient import TestClient
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "mapeaza.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.main import app
with TestClient(app, follow_redirects=False) as c:
yield c
get_settings.cache_clear()
@pytest.fixture()
def conn(client):
"""Conexiune directa la DB-ul deja initializat de client."""
from app.db import get_connection
c = get_connection()
yield c
c.close()
def _add_nomenclator(conn, cod="OE-1"):
conn.execute(
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
(cod, f"Operatie {cod}"),
)
conn.commit()
def _insert_needs_mapping(conn, cod_op="ITP-CHECK"):
payload = {
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1",
"data_prestatie": "2026-06-15", "odometru_final": "123456",
"prestatii": [{"cod_op_service": cod_op, "denumire": "Inspectie tehnica"}],
}
cur = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
"VALUES (?, ?, ?, ?)",
(f"k-{os.urandom(4).hex()}", 1, "needs_mapping", json.dumps(payload)),
)
conn.commit()
return int(cur.lastrowid)
def test_mapeaza_inline_auto_send_zero_in_form_tot_queued(client, conn):
"""POST /trimitere/{id}/mapeaza cu auto_send=0 explicit in form -> queued dupa US-001.
Vechi comportament: reresolve_account vedea auto_send=0 -> review_manual -> needs_mapping.
Nou comportament: has_no_auto_send returneaza False -> queued direct.
"""
_add_nomenclator(conn)
sid = _insert_needs_mapping(conn)
r = client.post(
f"/trimitere/{sid}/mapeaza",
data={
"cod_op_service": "ITP-CHECK",
"cod_prestatie": "OE-1",
"auto_send": "0",
},
)
assert r.status_code == 200
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
assert row["status"] == "queued", (
f"auto_send=0 in form nu trebuie sa tina randul dupa US-001, got {row['status']}"
)
def test_mapeaza_inline_fara_auto_send_form_tot_queued(client, conn):
"""POST /trimitere/{id}/mapeaza fara auto_send in form -> queued dupa US-001."""
_add_nomenclator(conn)
sid = _insert_needs_mapping(conn)
r = client.post(
f"/trimitere/{sid}/mapeaza",
data={"cod_op_service": "ITP-CHECK", "cod_prestatie": "OE-1"},
)
assert r.status_code == 200
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
assert row["status"] == "queued", (
f"fara auto_send -> asteptat queued, got {row['status']}"
)
def test_mapeaza_inline_raspuns_fara_mesaj_auto_send(client, conn):
"""Raspunsul la mapare nu contine text despre auto-send oprit."""
_add_nomenclator(conn)
sid = _insert_needs_mapping(conn)
r = client.post(
f"/trimitere/{sid}/mapeaza",
data={"cod_op_service": "ITP-CHECK", "cod_prestatie": "OE-1", "auto_send": "0"},
)
assert r.status_code == 200
body = r.text.lower()
assert "auto-send oprit" not in body, "raspunsul nu trebuie sa contina card 'auto-send oprit'"
assert "review manual inainte de trimitere" not in body, (
"raspunsul nu trebuie sa contina mesajul vechi de auto_send"
)
def test_mapeaza_inline_trigger_trimiteri_changed(client, conn):
"""Raspunsul are header HX-Trigger=trimiteriChanged indiferent de auto_send."""
_add_nomenclator(conn)
sid = _insert_needs_mapping(conn)
r = client.post(
f"/trimitere/{sid}/mapeaza",
data={"cod_op_service": "ITP-CHECK", "cod_prestatie": "OE-1", "auto_send": "0"},
)
assert r.status_code == 200
assert r.headers.get("HX-Trigger") == "trimiteriChanged"

View File

@@ -181,17 +181,18 @@ def test_tabel_fara_poll_periodic(client):
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
def test_nudge_date_noi_in_loc_de_poll(client):
"""Reimprospatarea live se face prin nudge-ul 'Date noi' (poller usor de versiune)
care NU atinge tabelul; utilizatorul reincarca explicit cand vrea."""
def test_auto_refresh_poller_la_versiune_noua(client):
"""Reimprospatarea la date noi externe se face prin pollerul de versiune,
care cheama reincarcaTrimiteri() automat — fara nudge manual (US-008).
Nudge-ul 'Date noi. Reincarca' a fost eliminat; pollerul face auto-refresh."""
acct = _create_account_user("poll2@test.com")
_login(client, "poll2@test.com")
_insert_submission(acct)
html = client.get("/?tab=acasa").text
assert 'id="nudge-trimiteri"' in html, "bannerul nudge 'Date noi' trebuie sa existe"
assert 'id="nudge-trimiteri"' not in html, "nudge-ul 'Date noi' trebuie eliminat (US-008)"
assert "/_fragments/trimiteri-versiune" in html, "pollerul de versiune trebuie configurat"
assert "reincarcaTrimiteri" in html, "reincarcarea manuala (Reincarca) trebuie expusa"
assert "reincarcaTrimiteri" in html, "auto-refresh prin reincarcaTrimiteri() trebuie prezent"
def test_trimiteriChanged_inca_reincarca(client):

231
tests/test_web_nav.py Normal file
View File

@@ -0,0 +1,231 @@
"""Teste US-005 (PRD 5.11): Link-uri navigatie sub contoare pe toate paginile.
TDD — testele sunt scrise INAINTE de implementare (RED), apoi devin GREEN.
Verifica:
- Sub contoare (#status-bar): rand cu link Trimiteri + Mapari (badge needs_mapping)
- Marcaj activ pe pagina curenta, via variabila de context tab_activ
- Logo ROMFAST + titlu linkeaza la / (Trimiteri)
- Hamburger capata Trimiteri (Acasa) ca prima intrare
"""
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-nav-{os.urandom(4).hex()}",
acct,
status,
json.dumps({
"vin": "WVWZZZ1KZAW001001",
"nr_inmatriculare": "B001NAV",
"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):
"""Client cu BD izolata + auth activat (login real)."""
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "nav_us005.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_nav_trimiteri_mapari_pe_mapari
# ============================================================
def test_nav_trimiteri_mapari_pe_mapari(client):
"""La tab=mapari, linkul Mapari e marcat activ (aria-current='page').
Linkul Trimiteri e prezent si inactiv.
Badge-ul needs_mapping apare pe linkul Mapari.
"""
acct = _create_account_user("nav1@test.com")
_ins(acct, status="needs_mapping") # badge pentru Mapari
_login(client, "nav1@test.com")
# Testam direct fragmentul status cu tab=mapari (asa il vede HTMX dupa implementare)
resp = client.get("/_fragments/status?tab=mapari")
assert resp.status_code == 200
html = resp.text
# Linkul Trimiteri (href="/") trebuie sa fie prezent
assert 'href="/"' in html, (
"Linkul Trimiteri (href='/') trebuie sa fie prezent sub contoare in #status-bar"
)
# Linkul Mapari trebuie sa fie prezent
assert '/?tab=mapari' in html, (
"Linkul Mapari (/?tab=mapari) trebuie sa fie prezent sub contoare in #status-bar"
)
# Mapari trebuie sa fie marcat activ (aria-current="page" pe elementul Mapari)
assert 'aria-current="page"' in html, (
"Linkul Mapari trebuie sa fie marcat activ cu aria-current='page' la tab=mapari"
)
# Trimiteri NU trebuie sa fie marcat activ (alta pagina e activa)
# Gasim tag-ul <a href="/"> si verificam ca NU contine aria-current
m = re.search(r'<a\b[^>]*href="/"[^>]*>', html)
assert m is not None, "Tag-ul <a href='/'> (Trimiteri) negasit in HTML"
tag_trimiteri = m.group(0)
assert 'aria-current' not in tag_trimiteri, (
f"Trimiteri NU trebuie sa fie activ la tab=mapari — tag gasit: {tag_trimiteri}"
)
# ============================================================
# test_nav_trimiteri_pe_jurnal
# ============================================================
def test_nav_trimiteri_pe_jurnal(client):
"""La tab=jurnal, ambele linkuri (Trimiteri + Mapari) sunt prezente,
dar niciunul nu e marcat activ (jurnal e pagina curenta).
"""
acct = _create_account_user("nav2@test.com")
_login(client, "nav2@test.com")
resp = client.get("/_fragments/status?tab=jurnal")
assert resp.status_code == 200
html = resp.text
# Ambele linkuri trebuie sa fie prezente
assert 'href="/"' in html, "Linkul Trimiteri (href='/') trebuie sa fie prezent la tab=jurnal"
assert '/?tab=mapari' in html, "Linkul Mapari trebuie sa fie prezent la tab=jurnal"
# La tab=jurnal, niciun link din nav nu trebuie sa aiba aria-current="page"
# (jurnal e tab-ul curent, nu Trimiteri si nici Mapari)
assert 'aria-current="page"' not in html, (
"La tab=jurnal, niciun link din status-nav nu trebuie sa fie aria-current='page'"
)
# ============================================================
# test_fragment_status_pastreaza_tab_la_self_refresh
# ============================================================
def test_fragment_status_pastreaza_tab_la_self_refresh(client):
"""Fragmentul status randat cu tab=mapari contine hx-get cu tab=mapari.
BUG 1: fara ?tab=, la poll-ul every 15s _status.html se re-randeaza fara
tab_activ -> nav-ul marcheaza gresit Trimiteri activ chiar daca userul e
pe ?tab=mapari. Fix: hx-get='/_fragments/status?tab={{ tab_activ }}'
in _status.html.
"""
acct = _create_account_user("refresh@test.com")
_login(client, "refresh@test.com")
resp = client.get("/_fragments/status?tab=mapari")
assert resp.status_code == 200
html = resp.text
# Fragmentul randat cu tab=mapari trebuie sa-si includa propriul hx-get
# cu tab=mapari, ca la self-refresh (every 15s / trimiteriChanged) sa
# primeasca acelasi tab si sa marcheze corect Mapari activ.
assert 'hx-get="/_fragments/status?tab=mapari"' in html, (
"status-bar randat cu tab=mapari trebuie sa contina hx-get cu tab=mapari "
"in el insusi (altfel self-refresh-ul pierde tab-ul activ)"
)
# ============================================================
# test_logo_linkeaza_acasa
# ============================================================
def test_logo_linkeaza_acasa(client):
"""Logo-ul ROMFAST (brand-logo) linkeaza la / (Trimiteri).
Logo-ul trebuie sa fie invelit intr-un <a href='/'> in header.
Aceasta face logo-ul clickabil pe toate paginile.
"""
acct = _create_account_user("logo@test.com")
_login(client, "logo@test.com")
resp = client.get("/")
assert resp.status_code == 200
html = resp.text
# Extrage sectiunea <header> pentru a izola cautarea
m_header = re.search(r'<header\b[^>]*>(.*?)</header>', html, re.DOTALL)
assert m_header, "<header> negasit in HTML"
header_html = m_header.group(1)
# Logo brand-logo trebuie sa fie prezent in header
assert 'brand-logo' in header_html, "Clasa brand-logo trebuie sa fie in header"
# Logo trebuie sa fie invelit intr-un <a href="/">
# Verificam: exista un <a href="/"> care contine class="brand-logo" in interiorul sau
assert re.search(r'<a\b[^>]*href="/"[^>]*>.*?brand-logo', header_html, re.DOTALL), (
"Logo-ul (class='brand-logo') trebuie sa fie intr-un <a href='/'> in header. "
"In prezent logo-ul nu e un link."
)
# Titlul "Gateway RAR AUTOPASS" trebuie sa fie si el in interiorul unui <a href="/">
# (PRD AC: Logo-ul ROMFAST + titlul linkeaza la /)
assert re.search(r'<a\b[^>]*href="/"[^>]*>.*?Gateway RAR AUTOPASS', header_html, re.DOTALL), (
"Titlul 'Gateway RAR AUTOPASS' trebuie sa fie intr-un <a href='/'> in header."
)

270
tests/test_web_refresh.py Normal file
View File

@@ -0,0 +1,270 @@
"""Teste US-008 (PRD 5.11) — Auto-refresh dupa actiuni proprii.
Comportament dorit:
1. Dupa actiune proprie (mapare inline, corectie, repune, commit), lista
Trimiteri se reincarca automat, fara click pe Reincarca.
Mecanism: server emite HX-Trigger: trimiteriChanged; #submissions-wrap
asculta 'trimiteriChanged from:body' si re-fetches imediat.
2. Filtrul activ si pagina curenta NU se reseteaza la auto-refresh.
Mecanism: hx-include="#filtre-trimiteri" pe #submissions-wrap.
3. Pollerul de fundal face auto-refresh direct la date noi (versiune diferita)
in loc sa afiseze nudge-ul "Date noi. Reincarca".
Decizie (documentata): nudge eliminat. Distinctia own vs externe nu e posibila
pe client fara sesiune dedicata — auto-refresh e mai consistent si mai simplu.
4. Elementul #nudge-trimiteri eliminat din template (dead code dupa schimbarea poller).
TDD: testele 1-2 sunt GREEN (comportament deja implementat — regresie).
testele 3-4 sunt RED→GREEN (implementate in aceeasi sesiune).
testele 5-6 (test_actiune_proprie_reincarca_automat, test_nudge_nu_mai_blocheaza)
sunt adaugate cu numele exacte din spec PRD (GREEN de la primul run — implementarea
existenta le satisface).
"""
from __future__ import annotations
import json
import os
import tempfile
import pytest
from fastapi.testclient import TestClient
# --------------------------------------------------------------------------- #
# Fixture #
# --------------------------------------------------------------------------- #
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us008.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
def _add_submission(account_id: int = 1, status: str = "queued", payload: dict | None = None) -> int:
"""Adauga un submission si returneaza id-ul sau.
status: starea dorita (implicit 'queued'); use 'needs_mapping' pentru testele de mapare.
payload: continut JSON (implicit minimal).
"""
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"us008_key_{account_id}_{status}", account_id, status,
json.dumps(payload or {"test": True})),
)
return cur.lastrowid
finally:
conn.close()
def _add_nomenclator(cod: str = "R-FRANE", nume: str = "Reparatie frane") -> None:
"""Insereaza un cod RAR in nomenclatorul local."""
from app.db import get_connection
conn = get_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?,?)",
(cod, nume),
)
finally:
conn.close()
# --------------------------------------------------------------------------- #
# Test 1 — GREEN (regresie): submissions-wrap reincarca la trimiteriChanged #
# --------------------------------------------------------------------------- #
def test_submissions_wrap_reincarca_la_trimiteriChanged(client):
"""#submissions-wrap se reincarca automat la evenimentul trimiteriChanged.
Actiunile proprii (mapare inline, corectie, repune, commit) emit
HX-Trigger: trimiteriChanged. #submissions-wrap asculta
'trimiteriChanged from:body' si re-fetches imediat, fara click pe Reincarca.
Nota: _coada.html se randeaza doar cand exista submission-uri (are_trimiteri=True).
Semanarea unui submission inainte de GET garanteaza randarea sectionii complete.
"""
_add_submission()
r = client.get("/_fragments/acasa")
assert r.status_code == 200, r.text
assert "submissions-wrap" in r.text, (
"#submissions-wrap lipseste din /_fragments/acasa (cu submission existent)"
)
assert "trimiteriChanged from:body" in r.text, (
"#submissions-wrap nu asculta 'trimiteriChanged from:body'"
"auto-refresh dupa actiuni proprii nu va functiona"
)
# --------------------------------------------------------------------------- #
# Test 2 — GREEN (regresie): filtrul nu se reseteaza la auto-refresh #
# --------------------------------------------------------------------------- #
def test_submissions_wrap_pastreaza_filtrul_la_auto_refresh(client):
"""Auto-refresh-ul nu reseteaza filtrul activ sau pagina curenta.
#submissions-wrap include #filtre-trimiteri la fiecare request HTMX.
La reincarcarea declansata de trimiteriChanged, filtrul curent (stare,
vehicul, data) si pagina curenta se retransmit automat.
"""
_add_submission()
r = client.get("/_fragments/acasa")
assert r.status_code == 200, r.text
html = r.text
assert 'hx-include="#filtre-trimiteri"' in html or "hx-include='#filtre-trimiteri'" in html, (
"hx-include=#filtre-trimiteri lipseste din #submissions-wrap — "
"filtrul se va reseta la auto-refresh"
)
# --------------------------------------------------------------------------- #
# Test 3 — RED: pollerul face auto-refresh, nu arata nudge #
# --------------------------------------------------------------------------- #
def test_poller_auto_refresh_nu_nudge(client):
"""Pollerul de fundal face auto-refresh la date noi, NU afiseaza nudge.
Pattern curent (RED): cand versiunea difera, pollerul face nudge.hidden = false.
Pattern dorit (GREEN): cand versiunea difera, pollerul cheama reincarcaTrimiteri()
care re-fetches #submissions-wrap pastrand filtrul curent.
Decizie: nudge eliminat complet (nu ramane 'doar pentru schimbari externe')
pentru ca distinctia propriu vs extern e imposibila pe client fara
mecanism de sesiune dedicat (too complex, no gain).
"""
r = client.get("/")
assert r.status_code == 200, r.text
html = r.text
# Pollerul NU mai seteaza nudge.hidden = false in handler-ul de versiune diferita.
# (Prezenta acestui pattern indica implementarea veche — RED.)
assert "nudge.hidden = false" not in html, (
"Pollerul inca arata nudge in loc de auto-refresh — "
"schimbati 'nudge.hidden = false' cu 'reincarcaTrimiteri()' in base.html"
)
# --------------------------------------------------------------------------- #
# Test 4 — RED: nudge-ul e eliminat din template #
# --------------------------------------------------------------------------- #
def test_nudge_eliminat_din_lista(client):
"""Elementul #nudge-trimiteri este eliminat din template-ul listei.
Dupa ce pollerul trece la auto-refresh, nudge-ul devine dead code.
Eliminarea lui simplifica template-ul si elimina un element de UI confuz
(utilizatorul vedea 'Date noi. Reincarca' chiar daca lista era actuala,
din cauza refreshului propriu care nu actualiza versiunea fast enough).
Nota: _coada.html se randeaza doar cand sunt submission-uri. Semanarea
asigura ca _coada.html e inclus in raspuns (altfel testul ar pasa vacuos).
"""
_add_submission()
r = client.get("/_fragments/acasa")
assert r.status_code == 200, r.text
assert "nudge-trimiteri" not in r.text, (
"#nudge-trimiteri inca exista in template — "
"eliminati elementul din _coada.html dupa ce pollerul e migrat la auto-refresh"
)
# --------------------------------------------------------------------------- #
# Test 5 (spec PRD): actiune proprie reincarca automat fara click Reincarca #
# --------------------------------------------------------------------------- #
def test_actiune_proprie_reincarca_automat(client):
"""Dupa o actiune proprie (mapare inline), lista se reincarca automat.
Verifica doua componente ale mecanismului:
1. Server-side: POST la mapare inline returneaza HX-Trigger: trimiteriChanged
in headerele raspunsului (indiferent de starea noua a submission-ului).
2. Client-side: #submissions-wrap asculta 'trimiteriChanged from:body' in
hx-trigger — HTMX va declansa re-fetch imediat la primirea headerului,
fara click pe Reincarca.
Testeaza calea mapare-inline; corectie/repune/commit sunt acoperite similar
(toate emit HX-Trigger: trimiteriChanged — verificate in test_import_commit.py).
"""
_add_nomenclator("R-FRANE", "Reparatie frane")
sub_id = _add_submission(
status="needs_mapping",
payload={"prestatii": [{"cod_op_service": "OP-FRANE"}]},
)
# POST mapare inline — emite HX-Trigger: trimiteriChanged
r = client.post(
f"/trimitere/{sub_id}/mapeaza",
data={"csrf_token": "", "cod_op_service": "OP-FRANE", "cod_prestatie": "R-FRANE"},
)
assert r.status_code == 200, r.text
# Server emite triggerul — HTMX va declansa auto-refresh pe client
hx = r.headers.get("HX-Trigger", "")
assert "trimiteriChanged" in hx, (
f"Mapare inline nu emite HX-Trigger: trimiteriChanged — "
f"lista nu se va reincarca automat. Header primit: {hx!r}"
)
# Client-side: #submissions-wrap asculta triggerul (markup existent in template)
r_acasa = client.get("/_fragments/acasa")
assert "trimiteriChanged from:body" in r_acasa.text, (
"#submissions-wrap nu asculta 'trimiteriChanged from:body'"
"auto-refresh nu va functiona dupa actiune proprie"
)
# --------------------------------------------------------------------------- #
# Test 6 (spec PRD): nudge nu mai blocheaza actualizarea #
# --------------------------------------------------------------------------- #
def test_nudge_nu_mai_blocheaza_actualizarea(client):
"""Nudge-ul 'Date noi. Reincarca' nu mai poate bloca actualizarea listei.
Inainte de US-008, mecanismul era: poller detecteaza versiune diferita →
arata nudge → utilizatorul trebuia sa apese 'Reincarca' manual.
Dupa US-008, nudge-ul e eliminat (decizie documentata mai jos) si pollerul
cheama direct reincarcaTrimiteri() → actualizare automata, fara click.
Decizie privind nudge: ELIMINAT complet (nu pastrat pentru schimbari externe).
Rationale: distinctia 'actiune proprie vs schimbare externa' nu e posibila
pe client fara mecanism dedicat de sesiune. Auto-refresh periodic (la versiune
diferita) acopera si schimbarile externe (worker, alt browser) fara friction.
"""
_add_submission() # are_trimiteri=True → _coada.html se randeaza
r_pagina = client.get("/_fragments/acasa")
assert r_pagina.status_code == 200, r_pagina.text
html = r_pagina.text
# Nudge eliminat: nu mai poate bloca utilizatorul cu un banner "Reincarca"
assert "nudge-trimiteri" not in html, (
"#nudge-trimiteri inca exista — poate bloca actualizarea (US-008 il elimina)"
)
# Pollerul JS nu mai seteaza nudge.hidden = false: nu poate "bloca" prin afisare nudge
r_home = client.get("/")
assert r_home.status_code == 200, r_home.text
assert "nudge.hidden = false" not in r_home.text, (
"Pollerul JS inca poate afisa nudge-ul — schimbati cu reincarcaTrimiteri()"
)
# Pollerul cheama reincarcaTrimiteri() la versiune diferita (auto-refresh)
assert "reincarcaTrimiteri" in r_home.text, (
"reincarcaTrimiteri() lipseste din poller — auto-refresh nu va functiona"
)

View File

@@ -1,6 +1,5 @@
"""Teste PRD 5.5 — uniformizare UI: US-001 (Acasa fara Ajutor), US-002 (Nomenclator grila
standard), US-003 (macro autosend compact). Stories de template/macro -> render direct Jinja
pentru US-002/003; US-001 prin TestClient pe fragmentul Acasa.
standard), US-003 (macro autosend) — actualizat dupa PRD 5.11 US-002 (macro neutralizat).
"""
from __future__ import annotations
@@ -53,32 +52,26 @@ def _render_macro(form_id="map-1", checked=True):
def test_autosend_pastreaza_name_si_prezenta():
"""Invariant backend: checkbox name=auto_send value=true (semantica de prezenta)."""
"""US-002 (PRD 5.11): macro autosend_toggle neutralizat — output gol, fara checkbox."""
html = _render_macro(checked=True)
assert 'type="checkbox"' in html
assert 'name="auto_send"' in html
assert 'value="true"' in html
assert 'form="map-1"' in html
assert "checked" in html
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din macro"
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
def test_autosend_nebifat_fara_checked():
"""US-002: macro neutralizat intoarce gol indiferent de checked."""
html = _render_macro(checked=False)
assert 'name="auto_send"' in html
assert "checked" not in html
assert 'name="auto_send"' not in html
assert html.strip() == ""
def test_autosend_compact_fara_proza_inline():
"""Proza explicativa de pe randuri (3.6) eliminata din CONTINUTUL vizibil — traieste in
panoul Ajutor (US-005). Tooltip-ul scurt (atribut title=) e acceptat, deci il scoatem
inainte de verificare."""
"""US-002: macro neutralizat nu contine nicio proza inline."""
html = _render_macro()
vizibil = re.sub(r'title="[^"]*"', "", html) # scoate atributul title (tooltip)
assert "La fisierele viitoare" not in vizibil
assert "Tine pentru verificare" not in vizibil
assert "nimic nu pleaca la RAR" not in vizibil
# ambele etichete de stare vizibile, compact
assert "Auto" in html and "Manual" in html
assert "La fisierele viitoare" not in html
assert "Tine pentru verificare" not in html
assert "nimic nu pleaca la RAR" not in html
assert html.strip() == ""
# ============================================================
@@ -155,16 +148,16 @@ def test_mapari_ajutor_disclosure_si_fara_proza_inline(client):
html = resp.text
# US-010: sectiunea de ajutor (<details class="ajutor-mapari">) eliminata
assert "ajutor-mapari" not in html
# antet de coloana compact
assert ">In coada<" in html
# US-002: coloana In coada scoasa din tabel
assert "In coada" not in html, "US-002: coloana 'In coada' scoasa din tabelul Mapari"
# proza inline veche eliminata de pe sectiuni
assert "sugestia fuzzy e preselectata) si salveaza" not in html
assert "Maparile operatie -> cod RAR retinute pentru contul tau" not in html
def test_mapari_comutator_compact_in_tabel(client):
"""US-002: tabul Mapari nu mai contine checkbox auto_send."""
acct = _login(client)
_seed_needs_mapping(acct)
html = client.get("/_fragments/mapari").text
assert 'name="auto_send"' in html
assert "Manual" in html and "Auto" in html
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din UI"