Files
rar-autopass/tests/test_web_import_stepper.py
Claude Agent 8d4ff3400e feat(5.13): carduri compacte mobil/tableta + fix editare preview (OOB tr) + toast
Dogfood pe import + Trimiteri (mobil/tableta <1024px), pur CSS + markup, backend
trimitere neatins:

- Card compact real pentru .tabel-trimiteri (preview + Trimiteri): vehicul=titlu,
  stare=pill dreapta-sus, operatie+cod, meta data/km muted, nota mica. Inlocuieste
  stiva generica eticheta+valoare (carduri de ~450px -> ~135px). Anuleaza regula
  desktop tr.trimitere-row > td{padding:11px} in blocul compact.
- FIX editare preview: OOB swap pe <tr> esua tacit in htmx 1.9 (un <tr> brut se
  pierde la parsarea unui fragment fara context de tabel) -> randul ramanea cu
  starea veche dupa salvare. Inlocuit cu reload complet al preview-ului prin
  HX-Trigger:reincarcaPreview + detalii randSalvat. /editeaza si /confirma-review
  folosesc helper-ul _raspuns_rand_salvat.
- Feedback post-salvare: toast global "Randul N actualizat · <stare>" + scroll +
  flash pe randul actualizat (base.html window.arataToast + listener randSalvat).
- Modal editare: Salveaza + Anuleaza pe acelasi rand (sistem .act): desktop text,
  mobil doua iconite Lucide 44px alaturate (save/x). Macro icon('x') + .act-primary.
- Randuri deja-trimise/duplicate colapsate implicit in preview + toggle "Arata N".
- Select "Operatii de mapat" full-width pe mobil (nu mai iese din viewport).
- Bara de filtre Trimiteri adaptata mobil: pills pe banda cu scroll orizontal,
  cautare vehicul proeminenta (nu 8 butoane full-width stivuite).
- Nota preview = culoarea camp-fix (accent) ca sa atraga atentia; hint-urile
  camp-fix per-camp scoase (campul Note e self-explanatory).
- Confirmare trimitere: scos campul email (Declarant); text mai clar
  ("Confirma numarul din N gata de trimis"). Backend confirmed_by ramane optional.

Teste: contractul OOB (rupt in browser) inlocuit cu noul contract
(reincarcaPreview + randSalvat) in test_web_preview_edit / test_preview_edit_ui /
test_import_review. Suita: 992 passed (exclus live).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:34:33 +00:00

294 lines
10 KiB
Python

