feat(5.13): carduri compacte mobil/tableta + fix editare preview (OOB tr) + toast

Dogfood pe import + Trimiteri (mobil/tableta <1024px), pur CSS + markup, backend
trimitere neatins:

- Card compact real pentru .tabel-trimiteri (preview + Trimiteri): vehicul=titlu,
  stare=pill dreapta-sus, operatie+cod, meta data/km muted, nota mica. Inlocuieste
  stiva generica eticheta+valoare (carduri de ~450px -> ~135px). Anuleaza regula
  desktop tr.trimitere-row > td{padding:11px} in blocul compact.
- FIX editare preview: OOB swap pe <tr> esua tacit in htmx 1.9 (un <tr> brut se
  pierde la parsarea unui fragment fara context de tabel) -> randul ramanea cu
  starea veche dupa salvare. Inlocuit cu reload complet al preview-ului prin
  HX-Trigger:reincarcaPreview + detalii randSalvat. /editeaza si /confirma-review
  folosesc helper-ul _raspuns_rand_salvat.
- Feedback post-salvare: toast global "Randul N actualizat · <stare>" + scroll +
  flash pe randul actualizat (base.html window.arataToast + listener randSalvat).
- Modal editare: Salveaza + Anuleaza pe acelasi rand (sistem .act): desktop text,
  mobil doua iconite Lucide 44px alaturate (save/x). Macro icon('x') + .act-primary.
- Randuri deja-trimise/duplicate colapsate implicit in preview + toggle "Arata N".
- Select "Operatii de mapat" full-width pe mobil (nu mai iese din viewport).
- Bara de filtre Trimiteri adaptata mobil: pills pe banda cu scroll orizontal,
  cautare vehicul proeminenta (nu 8 butoane full-width stivuite).
- Nota preview = culoarea camp-fix (accent) ca sa atraga atentia; hint-urile
  camp-fix per-camp scoase (campul Note e self-explanatory).
- Confirmare trimitere: scos campul email (Declarant); text mai clar
  ("Confirma numarul din N gata de trimis"). Backend confirmed_by ramane optional.

Teste: contractul OOB (rupt in browser) inlocuit cu noul contract
(reincarcaPreview + randSalvat) in test_web_preview_edit / test_preview_edit_ui /
test_import_review. Suita: 992 passed (exclus live).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-27 23:34:33 +00:00
parent bafaf05e83
commit 8d4ff3400e
18 changed files with 633 additions and 303 deletions

View File

