mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
test(web): smoke tests + static-analysis audits for the web UI
Guardrails so this branch's fix classes can't regress silently:
- test_web_smoke.py (24 tests): boots the pages blueprint with the same
dual registration app.py uses and asserts every page/partial returns 200
with its load-bearing markers (nav wiring, getting-started card, advanced
section, rotation order card, per-mode duration inputs), the /v3 legacy
alias serves everything, all critical static assets (incl. vendored
fontawesome/codemirror, PWA manifest/icons) are served, durations group
per plugin with the leftover bucket, and the advanced-hardware section
really contains the tuning fields. Would have caught this session's
unreachable-durations-page and orphaned-tab bugs instantly.
- test_web_static_audit.py (3 tests): (1) every responsive utility class
referenced in templates is actually defined in app.css - the
silently-no-op class bug that left the header search box invisible at
every width; (2) every url_for('static', ...) reference points to a real
file; (3) any JS file calling the debugLog global declares it in a
/* global */ header.
All 40 web tests pass (24 + 3 new, 13 existing) under pytest + Flask.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
69863f70cd
commit
23dcdc2d49
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Static-analysis audits for the web UI, as tests so CI enforces them.
|
||||
|
||||
1. Breakpoint utility audit: app.css hand-maintains a Tailwind-style utility
|
||||
subset, so a template can reference a responsive class (e.g. sm:block)
|
||||
that no CSS rule defines — it silently no-ops. This once left the header
|
||||
search box and system stats invisible at every screen width. The audit
|
||||
diffs classes used in templates against classes defined in app.css.
|
||||
|
||||
2. Asset reference audit: every url_for('static', filename=...) in the
|
||||
templates must point to a file that exists, so a renamed/moved asset
|
||||
can't ship as a broken <script>/<link>/<img>.
|
||||
|
||||
3. debugLog globals audit: any static JS file calling debugLog() (a global
|
||||
defined in base.html) must declare it in a /* global */ header so linting
|
||||
stays clean and the dependency is explicit.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
WEB = PROJECT_ROOT / "web_interface"
|
||||
TEMPLATES = WEB / "templates"
|
||||
STATIC = WEB / "static"
|
||||
APP_CSS = STATIC / "v3" / "app.css"
|
||||
|
||||
BP_PREFIXES = ("sm", "md", "lg", "xl", "2xl")
|
||||
|
||||
|
||||
def _template_files():
|
||||
return sorted(TEMPLATES.rglob("*.html"))
|
||||
|
||||
|
||||
def test_every_used_breakpoint_class_is_defined():
|
||||
used = set()
|
||||
class_attr = re.compile(r'class="([^"]*)"')
|
||||
bp_class = re.compile(r"\b(%s):[A-Za-z0-9_.-]+" % "|".join(BP_PREFIXES))
|
||||
for path in _template_files():
|
||||
for attr in class_attr.findall(path.read_text()):
|
||||
for m in bp_class.finditer(attr):
|
||||
used.add(m.group(0))
|
||||
|
||||
css = APP_CSS.read_text()
|
||||
defined = {
|
||||
m.group(0).lstrip(".").replace("\\:", ":")
|
||||
for m in re.finditer(
|
||||
r"\.(%s)\\:[A-Za-z0-9_-]+" % "|".join(BP_PREFIXES), css
|
||||
)
|
||||
}
|
||||
|
||||
missing = sorted(used - defined)
|
||||
assert not missing, (
|
||||
"Responsive utility classes referenced in templates but never defined "
|
||||
f"in app.css (they silently no-op): {missing}"
|
||||
)
|
||||
|
||||
|
||||
def test_every_static_url_for_points_to_a_real_file():
|
||||
ref = re.compile(
|
||||
r"url_for\(\s*['\"]static['\"]\s*,\s*filename\s*=\s*['\"]([^'\"]+)['\"]"
|
||||
)
|
||||
missing = []
|
||||
for path in _template_files():
|
||||
for filename in ref.findall(path.read_text()):
|
||||
if not (STATIC / filename).is_file():
|
||||
missing.append(f"{path.relative_to(PROJECT_ROOT)}: {filename}")
|
||||
assert not missing, f"Templates reference missing static assets: {missing}"
|
||||
|
||||
|
||||
def test_js_files_calling_debuglog_declare_the_global():
|
||||
undeclared = []
|
||||
for path in sorted((STATIC / "v3").rglob("*.js")):
|
||||
if "vendor" in path.parts:
|
||||
continue
|
||||
text = path.read_text()
|
||||
# Calls debugLog( but neither defines it nor declares the global
|
||||
calls = re.search(r"(?<![.\w])debugLog\(", text)
|
||||
defines = "window.debugLog" in text
|
||||
declares = re.search(r"/\*\s*global[^*]*\bdebugLog\b", text)
|
||||
if calls and not defines and not declares:
|
||||
undeclared.append(str(path.relative_to(PROJECT_ROOT)))
|
||||
assert not undeclared, (
|
||||
f"JS files call debugLog() without a /* global debugLog */ header: {undeclared}"
|
||||
)
|
||||
Reference in New Issue
Block a user