Eliminat zgomotul de trasabilitate (US-xxx, PRD x.x, Rn, OV-x, Tn, decizii/naratiune istorica) din 41 fisiere app/ + template-uri. Pastrate comentariile care documenteaza invarianti si logica ne-evidenta (idempotenta/hash, reconciliere anti-duplicat, RAR 500 esec definitiv, creds per cont, WAF User-Agent, 422 fara echo de parola, scope NULL->1), curatate doar de tokeni. Verificare: pentru cele 27 module .py curatate, structura de cod (tokeni non-comentariu/ non-string) e IDENTICA fata de HEAD -> doar comentarii/docstring-uri schimbate. Singura schimbare de cod e in tests/test_web_responsive.py (scos 3 assert pe markeri US-006/007/008, inlocuite de asertiunile structurale alaturate). 0 tokeni US/PRD reziduali in app/. Regresie: 896 passed, 1 deselected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
291 lines
11 KiB
Python
291 lines
11 KiB
Python
"""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):
|
|
"""`<meta name=viewport>` 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 `<td>`-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
|