1mod error;
12
13#[cfg(all(not(feature = "handler"), not(feature = "parser")))]
14compile_error!("microsoft-webui-wasm requires at least one of the `handler` or `parser` features");
15
16#[cfg(feature = "handler")]
17mod handler;
18#[cfg(feature = "parser")]
19mod parser;
20
21#[cfg(feature = "handler")]
22pub use handler::Protocol;
23#[cfg(feature = "parser")]
24pub use parser::build_protocol;
25
26#[cfg(all(test, feature = "handler", feature = "parser"))]
27mod tests {
28 use super::*;
29 use crate::error::WasmError;
30 use std::collections::HashMap;
31 use webui_protocol::WebUIProtocol;
32
33 fn render_files_for_test(
34 files: &HashMap<String, String>,
35 state_json: &str,
36 entry: &str,
37 request_path: &str,
38 ) -> Result<String, WasmError> {
39 let protocol = parser::parse_to_protocol(files, entry, &[])?;
40 handler::render_protocol_to_string(&protocol, state_json, entry, request_path, None)
41 }
42
43 #[test]
44 fn test_simple_render() {
45 let mut files = HashMap::new();
46 files.insert(
47 "index.html".to_string(),
48 "<h1>Hello, {{name}}!</h1>".to_string(),
49 );
50
51 let result = render_files_for_test(&files, r#"{"name": "WebUI"}"#, "index.html", "/");
52 assert_eq!(result.unwrap(), "<h1>Hello, WebUI!</h1>");
53 }
54
55 #[test]
56 fn test_missing_entry_file() {
57 let files = HashMap::new();
58 let result = render_files_for_test(&files, "{}", "index.html", "/");
59 assert!(result.is_err());
60 let err = result.unwrap_err().to_string();
61 assert!(err.contains("not found"), "Unexpected error: {}", err);
62 }
63
64 #[test]
65 fn test_build_protocol_surfaces_invalid_w_ref() {
66 let mut files = HashMap::new();
67 files.insert(
68 "index.html".to_string(),
69 "<my-card>Hi</my-card>".to_string(),
70 );
71 files.insert(
72 "my-card.html".to_string(),
73 r#"<div><input w-ref="myInput" /></div>"#.to_string(),
74 );
75
76 let result = parser::build_protocol_inner(&files, "index.html", &[]);
77 assert!(result.is_err());
78 let err = result.unwrap_err().to_string();
79 assert!(
80 err.contains("invalid w-ref binding"),
81 "Unexpected error: {err}"
82 );
83 assert!(
84 err.contains("component <my-card> · element <input>"),
85 "Unexpected error: {err}"
86 );
87 }
88
89 #[test]
90 fn test_render_files_surfaces_invalid_event_handler() {
91 let mut files = HashMap::new();
92 files.insert("index.html".to_string(), "<my-btn>x</my-btn>".to_string());
93 files.insert(
94 "my-btn.html".to_string(),
95 r#"<div><button @click="e.preventDefault()">x</button></div>"#.to_string(),
96 );
97
98 let result = render_files_for_test(&files, "{}", "index.html", "/");
99 assert!(result.is_err());
100 let err = result.unwrap_err().to_string();
101 assert!(
102 err.contains("invalid @click handler"),
103 "Unexpected error: {err}"
104 );
105 assert!(
106 err.contains("component <my-btn> · element <button>"),
107 "Unexpected error: {err}"
108 );
109 }
110
111 #[test]
112 fn test_with_component() {
113 let mut files = HashMap::new();
114 files.insert(
115 "index.html".to_string(),
116 "<my-card>World</my-card>".to_string(),
117 );
118 files.insert(
119 "my-card.html".to_string(),
120 "<template shadowrootmode=\"open\"><div class=\"card\"><slot></slot></div></template>"
121 .to_string(),
122 );
123
124 let result = render_files_for_test(&files, "{}", "index.html", "/");
125 assert!(result.is_ok(), "Render failed: {:?}", result);
126 let html = result.as_deref().unwrap_or("");
127 assert!(html.contains("card"), "Expected card class in: {}", html);
128 }
129
130 #[test]
131 fn test_with_for_loop() {
132 let mut files = HashMap::new();
133 files.insert(
134 "index.html".to_string(),
135 "<for each=\"item in items\">{{item.name}}, </for>".to_string(),
136 );
137
138 let state = r#"{"items": [{"name": "A"}, {"name": "B"}]}"#;
139 let result = render_files_for_test(&files, state, "index.html", "/");
140 assert!(result.is_ok(), "Render failed: {:?}", result);
141 let html = result.as_deref().unwrap_or("");
142 assert!(html.contains("A"), "Expected 'A' in: {}", html);
143 assert!(html.contains("B"), "Expected 'B' in: {}", html);
144 }
145
146 #[test]
147 fn test_with_if_condition() {
148 let mut files = HashMap::new();
149 files.insert(
150 "index.html".to_string(),
151 "<if condition=\"show\">Visible</if>".to_string(),
152 );
153
154 let result_true = render_files_for_test(&files, r#"{"show": true}"#, "index.html", "/");
155 assert_eq!(result_true.unwrap(), "Visible");
156
157 let result_false = render_files_for_test(&files, r#"{"show": false}"#, "index.html", "/");
158 assert_eq!(result_false.unwrap(), "");
159 }
160
161 #[test]
162 fn test_invalid_state_json() {
163 let mut files = HashMap::new();
164 files.insert("index.html".to_string(), "<p>Hi</p>".to_string());
165
166 let result = render_files_for_test(&files, "not json", "index.html", "/");
167 assert!(result.is_err());
168 let err = result.unwrap_err().to_string();
169 assert!(
170 err.contains("State JSON error"),
171 "Unexpected error: {}",
172 err
173 );
174 }
175
176 #[test]
177 fn test_component_with_css() {
178 let mut files = HashMap::new();
179 files.insert(
180 "index.html".to_string(),
181 "<my-card>Content</my-card>".to_string(),
182 );
183 files.insert(
184 "my-card.html".to_string(),
185 "<template shadowrootmode=\"open\"><p><slot></slot></p></template>".to_string(),
186 );
187 files.insert("my-card.css".to_string(), "p { color: red; }".to_string());
188
189 let result = render_files_for_test(&files, "{}", "index.html", "/");
190 assert!(result.is_ok(), "Render failed: {:?}", result);
191 let html = result.as_deref().unwrap_or("");
192 assert!(
193 html.contains(
194 r#"<style data-webui-resource="my-card" data-webui-strategy="style">p { color: red; }</style>"#,
195 ),
196 "Expected inline <style> tag in: {}",
197 html
198 );
199 assert!(
200 !html.contains("<link"),
201 "Should not have external <link> tag in: {}",
202 html
203 );
204 }
205
206 #[test]
207 fn test_raw_signal() {
208 let mut files = HashMap::new();
209 files.insert(
210 "index.html".to_string(),
211 "<div>{{{raw_html}}}</div>".to_string(),
212 );
213
214 let result =
215 render_files_for_test(&files, r#"{"raw_html": "<b>bold</b>"}"#, "index.html", "/");
216 assert!(result.is_ok(), "Render failed: {:?}", result);
217 let html = result.as_deref().unwrap_or("");
218 assert!(
219 html.contains("<b>bold</b>"),
220 "Expected raw HTML in: {}",
221 html
222 );
223 }
224
225 #[test]
226 fn test_static_html_passthrough() {
227 let mut files = HashMap::new();
228 files.insert(
229 "index.html".to_string(),
230 "<h1>Static</h1><p>Content</p>".to_string(),
231 );
232
233 let result = render_files_for_test(&files, "{}", "index.html", "/");
234 assert_eq!(result.unwrap(), "<h1>Static</h1><p>Content</p>");
235 }
236
237 #[test]
238 fn test_protocol_tokens_empty() {
239 let protocol = WebUIProtocol::new(HashMap::new());
240 let bytes = protocol.to_protobuf().unwrap();
241 let decoded = WebUIProtocol::from_protobuf(&bytes).unwrap();
242 assert!(decoded.tokens.is_empty());
243 }
244
245 #[test]
246 fn test_protocol_tokens_roundtrip() {
247 let tokens = vec![
248 "colorBrandBackground".to_string(),
249 "fontSizeBase300".to_string(),
250 ];
251 let protocol = WebUIProtocol::with_tokens(HashMap::new(), tokens.clone());
252 let bytes = protocol.to_protobuf().unwrap();
253 let decoded = WebUIProtocol::from_protobuf(&bytes).unwrap();
254 assert_eq!(decoded.tokens, tokens);
255 }
256
257 #[test]
258 fn test_protocol_tokens_preserves_order() {
259 let tokens = vec!["zeta".to_string(), "alpha".to_string(), "zeta".to_string()];
260 let protocol = WebUIProtocol::with_tokens(HashMap::new(), tokens.clone());
261 let bytes = protocol.to_protobuf().unwrap();
262 let decoded = WebUIProtocol::from_protobuf(&bytes).unwrap();
263 assert_eq!(decoded.tokens, tokens);
264 }
265}