Files
rar-autopass/tests/test_web_import_stepper.py
Claude Agent c9f9a1ca0e feat(5.16+5.17): tipografie/antet branded + tipuri cont, planuri si enforcement
PRD 5.16 — propagare design finalizata (system font stack, fara IBM Plex self-hostat):
- US-001/002/008: tokeni --font-ui/--font-mono (system stack) + scala --fs-*; zero
  @font-face si zero /static/fonts/; landing aliniat la acelasi stack
- US-003: RAR online = dot compact in antet + meniu burger; banda rosie DOAR pe blocat
  (invariant zero-silent-failures pastrat)
- US-010: antet "ROMFAST AUTOPASS" + nume service + /login brandeit 2 coloane + badge plan;
  meniu burger cu separatoare; gate strict pe is_authenticated
- US-011: selector tema pill icon+eticheta (reuse THEMES)
- US-004/005/006/007: bug-fix editor prestatii (picker cod+denumire, add_extra in mod
  operatii, cod ales se salveaza fara "+", Renunta inchide via closest)
- US-012/013: landing Autentificare->/login; wizard import colapsat + 4 pasi pe tokeni
- fix VERIFY E2E: contoare duplicate pe 390px (inline display:flex batea @media) -> CSS + test-lock

PRD 5.17 — tipuri de cont + trial Pro 30z + enforcement DUR:
- US-001/002/008: accounts.tier + trial_until (migrare aditiva defensiva); app/plans.py
  sursa unica (PLANS, FREE_MONTHLY_LIMIT=60, effective_tier(now injectabil), monthly_usage,
  CONSUMED_STATUSES); create_account trial Pro 30z; CLI set-tier (protejat id=1, audit)
- US-003/004/005: enforce volum 60/luna INAINTE de build_key pe ambele canale
  (PLAN_LIMITA_LUNARA, 3 niveluri + log_event); gate API Pro+ (PLAN_FARA_API 403 actionabil);
  valideaza/nomenclator raman permise; downgrade lazy; flag AUTOPASS_ENFORCE_PLANS (kill-switch)
- US-006: badge plan antet + linie burger + consum N/60 + warn>=80% + 6 stari + copy RO
  pluralizat + banner one-time trial->Gratuit + pagina Cont

Regresie: 1380 passed, 0 failed, 1 deselected (live). E2E browser pe 390/1280 confirmat.
Backend trimitere (worker/masina stari/idempotenta/contract RAR) NEATINS. Lucrul 5.18
(corpus kNN) ramane separat, necomis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:02:40 +00:00

385 lines
14 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"
# ---------------------------------------------------------------------------
# US-013 Teste: import colapsat + tokeni scala + pill-uri cu dot (PRD 5.16)
# ---------------------------------------------------------------------------
def test_import_colapsat_implicit(client):
"""Pe Acasa (first-run, fara trimiteri), sectiunea de import e deschisa implicit.
La first-run (are_trimiteri=False), <details> trebuie sa aiba atributul `open`.
Summary-ul trebuie sa contina textul slim 'Importa fisier' (bara colapsabila).
Verifica si ca <details id="import-details"> este prezent pe pagina principala.
"""
r = client.get("/")
assert r.status_code == 200
text = r.text
# Elementul <details> trebuie sa fie prezent
assert 'id="import-details"' in text, \
"Elementul <details id='import-details'> lipseste de pe pagina principala"
# La first-run (nu exista trimiteri), details trebuie sa fie deschis (atribut open)
assert 'id="import-details" open' in text, \
"La first-run, <details id='import-details'> trebuie sa aiba atributul 'open'"
# Textul summary trebuie sa contina 'Importa fisier' (bara slim colapsabila)
assert "Importa fisier" in text, \
"Textul 'Importa fisier' nu a fost gasit in summary-ul sectiunii de import"
def test_wizard_foloseste_scala_tokeni(client):
"""Fragmentele wizard-ului de import folosesc tokeni var(--fs-*) in loc de px hardcodat.
Verifica ca fragmentul de mapare coloane (_mapcoloane.html) si cel de upload
(_upload.html) contin referinte la tokenii de scala --fs-* in inline styles,
nu font-size hardcodat in px sub 12px.
"""
# Fragment upload (/_import/reset) → _upload.html
r_upload = client.get("/_import/reset")
assert r_upload.status_code == 200
upload_text = r_upload.text
# Tokenii trebuie sa apara in inline styles
assert "var(--fs-" in upload_text, \
"Tokenii var(--fs-*) nu au fost gasiti in fragmentul de upload (_upload.html)"
# Fragment mapare coloane → _mapcoloane.html
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
map_text = r_map.text
# Mapcoloane trebuie sa contina tokeni
assert "var(--fs-" in map_text, \
"Tokenii var(--fs-*) nu au fost gasiti in fragmentul mapare coloane (_mapcoloane.html)"
# Verifica ca nu exista font-size sub 12px hardcodat in fragmentele wizard
import re
for fragment_text, fragment_name in [(upload_text, "upload"), (map_text, "mapcoloane")]:
for size_str in re.findall(r'font-size:\s*(\d+)px', fragment_text):
size = int(size_str)
assert size >= 12, \
f"font-size:{size}px sub 12px gasit in fragmentul {fragment_name} — trebuie var(--fs-*)"
def test_preview_stari_pill_dot(client):
"""Pill-urile de stare din preview contin un dot consistent cu designul 5.16.
Verifica ca pill-urile din tabelul de preview si din rezumatul de stari contin
un element dot (span cu border-radius:99px ca inline style), consistent cu stripul
slim si cu designul 5.16 (dot + text, nu text gol).
Eticheta umana: din STARI_PREVIEW ('Gata de trimis', 'Cod RAR lipsa' etc.) — nicio
eticheta noua.
"""
_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 "confirm-form" in text or "Preview" in text, \
"Fragmentul de preview nu a fost randat"
# Pill-urile de stare trebuie sa contina un dot (span cu border-radius:99px)
assert "border-radius:99px" in text, \
"Dot-ul (border-radius:99px) nu a fost gasit in pill-urile de stare din preview"
# Etichetele umane din STARI_PREVIEW trebuie sa fie prezente (nicio eticheta noua)
# 'Gata de trimis' apare in rezumatul de stari (pill) sau in tabelul de randuri
assert "Gata de trimis" in text or "Cod RAR lipsa" in text or "Verifica valori" in text, \
"Etichetele umane din STARI_PREVIEW nu au fost gasite in preview"