main
py 163 lines 7.63 KB
Raw
1 from pathlib import Path
2 from html.parser import HTMLParser
3
4
5 PROJECT_ROOT = Path(__file__).resolve().parents[1]
6
7
8 class ScriptParser(HTMLParser):
9 def __init__(self) -> None:
10 super().__init__()
11 self.scripts: list[dict[str, str | None]] = []
12
13 def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
14 if tag == "script":
15 self.scripts.append(dict(attrs))
16
17
18 def test_bootstrap_is_local_and_deferred() -> None:
19 index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
20
21 assert "cdn.jsdelivr.net/npm/bootstrap" not in index_html
22 assert '<script defer src="vendor/bootstrap/bootstrap.bundle.min.js"></script>' in index_html
23 assert (PROJECT_ROOT / "webui" / "vendor" / "bootstrap" / "bootstrap.bundle.min.js").is_file()
24
25
26 def test_classic_startup_scripts_are_deferred() -> None:
27 index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
28 parser = ScriptParser()
29 parser.feed(index_html)
30
31 blocking_scripts = [
32 script["src"]
33 for script in parser.scripts
34 if script.get("src")
35 and script.get("type") != "module"
36 and "defer" not in script
37 and "async" not in script
38 ]
39
40 assert blocking_scripts == []
41
42
43 def test_splash_prepares_worker_before_replacing_document_in_place() -> None:
44 splash_html = (PROJECT_ROOT / "webui" / "splash.html").read_text(
45 encoding="utf-8"
46 )
47 index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
48 ui_server = (PROJECT_ROOT / "helpers" / "ui_server.py").read_text(encoding="utf-8")
49
50 assert 'fetch("/ui/asset-bundle"' in splash_html
51 assert 'const APP_DOCUMENT_PATH = "/ui/index"' in splash_html
52 assert "const appDocumentPromise = fetch(APP_DOCUMENT_PATH" in splash_html
53 assert 'navigator.serviceWorker.register(expectedUrl' in splash_html
54 assert "await sendBundle(worker, bundle, true)" in splash_html
55 assert 'const overlay = document.getElementById("startup-transition")' in splash_html
56 assert 'overlay.dataset.theme = "light"' in splash_html
57 assert "(bodyTag) => bodyTag + overlay.outerHTML" in splash_html
58 assert "document.open()" in splash_html
59 assert "document.write(appWithTransition)" in splash_html
60 assert "document.close()" in splash_html
61 assert "location.replace" not in splash_html
62 assert 'const APP_PATH = "/index.html"' not in splash_html
63 assert 'fetch("/ui/asset-bundle"' not in index_html
64 assert 'id="ui-asset-bundle"' not in index_html
65 assert '"/index.html"' in ui_server
66 assert '"/ui/index"' in ui_server
67 assert '"/safe"' in ui_server
68 assert "handlers.serve_splash" in ui_server
69 assert "handlers.serve_safe" in ui_server
70 assert "handlers.serve_index" in ui_server
71 assert '"/ui/asset-bundle"' in ui_server
72 assert "handlers.serve_ui_asset_bundle" in ui_server
73 assert '"/ui/asset-graph"' not in ui_server
74 assert "handlers.serve_ui_asset_graph" not in ui_server
75 assert "GZipMiddleware(" in ui_server
76 assert "minimum_size=GZIP_MINIMUM_RESPONSE_BYTES" in ui_server
77 assert "compresslevel=GZIP_COMPRESSION_LEVEL" in ui_server
78 assert 'response.headers["Content-Encoding"] = "gzip"' in ui_server
79 assert 'request.if_none_match.contains_weak(version)' in ui_server
80 assert 'response.set_etag(version, weak=True)' in ui_server
81
82
83 def test_safe_mode_disables_workers_before_rendering_index_directly() -> None:
84 safe_html = (PROJECT_ROOT / "webui" / "safe.html").read_text(encoding="utf-8")
85 index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
86 ui_server = (PROJECT_ROOT / "helpers" / "ui_server.py").read_text(encoding="utf-8")
87
88 assert "navigator.serviceWorker.getRegistrations()" in safe_html
89 assert "registration.unregister()" in safe_html
90 assert 'const DIRECT_PARAMETER = "__direct"' in safe_html
91 assert "location.replace(target.href)" in safe_html
92 assert 'fetch("/ui/asset-bundle"' not in safe_html
93 assert "navigator.serviceWorker.register" not in safe_html
94 assert ' src="' not in safe_html
95 assert ' href="/' not in safe_html
96 assert 'request.args.get("__direct") == "1"' in ui_server
97 assert "return await self.serve_index()" in ui_server
98 assert 'files.read_file("webui/safe.html")' in ui_server
99 assert 'safeUrl.searchParams.delete("__direct")' in index_html
100 assert "navigator.serviceWorker.getRegistrations()" in index_html
101 assert "registration.unregister()" in index_html
102 assert "navigator.serviceWorker.register" not in index_html
103
104
105 def test_only_the_icon_guard_stylesheet_blocks_application_paint() -> None:
106 index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
107
108 assert index_html.count('<link rel="stylesheet"') == 1
109 assert '<link rel="stylesheet" href="vendor/google/google-icons.css">' in index_html
110 assert '<script type="module" src="/js/icons.js"></script>' in index_html
111 assert index_html.count('rel="preload" as="style"') == 19
112 assert index_html.count("onload=\"this.onload=null;this.rel='stylesheet'\"") == 19
113 assert 'id="startup-splash"' not in index_html
114 assert 'document.addEventListener("webui-bundle-loaded"' not in index_html
115
116
117 def test_startup_splash_is_handed_to_the_index_and_fades_when_ready() -> None:
118 splash_html = (PROJECT_ROOT / "webui" / "splash.html").read_text(
119 encoding="utf-8"
120 )
121 index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
122 extensions_js = (PROJECT_ROOT / "webui" / "js" / "extensions.js").read_text(
123 encoding="utf-8"
124 )
125
126 assert 'id="startup-splash"' not in index_html
127 assert 'data-splash-theme="dark"' not in index_html
128 assert '<main id="startup-transition"' not in index_html
129 assert 'id="startup-transition-critical"' in index_html
130 assert 'document.addEventListener("webui-extensions-loaded"' in index_html
131 assert 'overlay.classList.add("startup-transition-leaving")' in index_html
132 assert 'document.fonts.load(\'24px "Material Symbols Outlined"\')' in index_html
133 assert (
134 'document.addEventListener("DOMContentLoaded", loadMaterialIcons, { once: true })'
135 in index_html
136 )
137 assert 'localStorage.getItem("darkMode") === "false"' in index_html
138 assert "webuiExtensions: {{webui_extension_manifest}}" in index_html
139 assert 'manifestExtensionPaths("html", extensionPoint)' in extensions_js
140 assert 'manifestExtensionPaths("js", extensionPoint)' in extensions_js
141 assert 'export let initialHtmlExtensionsLoaded = false' in extensions_js
142 assert 'const LOADING_SELECTOR = "x-component > .loading:empty, x-extension.loading"' in extensions_js
143 assert 'targetElement.classList.add("loading")' in extensions_js
144 assert 'targetElement.classList.remove("loading")' in extensions_js
145 assert 'document.dispatchEvent(new Event("webui-extensions-loaded"))' in extensions_js
146 assert "globalThis.Alpine.nextTick" in extensions_js
147 assert "pendingHtmlImports" not in extensions_js
148 assert "data-extension-loaded" not in extensions_js
149 assert '<svg xmlns="http://www.w3.org/2000/svg"' in splash_html
150 assert '<main id="startup-transition"' in splash_html
151 assert 'localStorage.getItem("darkMode") === "false"' in splash_html
152 assert ' src="' not in splash_html
153 assert ' href="/' not in splash_html
154
155
156 def test_generic_loading_indicator_has_a_shared_default_delay() -> None:
157 modals_css = (PROJECT_ROOT / "webui" / "css" / "modals.css").read_text(
158 encoding="utf-8"
159 )
160
161 assert "--loading-delay: 500ms" in modals_css
162 assert "fadeIn 500ms ease-out var(--loading-delay) forwards" in modals_css
163 assert "fadeIn 0s linear var(--loading-delay) forwards" in modals_css