main
py 111 lines 3.53 KB
Raw
1 import re
2 from pathlib import Path
3
4
5 PROJECT_ROOT = Path(__file__).resolve().parents[1]
6 WEBUI_ROOT = PROJECT_ROOT / "webui"
7 PLUGINS_ROOT = PROJECT_ROOT / "plugins"
8 LEGACY_ICON_SPAN = re.compile(
9 r"<span\b(?=[^>]*\b(?:material-symbols-outlined|material-icons-outlined)\b)",
10 re.IGNORECASE,
11 )
12
13
14 def first_party_icon_sources() -> list[Path]:
15 roots = [WEBUI_ROOT]
16 roots.extend(
17 path
18 for path in sorted(PLUGINS_ROOT.iterdir())
19 if path.is_dir() and path.name.startswith("_")
20 )
21 return [
22 path
23 for root in roots
24 for path in root.rglob("*")
25 if path.is_file()
26 and path.suffix in {".html", ".js"}
27 and not {"vendor", "node_modules"}.intersection(path.relative_to(root).parts)
28 ]
29
30
31 def iter_x_icons(source: str):
32 cursor = 0
33 while True:
34 start = source.find("<x-icon", cursor)
35 if start < 0:
36 return
37
38 quote = ""
39 end = start + len("<x-icon")
40 while end < len(source):
41 char = source[end]
42 if quote:
43 if char == quote and source[end - 1] != "\\":
44 quote = ""
45 elif char in {'"', "'"}:
46 quote = char
47 elif char == ">":
48 break
49 end += 1
50
51 close = source.find("</x-icon>", end + 1)
52 assert end < len(source) and close >= 0
53 yield source[start : end + 1], source[end + 1 : close]
54 cursor = close + len("</x-icon>")
55
56
57 def test_first_party_webui_uses_empty_named_x_icon_elements() -> None:
58 legacy_offenders: list[str] = []
59 malformed: list[str] = []
60 icon_count = 0
61
62 for path in first_party_icon_sources():
63 source = path.read_text(encoding="utf-8")
64 if LEGACY_ICON_SPAN.search(source):
65 legacy_offenders.append(str(path.relative_to(PROJECT_ROOT)))
66
67 for start_tag, content in iter_x_icons(source):
68 icon_count += 1
69 if (
70 content.strip()
71 or re.search(r"\sx-text\s*=", start_tag)
72 or not re.search(r"(?:^|\s)(?::name|name)\s*=", start_tag)
73 ):
74 malformed.append(
75 f"{path.relative_to(PROJECT_ROOT)}: {start_tag[:160]}"
76 )
77
78 assert icon_count >= 500
79 assert legacy_offenders == []
80 assert malformed == []
81
82
83 def test_x_icon_is_font_backed_and_keeps_legacy_plugin_compatibility() -> None:
84 icons_js = (WEBUI_ROOT / "js" / "icons.js").read_text(encoding="utf-8")
85 icon_css = (WEBUI_ROOT / "vendor" / "google" / "google-icons.css").read_text(
86 encoding="utf-8"
87 )
88 index_html = (WEBUI_ROOT / "index.html").read_text(encoding="utf-8")
89
90 assert '<script type="module" src="/js/icons.js"></script>' in index_html
91 assert 'customElements.define(' in icons_js
92 assert 'return ["name"]' in icons_js
93 assert 'this.classList.add("material-symbols-outlined")' in icons_js
94 assert ".material-symbols-outlined" in icons_js
95 assert ".material-icons-outlined" in icons_js
96 assert "maskImage" not in icons_js
97 assert "/vendor/icons/" not in icons_js
98
99 assert "x-icon," in icon_css
100 assert ".material-symbols-outlined," in icon_css
101 assert ".material-icons-outlined" in icon_css
102 assert "width: 1em !important" in icon_css
103 assert "overflow: hidden !important" in icon_css
104 assert (
105 "document.fonts.load('24px \"Material Symbols Outlined\"')"
106 in index_html
107 )
108 assert (
109 'document.addEventListener("DOMContentLoaded", loadMaterialIcons, { once: true })'
110 in index_html
111 )