@@ -316,9 +316,10 @@ def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client):
Verifica:
- Raspuns 200
- reviewed=1 in DB
- Raspuns contine OOB cu pill 'Gata de trimis' (starea ok)
- Header HX-Trigger-After-Settle: inchideModal
- HX-Trigger: randSalvat cu noua stare 'Gata de trimis' (pentru toast)
- HX-Trigger: reincarcaPreview + HX-Trigger-After-Settle: inchideModal
"""
import json as _json
_seed_op1()
iid = _upload_and_preview_needs_review(client)
@@ -334,14 +335,11 @@ def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client):
assert _get_reviewed(iid, 0) == 1, \
"reviewed trebuie sa fie 1 in DB dupa confirmare"
# Raspuns contine OOB cu randul actualizat
html = r.text
assert 'id="preview-row-0"' in html or "preview-row-0" in html, \
"Raspunsul trebuie sa contina randul actualizat (OOB)"
# Starea a devenit ok
assert "Gata de trimis" in html or "s-ok" in html, \
"Dupa confirmare, randul trebuie sa fie ok (pill 'Gata de trimis')"
# Contractul nou: reload preview + randSalvat cu noua stare (nu OOB pe <tr>).
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
assert trig.get("reincarcaPreview") is True, "confirma-review trebuie sa ceara reincarcaPreview"
assert trig.get("randSalvat", {}).get("stare") == "Gata de trimis", \
"Dupa confirmare, randSalvat.stare trebuie sa fie 'Gata de trimis' (pentru toast)"
# Modal se inchide
trigger = r.headers.get("HX-Trigger-After-Settle", "")
@@ -585,16 +583,17 @@ def test_confirma_review_form_nu_foloseste_hx_swap_none():
)
def test_confirma_review_raspuns_contine_script_updateN(client):
"""Bug B1 (functional): raspunsul POST confirma-review contine scriptul
updateN in payload-ul principal (nu doar OOB), astfel ca htmx il va executa
cand face swap in #detaliu-modal-body.
def test_confirma_review_cere_reincarcarea_preview(client):
"""Contractul nou (dogfood 5.13): confirma-review NU mai depinde de scriptul updateN
din payload (care, cu OOB pe <tr> rupt, lasa randul stale). Acum cere reincarcaPreview,
iar preview-ul reincarcat re-randeaza contorul si butonul de confirmare cu n_confirmat
corect server-side — deci problema B1 (n_confirmat stale -> 422) dispare structural.
Verifica:
- Raspuns 200
- Raspunsul contine 'window.updateN' (scriptul de recalcul contor)
- Raspunsul contine 'updateN' inainte de ultimul OOB-element (@script tag nu e OOB)
- HX-Trigger contine reincarcaPreview (reincarca contorul/confirmarea, fresh)
"""
import json as _json
_seed_op1()
iid = _upload_and_preview_needs_review(client)
@@ -602,17 +601,8 @@ def test_confirma_review_raspuns_contine_script_updateN(client):
r = client.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf})
assert r.status_code == 200, r.text
html = r.text
# Scriptul trebuie sa fie in raspuns
assert "window.updateN" in html or "updateN" in html, (
"Raspunsul confirma-review trebuie sa contina scriptul updateN "
"pentru ca htmx sa-l execute la swap in #detaliu-modal-body."
)
# Scriptul NU trebuie sa aiba hx-swap-oob (altfel nu ar fi executat nici asa)
script_idx = html.rfind("<script>")
assert script_idx >= 0, "Tag-ul <script> nu a fost gasit in raspuns"
script_content = html[script_idx:]
assert "hx-swap-oob" not in script_content, (
"Scriptul updateN NU trebuie sa aiba hx-swap-oob — trebuie sa fie in "
"continutul principal pentru executie."
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
assert trig.get("reincarcaPreview") is True, (
"confirma-review trebuie sa ceara reincarcaPreview — preview-ul reincarcat aduce "
"n_confirmat corect server-side (fara dependenta de scriptul updateN din payload)."
)

View File

@@ -95,17 +95,23 @@ def test_editeaza_intra_in_mod_editare_form_propriu(client):
assert 'name="data_prestatie"' in html and 'name="vin"' in html
def test_salveaza_reda_doar_randul(client):
"""POST editeaza: raspuns = fragmentul randului + OOB contoare, NU tot #import-section (D-3.1)."""
def test_salveaza_cere_reincarcare_si_toast(client):
"""POST editeaza: raspuns minimal + HX-Trigger(reincarcaPreview + randSalvat).
Contractul nou (dogfood 5.13): nu mai facem OOB swap pe <tr> (fragil in htmx 1.9 ->
randul ramanea cu starea veche). Raspunsul cere reincarcarea preview-ului si emite
detaliile randului salvat pentru toast/evidentiere."""
import json as _json
_seed_op1()
iid = _upload_and_preview(client)
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={"data_prestatie": "2026-06-10"})
assert r.status_code == 200
html = r.text
assert 'id="preview-row-0"' in html
# OOB pe rezumat (contoare), NU re-randarea sectiunii intregi.
assert 'id="preview-rezumat"' in html and 'hx-swap-oob="true"' in html
assert 'id="import-section"' not in html
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
assert trig.get("reincarcaPreview") is True
assert trig.get("randSalvat", {}).get("nr") == 1
# Raspunsul e doar un stub; randul real vine din reload-ul preview-ului.
assert 'id="preview-row-0"' not in r.text
assert 'id="import-section"' not in r.text
def test_enter_in_camp_editare_nu_declanseaza_confirm(client):

View File

