PRD 5.16 — propagare design finalizata (system font stack, fara IBM Plex self-hostat): - US-001/002/008: tokeni --font-ui/--font-mono (system stack) + scala --fs-*; zero @font-face si zero /static/fonts/; landing aliniat la acelasi stack - US-003: RAR online = dot compact in antet + meniu burger; banda rosie DOAR pe blocat (invariant zero-silent-failures pastrat) - US-010: antet "ROMFAST AUTOPASS" + nume service + /login brandeit 2 coloane + badge plan; meniu burger cu separatoare; gate strict pe is_authenticated - US-011: selector tema pill icon+eticheta (reuse THEMES) - US-004/005/006/007: bug-fix editor prestatii (picker cod+denumire, add_extra in mod operatii, cod ales se salveaza fara "+", Renunta inchide via closest) - US-012/013: landing Autentificare->/login; wizard import colapsat + 4 pasi pe tokeni - fix VERIFY E2E: contoare duplicate pe 390px (inline display:flex batea @media) -> CSS + test-lock PRD 5.17 — tipuri de cont + trial Pro 30z + enforcement DUR: - US-001/002/008: accounts.tier + trial_until (migrare aditiva defensiva); app/plans.py sursa unica (PLANS, FREE_MONTHLY_LIMIT=60, effective_tier(now injectabil), monthly_usage, CONSUMED_STATUSES); create_account trial Pro 30z; CLI set-tier (protejat id=1, audit) - US-003/004/005: enforce volum 60/luna INAINTE de build_key pe ambele canale (PLAN_LIMITA_LUNARA, 3 niveluri + log_event); gate API Pro+ (PLAN_FARA_API 403 actionabil); valideaza/nomenclator raman permise; downgrade lazy; flag AUTOPASS_ENFORCE_PLANS (kill-switch) - US-006: badge plan antet + linie burger + consum N/60 + warn>=80% + 6 stari + copy RO pluralizat + banner one-time trial->Gratuit + pagina Cont Regresie: 1380 passed, 0 failed, 1 deselected (live). E2E browser pe 390/1280 confirmat. Backend trimitere (worker/masina stari/idempotenta/contract RAR) NEATINS. Lucrul 5.18 (corpus kNN) ramane separat, necomis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
879 lines
38 KiB
Python
879 lines
38 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
|
|
|
|
|
|
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()
|
|
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
|
|
|
|
|
|
# ============================================================
|
|
# PRD 5.12 US-008: responsive tableta+mobil + header fara suprapuneri
|
|
# ============================================================
|
|
|
|
|
|
def test_header_are_breakpoint_tableta(client):
|
|
"""US-008 RED: exista reguli @media intre 768 si 1024 pentru header.
|
|
Desktop: grid 3-coloane + min-height:92px. Pe tableta (768-1024px) nu exista
|
|
inca breakpoint — logo+titlu+badge+tema+versiune+hamburger se inghesuie si se suprapun."""
|
|
_create_account_user("bt@test.com")
|
|
_login(client, "bt@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
# Bloc media dedicat tabletei (range min-width:768 si max-width: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 contine reguli pentru header sau elemente de header.
|
|
idx = html.find("@media (min-width:768px) and (max-width:1024px)")
|
|
bloc = html[idx:idx + 800]
|
|
assert "header" in bloc or ".brand-logo" in bloc, \
|
|
"Blocul tableta nu are reguli pentru header sau .brand-logo"
|
|
|
|
|
|
def test_header_elemente_nu_au_min_height_fix_pe_mobil(client):
|
|
"""US-008 RED: header-ul nu forteaza min-height:92px pe tableta si mobil.
|
|
Regula de baza (desktop) are min-height:92px; pe tableta (768-1024px)
|
|
lipseste resetarea -> inghesuire garantata la ~820px."""
|
|
_create_account_user("mh@test.com")
|
|
_login(client, "mh@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
# Regula de baza desktop are min-height:92px (sa nu dispara).
|
|
assert "min-height:92px" in html, "Regula desktop min-height:92px a disparut"
|
|
|
|
# Blocul tableta (768-1024px) trebuie sa reseteze min-height pe header.
|
|
idx_t = html.find("@media (min-width:768px) and (max-width:1024px)")
|
|
assert idx_t != -1, "Lipseste blocul @media tableta (768-1024px)"
|
|
tableta = html[idx_t:idx_t + 800]
|
|
assert "min-height:0" in tableta, \
|
|
"Blocul tableta nu reseteaza min-height pentru header"
|
|
|
|
# Blocul mobil (<768px) reseteaza si el min-height (regresie: nu a disparut).
|
|
mobil = _bloc_mobil_principal(html)
|
|
assert "min-height:0" in mobil, "Blocul mobil a pierdut resetarea min-height pe header"
|
|
|
|
|
|
def test_modal_full_screen_pe_mobil(client):
|
|
"""US-008 D#13 verificare: regula full-screen mobil pentru modal exista in base.html
|
|
(@media max-width:767px) si se aplica modalului global prin clasa modal-overlay.
|
|
VERIFICA prezenta regulii, NU re-implementa."""
|
|
_create_account_user("mfp@test.com")
|
|
_login(client, "mfp@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
# Regula CSS full-screen exista in blocul @media (max-width:767px) {.
|
|
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.
|
|
assert "44px" in mobil, "Butonul modal-close nu are tinta touch 44px pe mobil"
|
|
|
|
# Modalul global din HTML foloseste clasa modal-overlay -> prinde regula CSS.
|
|
assert 'class="modal-overlay"' in html, \
|
|
"Modalul global nu are class=modal-overlay (nu prinde regula full-screen)"
|
|
# 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)"
|
|
|
|
|
|
# ============================================================
|
|
# PRD 5.15 US-002: componente de design slim (CSS, fara consumatori)
|
|
# ============================================================
|
|
|
|
|
|
def _bloc_componente_slim(html: str) -> str:
|
|
"""Extrage blocul CSS dintre sentinelii SENTINEL-COMPONENTE-SLIM (inceput si sfarsit).
|
|
Testeaza existenta ambilor sentineli si returneaza continutul dintre ei.
|
|
"""
|
|
sentinel = "SENTINEL-COMPONENTE-SLIM"
|
|
i = html.find(sentinel)
|
|
assert i != -1, "Lipseste SENTINEL-COMPONENTE-SLIM in base.html (US-002 PRD 5.15)"
|
|
i2 = html.find(sentinel, i + len(sentinel))
|
|
assert i2 != -1, "Lipseste al doilea SENTINEL-COMPONENTE-SLIM (sfarsit bloc US-002)"
|
|
return html[i : i2 + len(sentinel)]
|
|
|
|
|
|
def test_clasa_contor_card(client):
|
|
""".contor-card: fundal --card2, bordura --line, radius 8px, padding.
|
|
Sub-elemente: .contor-cifra (cifra mare bold), .contor-label (muted), .contor-sub (mono).
|
|
"""
|
|
_create_account_user("cc2@test.com")
|
|
_login(client, "cc2@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
assert ".contor-card" in html, ".contor-card lipseste din CSS (base.html)"
|
|
|
|
bloc = _bloc_componente_slim(html)
|
|
assert "var(--card2)" in bloc, ".contor-card nu foloseste var(--card2) ca fundal"
|
|
assert "var(--line)" in bloc, ".contor-card nu are bordura var(--line)"
|
|
assert "border-radius:8px" in bloc, ".contor-card lipseste border-radius:8px"
|
|
assert ".contor-cifra" in bloc, "sub-elementul .contor-cifra lipseste din bloc"
|
|
assert ".contor-label" in bloc, "sub-elementul .contor-label lipseste din bloc"
|
|
assert "var(--muted)" in bloc, ".contor-label nu foloseste var(--muted)"
|
|
|
|
|
|
def test_clasa_lista_slim(client):
|
|
""".lista-trimiteri-slim + .trimitere-slim: separator --line2, padding, tinta min-height:44px.
|
|
Sub-elemente: .slim-vin (mono) si .slim-meta (muted 11px).
|
|
"""
|
|
_create_account_user("ls2@test.com")
|
|
_login(client, "ls2@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
assert ".lista-trimiteri-slim" in html, ".lista-trimiteri-slim lipseste din CSS (base.html)"
|
|
assert ".trimitere-slim" in html, ".trimitere-slim lipseste din CSS (base.html)"
|
|
|
|
bloc = _bloc_componente_slim(html)
|
|
assert "var(--line2)" in bloc, ".trimitere-slim nu foloseste var(--line2) ca separator"
|
|
assert "min-height:44px" in bloc, ".trimitere-slim nu are tinta min-height:44px"
|
|
assert ".slim-vin" in bloc, ".slim-vin lipseste din bloc"
|
|
assert ".slim-meta" in bloc, ".slim-meta lipseste din bloc"
|
|
assert "var(--muted)" in bloc, ".slim-meta nu foloseste var(--muted)"
|
|
|
|
|
|
def test_clasa_camp_slim(client):
|
|
""".camp-slim CSS exista cu fundal --card2.
|
|
Macro-ul camp() din _macros.html suporta parametrul slim=False ca default.
|
|
Default slim=False garanteaza ca randarea actuala ramane neschimbata.
|
|
"""
|
|
_create_account_user("cslim2@test.com")
|
|
_login(client, "cslim2@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
assert ".camp-slim" in html, ".camp-slim lipseste din CSS (base.html)"
|
|
|
|
bloc = _bloc_componente_slim(html)
|
|
assert "var(--card2)" in bloc, ".camp-slim nu foloseste var(--card2) ca fundal"
|
|
|
|
# Macro-ul camp() din _macros.html trebuie sa aiba parametrul slim
|
|
macros_path = os.path.join(
|
|
os.path.dirname(__file__), "..", "app", "web", "templates", "_macros.html"
|
|
)
|
|
with open(macros_path, encoding="utf-8") as f:
|
|
macros = f.read()
|
|
assert "slim" in macros, "macro-ul camp() nu are parametrul slim in _macros.html"
|
|
assert "slim=False" in macros, (
|
|
"macro-ul camp() nu are slim=False ca default — randarea actuala poate fi rupta"
|
|
)
|
|
|
|
|
|
def test_clasa_chips(client):
|
|
""".chips (container) + .chip (item): accent 18%, font 10-11px.
|
|
.chip-del: buton de stergere accesibil (element separat in CSS).
|
|
"""
|
|
_create_account_user("chp2@test.com")
|
|
_login(client, "chp2@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
assert ".chips" in html, ".chips lipseste din CSS (base.html)"
|
|
assert ".chip" in html, ".chip lipseste din CSS (base.html)"
|
|
|
|
bloc = _bloc_componente_slim(html)
|
|
assert "var(--accent)" in bloc, ".chip nu foloseste var(--accent)"
|
|
assert "18%" in bloc, ".chip nu are fundal accent 18% (color-mix accent 18%)"
|
|
assert "11px" in bloc or "10px" in bloc, ".chip nu are font 10-11px"
|
|
assert ".chip-del" in bloc, ".chip-del (buton de stergere) lipseste din bloc"
|
|
|
|
|
|
def test_fara_hex_in_componente_noi(client):
|
|
"""Zero hex literal in blocul CSS nou (SENTINEL-COMPONENTE-SLIM).
|
|
Toate culorile folosesc EXCLUSIV var(--token), zero #rrggbb hardcodat.
|
|
Ancorat pe SENTINEL ca sa nu scaneze blocul CSS vechi (unde exista #fff).
|
|
"""
|
|
_create_account_user("hexfree2@test.com")
|
|
_login(client, "hexfree2@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
bloc = _bloc_componente_slim(html)
|
|
# Cauta hex literals in proprietati CSS de culoare
|
|
hex_in_props = re.findall(
|
|
r"(?:color|background|border(?:-color)?|fill|stroke)\s*:[^;{}]*?"
|
|
r"(#[0-9a-fA-F]{3,8})",
|
|
bloc,
|
|
)
|
|
assert not hex_in_props, (
|
|
f"Hex literal gasit in componente noi US-002 — folositi var(--token): {hex_in_props}"
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# PRD 5.15 US-004: lista slim trimiteri — layout consistent desktop + <=1024px
|
|
# ============================================================
|
|
|
|
|
|
def test_lista_slim_randeaza_si_are_tinta_touch(client):
|
|
"""US-004: lista slim randeaza cu .trimitere-slim; tinta touch >=44px
|
|
e garantata de CSS (min-height:44px din blocul SENTINEL-COMPONENTE-SLIM).
|
|
Cardurile .tabel-trimiteri din 5.8 nu regreseaza: regula tabel-trimiteri
|
|
thead display:none (card pe mobil) exista in continuare in base.html.
|
|
"""
|
|
acct = _create_account_user("slim_resp@test.com")
|
|
_insert_submission(acct, status="sent")
|
|
_login(client, "slim_resp@test.com")
|
|
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
# Lista slim randeaza (elementele sunt injectate via hx-get="/_fragments/submissions"
|
|
# -> testam ca clasele CSS sunt prezente in base.html, gata sa fie consumate)
|
|
bloc = _bloc_componente_slim(html)
|
|
assert "lista-trimiteri-slim" in bloc, \
|
|
".lista-trimiteri-slim lipseste din blocul CSS slim (US-002 prerequisite)"
|
|
assert "trimitere-slim" in bloc, \
|
|
".trimitere-slim lipseste din blocul CSS slim"
|
|
assert "min-height:44px" in bloc, \
|
|
".trimitere-slim nu are min-height:44px — tinta touch mobil garantata"
|
|
|
|
# Regresie guard: regula card per rand 5.8 supravietuieste (o coloana pe mobil)
|
|
mobil = _bloc_mobil_principal(html)
|
|
assert ".tabel-trimiteri thead { display:none; }" in mobil, \
|
|
"Regula card 5.8 (.tabel-trimiteri thead display:none) a disparut din CSS"
|
|
|
|
|
|
def test_lista_slim_layout_tableta_1024(client):
|
|
"""US-004: blocul tableta (768-1024px) nu rupe lista slim.
|
|
.trimitere-slim e o lista stivuita (o coloana), fara grila 2/rand.
|
|
Regula tableta cardifica listele existente (thead display:none) fara a elimina slim.
|
|
"""
|
|
_create_account_user("slim_tab@test.com")
|
|
_login(client, "slim_tab@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
# Blocul tableta exista (PRD 5.12/5.13 — pastrat)
|
|
assert "@media (min-width:768px) and (max-width:1024px)" in html, \
|
|
"Lipseste blocul @media tableta — regresia 5.12"
|
|
|
|
idx_t = html.find("@media (min-width:768px) and (max-width:1024px)")
|
|
tableta = html[idx_t:idx_t + 800]
|
|
|
|
# Tableta ascunde thead (card per rand, o coloana) — lista slim e deja o coloana
|
|
assert "thead" in tableta, \
|
|
"Blocul tableta nu contine reguli pentru thead"
|
|
|
|
# Lista slim (ul.lista-trimiteri-slim) e o coloana prin constructie (flex-direction:column
|
|
# implicit pe ul); nu trebuie repeat(2) in CSS.
|
|
assert "repeat(2" not in html, \
|
|
"CSS contine repeat(2 — listele NU trebuie sa fie 2/rand pana la 1024px"
|
|
|
|
|
|
# ============================================================
|
|
# PRD 5.15 US-008: regresie componente slim + fara overflow orizontal
|
|
# ============================================================
|
|
|
|
|
|
def test_slim_list_fara_overflow_orizontal_css(client):
|
|
"""US-008: lista slim (.trimitere-slim) nu forteaza overflow orizontal pe 390px / 1280px.
|
|
|
|
Verifica la nivel CSS / markup (nu browser): display:flex garanteaza adaptarea
|
|
naturala la latimea containerului; niciun min-width fix mai mare de 390px pe elementele
|
|
din blocul SENTINEL-COMPONENTE-SLIM (ar depasi viewport-ul mobil de 390px).
|
|
Ancorare pe SENTINEL-COMPONENTE-SLIM — nu pe felii fixe din CSS global.
|
|
"""
|
|
_create_account_user("noovf_slim@test.com")
|
|
_login(client, "noovf_slim@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
bloc = _bloc_componente_slim(html)
|
|
|
|
# display:flex pe .trimitere-slim asigura adaptarea la latimea oricarui viewport
|
|
assert "display:flex" in bloc, (
|
|
".trimitere-slim nu are display:flex in SENTINEL-COMPONENTE-SLIM — "
|
|
"layout nu se adapteaza la viewport; poate cauza overflow orizontal."
|
|
)
|
|
|
|
# nicio valoare min-width > 390 in blocul slim (ar depasi viewport-ul mobil 390px)
|
|
min_widths = re.findall(r'min-width:(\d+)px', bloc)
|
|
for w in min_widths:
|
|
assert int(w) <= 390, (
|
|
f"min-width:{w}px in SENTINEL-COMPONENTE-SLIM poate cauza overflow orizontal "
|
|
f"pe viewport de 390px (mobil). Verificati daca e pe .trimitere-slim."
|
|
)
|
|
|
|
|
|
def test_strip_sanatate_fara_hex_hardcodat():
|
|
"""US-008: _status.html (strip sanatate D6 + contoare-contor) nu contine hex literal de culoare.
|
|
|
|
Garanteaza ca strip-ul adapteaza la temele luminoase (hartie/light) si intunecate (grafit/dark)
|
|
exclusiv prin var(--token), NU prin valori hex hardcodate care ar ramane aceleasi
|
|
indiferent de tema selectata.
|
|
|
|
Complement la test_fara_hex_in_componente_noi (care verifica SENTINEL din base.html).
|
|
Strip sanatate traieste in _status.html, deci e verificat separat.
|
|
"""
|
|
from pathlib import Path
|
|
templates_dir = Path(__file__).parent.parent / "app" / "web" / "templates"
|
|
content = (templates_dir / "_status.html").read_text(encoding="utf-8")
|
|
|
|
# Hex literals in context de proprietate CSS de culoare (color/background/border + inline style)
|
|
hex_in_culori = re.findall(
|
|
r'(?:color|background|border)\s*[:=][^;{}\n"\']*?(#[0-9a-fA-F]{6,8})\b',
|
|
content,
|
|
)
|
|
assert not hex_in_culori, (
|
|
f"Hex literal de culoare in _status.html — strip sanatate va arata gresit pe "
|
|
f"tema hartie (luminoasa) / light. Folositi var(--token). Gasite: {hex_in_culori}"
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# PRD 5.16 US-010: Titlu ROMFAST AUTOPASS + account_name in antet
|
|
# ============================================================
|
|
|
|
def test_titlu_romfast_autopass(client):
|
|
"""US-010 (PRD 5.16): titlul din antet si tag-ul <title> sunt 'ROMFAST AUTOPASS',
|
|
nu 'Gateway RAR AUTOPASS'."""
|
|
_create_account_user("titlutest@test.com", name="Service Titlu")
|
|
_login(client, "titlutest@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
assert "ROMFAST AUTOPASS" in html, \
|
|
"Titlul 'ROMFAST AUTOPASS' lipseste din antet (US-010 PRD 5.16)"
|
|
assert "Gateway RAR AUTOPASS" not in html, \
|
|
"Titlul vechi 'Gateway RAR AUTOPASS' inca prezent — inlocuieste cu 'ROMFAST AUTOPASS'"
|
|
|
|
|
|
def test_header_arata_nume_service_logat(client):
|
|
"""US-010 (PRD 5.16): cand utilizatorul e logat, antetul afiseaza numele service-ului
|
|
(accounts.name) ca sub-titlu cu clasa .h-sub."""
|
|
_create_account_user("numeservice@test.com", name="Service Auto Cluj SRL")
|
|
_login(client, "numeservice@test.com")
|
|
html = client.get("/?tab=acasa").text
|
|
|
|
assert "Service Auto Cluj SRL" in html, \
|
|
"Numele service-ului nu apare in antet (US-010 PRD 5.16) — verifica .h-sub"
|
|
assert "h-sub" in html, \
|
|
"Clasa .h-sub lipseste din antet (US-010 PRD 5.16) — sub-titlul account_name lipseste"
|
|
|
|
|
|
def test_login_branded_nu_schelet(client):
|
|
"""US-010 (PRD 5.16): /login are layout 2-coloane branduit cu clasa .login-shell,
|
|
titlul 'ROMFAST AUTOPASS', si formular cu POST /login (CSRF intact)."""
|
|
resp = client.get("/login")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
|
|
assert "login-shell" in html, \
|
|
"Clasa .login-shell lipseste din /login (US-010 PRD 5.16) — layout 2-coloane nenimplementat"
|
|
assert "login-aside" in html, \
|
|
"Clasa .login-aside lipseste — coloana stanga de brand lipseste (US-010)"
|
|
assert "ROMFAST AUTOPASS" in html, \
|
|
"Titlul 'ROMFAST AUTOPASS' lipseste din /login (US-010 PRD 5.16)"
|
|
# Formular intact: POST /login cu csrf_token
|
|
assert 'action="/login"' in html, "Actiunea formularului /login s-a schimbat — CSRF route invalida"
|
|
assert 'name="csrf_token"' in html, "csrf_token lipseste din formular — securitate compromisa"
|
|
assert 'name="parola"' in html, "Campul 'parola' lipseste din formular"
|
|
|
|
|
|
# ============================================================
|
|
# PRD 5.17 T7 (US-007): landing copy — limita 60 + trial Pro
|
|
# PRD 5.16 US-012: Autentificare → /login
|
|
# ============================================================
|
|
|
|
def _citeste_landing() -> str:
|
|
"""Returneaza continutul landing.html (template static; variabilele Jinja2 nu
|
|
afecteaza copy-ul de limita/plan/buton verificat mai jos)."""
|
|
from pathlib import Path
|
|
p = Path(__file__).parent.parent / "app" / "web" / "templates" / "landing.html"
|
|
assert p.exists(), f"landing.html negasit la {p}"
|
|
return p.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_landing_limita_60():
|
|
"""5.17 T7 (US-007): limita planului Gratuit este 60 de prestatii/luna in landing,
|
|
nu 100. Verifica meta description, announce bar, hero badge, cardul Gratuit si
|
|
CTA-ul final."""
|
|
html = _citeste_landing()
|
|
|
|
assert "100 de prestații" not in html, \
|
|
"'100 de prestații' inca prezent in landing — limita trebuie sa fie 60 (5.17 T7)"
|
|
assert "100 prestații" not in html, \
|
|
"'100 prestații' inca prezent in landing — limita trebuie sa fie 60 (5.17 T7)"
|
|
assert "60 de prestații" in html, \
|
|
"'60 de prestații' lipseste din landing — verifica meta, announce bar, cardul Gratuit (5.17 T7)"
|
|
assert "60 prestații" in html, \
|
|
"'60 prestații' lipseste din hero badge in landing (5.17 T7)"
|
|
assert "60 de prezentări" in html, \
|
|
"'60 de prezentări' lipseste din CTA-ul final al landing-ului (5.17 T7)"
|
|
|
|
|
|
def test_landing_trial_pro_nu_premium():
|
|
"""5.17 T7 (US-007): trial-ul de 30 de zile este pe Pro, NU pe Premium.
|
|
Verifica sectiunea PRICING (subtitle) si sectiunea AUTH (lista beneficii)."""
|
|
html = _citeste_landing()
|
|
|
|
assert "Pro gratuit 30 de zile" in html, \
|
|
"'Pro gratuit 30 de zile' lipseste din landing — verifica sectiunile PRICING + AUTH (5.17 T7)"
|
|
assert "Premium gratuit 30 de zile" not in html, \
|
|
"'Premium gratuit 30 de zile' inca in landing — trial-ul e pe Pro, nu Premium (5.17 T7)"
|
|
|
|
|
|
def test_landing_autentificare_link_login():
|
|
"""5.16 US-012: butonul 'Autentificare' din header-ul landing este un link <a href='/login'>
|
|
cu clasa auth-login-link, NU un buton care deschide modalul de login.
|
|
CSS-ul responsive (.lp-hactions) trebuie sa foloseasca noul selector, nu cel vechi."""
|
|
html = _citeste_landing()
|
|
|
|
# Link real catre /login in header (cu clasa de identificare)
|
|
assert 'href="/login"' in html, \
|
|
"href='/login' lipseste din landing — 'Autentificare' din header trebuie sa fie link (5.16 US-012)"
|
|
assert "auth-login-link" in html, \
|
|
"Clasa auth-login-link lipseste — header 'Autentificare' nu a fost convertit la <a> (5.16 US-012)"
|
|
# CSS-ul responsive ascunde linkul pe <430px prin noul selector (nu cel vechi cu atribute)
|
|
assert "a.auth-login-link" in html, \
|
|
"Selectorul CSS 'a.auth-login-link' lipseste — CSS responsive neactualizat (5.16 US-012)"
|
|
# Selectorul CSS vechi cu [data-act="auth"][data-tab="login"] nu mai exista in CSS
|
|
assert '[data-act="auth"][data-tab="login"]' not in html, \
|
|
"Selectorul CSS vechi [data-act='auth'][data-tab='login'] inca prezent (5.16 US-012)"
|
|
|
|
|
|
def test_contoare_desktop_ascunse_pe_mobil_fara_inline_display():
|
|
"""US-002 (PRD 5.16): pe <=560px se vad DOAR contoarele compacte, nu si cele 5 carduri mari.
|
|
|
|
Regresie prinsa la VERIFY E2E (390px): un inline `style="display:flex"` pe `.contoare-desktop`
|
|
batea regula `@media (max-width:560px) { .contoare-desktop { display:none } }` (inline > stylesheet)
|
|
-> contoare DUPLICATE pe mobil. Lock: `display:flex` sta in CSS (nu inline pe element), iar media
|
|
query-ul ascunde cardurile mari pe mobil.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
tdir = Path(__file__).parent.parent / "app" / "web" / "templates"
|
|
base = (tdir / "base.html").read_text(encoding="utf-8")
|
|
status = (tdir / "_status.html").read_text(encoding="utf-8")
|
|
|
|
# _status.html: containerul de carduri NU mai are inline display (altfel bate media query-ul).
|
|
assert 'class="contoare-desktop" style="display:flex' not in status, (
|
|
"containerul .contoare-desktop are inline display:flex -> media query-ul nu-l mai poate ascunde pe mobil"
|
|
)
|
|
# base.html: regula CSS default (display:flex) + ascunderea pe <=560px.
|
|
assert re.search(r"\.contoare-desktop\s*\{[^}]*display:\s*flex", base), (
|
|
"lipseste regula CSS .contoare-desktop { display:flex } in base.html"
|
|
)
|
|
assert re.search(r"\.contoare-desktop\s*\{[^}]*display:\s*none", base), (
|
|
"lipseste ascunderea .contoare-desktop { display:none } (media <=560px) in base.html"
|
|
)
|