fix: icon()-Macro mit ignore-missing + Coverage-Test

Folge zum scales.svg-Vorfall (commit 01ea766):

1. icon.html: `{% include … ignore missing %}` — fehlende SVG-Files
   rendern jetzt leeren Span statt einen 500 auszuloesen. data-icon-
   Attribut zeigt den angefragten Namen, hilft im DevTools-Inspector.
2. tests/test_icons.py: scannt alle templates/-Files nach
   icon("name")-Aufrufen und prueft, dass jedes referenzierte Icon
   als SVG-File existiert. 4 Tests, alle gruen — verhindert dass
   solche Aufrufe in Zukunft unentdeckt durchrutschen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-06 17:35:59 +02:00
parent 01ea7665cc
commit c3d4ab186f
2 changed files with 65 additions and 1 deletions

View File

@ -1 +1,2 @@
{% macro icon(name, size=16, cls="") %}<span class="v2-icon {{ cls }}" style="display:inline-flex;width:{{ size }}px;height:{{ size }}px;flex-shrink:0;vertical-align:middle;" aria-hidden="true">{% include "v2/icons/phosphor/" ~ name ~ ".svg" %}</span>{% endmacro %} {# `ignore missing`: fehlendes Icon-File rendert leeren Span statt 500. #}
{% macro icon(name, size=16, cls="") %}<span class="v2-icon {{ cls }}" style="display:inline-flex;width:{{ size }}px;height:{{ size }}px;flex-shrink:0;vertical-align:middle;" aria-hidden="true" data-icon="{{ name }}">{% include "v2/icons/phosphor/" ~ name ~ ".svg" ignore missing %}</span>{% endmacro %}

63
tests/test_icons.py Normal file
View File

@ -0,0 +1,63 @@
"""Verify dass jeder im Template-Code referenzierte Icon-Name als
phosphor/<name>.svg-File existiert.
Hintergrund: Vor diesem Test gab es einen 500 nach Login, weil
`base.html` `icon("scales")` aufrief, das SVG aber nicht im Repo lag
(commit 741faae). Mit `ignore missing` rendert das Macro jetzt leer
statt zu crashen aber wir wollen trotzdem in CI sehen, dass kein
Aufruf ins Leere läuft.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
TEMPLATES_DIR = REPO_ROOT / "app" / "templates"
ICONS_DIR = TEMPLATES_DIR / "v2" / "icons" / "phosphor"
# Matcht: icon("name", …) oder icon('name', …) — Default-Quote +/-.
ICON_CALL_PATTERN = re.compile(r"""\bicon\(\s*['"]([a-z0-9-]+)['"]""")
def _collect_referenced_icons() -> set[str]:
"""Sammle alle Icon-Namen, die irgendwo in templates/ aufgerufen werden."""
referenced: set[str] = set()
for path in TEMPLATES_DIR.rglob("*.html"):
text = path.read_text(encoding="utf-8")
for m in ICON_CALL_PATTERN.finditer(text):
referenced.add(m.group(1))
return referenced
def _existing_icons() -> set[str]:
"""Set der existierenden phosphor-Icon-Namen (ohne .svg)."""
if not ICONS_DIR.exists():
return set()
return {p.stem for p in ICONS_DIR.glob("*.svg")}
class TestIconCoverage:
def test_phosphor_dir_exists(self):
assert ICONS_DIR.is_dir(), f"erwartetes Verzeichnis fehlt: {ICONS_DIR}"
def test_at_least_one_icon_present(self):
icons = _existing_icons()
assert icons, "keine Icon-Files im phosphor/-Verzeichnis"
def test_at_least_one_call_referenced(self):
refs = _collect_referenced_icons()
assert refs, "keine icon('name')-Aufrufe in Templates gefunden"
def test_every_referenced_icon_has_file(self):
"""Jeder im Template referenzierte Icon-Name muss als SVG-File existieren."""
referenced = _collect_referenced_icons()
existing = _existing_icons()
missing = referenced - existing
assert not missing, (
f"{len(missing)} referenzierte Icons fehlen in "
f"{ICONS_DIR.relative_to(REPO_ROOT)}: {sorted(missing)}"
)