@@ -232,21 +232,21 @@ def test_stepper_pas3_la_preview_direct_mapare_retinuta(client):
def test_stepper_marcheaza_pasii_facuti(client):
"""In preview (pas 3), pasii 1 si 2 sunt marcati ca facuti (clasa 'facut').
"""In preview (pas 3), pasii 1 si 2 sunt marcati ca facuti (clasa is-done).
Verifica prin prezenta clasei CSS sau a marcajului vizual de 'facut'.
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 "facut" trebuie sa apara pentru pasii 1 si 2 (index < pas curent)
assert "facut" in text, \
"Clasa/marcajul 'facut' nu a fost gasit in preview (pasii 1 si 2 ar trebui marcati ca facuti)"
# Numarul de aparitii: cel putin 2 pasi marcati ca facuti
count_facut = text.count("facut")
assert count_facut >= 2, \
f"Asteptat cel putin 2 pasi marcati ca 'facut' in preview, gasit {count_facut}"
# 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):

View File

@@ -1,9 +1,13 @@
"""Teste US-011 (PRD 5.10): butoane icon salvare/stergere + dirty state pe Mapari.
"""Teste US-011 (PRD 5.10): butoane salvare/stergere + dirty state pe Mapari.
Actualizat PRD 5.13: superseda sistemul .icon-btn din 5.10 -> sistem .act
(desktop text / mobil iconita 44px).
Cerinte:
- Butoane .icon-btn mereu vizibile pe rand (nu ascunse in kebab)
- Butoane .act mereu vizibile pe rand (nu ascunse in kebab)
- Meniu kebab (<details class="kebab">) eliminat
- aria-label descriptiv pe fiecare buton icon
- aria-label pe fiecare buton .act (label scurt: "Salveaza" / "Sterge")
- Iconita Lucide (.act-ic svg) prezenta in output
- data-dirty-form pe butonul de salvare (permite JS dirty-state)
"""
@@ -75,7 +79,10 @@ def client(monkeypatch):
def test_butoane_icon_vizibile_pe_rand_salvate(client):
"""Butoanele de salvare/stergere in 'Mapari salvate' au clasa icon-btn (mereu vizibile)."""
"""Butoanele de salvare/stergere in 'Mapari salvate' au clasele .act (mereu vizibile pe rand).
PRD 5.13: .icon-btn inlocuit cu sistemul .act (act-save / act-del).
"""
acct = _create_account_user("actiuni_icon@test.com")
_seed_saved_mapping(acct)
_login(client, "actiuni_icon@test.com")
@@ -84,9 +91,13 @@ def test_butoane_icon_vizibile_pe_rand_salvate(client):
assert resp.status_code == 200
html = resp.text
assert 'class="icon-btn' in html, (
"Butoanele de actiune din 'Mapari salvate' trebuie sa aiba clasa 'icon-btn' "
"(mereu vizibile pe rand, nu ascunse in kebab)."
assert 'class="act act-save"' in html, (
"Butonul de salvare din 'Mapari salvate' trebuie sa aiba clasa 'act act-save' "
"(mereu vizibil pe rand, nu ascuns in kebab)."
)
assert 'class="act act-del"' in html, (
"Butonul de stergere din 'Mapari salvate' trebuie sa aiba clasa 'act act-del' "
"(mereu vizibil pe rand, nu ascuns in kebab)."
)
@@ -109,7 +120,11 @@ def test_fara_kebab_meniu(client):
def test_butoane_cu_aria_label(client):
"""Butoanele icon-btn au aria-label descriptiv."""
"""Butoanele .act din 'Mapari salvate' au aria-label si iconita Lucide (.act-ic).
PRD 5.13: aria-label scurt ("Salveaza" / "Sterge") pe butonul .act;
iconita SVG cu clasa .act-ic (Lucide stroke) prezenta pentru mobil.
"""
acct = _create_account_user("actiuni_aria@test.com")
_seed_saved_mapping(acct)
_login(client, "actiuni_aria@test.com")
@@ -118,10 +133,13 @@ def test_butoane_cu_aria_label(client):
assert resp.status_code == 200
html = resp.text
icon_btns = re.findall(r'<button[^>]+class="icon-btn[^"]*"[^>]*>', html)
assert icon_btns, "Trebuie sa existe butoane cu clasa icon-btn in 'Mapari salvate'."
assert any('aria-label' in btn for btn in icon_btns), (
"Cel putin un buton icon-btn trebuie sa aiba atributul aria-label descriptiv."
act_btns = re.findall(r'<button[^>]+class="act[^"]*"[^>]*>', html)
assert act_btns, "Trebuie sa existe butoane cu clasa 'act' in 'Mapari salvate'."
assert any('aria-label' in btn for btn in act_btns), (
"Cel putin un buton .act trebuie sa aiba atributul aria-label."
)
assert 'class="act-ic"' in html, (
"Iconita Lucide (.act-ic svg) trebuie sa fie prezenta in output (afisata pe mobil)."
)

View File

@@ -241,17 +241,23 @@ def test_editeaza_preview_serveste_fragment_modal(client):
"Fragmentul modal nu trebuie sa contina confirm-form"
def test_salvare_preview_inchide_modal_si_oob_rand(client):
"""POST /_import/{id}/rand/0/editeaza cu date valide → HX-Trigger-After-Settle: inchideModal
+ OOB pe rand (#preview-row-0) si contoare (#preview-rezumat).
def test_salvare_preview_inchide_modal_si_reincarca(client):
"""POST /_import/{id}/rand/0/editeaza cu date valide → inchide modalul + reincarca preview-ul.
Contractul a fost schimbat (dogfood 5.13): OOB swap pe <tr> esua tacit in htmx 1.9
(un <tr> brut se pierde la parsarea fragmentului) -> randul ramanea cu starea veche.
Acum raspunsul NU mai contine OOB pe rand; emite HX-Trigger:
- reincarcaPreview -> #import-section isi reincarca preview-ul complet;
- randSalvat (cu nr + stare) -> toast + evidentiere in base.html;
iar HX-Trigger-After-Settle: inchideModal inchide modalul.
Verifica:
- Status 200
- Header HX-Trigger-After-Settle contine 'inchideModal'
- Raspuns contine OOB pentru randul actualizat (hx-swap-oob prezent)
- Raspuns contine OOB pentru rezumat (#preview-rezumat)
- NU re-randeaza intreaga sectiune (#import-section absent)
- HX-Trigger-After-Settle contine 'inchideModal'
- HX-Trigger contine 'reincarcaPreview' si 'randSalvat' (cu numarul randului)
- Raspunsul NU re-randeaza inline randul/sectiunea (reload via GET)
"""
import json as _json
_seed_op1()
iid = _upload_and_preview(client)
@@ -259,26 +265,21 @@ def test_salvare_preview_inchide_modal_si_oob_rand(client):
"data_prestatie": "2026-06-15",
})
assert r.status_code == 200, r.text
html = r.text
# Header de inchidere modal
trigger = r.headers.get("HX-Trigger-After-Settle", "")
assert "inchideModal" in trigger, \
f"Header HX-Trigger-After-Settle trebuie sa contina 'inchideModal', gasit: '{trigger}'"
trigger_settle = r.headers.get("HX-Trigger-After-Settle", "")
assert "inchideModal" in trigger_settle, \
f"HX-Trigger-After-Settle trebuie sa contina 'inchideModal', gasit: '{trigger_settle}'"
# OOB pe randul actualizat
assert 'id="preview-row-0"' in html, \
"Raspunsul trebuie sa contina randul actualizat (#preview-row-0)"
assert "hx-swap-oob" in html, \
"Raspunsul trebuie sa contina OOB swap"
trigger = _json.loads(r.headers.get("HX-Trigger", "{}"))
assert trigger.get("reincarcaPreview") is True, \
"HX-Trigger trebuie sa ceara reincarcarea preview-ului (reincarcaPreview)"
assert "randSalvat" in trigger, "HX-Trigger trebuie sa contina detaliile randului salvat"
assert trigger["randSalvat"]["nr"] == 1, "randSalvat.nr = numarul (1-based) al randului editat"
assert "stare" in trigger["randSalvat"], "randSalvat trebuie sa contina noua stare (pentru toast)"
# OOB pe rezumatul stari
assert 'id="preview-rezumat"' in html, \
"Raspunsul trebuie sa contina OOB pe #preview-rezumat"
# NU re-randeaza intreaga sectiune de import
assert 'id="import-section"' not in html, \
"Editarea randului NU trebuie sa re-randeze intreaga sectiune #import-section"
# Raspunsul e doar un stub invizibil — randul real vine din reload, nu din OOB.
assert 'id="preview-row-0"' not in r.text, \
"Noul contract NU mai face OOB swap pe rand (reload complet via reincarcaPreview)"
def test_anuleaza_nu_lasa_rand_orfan(client):

