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>
385 lines
14 KiB
Python
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"
|