"""Teste US-004: wizard import cu stepper vizual (4 pasi numerotati).
TDD — testele sunt scrise INAINTE de implementare (RED), apoi se face GREEN.
Verifica:
- Pasul 1 activ (aria-current="step") in fragmentul de upload
- Pasul 2 activ in fragmentul mapare-coloane
- Pasul 3 activ in preview
- Pasii 1 si 2 marcati ca "facuti" in preview (clasa/marcaj)
- hx-target="#import-section" pastrat in fragmentele de import
- csrf_token prezent in formularele de import
"""
from __future__ import annotations
import io
import os
import re
import tempfile
import pytest
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.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() # izolare: limiterul login e global in-proces
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 _make_csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
import csv
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")
def _make_xlsx_bytes(rows: list[dict]) -> bytes:
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()
_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:
client.post("/v1/mapari", json={
"cod_op_service": cod_op,
"cod_prestatie": cod_prest,
"auto_send": True,
})
def _upload_and_get_import_id(client, rows=None) -> int:
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
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
assert m, f"Nu s-a gasit import_id in raspuns: {r.text[:500]}"
return int(m.group(1))
def _get_preview_via_mapare(client, import_id: int) -> str:
"""Salveaza maparea de coloane si returneaza textul raspunsului 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
return r.text
# ---------------------------------------------------------------------------
# US-004 Teste stepper
# ---------------------------------------------------------------------------
def test_stepper_pas1_la_upload(client):
"""Fragmentul de upload contine stepper-ul cu pasul 1 activ.
Verifica prezenta marcajului aria-current='step' pe pasul 'Incarca fisier'
sau clasa activa asociata pasului 1.
"""
r = client.get("/_import/reset")
assert r.status_code == 200
text = r.text
# Stepper-ul trebuie sa fie prezent
assert "stepper" in text or "pasi-import" in text or "step" in text.lower(), \
"Stepper-ul nu a fost gasit in fragmentul de upload"
# Pasul 1 trebuie sa aiba aria-current="step"
assert 'aria-current="step"' in text, \
"aria-current='step' nu a fost gasit in fragmentul de upload (pasul 1)"
# Textul pasului 1 trebuie sa fie prezent
assert "Incarca" in text, "Textul pasului 1 'Incarca' nu a fost gasit"
def test_stepper_pas1_via_tab_import(client):
"""Accesand /?tab=import, panoul contine stepper cu pasul 1 activ."""
r = client.get("/?tab=import")
assert r.status_code == 200
text = r.text
assert 'aria-current="step"' in text, \
"aria-current='step' nu a fost gasit in panoul Import (/?tab=import)"
assert "Incarca" in text, "Textul pasului 1 'Incarca' nu a fost gasit in panoul Import"
def test_stepper_pas2_la_mapare(client):
"""Fragmentul mapare-coloane contine stepper cu pasul 2 activ.
Declanseaza un upload cu coloane NEMAPATE ca sa primesti _mapcoloane.html.
"""
# Upload fara mapare salvata → trebuie sa vina _mapcoloane.html
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
text = r.text
# Trebuie sa fie formularul de mapare coloane
assert "mapare-coloane" in text, "Nu s-a primit fragmentul de mapare coloane"
# Stepper prezent
assert "stepper" in text or "step" in text.lower(), \
"Stepper-ul nu a fost gasit in fragmentul mapare-coloane"
# Pasul 2 trebuie sa aiba aria-current="step" cu textul "Potriveste"
# (pasul 1 e facut, pasul 2 e activ)
assert 'aria-current="step"' in text, \
"aria-current='step' nu a fost gasit in fragmentul mapare-coloane (pasul 2)"
assert "Potriveste" in text, "Textul pasului 2 'Potriveste' nu a fost gasit"
def test_stepper_pas3_la_preview(client):
"""Preview contine stepper cu pasul 3 activ.
Declanseaza upload + salvare mapare → se ajunge la preview.
"""
_seed_op_mapping(client)
import_id = _upload_and_get_import_id(client)
text = _get_preview_via_mapare(client, import_id)
# Preview trebuie sa fie prezent
assert "Preview" in text or "confirm-form" in text, \
"Nu s-a primit fragmentul de preview"
# Stepper prezent
assert "stepper" in text or "step" in text.lower(), \
"Stepper-ul nu a fost gasit in preview"
# Pasul 3 activ
assert 'aria-current="step"' in text, \
"aria-current='step' nu a fost gasit in preview (pasul 3)"
assert "Verifica" in text, "Textul pasului 3 'Verifica' nu a fost gasit in preview"
def test_stepper_pas3_la_preview_direct_mapare_retinuta(client):
"""Upload cu mapare retinuta sare direct la preview cu pasul 3 activ.
Primul upload + mapare memoreaza configuratia.
Al doilea upload cu acelasi antet sare direct la preview (pas 3).
Pasii 1 si 2 sunt implicit facuti (comportament stepper la pas=3).
"""
_seed_op_mapping(client)
import_id1 = _upload_and_get_import_id(client)
_get_preview_via_mapare(client, import_id1)
# Al doilea upload — mapare retinuta → preview direct
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
text = r.text
# Preview direct cu mesaj "Mapare retinuta"
assert "Mapare retinuta" in text, "Preview direct (mapare retinuta) nu a fost randat"
# Stepper prezent cu pasul 3 activ
assert 'aria-current="step"' in text, \
"aria-current='step' nu a fost gasit in preview direct (mapare retinuta)"
assert "Verifica" in text, "Textul pasului 3 'Verifica' nu a fost gasit in preview direct"
def test_stepper_marcheaza_pasii_facuti(client):
"""In preview (pas 3), pasii 1 si 2 sunt marcati ca facuti (clasa is-done).
Verifica prin prezenta clasei CSS is-done (doua aparitii: pasii 1 si 2).
"""
_seed_op_mapping(client)
import_id = _upload_and_get_import_id(client)
text = _get_preview_via_mapare(client, import_id)
# Clasa "is-done" trebuie sa apara pentru pasii 1 si 2 (index < pas curent)
assert "is-done" in text, \
"Clasa 'is-done' nu a fost gasita in preview (pasii 1 si 2 ar trebui marcati ca is-done)"
# Numarul de aparitii: cel putin 2 pasi marcati ca is-done
count_done = text.count("is-done")
assert count_done >= 2, \
f"Asteptat cel putin 2 pasi marcati ca 'is-done' in preview, gasit {count_done}"
def test_import_hx_target_in_tab(client):
"""Fragmentele de import pastreaza hx-target='#import-section'.
Fragmentul de upload (/_import/reset) trebuie sa contina
hx-target='#import-section' pentru ca HTMX sa actualizeze corect
containerul din panoul de tab, nu din alta parte.
"""
r = client.get("/_import/reset")
assert r.status_code == 200
text = r.text
assert 'hx-target="#import-section"' in text, \
"hx-target='#import-section' nu a fost gasit in fragmentul de upload"
# Wrapper-ul extern trebuie sa aiba id="import-section"
assert 'id="import-section"' in text, \
"id='import-section' nu a fost gasit in fragmentul de upload"
def test_import_forms_pastreaza_csrf(client):
"""Formularele de import contin csrf_token (input hidden cu valoare).
Testeaza atat fragmentul de upload cat si cel de mapare coloane.
"""
# Fragment upload
r_upload = client.get("/_import/reset")
assert r_upload.status_code == 200
text_upload = r_upload.text
# Trebuie sa contina campul csrf_token (poate fi gol in modul dev fara sesiune,
# dar campul trebuie sa existe)
assert 'name="csrf_token"' in text_upload, \
"name='csrf_token' nu a fost gasit in formularul de upload"
# Fragment mapare coloane
csv_bytes = _make_csv_bytes(_SAMPLE_ROWS)
r_map = client.post(
"/_import/upload",
files={"file": ("test.csv", csv_bytes, "text/csv")},
)
assert r_map.status_code == 200
text_map = r_map.text
if "mapare-coloane" in text_map: # s-a primit fragmentul de mapare
assert 'name="csrf_token"' in text_map, \
"name='csrf_token' nu a fost gasit in formularul mapare-coloane"