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:
@@ -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
|
||||
|
||||
@@ -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
250
tests/test_import_commit.py
Normal 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"
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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']}"
|
||||
)
|
||||
|
||||
283
tests/test_preview_import.py
Normal file
283
tests/test_preview_import.py
Normal 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 -> '
|
||||
# Deci [{'cod_op_service': ...}] devine [{'cod_op_service': ...}] in HTML.
|
||||
# Verificam secventa specifica a repr-ului HTML-escapata:
|
||||
assert "'cod_op_service'" not in html, (
|
||||
"Repr Python HTML-escapata ('cod_op_service') gasita in HTML — "
|
||||
"adaptorul trebuie sa formateze erorile uman INAINTE de randare in Note"
|
||||
)
|
||||
|
||||
|
||||
def test_preview_stare_eticheta_umana(client):
|
||||
"""Starea din pill-ul fiecarui rand este eticheta umana, nu codul brut.
|
||||
|
||||
Testeaza starile ok si needs_mapping (cele mai comune in preview).
|
||||
Celelalte stari (needs_review, already_sent, duplicate_in_file) sunt in testele ext.
|
||||
"""
|
||||
_seed_nomenclator_si_mapare()
|
||||
_, html = _upload_and_preview(client, _ROWS_OK_SI_UNMAPPED)
|
||||
|
||||
# Etichetele umane trebuie sa apara in HTML (din pill-urile randurilor)
|
||||
# "Gata de trimis" cu majuscula — nu confundam cu "gata de trimis" din
|
||||
# summary pills (deja prezente in codul curent cu lowercase)
|
||||
assert "Gata de trimis" in html, (
|
||||
"Eticheta umana 'Gata de trimis' lipsa — "
|
||||
"pill-ul randului ok afiseaza inca codul brut"
|
||||
)
|
||||
assert "Cod RAR lipsa" in html, (
|
||||
"Eticheta umana 'Cod RAR lipsa' lipsa — "
|
||||
"pill-ul randului needs_mapping afiseaza inca codul brut"
|
||||
)
|
||||
|
||||
# Codurile brute NU trebuie sa apara ca text vizibil al pill-ului de rand.
|
||||
# Cautam <span class="pill ...">ok</span> (codul brut ca text al pill-ului).
|
||||
assert re.search(r'class="pill[^"]*">ok<', html) is None, (
|
||||
"Pill cu text brut 'ok' gasit in randuri — trebuie 'Gata de trimis'"
|
||||
)
|
||||
assert re.search(r'class="pill[^"]*">needs_mapping<', html) is None, (
|
||||
"Pill cu text brut 'needs_mapping' gasit in randuri — trebuie 'Cod RAR lipsa'"
|
||||
)
|
||||
|
||||
|
||||
def test_preview_stare_eticheta_umana_duplicate(client):
|
||||
"""Starea duplicate_in_file afiseaza eticheta umana 'Duplicat in fisier'."""
|
||||
_seed_nomenclator_si_mapare()
|
||||
_, html = _upload_and_preview(client, _ROWS_DUPLICATE)
|
||||
|
||||
# Confirma ca exista randuri duplicate_in_file
|
||||
has_dup = "duplicate_in_file" in html or "Duplicat in fisier" in html
|
||||
assert has_dup, "Testul necesita randuri duplicate_in_file — fixture gresit"
|
||||
|
||||
assert "Duplicat in fisier" in html, (
|
||||
"Eticheta umana 'Duplicat in fisier' lipsa — "
|
||||
"pill-ul randului duplicate_in_file afiseaza inca codul brut"
|
||||
)
|
||||
assert re.search(r'class="pill[^"]*">duplicate_in_file<', html) is None, (
|
||||
"Pill cu text brut 'duplicate_in_file' gasit — trebuie 'Duplicat in fisier'"
|
||||
)
|
||||
|
||||
|
||||
def test_nota_umana_preview_needs_mapping_cu_flag_prioritizeaza_unmapped():
|
||||
"""needs_mapping + flag -> Note afiseaza 'Cod RAR lipsa', nu textul flag-ului.
|
||||
|
||||
BUG 3: nota_umana_preview verifica `if flags:` inaintea ramurei unmapped.
|
||||
Un rand needs_mapping care are si un flag (ex. VIN numeric) afisa textul
|
||||
flag-ului in loc de 'Cod RAR lipsa pentru: COD' — exact confuzia US-003
|
||||
voia s-o evite (userul corecteaza data si ramane blocat pe cod).
|
||||
Fix: cand status == 'needs_mapping', prioritizeaza ramura unmapped.
|
||||
"""
|
||||
from app.web.labels import nota_umana_preview
|
||||
|
||||
errors = [{"unmapped": [{"cod_op_service": "OP-TEST", "denumire": "Op Test"}]}]
|
||||
flags = ["VIN numeric (12345) — verificati seria sasiului"]
|
||||
|
||||
result = nota_umana_preview("needs_mapping", errors, flags)
|
||||
|
||||
assert "Cod RAR lipsa" in result, (
|
||||
f"needs_mapping cu flag trebuie sa afiseze 'Cod RAR lipsa', "
|
||||
f"nu textul flag-ului — primit: {result!r}"
|
||||
)
|
||||
assert "VIN numeric" not in result, (
|
||||
f"needs_mapping cu flag NU trebuie sa afiseze textul flag-ului — primit: {result!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_preview_foloseste_clasa_tabel_trimiteri(client):
|
||||
"""Tabelul de preview foloseste clasa CSS .tabel-trimiteri (nu doar .tablewrap).
|
||||
|
||||
Necesara pentru:
|
||||
- table-layout:fixed (fara overflow orizontal la 1280px)
|
||||
- carduri <768px: td::before citeste data-eticheta
|
||||
- col-* latimi pentru cele 4 coloane extra (col-km, col-note, col-verificat, col-actiuni)
|
||||
"""
|
||||
_, html = _upload_and_preview(client, _ROWS_UNMAPPED)
|
||||
|
||||
assert "tabel-trimiteri" in html, (
|
||||
"Clasa CSS 'tabel-trimiteri' lipsa din tabelul de preview — "
|
||||
"necesara pentru table-layout:fixed si carduri mobile"
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
130
tests/test_web_acasa.py
Normal 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
286
tests/test_web_filtre.py
Normal 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
160
tests/test_web_mapari.py
Normal 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
129
tests/test_web_mapeaza.py
Normal 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"
|
||||
@@ -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
231
tests/test_web_nav.py
Normal 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
270
tests/test_web_refresh.py
Normal 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"
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user