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:
286
tests/test_web_filtre.py
Normal file
286
tests/test_web_filtre.py
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user