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>
251 lines
9.2 KiB
Python
251 lines
9.2 KiB
Python
"""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"
|
|
)
|