ferro_json_ui/runtime/
mod.rs1mod dismissibles;
9mod dropdowns;
10mod form_guards;
11mod hero_lazy;
12mod kanban;
13mod modals;
14mod notifications;
15mod product_tiles;
16mod scroll_preserve;
17mod sidebar;
18mod sse;
19mod tabs;
20mod toasts;
21
22use std::sync::LazyLock;
23
24pub static FERRO_RUNTIME_JS: LazyLock<String> = LazyLock::new(|| {
28 let mut s = String::with_capacity(8 * 1024);
29 s.push_str("(function() {\n 'use strict';\n");
30 s.push_str(sse::SOURCE);
31 s.push_str(tabs::SOURCE);
32 s.push_str(toasts::SOURCE);
33 s.push_str(dismissibles::SOURCE);
34 s.push_str(notifications::SOURCE);
35 s.push_str(dropdowns::SOURCE);
36 s.push_str(modals::SOURCE);
37 s.push_str(sidebar::SOURCE);
38 s.push_str(form_guards::SOURCE);
39 s.push_str(product_tiles::SOURCE);
40 s.push_str(kanban::SOURCE);
41 s.push_str(scroll_preserve::SOURCE);
42 s.push_str(hero_lazy::SOURCE);
43 s.push_str(
44 "\n function ferroRuntime() {\n\
45 \x20 setupScrollPreserve();\n\
46 \x20 setupSSE();\n\
47 \x20 setupTabs();\n\
48 \x20 setupDismissibles();\n\
49 \x20 setupNotifications();\n\
50 \x20 setupDropdowns();\n\
51 \x20 setupKanban();\n\
52 \x20 setupSidebar();\n\
53 \x20 setupFormGuards();\n\
54 \x20 setupProductTiles();\n\
55 \x20 setupModals();\n\
56 \x20 setupToasts();\n\
57 \x20 setupLazyHeroes();\n\
58 \x20 }\n\
59 \x20 document.addEventListener('DOMContentLoaded', ferroRuntime);\n\
60 })();\n",
61 );
62 s
63});
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn variant_classes_use_semantic_tokens() {
71 assert!(FERRO_RUNTIME_JS.contains("bg-primary"));
72 assert!(FERRO_RUNTIME_JS.contains("bg-success"));
73 assert!(FERRO_RUNTIME_JS.contains("bg-warning"));
74 assert!(FERRO_RUNTIME_JS.contains("bg-destructive"));
75 assert!(!FERRO_RUNTIME_JS.contains("bg-blue-500"));
76 assert!(!FERRO_RUNTIME_JS.contains("bg-green-500"));
77 assert!(!FERRO_RUNTIME_JS.contains("bg-yellow-500"));
78 assert!(!FERRO_RUNTIME_JS.contains("bg-red-500"));
79 assert!(!FERRO_RUNTIME_JS.contains("data-toast-variant"));
82 assert!(!FERRO_RUNTIME_JS.contains("duration-300"));
85 assert!(!FERRO_RUNTIME_JS.contains("duration-150"));
86 assert!(FERRO_RUNTIME_JS.contains("duration-base"));
87 assert!(FERRO_RUNTIME_JS.contains("transitionend"));
88 }
89
90 #[test]
91 fn tab_switcher_uses_semantic_tokens() {
92 assert!(FERRO_RUNTIME_JS.contains("border-primary"));
93 assert!(FERRO_RUNTIME_JS.contains("text-primary"));
94 assert!(FERRO_RUNTIME_JS.contains("text-text-muted"));
95 assert!(!FERRO_RUNTIME_JS.contains("border-blue-600"));
96 assert!(!FERRO_RUNTIME_JS.contains("text-blue-600"));
97 assert!(!FERRO_RUNTIME_JS.contains("text-gray-500"));
98 }
99
100 #[test]
101 fn toast_uses_semantic_text_color() {
102 assert!(FERRO_RUNTIME_JS.contains("text-primary-foreground"));
103 assert!(!FERRO_RUNTIME_JS.contains("text-white"));
104 }
105
106 #[test]
111 fn toast_tone_classes_match_ssr() {
112 use crate::render::classes::{
113 TOAST_TONE_DESTRUCTIVE, TOAST_TONE_NEUTRAL, TOAST_TONE_SUCCESS, TOAST_TONE_WARNING,
114 };
115 for tone_classes in [
116 TOAST_TONE_NEUTRAL,
117 TOAST_TONE_SUCCESS,
118 TOAST_TONE_WARNING,
119 TOAST_TONE_DESTRUCTIVE,
120 ] {
121 assert!(
122 FERRO_RUNTIME_JS.contains(tone_classes),
123 "runtime VARIANT_CLASSES drifted from SSR toast tone classes: missing `{tone_classes}`"
124 );
125 }
126 assert!(
127 FERRO_RUNTIME_JS.contains("backdrop-blur-md"),
128 "runtime toast shell drifted from SSR: missing `backdrop-blur-md`"
129 );
130 }
131
132 #[test]
137 fn test_runtime_contains_popover_dropdown_wiring() {
138 assert!(FERRO_RUNTIME_JS.contains("data-popover-menu"));
139 assert!(FERRO_RUNTIME_JS.contains(":popover-open"));
140 assert!(FERRO_RUNTIME_JS.contains("hidePopover"));
141 assert!(FERRO_RUNTIME_JS.contains("positionUnderTrigger"));
142 assert!(FERRO_RUNTIME_JS.contains("getBoundingClientRect"));
143 }
144
145 #[test]
146 fn test_runtime_contains_modal_wiring() {
147 assert!(FERRO_RUNTIME_JS.contains("setupModals"));
148 assert!(FERRO_RUNTIME_JS.contains("data-modal-open"));
149 assert!(FERRO_RUNTIME_JS.contains("showModal"));
150 assert!(FERRO_RUNTIME_JS.contains("data-modal-close"));
151 }
152
153 #[test]
154 fn test_runtime_contains_toast_from_url() {
155 assert!(FERRO_RUNTIME_JS.contains("initToastFromUrl"));
156 assert!(FERRO_RUNTIME_JS.contains("URLSearchParams"));
157 assert!(FERRO_RUNTIME_JS.contains("history.replaceState"));
158 }
159
160 #[test]
161 fn runtime_contains_init_tab_from_url() {
162 assert!(
163 FERRO_RUNTIME_JS.contains("initTabFromUrl"),
164 "FERRO_RUNTIME_JS must include initTabFromUrl for F3 — URL-driven tab init"
165 );
166 assert!(
167 FERRO_RUNTIME_JS.contains("URLSearchParams"),
168 "FERRO_RUNTIME_JS must use URLSearchParams to parse ?tab= for initTabFromUrl"
169 );
170 }
171
172 #[test]
173 fn bundle_contains_dispatcher() {
174 assert!(FERRO_RUNTIME_JS.contains("function ferroRuntime()"));
175 assert!(FERRO_RUNTIME_JS.contains("DOMContentLoaded"));
176 assert!(FERRO_RUNTIME_JS.contains("ferroRuntime"));
177 }
178
179 #[test]
180 fn bundle_contains_all_setup_functions() {
181 for fn_name in [
182 "setupSSE",
183 "setupTabs",
184 "setupToasts",
185 "setupSidebar",
186 "setupDropdowns",
187 "setupModals",
188 "setupDismissibles",
189 "setupNotifications",
190 "setupFormGuards",
191 "setupProductTiles",
192 "setupKanban",
193 "setupScrollPreserve",
194 "setupLazyHeroes",
195 ] {
196 assert!(
197 FERRO_RUNTIME_JS.contains(fn_name),
198 "bundle missing {fn_name}"
199 );
200 }
201 }
202
203 #[test]
204 fn bundle_is_single_iife() {
205 assert!(FERRO_RUNTIME_JS.starts_with("(function() {"));
206 assert!(FERRO_RUNTIME_JS.trim_end().ends_with("})();"));
207 }
208
209 #[test]
210 fn dispatcher_invokes_every_setup() {
211 let js: &str = FERRO_RUNTIME_JS.as_str();
212 let dispatcher_start = js.find("function ferroRuntime()").unwrap();
213 let dispatcher = &js[dispatcher_start..];
214 for call in [
215 "setupSSE();",
216 "setupTabs();",
217 "setupToasts();",
218 "setupSidebar();",
219 "setupDropdowns();",
220 "setupModals();",
221 "setupDismissibles();",
222 "setupNotifications();",
223 "setupFormGuards();",
224 "setupProductTiles();",
225 "setupKanban();",
226 "setupScrollPreserve();",
227 "setupLazyHeroes();",
228 ] {
229 assert!(dispatcher.contains(call), "dispatcher missing {call}");
230 }
231 }
232
233 #[test]
234 fn runtime_contains_lazy_hero_setup() {
235 assert!(FERRO_RUNTIME_JS.contains("setupLazyHeroes"));
236 assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero"));
237 assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero-margin"));
238 assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero-promoted"));
239 assert!(FERRO_RUNTIME_JS.contains("IntersectionObserver"));
240 assert!(FERRO_RUNTIME_JS.contains("preload"));
241 assert!(FERRO_RUNTIME_JS.contains("'auto'"));
244 assert!(FERRO_RUNTIME_JS.contains("unobserve"));
245 }
246}