5.15 (propagare design + dashboard editare) si 5.14 (mapare LLM distilata) inchise dupa /code-review high. 8 buguri reparate TDD: - HIGH modal nu se deschidea pe randul slim (base.html: trimitere-slim) - HIGH /repune trunchia prestatii (declaratie incompleta la RAR) -> iterare peste existing, codes pozitional - HIGH embeddings incarca model ~230MB degeaba pe corpus gol -> poarta has_corpus() - HIGH picker chips gol pe re-render eroare -> conn/account_id pe toate ramurile - MED obs re-derivat dupa stergere explicita -> _merge_override pastreaza obs='' - MED mapare salvata fara denumire poluă GOLD -> _record_gold_validation guard - MED typo nome_prestatie -> nume_prestatie in select /repune - MED bucketare timp +3h gresita iarna -> SQLite localtime + TZ=Europe/Bucharest Embeddings WIRE-uit functional (PRD #15, decizie user): ensure_embeddings_corpus construieste corpus din nomenclator, gated pe AUTOPASS_EMBEDDINGS_ENABLED (default off). Marime model corectata ~50MB->~230MB (estimare PRD gresita). Cleanup: hoist load_* din bucla bulk-fix; import re la top. Regresie: 1256 passed, 1 deselected (live), 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
497 lines
19 KiB
Python
497 lines
19 KiB
Python
"""Teste TDD pentru US-007 (PRD 5.15): formular editare slim.
|
|
|
|
RED -> implementare -> GREEN.
|
|
|
|
AC-uri verificate:
|
|
- 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):
|
|
"""US-007 AC1: formularul slim are UN SINGUR input name='vin'.
|
|
|
|
Fara camp 'Confirma VIN' — PRD si contractul RAR cer un singur VIN.
|
|
RED: daca ar exista doua campuri VIN sau un camp 'confirma_vin', testul pica.
|
|
"""
|
|
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):
|
|
"""US-007 AC2: formularul are textarea name='obs' pentru Observatii (US-005).
|
|
|
|
RED: obs nu e inca in _form_editare.html (US-005 adauga backend-ul, US-007 adauga UI-ul).
|
|
"""
|
|
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. "
|
|
"US-007 adauga campul obs (textarea) in _form_editare.html."
|
|
)
|
|
|
|
# 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):
|
|
"""US-007 AC3: submission cu cod_prestatie setat afiseaza chip cu hidden input.
|
|
|
|
RED: _form_editare.html nu are inca sectiunea de chips.
|
|
"""
|
|
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
|
|
|
|
# Trebuie sa existe un input (de obicei hidden) cu name="cod_prestatie" si valoarea "OE-1"
|
|
has_cod_prestatie_chip = bool(
|
|
re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', html) or
|
|
re.search(r'<input[^>]+value="OE-1"[^>]+name="cod_prestatie"', html)
|
|
)
|
|
assert has_cod_prestatie_chip, (
|
|
"Formularul trebuie sa contina un hidden input cu name='cod_prestatie' value='OE-1' "
|
|
"reprezentand chip-ul de prestatie. "
|
|
"US-007 adauga sectiunea de chips in _form_editare.html."
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Test 4: Endpoint /form-chips re-randeaza sectiunea chips #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_adauga_sterge_chip(client):
|
|
"""US-007 AC (E6): POST /form-chips cu action=add re-randeaza chips cu noul cod.
|
|
|
|
RED: endpoint-ul /form-chips nu exista inca.
|
|
"""
|
|
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)
|
|
resp = client.post(
|
|
"/form-chips",
|
|
data={
|
|
"csrf_token": csrf,
|
|
# Starea curenta: o operatie fara cod
|
|
"chip_op_service": ["Op-A"],
|
|
"chip_denumire": ["Schimb ulei motor"],
|
|
"cod_prestatie": [""], # nemaapat initial
|
|
# Actiunea
|
|
"chips_action": "add",
|
|
"chips_add_op_index": "0",
|
|
"chips_add_cod_0": "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 hidden input cu valoarea OE-1
|
|
has_hidden = bool(
|
|
re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', html) or
|
|
re.search(r'<input[^>]+value="OE-1"[^>]+name="cod_prestatie"', html)
|
|
)
|
|
assert has_hidden, (
|
|
"Dupa add, trebuie sa existe un input cu name='cod_prestatie' value='OE-1' "
|
|
f"in raspunsul /form-chips. html[:600]={html[:600]}"
|
|
)
|
|
|
|
|
|
def test_sterge_chip(client):
|
|
"""US-007: POST /form-chips cu action=remove sterge chip-ul la indexul dat.
|
|
|
|
RED: endpoint-ul /form-chips nu exista inca.
|
|
"""
|
|
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 chip-ul de la index 0 (OE-1 existent)
|
|
resp = client.post(
|
|
"/form-chips",
|
|
data={
|
|
"csrf_token": csrf,
|
|
# Starea curenta: OE-1 mapat pe Op-A
|
|
"chip_op_service": ["Op-A"],
|
|
"chip_denumire": ["Schimb ulei motor"],
|
|
"cod_prestatie": ["OE-1"],
|
|
# Actiunea: sterge indexul 0
|
|
"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 chip (input hidden cu acea valoare)
|
|
has_oe1_chip = bool(
|
|
re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', html) or
|
|
re.search(r'<input[^>]+value="OE-1"[^>]+name="cod_prestatie"', html)
|
|
)
|
|
assert not has_oe1_chip, (
|
|
"Dupa remove, OE-1 NU mai trebuie sa apara ca chip "
|
|
f"(hidden input cu cod_prestatie=OE-1). html[:500]={html[:500]}"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Test 5: Acelasi _form_editare.html in ambele modale #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_form_slim_in_ambele_modale():
|
|
"""US-007 AC4: _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):
|
|
"""US-007 D10c: 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.
|
|
RED: _form_editare.html nu are inca logica de reveal conditionat.
|
|
"""
|
|
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):
|
|
"""US-007 E6: POST /form-chips cu R-ODO in chips -> raspunsul marcheaza reveal odo.
|
|
|
|
RED: endpoint-ul nu exista + logica de reveal nu e implementata.
|
|
"""
|
|
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 al implementarii noi:
|
|
# chip-warn (clasa warn pe chip R-ODO) SAU data-has-r-odo="true"
|
|
# Aceste marcheri nu exista inainte de implementarea US-007.
|
|
has_r_odo_signal = (
|
|
"chip-warn" in html or # chip-ul R-ODO e stilat warn (CSS existent din US-002)
|
|
'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: Picker per operatie (E4 binding) -- format op-row #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_picker_per_operatie_in_form(client):
|
|
"""US-007 E4: operatie nemapata (needs_mapping) -> formularul afiseaza picker pe operatie.
|
|
|
|
RED: _form_editare.html nu are inca sectiunea de chips cu op-rows.
|
|
"""
|
|
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
|
|
|
|
# Operatia REVIZIE PERIODICA trebuie sa apara in form (op-row cu clasa specifica US-007)
|
|
# clasa "op-row" din CSS base.html (US-002) e adaugata NUMAI de chips_prestatii.html nou
|
|
has_op_row = "op-row" in html
|
|
assert has_op_row, (
|
|
"Formularul trebuie sa contina clasa 'op-row' (din US-002 CSS) "
|
|
"pentru picker-ul per-operatie (E4 binding). "
|
|
"Aceasta clasa e adaugata de _chips_prestatii.html in US-007. "
|
|
f"html[:600]={html[:600]}"
|
|
)
|
|
# Operatia REVIZIE PERIODICA trebuie sa apara in context op-row
|
|
assert "REVIZIE PERIODICA" in html, (
|
|
"Operatia 'REVIZIE PERIODICA' trebuie sa apara in formularul de editare (op-row). "
|
|
f"html[:500]={html[:500]}"
|
|
)
|