"""Teste PRD 5.9 US-006: fundatie responsive — viewport, header/nav colapsabil, modal full-screen pe mobil, breakpoint-uri consistente. Verificam markup-ul + CSS server-side randat (TestClient): prezenta meta viewport, existenta unei reguli `@media (max-width:767px)` care trece modalul pe full-screen (`.modal-dialog`) cu buton `x` >=44px, si structura de nav colapsabil (meniul de cont hamburger + tinte touch >=44px). Verificarile vizuale efective (375px, fara scroll orizontal) sunt deferate la VERIFY (gstack browser). """ 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 _insert_submission(acct: int, status: str = "needs_data") -> int: from app.db import get_connection conn = get_connection() try: p = { "vin": "WVWZZZ1JZXW000888", "nr_inmatriculare": "B888ZZZ", "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() 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 @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "responsive.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_viewport_meta_prezent(client): """`` cu width=device-width prezent in base.html.""" _create_account_user("vp@test.com") _login(client, "vp@test.com") html = client.get("/?tab=acasa").text assert 'name="viewport"' in html assert "width=device-width" in html assert "initial-scale=1" in html def test_modal_fullscreen_clasa_mobil(client): """Sub 768px modalul devine full-screen: o regula `@media (max-width:767px)` pune `.modal-dialog` la latime/inaltime pline (fara latimea marginita de desktop) si butonul `x` la >=44px. Desktop pastreaza regula centrata (`max-width:680px`).""" _create_account_user("mf@test.com") _login(client, "mf@test.com") html = client.get("/?tab=acasa").text # Regula de baza (desktop) ramane: dialog centrat cu latime marginita. assert "max-width:680px" in html # Exista un bloc media mobil care vizeaza modalul. assert "@media (max-width:767px)" in html # Dialogul ocupa tot ecranul pe mobil (latime/inaltime pline, fara border-radius lateral). mobil = html[html.find("@media (max-width:767px)"):] assert "100vw" in mobil or "width:100%" in mobil assert "100vh" in mobil # Butonul de inchidere >=44px pe mobil (tinta touch). assert "44px" in mobil def test_nav_colapsabil_sub_breakpoint(client): """Nav colapsabil: meniul de cont e un buton hamburger (☰) ascuns intr-un dropdown, iar tintele touch (icon-btn, tab-link, itemi meniu) ajung la >=44px sub breakpoint.""" _create_account_user("nav@test.com") _login(client, "nav@test.com") html = client.get("/?tab=acasa").text # Hamburger-ul de cont exista si dropdown-ul e colapsat (hidden) implicit. assert 'id="cont-menu-toggle"' in html assert "☰" in html # pictograma hamburger assert 'id="cont-menu"' in html assert 'class="cont-menu"' in html # Tab-bar-ul (nav principal) exista si e scrollabil orizontal (nu deborda pagina). assert "tab-bar" in html assert "overflow-x:auto" in html # Sub breakpoint, tintele touch din header/nav cresc la >=44px. mobil = html[html.find("@media (max-width:767px)"):] assert "44px" in mobil # ============================================================ # PRD 5.9 US-007: responsive paginile de continut # (Mapari, Cont, Nomenclator, Integrare, Jurnal, Admin) # ============================================================ def _seed_nomenclator(items): from app.mapping import upsert_nomenclator from app.db import get_connection conn = get_connection() try: upsert_nomenclator(conn, items) conn.commit() finally: conn.close() def _seed_event(account_id, tip="test", mesaj="eveniment de test"): from app.observ import log_event from app.db import get_connection conn = get_connection() try: log_event(tip, account_id=account_id, mesaj=mesaj, conn=conn) conn.commit() finally: conn.close() def test_tabele_continut_au_clasa_responsive(client): """R12 politica per-tabel: - Mapari (actionabil) = CARD per rand: clasa proprie `.tabel-card` + `data-eticheta` pe ``-uri. - Jurnal / Nomenclator (dense read-only) = `.tablewrap` (scroll orizontal CONTAINED), FARA `.tabel-card`. Definitia regulii de card traieste in base.html, scopata SEPARAT de `.tabel-trimiteri`. """ acct = _create_account_user("t7@test.com") _login(client, "t7@test.com") # --- Mapari = card. Tabelul „Reguli automate (text)" e mereu randat (rand de adaugare), # deci nu depinde de date seedate. --- mapari = client.get("/?tab=mapari").text assert "tabel-card" in mapari assert 'data-eticheta="Cod RAR"' in mapari assert 'data-eticheta="Daca operatia contine"' in mapari # Regula de card e definita o data in base.html, scopata pe `.tabel-card`. assert ".tabel-card thead" in mapari # --- Jurnal = scroll contained. Cu un eveniment seedat, tabelul apare in `.tablewrap`, # NU ca `.tabel-card`. --- _seed_event(acct) jurnal = client.get("/?tab=jurnal").text assert "tablewrap" in jurnal # Sectiunea de jurnal NU foloseste cardul actionabil. body = jurnal[jurnal.find('id="jurnal-section"'):] assert "tabel-card" not in body # --- Nomenclator = scroll contained (dens read-only). --- _seed_nomenclator([{"codPrestatie": "OE-2", "numePrestatie": "Revizie"}]) nomen = client.get("/?tab=nomenclator").text assert "tablewrap" in nomen def test_formulare_full_width_mobil(client): """Formularele de continut (Cont, Integrare test, filtre Jurnal) stiveaza pe o coloana sub 767px: inputuri full-width + butoane >=44px. Verificam ancorele de scope (id-uri) plus regulile CSS mobil din base.html (toate scopate sub `@media (max-width:767px)`).""" _create_account_user("f7@test.com") _login(client, "f7@test.com") cont = client.get("/?tab=cont").text assert 'id="card-cont"' in cont # Regulile mobil de formular exista si sunt scopate pe sectiunile de continut. mobil = cont[cont.find("@media (max-width:767px)"):] assert "#card-cont button" in mobil assert "min-height:44px" in mobil assert "width:100% !important" in mobil # suprascrie latimile inline doar pe mobil integrare = client.get("/?tab=integrare").text assert 'id="form-test-cheie"' in integrare # Filtrele de jurnal (form mereu prezent, indiferent de date) primesc scope-ul mobil. jurnal = client.get("/?tab=jurnal").text assert 'id="filtre-jurnal"' in jurnal assert "#jurnal-section #filtre-jurnal button" in jurnal def test_carduri_trimiteri_5_8_supravietuiesc(client): """Regresie R12: scoparea `.tabel-card` (US-007) NU trebuie sa atinga blocul `.tabel-trimiteri @media(max-width:767px)` din 5.8 — cardurile de trimiteri raman.""" _create_account_user("r58@test.com") _login(client, "r58@test.com") html = client.get("/?tab=acasa").text mobil = html[html.find("@media (max-width:767px)"):] # Cardurile de trimiteri 5.8 (clasa proprie, separata de `.tabel-card`). assert ".tabel-trimiteri thead { display:none; }" in mobil assert ".tabel-trimiteri td::before" in mobil # Cele doua mecanisme coexista, scopate distinct. assert ".tabel-card thead" in mobil # ============================================================ # PRD 5.9 US-008: responsive Acasa (upload, status, filtre) + login/signup # ============================================================ def test_acasa_fara_scroll_orizontal_mobil(client): """US-008: pe Acasa sub 767px zona de upload, bara de status si bara de filtre stiveaza pe O coloana, cu inputuri/butoane full-width >=44px. Verificam ancorele de scope (id-uri) + regulile CSS mobil din base.html (toate sub `@media (max-width:767px)`), fara sa atingem cardurile de trimiteri 5.8 (verificate separat).""" acct = _create_account_user("a8@test.com") _insert_submission(acct) # sectiunea Trimiteri (filtre + wrap) apare doar cu randuri _login(client, "a8@test.com") html = client.get("/?tab=acasa").text # Ancore de scope prezente in markup. assert 'id="import-section"' in html assert 'id="status-bar"' in html assert 'id="filtre-trimiteri"' in html mobil = html[html.find("@media (max-width:767px)"):] # Bara de upload: zona de drop trece pe coloana, butonul de alegere full-width. assert "#import-section .drop-zone" in mobil assert "#import-section #upload-btn" in mobil # Bara de filtre: o coloana, controale full-width, buton >=44px. assert "#filtre-trimiteri" in mobil filtre = mobil[mobil.find("#filtre-trimiteri"):] assert "width:100% !important" in filtre # suprascrie latimile inline (max-width:180px etc.) assert "min-height:44px" in mobil # Bara de status stiveaza pe coloana (scope dedicat). assert "#status-bar" in mobil def test_login_signup_full_width_mobil(client): """US-008: login.html si signup.html randeaza un card centrat (`.auth-card`, margin auto) care nu depaseste latimea pe mobil (max-width:100% sub 767px), cu inputuri full-width. Rutele `/login` si `/signup` sunt publice (fara autentificare).""" for ruta in ("/login", "/signup"): html = client.get(ruta).text # Card de autentificare marcat si centrat. assert "auth-card" in html, ruta assert "margin:40px auto" in html or "margin:24px auto" in html, ruta # Inputurile sunt full-width. assert "width:100%" in html, ruta # Regula mobil: cardul nu depaseste viewport-ul. mobil = html[html.find("@media (max-width:767px)"):] assert ".auth-card" in mobil, ruta assert "max-width:100%" in mobil, ruta