Files
rar-autopass/tests/test_import_ui.py
Claude Agent 4a1d28749a feat(web): dashboard ergonomic cu tab-uri, stepper import si microcopy uman (3.4)
Reorganizeaza interfata web pe trei principii, fara a atinge backend-ul de
trimitere (worker, mapping, idempotency, masina de stari neatinse):

- US-001 app/web/labels.py: modul pur stari tehnice -> text uman + clasa CSS
- US-002 bara status /_fragments/status: microcopy uman, defalcare blocate, scoped cont
- US-003 shell 6 tab-uri (Acasa/Import/Coada/Mapari/Cont/Nomenclator): deep-link
  ?tab=, panou activ randat server-side, fragmente inactive lazy, ARIA real
- US-004 stepper import 4 pasi (pur vizual; hx-target + csrf pastrate)
- US-005 Acasa onboarding checklist auto-bifat + colaps + empty states prietenoase

Reparat in cursul VERIFY/CLOSE: izolare teste (reset ratelimit._hits in fixturi),
regresie avertisment "cont in asteptare de activare" (re-introdus in bara status),
culori hardcodate -> variabile paleta. 434 teste pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:26:10 +00:00

457 lines
17 KiB
Python

"""Teste UI web import (U5) — upload → mapare coloane → preview → confirmare.
Verifica:
- Dashboard randeaza sectiunea de upload
- Upload xlsx → mapare noua → fragment _mapcoloane returnat
- Upload xlsx cu mapare existenta → preview direct
- Salvare mapare coloane → preview randat
- Preview afiseaza rezumat stari si randul tabelului
- Confirmare cu N corect → succes (in coada)
- Confirmare cu N gresit → eroare explicita
- Reset → drop zone gol
- Erori upload (fisier invalid, prea mare, header neclar)
- Sheet selector la multi-sheet xlsx
"""
from __future__ import annotations
import io
import os
import tempfile
import pytest
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
# Comportament in mod dev (fallback cont 1, fara login/CSRF); auth web e
# default ON in prod — testat separat in test_web_*.
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.main import app
from fastapi.testclient import TestClient
with TestClient(app) as c:
yield c
get_settings.cache_clear()
def _make_xlsx_bytes(rows: list[dict]) -> bytes:
"""Construieste un xlsx minimal cu openpyxl pentru fixture teste."""
openpyxl = pytest.importorskip("openpyxl")
wb = openpyxl.Workbook()
ws = wb.active
if not rows:
return b""
headers = list(rows[0].keys())
ws.append(headers)
for row in rows:
ws.append([row.get(h) for h in headers])
buf = io.BytesIO()
wb.save(buf)
return buf.getvalue()
def _make_csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
"""Construieste un CSV minimal pentru fixture teste."""
import csv
import io
buf = io.StringIO()
if not rows:
return b""
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep)
writer.writeheader()
writer.writerows(rows)
return buf.getvalue().encode("utf-8")
_SAMPLE_ROWS = [
{
"VIN": "WVWZZZ1KZAW000123",
"Nr inmatriculare": "B001TST",
"Data prestatie": "15.06.2026",
"Odometru final": "123456",
"Operatie": "Revizie",
},
{
"VIN": "WVWZZZ1KZAW000456",
"Nr inmatriculare": "B002TST",
"Data prestatie": "16.06.2026",
"Odometru final": "200000",
"Operatie": "Revizie",
},
]
def _seed_op_mapping(client, cod_op: str = "Revizie", cod_prest: str = "OE-1") -> None:
"""Seeda o mapare de operatii cod_op → cod_prestatie via API."""
client.post("/v1/mapari", json={
"cod_op_service": cod_op,
"cod_prestatie": cod_prest,
"auto_send": True,
})
# --------------------------------------------------------------------------- #
# Dashboard #
# --------------------------------------------------------------------------- #
def test_dashboard_contine_drop_zone(client):
"""Tab-ul Import randeaza sectiunea de upload cu drop zone si mesaj warmth.
Dupa US-003 sectiunea de import e in tab-ul Import (?tab=import), nu pe pagina principala.
"""
r = client.get("/?tab=import")
assert r.status_code == 200
assert "Primul fisier" in r.text
assert "drop-zone" in r.text
assert "NU se trimite nimic" in r.text
assert "import-section" in r.text
# --------------------------------------------------------------------------- #
# Upload xlsx — mapare noua #
# --------------------------------------------------------------------------- #
def test_upload_xlsx_fara_mapare_arata_formular_mapare(client):
"""Upload xlsx fara mapare salvata → fragment mapare coloane."""
xlsx = _make_xlsx_bytes(_SAMPLE_ROWS)
r = client.post(
"/_import/upload",
files={"file": ("test.xlsx", xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert r.status_code == 200
# Formular de mapare coloane
assert "Mapare coloane" in r.text
assert "mapare-coloane" in r.text # URL in form action
# Coloanele din fisier apar in formular
assert "VIN" in r.text
assert "Data prestatie" in r.text
# Sugestii fuzzy pentru VIN
assert "vin" in r.text.lower()
def test_upload_csv_fara_mapare_arata_formular_mapare(client):
"""Upload CSV cu separator ; → formular mapare coloane."""
csv_bytes = _make_csv_bytes(_SAMPLE_ROWS)
r = client.post(
"/_import/upload",
files={"file": ("test.csv", csv_bytes, "text/csv")},
)
assert r.status_code == 200
assert "Mapare coloane" in r.text
# --------------------------------------------------------------------------- #
# Salvare mapare coloane → preview #
# --------------------------------------------------------------------------- #
def _upload_and_get_import_id(client, rows=None) -> int:
"""Helper: incarca fisier si extrage import_id din raspuns."""
xlsx = _make_xlsx_bytes(rows or _SAMPLE_ROWS)
r = client.post(
"/_import/upload",
files={"file": ("test.xlsx", xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert r.status_code == 200
# Extrage import_id din URL-ul form action din raspuns
text = r.text
# Form action contine /_import/{id}/mapare-coloane
import re
m = re.search(r"/_import/(\d+)/mapare-coloane", text)
assert m, f"Nu s-a gasit import_id in raspuns: {text[:500]}"
return int(m.group(1))
def test_salvare_mapare_coloane_arata_preview(client):
"""Dupa salvarea maparii de coloane, raspunsul contine preview-ul."""
# Asigura ca nomenclatorul are OE-1 (seeding automat la init_db)
import_id = _upload_and_get_import_id(client)
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
# Preview trebuie sa contina elementele cheie
assert "Preview" in r.text
assert "confirm-form" in r.text
assert "n-confirmat" in r.text
# Rezumat stari
assert "gata de trimis" in r.text or "ok" in r.text
def test_preview_arata_randul_vin(client):
"""Preview contine VIN-ul din fisier."""
import_id = _upload_and_get_import_id(client)
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",
},
)
r = client.get(f"/_import/{import_id}/preview")
assert r.status_code == 200
assert "WVWZZZ1KZAW000123" in r.text
# --------------------------------------------------------------------------- #
# Upload cu mapare existenta → preview direct #
# --------------------------------------------------------------------------- #
def test_upload_cu_mapare_existenta_sare_direct_la_preview(client):
"""Al doilea upload cu acelasi antet → preview imediat (mapare retinuta)."""
# Primul upload + salvare mapare
import_id1 = _upload_and_get_import_id(client)
client.post(
f"/_import/{import_id1}/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",
},
)
# Al doilea upload cu acelasi antet
xlsx = _make_xlsx_bytes(_SAMPLE_ROWS)
r = client.post(
"/_import/upload",
files={"file": ("test2.xlsx", xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert r.status_code == 200
# Trebuie sa ajunga direct la preview, nu la mapare
assert "Preview" in r.text
assert "confirm-form" in r.text
assert "Mapare retinuta aplicata automat" in r.text
# --------------------------------------------------------------------------- #
# Confirmare (gate HARD) #
# --------------------------------------------------------------------------- #
def _setup_preview(client) -> int:
"""Upload + mapare + seeda operatii + intoarce import_id gata de confirmare."""
_seed_op_mapping(client) # "Revizie" → "OE-1"
import_id = _upload_and_get_import_id(client)
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",
},
)
return import_id
def test_confirmare_n_corect_pune_in_coada(client):
"""Confirmare cu N corect → randurile ok ajung in coada."""
import_id = _setup_preview(client)
# Compute preview pentru a afla n_ok
r_prev = client.get(f"/_import/{import_id}/preview")
assert r_prev.status_code == 200
# Citeste summary din DB
from app.db import get_connection
conn = get_connection()
try:
batch = conn.execute(
"SELECT ok FROM import_batches WHERE id=?", (import_id,)
).fetchone()
n_ok = batch["ok"]
finally:
conn.close()
assert n_ok > 0, "Asteptat cel putin un rand ok dupa seeding corect"
r = client.post(
f"/_import/{import_id}/confirma",
data={"n_confirmat": str(n_ok), "confirmed_by": "test@test.ro"},
)
assert r.status_code == 200
# Succes → drop zone cu mesaj
assert "S-au pus in coada" in r.text or "prezentari" in r.text
# Sectiunea se reseteaza la drop zone
assert "drop-zone" in r.text
# Verifica ca submissions au fost create
conn2 = get_connection()
try:
n = conn2.execute(
"SELECT COUNT(*) FROM submissions WHERE batch_id=?", (import_id,)
).fetchone()[0]
assert n == n_ok, f"Asteptat {n_ok} submissions, gasit {n}"
finally:
conn2.close()
def test_confirmare_n_gresit_arata_eroare(client):
"""Confirmare cu N gresit → eroare clara, nu enqueue."""
import_id = _setup_preview(client)
client.get(f"/_import/{import_id}/preview") # calculeaza si stocheaza starea
r = client.post(
f"/_import/{import_id}/confirma",
data={"n_confirmat": "99", "confirmed_by": ""},
)
assert r.status_code == 200
# Trebuie sa arate eroare de confirmare sau preview cu eroare
assert (
"difera" in r.text
or "Numarul confirmat" in r.text
or "Niciun rand ok" in r.text
)
# --------------------------------------------------------------------------- #
# Reset #
# --------------------------------------------------------------------------- #
def test_reset_arata_drop_zone_gol(client):
"""GET /_import/reset → drop zone gol fara mesaje."""
r = client.get("/_import/reset")
assert r.status_code == 200
assert "drop-zone" in r.text
assert "Primul fisier" in r.text
assert "import-section" in r.text
# --------------------------------------------------------------------------- #
# Erori upload #
# --------------------------------------------------------------------------- #
def test_upload_fisier_invalid_arata_eroare(client):
"""Upload fisier invalid → mesaj de eroare in drop zone."""
r = client.post(
"/_import/upload",
files={"file": ("test.xlsx", b"not a real xlsx file", "application/octet-stream")},
)
assert r.status_code == 200
# Trebuie sa arate drop zone cu eroare
assert "drop-zone" in r.text or "import-section" in r.text
# Eroare vizibila
assert "nerecunoscut" in r.text.lower() or "invalid" in r.text.lower() or "eroare" in r.text.lower()
def test_upload_fisier_csv_antet_o_coloana_arata_eroare(client):
"""CSV cu o singura coloana reala → header acceptat sau eroare gestionata."""
bad = b"date_fara_header\nval1\nval2\n"
r = client.post(
"/_import/upload",
files={"file": ("test.csv", bad, "text/csv")},
)
# Fie detecteaza header OK fie arata eroare
assert r.status_code == 200
assert "import-section" in r.text
# --------------------------------------------------------------------------- #
# Multi-sheet xlsx #
# --------------------------------------------------------------------------- #
def test_upload_multi_sheet_arata_selector(client):
"""xlsx cu mai multe foi → selector de foaie in drop zone."""
openpyxl = pytest.importorskip("openpyxl")
wb = openpyxl.Workbook()
ws1 = wb.active
ws1.title = "Date"
ws1.append(["VIN", "Data"])
ws1.append(["WVW001", "15.06.2026"])
ws2 = wb.create_sheet("Raport")
ws2.append(["Total"])
ws2.append(["1"])
buf = io.BytesIO()
wb.save(buf)
xlsx = buf.getvalue()
r = client.post(
"/_import/upload",
files={"file": ("multi.xlsx", xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert r.status_code == 200
# Trebuie sa arate selector de foi
assert "foi" in r.text.lower() or "sheet" in r.text.lower() or "foaie" in r.text.lower()
# Foile trebuie sa apara ca optiuni
assert "Date" in r.text or "Raport" in r.text
# --------------------------------------------------------------------------- #
# Elemente a11y (D10/D11/D12) #
# --------------------------------------------------------------------------- #
def test_preview_contine_banner_declarant(client):
"""Preview contine bannerul declarant (D12) cu text despre ireversibil."""
import_id = _setup_preview(client)
r = client.get(f"/_import/{import_id}/preview")
assert r.status_code == 200
assert "declarantul" in r.text
assert "ireversibil" in r.text
assert "banner" in r.text
def test_preview_contine_checkboxuri_needs_review(client):
"""Randurile needs_review au checkbox 'verificat' (D11)."""
# Cream un rand cu VIN numeric → needs_review
rows_with_review = [
{
"VIN": "1234567890", # VIN numeric → coercion flag → needs_review
"Nr inmatriculare": "B001TST",
"Data prestatie": "15.06.2026",
"Odometru final": "123456",
"Operatie": "OE-1",
}
]
import_id = _upload_and_get_import_id(client, rows=rows_with_review)
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",
},
)
r = client.get(f"/_import/{import_id}/preview")
assert r.status_code == 200
# Checkbox reviewed_rows prezent pentru randul needs_review
assert "reviewed_rows" in r.text
assert "needs_review" in r.text
def test_preview_duplicate_in_file_are_text(client):
"""Randurile duplicate_in_file arata text 'dubla cu randul N' (D10 — nu doar culoare)."""
_seed_op_mapping(client, "Revizie", "OE-1")
# Doua randuri identice → duplicate_in_file
dup_rows = [
{"VIN": "WVWZZZ1KZAW000123", "Nr inmatriculare": "B001TST",
"Data prestatie": "15.06.2026", "Odometru final": "123456", "Operatie": "Revizie"},
{"VIN": "WVWZZZ1KZAW000123", "Nr inmatriculare": "B001TST",
"Data prestatie": "15.06.2026", "Odometru final": "123456", "Operatie": "Revizie"},
]
import_id = _upload_and_get_import_id(client, rows=dup_rows)
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",
},
)
r = client.get(f"/_import/{import_id}/preview")
assert r.status_code == 200
# Text explicit pentru duplicate_in_file (nu doar culoare — cerinta daltonism D10)
assert "dubla cu randul" in r.text
assert "duplicate_in_file" in r.text