"""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}')