"""Teste PRD 5.9 US-003: detaliul trimiterii se deschide intr-un MODAL global (#modal-detaliu), in afara zonei de poll (#submissions-wrap). Verificam markup-ul server-side: containerul modal e global si plasat IN AFARA #submissions-wrap (de fapt sibling al
, ca `inert` pe
sa nu-l prinda), corpul #detaliu-modal-body e tinta de swap, iar fragmentul de detaliu (forme corectie/ mapare/lifecycle) tinteste corpul modalului — NU vechiul #detaliu-{id} / #trimitere-detaliu. Focus-trap / scroll-lock / inert sunt logica JS in base.html (verificam hook-urile). """ from __future__ import annotations import json import os import re import tempfile import pytest from starlette.testclient import TestClient def _create_account_user(email: str, name: str = "Service", 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, name, 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 resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)}) assert resp.status_code == 303 def _insert_submission(acct: int, status: str = "needs_data") -> int: from app.db import get_connection conn = get_connection() try: p = { "vin": "WVWZZZ1JZXW000777", "nr_inmatriculare": "B777ZZZ", "data_prestatie": "2026-06-18", "odometru_final": "55000", "prestatii": [{"cod_prestatie": "R-FRANE", "denumire": "Reparatie frane"}], } cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json) " "VALUES (?, ?, ?, ?)", (f"k-{status}-{os.urandom(4).hex()}", acct, status, json.dumps(p)), ) conn.commit() return int(cur.lastrowid) finally: conn.close() @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "modal.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() def test_modal_container_in_afara_submissions_wrap(client): """Containerul modal global exista, e dialog a11y si e plasat IN AFARA #submissions-wrap (sibling al
, dupa
).""" acct = _create_account_user("modal@test.com") _insert_submission(acct, "needs_data") # sectiunea Trimiteri (wrap) apare doar cu randuri _login(client, "modal@test.com") html = client.get("/?tab=acasa").text # containerul modal + corpul de swap assert 'id="modal-detaliu"' in html, "lipseste containerul modal global" assert 'id="detaliu-modal-body"' in html, "lipseste corpul de swap al modalului" # rol de dialog modal + heading legat prin aria-labelledby assert 'role="dialog"' in html assert 'aria-modal="true"' in html assert 'aria-labelledby="detaliu-modal-titlu"' in html # buton de inchidere cu aria-label (R7) assert "modal-close" in html assert 'aria-label="Inchide detaliul"' in html # Plasament: modalul e DUPA
, deci in afara
si a #submissions-wrap # (care traieste in panoul din
). inert pe
nu-l prinde (R7). idx_wrap = html.find('id="submissions-wrap"') idx_main_close = html.find("
") idx_modal = html.find('id="modal-detaliu"') assert idx_wrap != -1 and idx_main_close != -1 and idx_modal != -1 assert idx_wrap < idx_main_close < idx_modal, "modalul trebuie sa fie in afara
/#submissions-wrap" # Vechiul panou inert eliminat; fara mecanismul inline 5.8 in pagina. assert 'id="trimitere-detaliu"' not in html assert 'class="detaliu-rand"' not in html assert "marcheazaDetaliuDeschis" not in html def test_fragment_detaliu_tinteste_modalul(client): """Randul declanseaza modalul (hx-target=#detaliu-modal-body) si fragmentul de detaliu (forme corectie/mapare/lifecycle) tinteste tot corpul modalului — NU vechiul container per-rand #detaliu-{id} sau #trimitere-detaliu.""" acct = _create_account_user("frag@test.com") sid = _insert_submission(acct, "needs_data") _login(client, "frag@test.com") # 1. Randul din tabel tinteste corpul modalului; fara rand-sibling / chevron / aria-expanded. lista = client.get("/_fragments/submissions") assert lista.status_code == 200 h = lista.text assert 'hx-target="#detaliu-modal-body"' in h, "randul trebuie sa tinteasca corpul modalului" assert 'hx-target="#detaliu-%d"' % sid not in h assert 'class="detaliu-rand"' not in h assert 'aria-expanded' not in h assert "chevron" not in h assert 'aria-haspopup="dialog"' in h assert 'role="button"' in h and 'tabindex="0"' in h # 2. Fragmentul de detaliu: formele tintesc corpul modalului, nu containerul vechi. det = client.get(f"/_fragments/trimitere/{sid}") assert det.status_code == 200 d = det.text assert 'hx-target="#detaliu-modal-body"' in d assert 'hx-target="#detaliu-%d"' % sid not in d assert 'hx-target="#trimitere-detaliu"' not in d # heading legat de aria-labelledby al dialogului assert 'id="detaliu-modal-titlu"' in d def test_modal_hookuri_js_prezente(client): """Logica de modal (focus-trap, scroll-lock, inert pe
, inchidere pe succes) e prezenta in base.html — hook-urile cheie exista.""" _create_account_user("hook@test.com") _login(client, "hook@test.com") js = client.get("/?tab=acasa").text assert "modal-detaliu" in js # focus-trap + scroll-lock + inert pe ancestor stabil assert "trapFocus" in js or "Tab" in js assert "modal-open" in js assert "inert" in js # inchidere pe succes corectie/sterge (listener pe evenimentul HX-Trigger) assert "inchideModal" in js # API public pastrat (butoanele/rutele pot inchide modalul) assert "window.inchideDetaliu" in js # --- Tabelul nu se reincarca singur: modalul + selectia sunt sigure --------- # Tabelul (#submissions-wrap) nu mai are poll periodic; se reincarca DOAR la load, # la actiunile utilizatorului (trimiteriChanged) sau la apasarea pe Reincarca (nudge). # Asa, modalul deschis si bifele de bulk nu pot fi sterse de un timer. def test_tabel_fara_poll_periodic(client): """#submissions-wrap nu are trigger periodic (`every Ns`) — niciun timer nu poate reseta modalul deschis sau selectia de bulk in timpul interactiunii.""" acct = _create_account_user("poll1@test.com") _login(client, "poll1@test.com") _insert_submission(acct) html = client.get("/?tab=acasa").text assert 'id="submissions-wrap"' in html wrap = html[html.find('id="submissions-wrap"'):] wrap = wrap[:wrap.find(">") + 1] assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}" def test_nudge_date_noi_in_loc_de_poll(client): """Reimprospatarea live se face prin nudge-ul 'Date noi' (poller usor de versiune) care NU atinge tabelul; utilizatorul reincarca explicit cand vrea.""" acct = _create_account_user("poll2@test.com") _login(client, "poll2@test.com") _insert_submission(acct) html = client.get("/?tab=acasa").text assert 'id="nudge-trimiteri"' in html, "bannerul nudge 'Date noi' trebuie sa existe" assert "/_fragments/trimiteri-versiune" in html, "pollerul de versiune trebuie configurat" assert "reincarcaTrimiteri" in html, "reincarcarea manuala (Reincarca) trebuie expusa" def test_trimiteriChanged_inca_reincarca(client): """Actiunile utilizatorului (corectie / stergere) reincarca tabelul prin canalul `trimiteriChanged`, pastrand filtrul curent (hx-include #filtre-trimiteri).""" acct = _create_account_user("poll3@test.com") _login(client, "poll3@test.com") _insert_submission(acct) html = client.get("/?tab=acasa").text wrap = html[html.find('id="submissions-wrap"'):] wrap = wrap[:wrap.find(">") + 1] assert "trimiteriChanged from:body" in wrap, "tabelul trebuie sa reincarce pe trimiteriChanged" assert 'hx-include="#filtre-trimiteri"' in wrap, "reincarcarea trebuie sa pastreze filtrul"