Files
rar-autopass/tests/test_web_form_editare_slim.py
Claude Agent 56ebe1f7cb feat(editor): editor prestatii unificat in modal - un select, un rand de chips (PRD 5.23)
Inlocuieste cele 3 controale de asociere cod RAR cu un singur rand de chips
+ un singur select care adauga instant la change. Stare = un hidden chips_state
JSON versionat; post_form_chips redus la 2 actiuni (add/remove). Optgroup
Sugestii (fuzzy/k-NN) + optiune "Nu se declara la RAR" in select, cu tinta
implicita evidentiata si placeholder care o numeste. exclus persistat prin
payload_json (treapta noua de precedenta in resolve_prestatii, round-trip complet).

Siguranta: itemii exclusi sunt scosi din payload la momentul trimiterii
(worker split_prestatii_excluse inainte de build_rar_payload + filtru defensiv),
ca sa nu ajunga NICIODATA la RAR ca codPrestatie:null.

Suita: 1680 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:45:34 +00:00

471 lines
18 KiB
Python

"""Teste formular editare slim.
Verificari:
- Un singur camp VIN (fara "Confirma VIN").
- Textarea obs (Observatii) prezent in formular.
- Chips multi-select prestatii cu hidden inputs name="cod_prestatie".
- Endpoint /form-chips re-randeaza sectiunea chips (add/remove).
- Acelasi _form_editare.html in ambele modale (trimitere detaliu + editare preview).
- Reveal dinamic odometru initial cand chips contin R-ODO/I-ODO.
"""
from __future__ import annotations
import json
import os
import re
import tempfile
from pathlib import Path
import pytest
from starlette.testclient import TestClient
TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "app" / "web" / "templates"
# --------------------------------------------------------------------------- #
# Fixtures #
# --------------------------------------------------------------------------- #
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "slim_form.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()
# --------------------------------------------------------------------------- #
# Helpere #
# --------------------------------------------------------------------------- #
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, "Service Test", 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 nu gasit in login"
resp = client.post(
"/login",
data={"email": email, "parola": password, "csrf_token": m.group(1)},
)
assert resp.status_code == 303
def _csrf(client) -> str:
resp = client.get("/?tab=coada")
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
assert m, "csrf_token nu gasit in dashboard"
return m.group(1)
def _insert(acct: int, *, status: str, payload: dict) -> 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-{os.urandom(6).hex()}", acct, status, json.dumps(payload)),
)
conn.commit()
return int(cur.lastrowid)
finally:
conn.close()
def _seed_cod(cod: str, denumire: str = "Prestatie test") -> None:
from app.db import get_connection
conn = get_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
(cod, denumire),
)
conn.commit()
finally:
conn.close()
def _payload_needs_data_cu_cod(vin: str = "WVWZZZ1JZXW0US007A") -> dict:
"""Payload needs_data: cod RAR setat, dar odometru_final gol."""
return {
"vin": vin,
"nr_inmatriculare": "B200AA",
"data_prestatie": "2026-06-20",
"odometru_final": "", # gol -> needs_data
"prestatii": [{"cod_prestatie": "OE-1", "cod_op_service": "Op-A", "denumire": "Schimb ulei"}],
}
def _payload_cu_ops(vin: str, ops: list[tuple]) -> dict:
"""Payload cu prestatii avand cod_op_service (needs_mapping)."""
return {
"vin": vin,
"nr_inmatriculare": "B100AAA",
"data_prestatie": "2026-06-10",
"odometru_final": "50000",
"prestatii": [
{"cod_op_service": op, "denumire": den}
for op, den in ops
],
}
def _payload_cu_r_odo(vin: str = "WVWZZZ1JZXW0RODO1") -> dict:
"""Payload needs_data cu R-ODO in chips — declanseaza reveal odometru initial."""
return {
"vin": vin,
"nr_inmatriculare": "B300RO",
"data_prestatie": "2026-06-20",
"odometru_final": "39000",
# odometru_initial ABSENT -> needs_data cand R-ODO
"prestatii": [{"cod_prestatie": "R-ODO", "cod_op_service": "", "denumire": "Revizie odometru"}],
}
# --------------------------------------------------------------------------- #
# Test 1: UN SINGUR camp VIN (fara "Confirma VIN") #
# --------------------------------------------------------------------------- #
def test_un_singur_vin(client):
"""Formularul slim are UN SINGUR input name='vin'.
Fara camp 'Confirma VIN' — contractul RAR cere un singur VIN.
"""
acct = _create_account_user("vin.unic@test.com")
_login(client, "vin.unic@test.com")
_seed_cod("OE-1")
sid = _insert(acct, status="needs_data", payload=_payload_needs_data_cu_cod())
resp = client.get(f"/_fragments/trimitere/{sid}")
assert resp.status_code == 200, resp.text[:300]
html = resp.text
# Exact un singur input cu name="vin"
vin_inputs = re.findall(r'<input[^>]+name="vin"[^>]*>', html)
assert len(vin_inputs) == 1, (
f"Trebuie exact UN input name='vin', gasit {len(vin_inputs)}: {vin_inputs}"
)
# Fara camp "Confirma VIN" sau "confirma_vin"
assert "confirma_vin" not in html.lower(), (
"Formular NU trebuie sa aiba camp 'confirma_vin' (VIN unic per contract RAR)"
)
assert "confirma vin" not in html.lower(), (
"Formular NU trebuie sa afiseze eticheta 'confirma vin'"
)
# --------------------------------------------------------------------------- #
# Test 2: Camp Observatii (textarea name="obs") #
# --------------------------------------------------------------------------- #
def test_camp_observatii_prezent(client):
"""Formularul are textarea name='obs' pentru Observatii."""
acct = _create_account_user("obs.forma@test.com")
_login(client, "obs.forma@test.com")
_seed_cod("OE-1")
sid = _insert(acct, status="needs_data", payload=_payload_needs_data_cu_cod())
resp = client.get(f"/_fragments/trimitere/{sid}")
assert resp.status_code == 200
html = resp.text
# textarea cu name="obs" trebuie sa existe
has_textarea_obs = bool(
re.search(r'<textarea[^>]+name="obs"', html) or
re.search(r'<textarea[^>]*name=["\']obs["\']', html)
)
assert has_textarea_obs, (
"Formularul trebuie sa contina <textarea name='obs'> pentru Observatii."
)
# Eticheta "Observatii" sau "obs" vizibila
assert re.search(r'[Oo]bservat', html), (
"Formularul trebuie sa afiseze eticheta 'Observatii'"
)
# --------------------------------------------------------------------------- #
# Test 3: Chips multi-select prestatii (hidden inputs name="cod_prestatie") #
# --------------------------------------------------------------------------- #
def test_chips_multi_select_prestatii(client):
"""Submission cu cod_prestatie setat afiseaza chip cu OE-1 in starea hidden chips_state."""
acct = _create_account_user("chips.test@test.com")
_login(client, "chips.test@test.com")
_seed_cod("OE-1", "Schimb ulei motor")
sid = _insert(acct, status="needs_data", payload=_payload_needs_data_cu_cod())
resp = client.get(f"/_fragments/trimitere/{sid}")
assert resp.status_code == 200
html = resp.text
# Starea chips traieste intr-UN SINGUR hidden JSON versionat (D4, PRD 5.23).
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
assert m, "Formularul trebuie sa contina hidden input name='chips_state'"
payload = json.loads(m.group(1).replace("&#34;", '"'))
coduri = [it["cod"] for it in payload["items"]]
assert "OE-1" in coduri, (
f"chips_state trebuie sa contina codul OE-1 al prestatiei. items={payload['items']}"
)
# --------------------------------------------------------------------------- #
# Test 4: Endpoint /form-chips re-randeaza sectiunea chips #
# --------------------------------------------------------------------------- #
def test_adauga_sterge_chip(client):
"""POST /form-chips cu action=add re-randeaza chips cu noul cod."""
acct = _create_account_user("form.chips@test.com")
_login(client, "form.chips@test.com")
_seed_cod("OE-1", "Schimb ulei motor")
csrf = _csrf(client)
# POST /form-chips: adauga OE-1 la prima operatie (index 0), stare curenta = un
# singur hidden chips_state (JSON versionat, D4).
state = json.dumps({"v": 1, "items": [
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
]})
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"chips_state": state,
"chips_action": "add",
"chips_target_index": "0",
"chips_pick": "OE-1",
},
)
assert resp.status_code == 200, f"/form-chips a returnat {resp.status_code}: {resp.text[:400]}"
html = resp.text
# Dupa add, chip-ul cu OE-1 trebuie sa fie in HTML
assert "OE-1" in html, (
f"Dupa add, OE-1 trebuie sa apara in raspunsul /form-chips. html[:500]={html[:500]}"
)
# Si in starea hidden chips_state
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
assert m, f"hidden chips_state lipsa. html[:600]={html[:600]}"
payload = json.loads(m.group(1).replace("&#34;", '"'))
assert payload["items"][0]["cod"] == "OE-1", (
f"Dupa add, chips_state trebuie sa contina OE-1. items={payload['items']}"
)
def test_sterge_chip(client):
"""POST /form-chips cu action=remove sterge codul chip-ului la indexul dat."""
acct = _create_account_user("form.chips.del@test.com")
_login(client, "form.chips.del@test.com")
_seed_cod("OE-1", "Schimb ulei motor")
csrf = _csrf(client)
# POST /form-chips: sterge codul chip-ului de la index 0 (OE-1 existent, mapat pe Op-A)
state = json.dumps({"v": 1, "items": [
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
]})
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"chips_state": state,
"chips_action": "remove",
"chips_remove_index": "0",
},
)
assert resp.status_code == 200, f"/form-chips remove a returnat {resp.status_code}: {resp.text[:400]}"
html = resp.text
# Dupa remove, OE-1 nu mai apare ca cod in starea chips (operatia RAMANE, nemapata)
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
assert m
payload = json.loads(m.group(1).replace("&#34;", '"'))
coduri = [it["cod"] for it in payload["items"]]
assert "OE-1" not in coduri, (
f"Dupa remove, OE-1 NU mai trebuie sa apara ca cod in chips_state. items={payload['items']}"
)
assert payload["items"][0]["op"] == "Op-A", "Operatia trebuie sa ramana (doar codul dispare)"
# --------------------------------------------------------------------------- #
# Test 5: Acelasi _form_editare.html in ambele modale #
# --------------------------------------------------------------------------- #
def test_form_slim_in_ambele_modale():
"""_form_editare.html e inclus ATAT in _trimitere_detaliu.html
CAT SI in _editare_preview_modal.html (fara duplicare logica).
"""
sursa_detaliu = (TEMPLATES_DIR / "_trimitere_detaliu.html").read_text(encoding="utf-8")
sursa_preview = (TEMPLATES_DIR / "_editare_preview_modal.html").read_text(encoding="utf-8")
assert "_form_editare.html" in sursa_detaliu, (
"_trimitere_detaliu.html trebuie sa includa _form_editare.html (DRY)"
)
assert "_form_editare.html" in sursa_preview, (
"_editare_preview_modal.html trebuie sa includa _form_editare.html (DRY)"
)
# --------------------------------------------------------------------------- #
# Test 6: Reveal dinamic odometru initial la R-ODO (D10c, E6) #
# --------------------------------------------------------------------------- #
def test_reveal_odometru_la_R_ODO(client):
"""Cand chips contin R-ODO, campul odometru initial e dezvaluit
si marcat ca necesar (bordura warn + label).
Cand NU contine R-ODO, sectiunea e ascunsa/discreta.
"""
acct = _create_account_user("odo.reveal@test.com")
_login(client, "odo.reveal@test.com")
_seed_cod("R-ODO", "Revizie odometru")
_seed_cod("OE-1", "Schimb ulei")
# Submission cu R-ODO -> reveal activ
sid_rodo = _insert(acct, status="needs_data", payload=_payload_cu_r_odo())
resp_rodo = client.get(f"/_fragments/trimitere/{sid_rodo}")
assert resp_rodo.status_code == 200
html_rodo = resp_rodo.text
# Cand R-ODO e prezent: campul odometru_initial trebuie sa fie dezvaluit
# cu un MARKER SPECIFIC al implementarii noi:
# - clasa "odo-initial-warn" pe div-ul sectiunii
# - SAU textul "necesar pentru r-odo" in label (exact, case-insensitive)
# Aceste lucruri NU exista in implementarea curenta (care arata mereu campul fara marker).
has_r_odo_reveal = (
"odo-initial-warn" in html_rodo or
"necesar pentru r-odo" in html_rodo.lower() or
"necesar pentru i-odo" in html_rodo.lower()
)
assert has_r_odo_reveal, (
"Cand chips contin R-ODO, formularul trebuie sa dezvaluie sectiunea odometru initial "
"cu clasa 'odo-initial-warn' sau text 'necesar pentru R-ODO'. "
f"html_rodo[:800]={html_rodo[:800]}"
)
# Submission fara R-ODO -> reveal inactiv (campul discret sau ascuns)
payload_fara_rodo = {
"vin": "WVWZZZ1JZXW0NOROD1",
"nr_inmatriculare": "B100AA",
"data_prestatie": "2026-06-20",
"odometru_final": "", # needs_data din alt motiv
"prestatii": [{"cod_prestatie": "OE-1", "cod_op_service": "", "denumire": "Schimb ulei"}],
}
sid_norm = _insert(acct, status="needs_data", payload=payload_fara_rodo)
resp_norm = client.get(f"/_fragments/trimitere/{sid_norm}")
assert resp_norm.status_code == 200
html_norm = resp_norm.text
# Fara R-ODO: "necesar pentru R-ODO" nu trebuie sa apara
has_r_odo_text_in_norm = (
"necesar pentru r-odo" in html_norm.lower() or
"necesar pentru i-odo" in html_norm.lower() or
"odo-initial-warn" in html_norm
)
assert not has_r_odo_text_in_norm, (
"Fara R-ODO in chips, formularul NU trebuie sa arate 'necesar pentru R-ODO'. "
f"html_norm[:800]={html_norm[:800]}"
)
# --------------------------------------------------------------------------- #
# Test 7: /form-chips via HTMX returneaza sectiune si cu reveal R-ODO #
# --------------------------------------------------------------------------- #
def test_form_chips_reveal_r_odo(client):
"""POST /form-chips cu R-ODO in chips -> raspunsul marcheaza reveal odo."""
acct = _create_account_user("chips.rodo@test.com")
_login(client, "chips.rodo@test.com")
_seed_cod("R-ODO", "Revizie odometru")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
# Stare curenta: R-ODO deja in chips (flat)
"chip_op_service": [""],
"chip_denumire": ["Revizie odometru"],
"cod_prestatie": ["R-ODO"],
# Nicio actiune — justa re-randare
"chips_action": "",
},
)
assert resp.status_code == 200, f"/form-chips R-ODO a returnat {resp.status_code}"
html = resp.text
# In sectiunea chip, R-ODO trebuie sa apara (chip warn sau chip normal)
assert "R-ODO" in html, (
f"R-ODO trebuie sa apara in raspunsul /form-chips. html[:500]={html[:500]}"
)
# Indicatorul has_r_odo trebuie sa fie un marker SPECIFIC:
# chip-warn (clasa warn pe chip R-ODO) SAU data-has-r-odo="true"
has_r_odo_signal = (
"chip-warn" in html or # chip-ul R-ODO e stilat warn
'data-has-r-odo="true"' in html # sau data-attr explicit
)
assert has_r_odo_signal, (
"Cand R-ODO e in chips, raspunsul /form-chips trebuie sa contina "
"class='chip-warn' pe chip-ul R-ODO sau data-has-r-odo='true'. "
f"html[:600]={html[:600]}"
)
# --------------------------------------------------------------------------- #
# Test 8: Chip warning pentru operatie nemapata (control unificat, PRD 5.23) #
# --------------------------------------------------------------------------- #
def test_picker_per_operatie_in_form(client):
"""Operatie nemapata (needs_mapping) -> formularul afiseaza chip warning + selectul unic.
PRD 5.23 US-002: controlul unificat elimina `.op-row` (rand per operatie cu
picker propriu); operatia nemapata devine un chip warning in containerul
unic `.chips`, iar selectul e unul singur (`chips_pick`, id `chips-picker`).
"""
acct = _create_account_user("picker.op@test.com")
_login(client, "picker.op@test.com")
# NU seed-uim nicio mapare -> operatia ramane nemapata
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
"WVWZZZ1JZXW0OP001",
[("REVIZIE PERIODICA", "Revizie periodica anuala")],
))
resp = client.get(f"/_fragments/trimitere/{sid}")
assert resp.status_code == 200
html = resp.text
# Controlul unificat (PRD 5.23): fara .op-row, UN singur container .chips.
assert "op-row" not in html, (
"op-row a fost eliminat de controlul unificat (PRD 5.23 US-002). "
f"html[:600]={html[:600]}"
)
assert 'class="chips"' in html
assert 'id="chips-picker"' in html
# Operatia REVIZIE PERIODICA trebuie sa apara ca chip warning
assert "REVIZIE PERIODICA" in html, (
"Operatia 'REVIZIE PERIODICA' trebuie sa apara in formularul de editare "
f"(chip warning). html[:500]={html[:500]}"
)