first cleanup iter
CI - Security, Lint & Tests / validate (push) Failing after 25m59s

This commit is contained in:
cedrick2711
2026-09-09 08:26:47 -04:00
parent 101e3ae880
commit e53bc3b80d
35 changed files with 2138 additions and 2187 deletions
+133
View File
@@ -0,0 +1,133 @@
"""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'(?<!\w)\.\w[a-zA-Z0-9_-]*', content):
yield match.group(0).lstrip('.')
def _cached_templates():
for root, _dirs, files in os.walk(_TEMPLATE_ROOT):
for name in files:
if name.endswith('.html'):
path = os.path.join(root, name)
with open(path, encoding='utf-8') as f:
yield path, f.read()
class _ClassNameCollector(HTMLParser):
def __init__(self):
super().__init__()
self.classes = set()
def handle_starttag(self, tag, attrs):
for name, value in attrs:
if name == 'class' and value and '{{' not in value and '{%' not in value:
self.classes.update(value.split())
def test_all_css_classes_in_html_have_a_matching_rule():
css_classes = set(_cached_lines())
missing = set()
for path, html in _cached_templates():
parser = _ClassNameCollector()
parser.feed(html)
for cls in parser.classes:
if cls not in css_classes and not cls.startswith(('fc-', 'fa-')) and cls:
missing.add(f'{os.path.relpath(path, _TEMPLATE_ROOT)}: .{cls}')
if len(missing) > 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}')