feat(ux): import compact + preview format Trimiteri + navigatie + scoatere auto_send (5.11)

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>
This commit is contained in:
Claude Agent
2026-06-26 15:16:28 +00:00
parent 412102b9b1
commit 283299ff20
34 changed files with 3079 additions and 389 deletions

286
tests/test_web_filtre.py Normal file
View File

@@ -0,0 +1,286 @@
"""Teste US-004 (PRD 5.11): Rand filtre Trimiteri — layout + stil ca referinta.
TDD — testele sunt scrise INAINTE de implementare (RED), apoi devin GREEN.
Verifica:
- Quick-pills de data (Azi/7 zile/30 zile) in STANGA, inainte de campul vehicul si de pills-stare
- Pill-urile folosesc stil uniform (color-mix la hover, nu filter:brightness)
- Quick-pills seteaza data_de/data_pana si reincarca lista pastrand starea activa
"""
from __future__ import annotations
import json
import os
import re
import tempfile
import pytest
from starlette.testclient import TestClient
# ============================================================
# Helpers
# ============================================================
def _create_account_user(email: str, password: str = "parolasecreta10"):
from app.accounts import create_account
from app.users import create_user
from app.db import get_connection
conn = get_connection()
try:
acct_id = create_account(conn, f"Service {email}", active=True)
create_user(conn, acct_id, email, password)
return acct_id
finally:
conn.close()
def _login(client, email: str, password: str = "parolasecreta10") -> None:
resp = client.get("/login")
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
assert m, "csrf_token negasit pe /login"
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)})
assert resp.status_code == 303, f"Login esuat: {resp.status_code}"
def _ins(acct: int, *, status: str = "needs_mapping") -> int:
from app.db import get_connection
conn = get_connection()
try:
cur = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) VALUES (?, ?, ?, ?)",
(
f"k-us004-{os.urandom(4).hex()}",
acct,
status,
json.dumps({
"vin": "WVWZZZ1KZAW009999",
"nr_inmatriculare": "B001TST",
"data_prestatie": "2026-06-20",
"odometru_final": "100000",
"prestatii": [{"cod_prestatie": "OE-1"}],
}),
),
)
conn.commit()
return int(cur.lastrowid)
finally:
conn.close()
# ============================================================
# Fixture
# ============================================================
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "filtre_us004.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
from app.config import get_settings
get_settings.cache_clear()
from app.web import ratelimit
ratelimit._hits.clear()
from app.main import app
with TestClient(app, follow_redirects=False) as c:
yield c
ratelimit._hits.clear()
get_settings.cache_clear()
# ============================================================
# test_pill_uri_in_stanga_controalelor
# ============================================================
def test_pill_uri_in_stanga_controalelor(client):
"""Quick-pills de data (Azi/7 zile/30 zile) apar in STANGA formularului de filtre.
Ordinea in DOM: quick-pills → camp cautare vehicul → pills stare.
Pill-urile de stare NU mai stau izolate la dreapta butonului Filtreaza cu margin-left:auto
pe un span separat — layout-ul e controlat explicit prin pozitia in form.
"""
acct = _create_account_user("stanga@test.com")
_ins(acct, status="needs_mapping")
_login(client, "stanga@test.com")
resp = client.get("/?tab=acasa")
assert resp.status_code == 200
html = resp.text
# Quick-pills trebuie sa fie prezente in forma
assert "Azi" in html, "Quick-pill 'Azi' trebuie sa fie prezent in bara de filtre"
assert "7 zile" in html, "Quick-pill '7 zile' trebuie sa fie prezent in bara de filtre"
assert "30 zile" in html, "Quick-pill '30 zile' trebuie sa fie prezent in bara de filtre"
# DOM order: quick-pills STANGA (index mai mic in HTML) fata de campul vehicul
idx_azi = html.find("Azi")
idx_vehicul = html.find('id="f-vehicul"')
assert idx_azi != -1, "'Azi' nu s-a gasit in HTML"
assert idx_vehicul != -1, "'f-vehicul' nu s-a gasit in HTML"
assert idx_azi < idx_vehicul, (
"Quick-pill 'Azi' trebuie sa apara INAINTE de campul f-vehicul in DOM (stanga)"
)
# Pills stare (pills-categorii) la DREAPTA (dupa vehicul)
idx_pills_cat = html.find('id="pills-categorii"')
assert idx_pills_cat != -1, "pills-categorii nu s-a gasit in HTML"
assert idx_vehicul < idx_pills_cat, (
"Campul vehicul trebuie sa apara INAINTE de pills-categorii in DOM (pills-stare la dreapta)"
)
# Quick-pills apar si inainte de pills-categorii (stanga totala)
assert idx_azi < idx_pills_cat, (
"Quick-pills de data trebuie sa apara INAINTE de pills-categorii"
)
# ============================================================
# test_pill_categorie_stil_uniform
# ============================================================
def test_pill_categorie_stil_uniform(client):
"""Pill-urile au un singur stil uniform: hover cu color-mix, activ suprima hover.
- Hover trebuie sa foloseasca color-mix(in srgb, currentColor 12%, transparent)
si NU filter:brightness (care devenea rosu plin si ilizibil).
- Focus :focus-visible pastrat pe pill-cat.
- Pill-cat-reset activ = --accent; pill-cat activ = culoarea categoriei (nu toate accent).
"""
acct = _create_account_user("stil@test.com")
_login(client, "stil@test.com")
resp = client.get("/")
assert resp.status_code == 200
html = resp.text
# CSS-ul trebuie sa contina color-mix pentru hover pe pill-cat
assert "color-mix" in html, (
"pill-cat:hover trebuie sa foloseasca color-mix, nu filter:brightness"
)
# CSS-ul NU trebuie sa foloseasca filter:brightness pe .pill-cat:hover
m = re.search(r'\.pill-cat:hover\s*\{([^}]*)\}', html)
if m:
hover_rule = m.group(1)
assert "brightness" not in hover_rule, (
f"pill-cat:hover NU trebuie sa contina filter:brightness — regula gasita: {hover_rule}"
)
# focus-visible pastrat pe pill-cat
assert "pill-cat:focus-visible" in html, (
"pill-cat trebuie sa pastreze regula :focus-visible cu outline"
)
# Pill-cat-reset activ foloseste --accent (nu culoarea categoriei)
assert "pill-cat-reset" in html, "Clasa pill-cat-reset trebuie sa existe pentru butonul Toate"
assert "var(--accent)" in html, (
"Pill Toate activ trebuie sa foloseasca var(--accent)"
)
# ============================================================
# test_quick_pills_data_seteaza_interval
# ============================================================
def test_quick_pills_data_seteaza_interval(client):
"""Quick-pills de data seteaza data_de/data_pana (preset) si reincarca lista HTMX.
Pastrand pill-ul de stare activ: setDataRange NU schimba campul #f-status.
"""
acct = _create_account_user("datepill@test.com")
_ins(acct, status="needs_mapping")
_login(client, "datepill@test.com")
resp = client.get("/?tab=acasa")
assert resp.status_code == 200
html = resp.text
# Trebuie sa existe un mecanism JS care seteaza data_de si data_pana
assert "setDataRange" in html, (
"Quick-pills trebuie sa apeleze setDataRange (functie JS pentru setarea intervalului de date)"
)
# setDataRange trebuie sa seteze campul data_de/data_pana (prin id sau name)
assert "f-data-de" in html or "data_de" in html, (
"setDataRange trebuie sa seteze campul data_de (prin id f-data-de sau name data_de)"
)
assert "f-data-pana" in html or "data_pana" in html, (
"setDataRange trebuie sa seteze campul data_pana (prin id f-data-pana sau name data_pana)"
)
# Lista trebuie sa se reincarce prin form (HTMX) la click pe quick-pill
assert "/_fragments/submissions" in html, (
"Formularul de filtre trebuie sa trimita catre /_fragments/submissions"
)
# setDataRange NU trebuie sa schimbe campul de status (pastreaza pill-ul de stare activ)
# Verificam ca in JS-ul setDataRange nu se face `hs.value = ` (schimbare status)
# prin cautarea functiei in HTML
idx_fn = html.find("setDataRange")
assert idx_fn != -1
# Extrage corpul functiei (pana la urmatoarea definitie de functie mare)
fn_body = html[idx_fn:idx_fn + 800]
assert "f-status" not in fn_body or "hs.value" not in fn_body[:fn_body.find("f-data")], (
"setDataRange NU trebuie sa modifice campul f-status (pastreaza filtrul de stare)"
)
# ============================================================
# test_custom_pill_prezent_si_dezvaluie_campuri
# ============================================================
def test_custom_pill_prezent_si_dezvaluie_campuri(client):
"""Butonul Custom este al 4-lea quick-pill si dezvaluie campurile de data manuala.
AC US-004: Azi / 7 zile / 30 zile / Custom (4 quick-pills).
Custom NU seteaza un preset; dezvaluie #custom-date-fields cu focus pe #f-data-de.
Campurile #f-data-de/#f-data-pana sunt de tip 'date' (nu hidden) pentru interactiune.
"""
acct = _create_account_user("custom@test.com")
_ins(acct, status="needs_mapping")
_login(client, "custom@test.com")
resp = client.get("/?tab=acasa")
assert resp.status_code == 200
html = resp.text
# Butonul Custom trebuie sa fie prezent in quick-pills
assert "Custom" in html, "Butonul 'Custom' trebuie sa fie prezent in bara de filtre (4 quick-pills)"
# Butonul Custom apeleaza setDataRange cu 'custom'
assert "setDataRange" in html and "'custom'" in html, (
"Butonul Custom trebuie sa apeleze setDataRange(this,'custom')"
)
# In ramura 'custom' din JS, NU se apeleaza requestSubmit/form.submit
# (se dezvaluie campurile; utilizatorul introduce datele si form-ul submite la change)
# Cautam in JS (range === 'custom'), nu in atributul onclick al butonului
idx_fn = html.find("range === 'custom'")
assert idx_fn != -1, "Conditia `range === 'custom'` trebuie sa existe in JS (setDataRange)"
# Cautam in blocul imediat urmator conditiei: trebuie sa apara 'return'
# INAINTE de 'requestSubmit' (dovada ca nu submite automat in ramura custom)
block_custom = html[idx_fn:idx_fn + 500]
idx_return = block_custom.find("return")
idx_submit = block_custom.find("requestSubmit")
assert idx_return != -1, (
"Ramura 'custom' din setDataRange trebuie sa contina 'return' pentru a nu submite automat"
)
assert idx_submit == -1 or idx_return < idx_submit, (
"Ramura 'custom' NU trebuie sa apeleze requestSubmit inainte de 'return'"
)
# Campurile de data trebuie sa existe si sa fie de tip 'date' (nu hidden)
# pentru ca utilizatorul sa le poata interactiona in modul Custom
import re
m_de = re.search(r'<input[^>]+id="f-data-de"[^>]*>', html)
assert m_de, "Input #f-data-de negasit in HTML"
tag_de = m_de.group(0)
assert 'type="hidden"' not in tag_de, (
"Input #f-data-de NU trebuie sa fie type='hidden' — trebuie sa fie tip 'date' "
"pentru interactiune in modul Custom"
)