View File

@@ -65,6 +65,14 @@ def _login(client, email: str, password: str = "parolasecreta10") -> None:
assert resp.status_code == 303
def _bloc_mobil_principal(html: str) -> str:
"""Felie din CSS de la sentinel-ul blocului mobil principal pana la `</style>`."""
i = html.find("SENTINEL-TESTE-MOBIL")
assert i != -1, "Lipseste SENTINEL-TESTE-MOBIL in base.html (vezi PRD 5.13 Wave 0)"
j = html.find("</style>", i)
return html[i:(j if j != -1 else len(html))]
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
@@ -332,10 +340,7 @@ def test_header_elemente_nu_au_min_height_fix_pe_mobil(client):
"Blocul tableta nu reseteaza min-height pentru header"
# Blocul mobil (<768px) reseteaza si el min-height (regresie: nu a disparut).
# Folosim `{` ca sa nu potrivim mentionarile din comentarii CSS.
mobil_idx = html.find("@media (max-width:767px) {")
assert mobil_idx != -1
mobil = html[mobil_idx:mobil_idx + 5000]
mobil = _bloc_mobil_principal(html)
assert "min-height:0" in mobil, "Blocul mobil a pierdut resetarea min-height pe header"
@@ -348,10 +353,7 @@ def test_modal_full_screen_pe_mobil(client):
html = client.get("/?tab=acasa").text
# Regula CSS full-screen exista in blocul @media (max-width:767px) {.
# Folosim varianta cu `{` ca sa NU potrivim mentionarile din comentarii CSS.
mobil_idx = html.find("@media (max-width:767px) {")
assert mobil_idx != -1, "Nu exista bloc @media (max-width:767px) { in CSS"
mobil = html[mobil_idx:mobil_idx + 5000]
mobil = _bloc_mobil_principal(html)
assert "100vw" in mobil, "Dialogul nu are latime 100vw pe mobil"
assert "100vh" in mobil, "Dialogul nu are inaltime 100vh pe mobil"
# Butonul de inchidere >=44px (tinta touch) pe mobil.
@@ -363,3 +365,138 @@ def test_modal_full_screen_pe_mobil(client):
# Target swap pentru editare preview (US-006) exista in DOM.
assert 'id="detaliu-modal-body"' in html, \
"Target #detaliu-modal-body lipseste din base.html"
# ============================================================
# PRD 5.13: guard-uri responsive card mobil + sistem actiuni + stepper compact
# ============================================================
def test_card_mobil_fara_break_vertical_120px(client):
"""P0: blocul mobil principal NU mai forteaza min-width:120px pe eticheta
(cauza break-ului vertical caracter-cu-caracter). Eticheta stivuita deasupra
valorii: td::before cu display:block. Checkbox + # ascunse pe card."""
_create_account_user("card120@test.com")
_login(client, "card120@test.com")
html = client.get("/?tab=acasa").text
bloc = _bloc_mobil_principal(html)
# min-width:120px nu mai exista in blocul mobil.
assert "min-width:120px" not in bloc, \
"min-width:120px inca prezent in blocul mobil — cauzeaza break vertical"
# td::before stivuieste eticheta deasupra valorii (display:block).
assert ".tabel-trimiteri td::before" in bloc, \
".tabel-trimiteri td::before lipseste din blocul mobil"
assert "display:block" in bloc, \
"display:block lipseste din blocul mobil (eticheta nu e stivuita)"
# col-chk si col-id ascunse pe card (nu ocupa spatiu).
assert ".tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }" in bloc, \
"col-chk/col-id nu sunt ascunse in blocul mobil"
def test_sistem_act_desktop_text_mobil_icon(client):
"""Sistemul .act: desktop = iconita ascunsa (text vizibil),
mobil = text ascuns, iconita vizibila 44px (tinta touch)."""
_create_account_user("act_sys@test.com")
_login(client, "act_sys@test.com")
html = client.get("/?tab=acasa").text
bloc = _bloc_mobil_principal(html)
# Desktop: act-ic ascunsa implicit (display:none in regula de baza).
assert ".act .act-ic { width:18px; height:18px; display:none; }" in html, \
"Regula desktop care ascunde .act-ic lipseste din CSS"
# Mobil: text ascuns, iconita vizibila.
assert ".act .act-tx { display:none; }" in bloc, \
".act .act-tx nu e ascuns in blocul mobil"
assert ".act .act-ic { display:inline-block; }" in bloc, \
".act .act-ic nu devine vizibila in blocul mobil"
# .act are tinta touch >=44px pe mobil.
assert ".act { min-width:44px" in bloc, \
".act nu are min-width:44px in blocul mobil"
def _seed_saved_mapping_responsive(acct_id: int) -> None:
"""Insereaza o mapare salvata in operations_mapping (pentru test act aria-label)."""
from app.db import get_connection
conn = get_connection()
try:
conn.execute(
"INSERT INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) "
"VALUES (?, ?, ?, ?)",
(acct_id, "OP-RESP-99", "OE-1", 1),
)
conn.commit()
finally:
conn.close()
def test_act_btn_aria_label_invariant(client):
"""Invariant a11y: butoanele .act din Mapari au mereu aria-label
(accesibil in modul icon-only pe mobil) si iconita .act-ic."""
acct = _create_account_user("actaria@test.com")
_seed_saved_mapping_responsive(acct)
_login(client, "actaria@test.com")
html = client.get("/_fragments/mapari").text
# Butoane cu clasa act exista in fragmentul de mapari.
act_btns = re.findall(r'<button[^>]+class="act[^"]*"[^>]*>', html)
assert act_btns, "Trebuie sa existe butoane cu clasa 'act' in fragmentul Mapari"
# Fiecare buton .act are aria-label (accesibil cand textul e ascuns pe mobil).
assert any("aria-label" in btn for btn in act_btns), \
"Niciun buton .act nu are aria-label — inaccessibil in modul icon-only"
# Iconita .act-ic prezenta in markup (afisata pe mobil).
assert 'class="act-ic"' in html, \
"Iconita .act-ic lipseste din fragmentul Mapari"
def test_stepper_compact_clase(client):
"""Stepper compact (5.13): clasele stepper-track, stepper-collapsed,
stepper-progress prezente; textul 'Pasul N din 4' randat; vechile clase
stepper-pas-- absente; comutarea la <1024px declarata in CSS."""
_create_account_user("stepper@test.com")
_login(client, "stepper@test.com")
html = client.get("/?tab=acasa").text
# Elementele de structura ale stepper-ului compact.
assert "stepper-track" in html, "stepper-track lipseste din markup"
assert "stepper-collapsed" in html, "stepper-collapsed lipseste din markup"
assert "stepper-progress" in html, "stepper-progress lipseste din markup"
# Textul de progres in forma colapsata (<1024px).
assert "Pasul 1 din 4" in html, "Textul 'Pasul 1 din 4' lipseste din markup"
# Vechile clase anti-pattern cu stepper-pas-- nu mai exista.
assert "stepper-pas--" not in html, \
"Clasa veche stepper-pas-- inca prezenta — curata markup-ul"
# CSS declara comutarea la <1024px (track ascuns, collapsed afisat).
assert "@media (max-width:1024px)" in html, \
"Lipseste regula @media (max-width:1024px) pentru comutarea stepper-ului"
def test_liste_actionabile_o_coloana_pana_1024(client):
"""Guard scope (decizie 5.13): listele actionabile raman O COLOANA
pana la 1024px — fara grila 2/rand (repeat(2)). Blocul tableta 768-1024px
cardifica (thead ascuns, card per rand)."""
_create_account_user("ocoloana@test.com")
_login(client, "ocoloana@test.com")
html = client.get("/?tab=acasa").text
# Nicaieri in CSS nu apare grila 2/rand (repeat(2, ...)).
assert "repeat(2" not in html, \
"CSS contine repeat(2 — listele actionabile NU trebuie sa fie 2/rand pana la 1024px"
# Exista blocul tableta (768-1024px).
assert "@media (min-width:768px) and (max-width:1024px)" in html, \
"Lipseste blocul @media tableta (min-width:768px) and (max-width:1024px)"
# Blocul tableta cardifica listele (thead ascuns = card per rand, o coloana).
assert ".tabel-trimiteri thead, .tabel-card thead { display:none; }" in html, \
"Blocul tableta nu ascunde thead-ul pentru cardificare (o coloana)"