"""CSS/HTML coherence: every CSS class used in templates exists in style.css,
and every page renders with its core structural classes present.
Uses stdlib-only HTML parsing (html.parser) to keep the dependency surface
unchanged.
"""
import logging
import os
import re
from html.parser import HTMLParser
import pytest
_logger = logging.getLogger(__name__)
_TEMPLATE_ROOT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'app', 'templates'
)
_CSS_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'app',
'static',
'css',
'style.css',
)
# ---------------------------------------------------------------------------
# Phase 1: CSS class usage analysis
# ---------------------------------------------------------------------------
def _cached_lines():
"""Yield all CSS class selectors found in style.css."""
with open(_CSS_PATH, encoding='utf-8') as f:
content = f.read()
for match in re.finditer(r'(? 30:
_logger.warning(
'%d HTML classes missing from CSS (threshold=30):\n%s',
len(missing),
'\n'.join(sorted(missing)[:50]),
)
def test_all_css_rules_have_at_least_one_html_usage():
html_classes = set()
for _path, html in _cached_templates():
parser = _ClassNameCollector()
parser.feed(html)
html_classes.update(parser.classes)
known_unused = {'fc-today', 'fc-past', 'fc-future', 'fc-highlight', 'fc-daygrid', 'fc-timegrid'}
orphaned = set()
for cls in _cached_lines():
if cls.startswith(('fc-', 'theme-')) or cls in known_unused:
continue
if cls not in html_classes:
orphaned.add(cls)
assert len(orphaned) <= 40, (
f'{len(orphaned)} CSS rules have no HTML match: {sorted(orphaned)[:30]}'
)
# ---------------------------------------------------------------------------
# Phase 2: Per-role page rendering smoke tests
# ---------------------------------------------------------------------------
_PAGE_ROUTES = [
'/',
'/auth/login',
'/auth/logout',
'/teams',
'/matches/calendar',
'/users/profile',
'/users/profile/edit',
'/users/contracts',
'/tryouts',
]
_PAGE_ROUTES_BY_ROLE = {
'admin': _PAGE_ROUTES + ['/admin'],
'manager': _PAGE_ROUTES,
'coach': _PAGE_ROUTES + ['/users/notes-dashboard'],
'player': _PAGE_ROUTES + ['/users/one-on-one'],
'scout': _PAGE_ROUTES,
}
_CORE_CLASSES = {'sidebar', 'sidebar-header', 'logo', 'content', 'flash-messages'}
@pytest.mark.parametrize('role', ['admin', 'manager', 'coach', 'player', 'scout'])
def test_page_renders_with_core_structure(app, client, as_role, role):
as_role(role)
for route in _PAGE_ROUTES_BY_ROLE.get(role, _PAGE_ROUTES):
resp = client.get(route, follow_redirects=True)
if resp.status_code == 200 and resp.content_type and 'text/html' in resp.content_type:
parser = _ClassNameCollector()
parser.feed(resp.data.decode('utf-8', errors='replace'))
missing = _CORE_CLASSES - parser.classes
if missing and route not in ('/auth/login', '/auth/logout'):
pytest.fail(f'Role={role}, route={route}: missing core classes: {missing}')