Skip to main content

ferro_json_ui/runtime/
mod.rs

1//! Built-in JavaScript runtime for ferro-json-ui (split per concern).
2//!
3//! Each submodule contributes one `setup*` function. The assembled bundle
4//! wraps them in a single IIFE with a `ferroRuntime()` dispatcher invoked
5//! on DOMContentLoaded. The emitted output is a single string — no extra
6//! HTTP requests.
7
8mod 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
24/// Assembled JS runtime bundle. Lazily concatenated from per-concern
25/// submodules on first access; the resulting string is stable for the
26/// process lifetime.
27pub 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    }
80
81    #[test]
82    fn tab_switcher_uses_semantic_tokens() {
83        assert!(FERRO_RUNTIME_JS.contains("border-primary"));
84        assert!(FERRO_RUNTIME_JS.contains("text-primary"));
85        assert!(FERRO_RUNTIME_JS.contains("text-text-muted"));
86        assert!(!FERRO_RUNTIME_JS.contains("border-blue-600"));
87        assert!(!FERRO_RUNTIME_JS.contains("text-blue-600"));
88        assert!(!FERRO_RUNTIME_JS.contains("text-gray-500"));
89    }
90
91    #[test]
92    fn toast_uses_semantic_text_color() {
93        assert!(FERRO_RUNTIME_JS.contains("text-primary-foreground"));
94        assert!(!FERRO_RUNTIME_JS.contains("text-white"));
95    }
96
97    /// Popover-based dropdown wiring: the panel uses the HTML `popover`
98    /// attribute so the browser lifts it into the top layer (escaping any
99    /// `overflow:hidden` ancestor and the surrounding z-index stack). We
100    /// supply only the anchor positioning and the close-on-scroll behavior.
101    #[test]
102    fn test_runtime_contains_popover_dropdown_wiring() {
103        assert!(FERRO_RUNTIME_JS.contains("data-popover-menu"));
104        assert!(FERRO_RUNTIME_JS.contains(":popover-open"));
105        assert!(FERRO_RUNTIME_JS.contains("hidePopover"));
106        assert!(FERRO_RUNTIME_JS.contains("positionUnderTrigger"));
107        assert!(FERRO_RUNTIME_JS.contains("getBoundingClientRect"));
108    }
109
110    #[test]
111    fn test_runtime_contains_modal_wiring() {
112        assert!(FERRO_RUNTIME_JS.contains("setupModals"));
113        assert!(FERRO_RUNTIME_JS.contains("data-modal-open"));
114        assert!(FERRO_RUNTIME_JS.contains("showModal"));
115        assert!(FERRO_RUNTIME_JS.contains("data-modal-close"));
116    }
117
118    #[test]
119    fn test_runtime_contains_toast_from_url() {
120        assert!(FERRO_RUNTIME_JS.contains("initToastFromUrl"));
121        assert!(FERRO_RUNTIME_JS.contains("URLSearchParams"));
122        assert!(FERRO_RUNTIME_JS.contains("history.replaceState"));
123    }
124
125    #[test]
126    fn runtime_contains_init_tab_from_url() {
127        assert!(
128            FERRO_RUNTIME_JS.contains("initTabFromUrl"),
129            "FERRO_RUNTIME_JS must include initTabFromUrl for F3 — URL-driven tab init"
130        );
131        assert!(
132            FERRO_RUNTIME_JS.contains("URLSearchParams"),
133            "FERRO_RUNTIME_JS must use URLSearchParams to parse ?tab= for initTabFromUrl"
134        );
135    }
136
137    #[test]
138    fn bundle_contains_dispatcher() {
139        assert!(FERRO_RUNTIME_JS.contains("function ferroRuntime()"));
140        assert!(FERRO_RUNTIME_JS.contains("DOMContentLoaded"));
141        assert!(FERRO_RUNTIME_JS.contains("ferroRuntime"));
142    }
143
144    #[test]
145    fn bundle_contains_all_setup_functions() {
146        for fn_name in [
147            "setupSSE",
148            "setupTabs",
149            "setupToasts",
150            "setupSidebar",
151            "setupDropdowns",
152            "setupModals",
153            "setupDismissibles",
154            "setupNotifications",
155            "setupFormGuards",
156            "setupProductTiles",
157            "setupKanban",
158            "setupScrollPreserve",
159            "setupLazyHeroes",
160        ] {
161            assert!(
162                FERRO_RUNTIME_JS.contains(fn_name),
163                "bundle missing {fn_name}"
164            );
165        }
166    }
167
168    #[test]
169    fn bundle_is_single_iife() {
170        assert!(FERRO_RUNTIME_JS.starts_with("(function() {"));
171        assert!(FERRO_RUNTIME_JS.trim_end().ends_with("})();"));
172    }
173
174    #[test]
175    fn dispatcher_invokes_every_setup() {
176        let js: &str = FERRO_RUNTIME_JS.as_str();
177        let dispatcher_start = js.find("function ferroRuntime()").unwrap();
178        let dispatcher = &js[dispatcher_start..];
179        for call in [
180            "setupSSE();",
181            "setupTabs();",
182            "setupToasts();",
183            "setupSidebar();",
184            "setupDropdowns();",
185            "setupModals();",
186            "setupDismissibles();",
187            "setupNotifications();",
188            "setupFormGuards();",
189            "setupProductTiles();",
190            "setupKanban();",
191            "setupScrollPreserve();",
192            "setupLazyHeroes();",
193        ] {
194            assert!(dispatcher.contains(call), "dispatcher missing {call}");
195        }
196    }
197
198    #[test]
199    fn runtime_contains_lazy_hero_setup() {
200        assert!(FERRO_RUNTIME_JS.contains("setupLazyHeroes"));
201        assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero"));
202        assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero-margin"));
203        assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero-promoted"));
204        assert!(FERRO_RUNTIME_JS.contains("IntersectionObserver"));
205        assert!(FERRO_RUNTIME_JS.contains("preload"));
206        // JS source uses single quotes (`setAttribute('preload', 'auto')`),
207        // matching sibling-runtime convention; assert the single-quoted literal.
208        assert!(FERRO_RUNTIME_JS.contains("'auto'"));
209        assert!(FERRO_RUNTIME_JS.contains("unobserve"));
210    }
211}