1use std::fs;
9use std::{collections::HashMap, path::PathBuf};
10use tempfile::TempDir;
11pub use webui_protocol;
12
13#[macro_export]
17macro_rules! test_json {
18 ($($json:tt)+) => {{
19 #[allow(clippy::disallowed_methods)]
20 let value = serde_json::json!($($json)+);
21 value
22 }};
23}
24
25#[macro_export]
42macro_rules! assert_fragments {
43 ($fragments:expr, [ $($matcher:expr),* $(,)? ]) => {{
44 let matchers: Vec<$crate::FragmentMatcher> = vec![$($matcher),*];
45 $crate::assert_fragment_list(&$fragments, &matchers);
46 }};
47}
48
49#[macro_export]
51macro_rules! assert_stream {
52 ($records:expr, $stream_id:expr, [ $($matcher:expr),* $(,)? ]) => {{
53 let stream = $records.get($stream_id)
54 .unwrap_or_else(|| panic!("Missing stream: {}", $stream_id));
55 let matchers: Vec<$crate::FragmentMatcher> = vec![$($matcher),*];
56 $crate::assert_fragment_list(&stream.fragments, &matchers);
57 }};
58}
59
60#[derive(Debug)]
64pub enum FragmentMatcher {
65 Raw(String),
66 Signal {
67 value: String,
68 raw: bool,
69 },
70 Attribute(AttrMatcher),
71 Component(String),
72 ForLoop {
73 item: String,
74 collection: String,
75 template: String,
76 },
77 IfCond {
78 template: String,
79 },
80}
81
82#[derive(Debug, Default)]
84pub struct AttrMatcher {
85 pub name: String,
86 pub value: Option<String>,
87 pub template: Option<String>,
88 pub complex: bool,
89 pub attr_start: bool,
90 pub attr_skip: bool,
91 pub raw_value: bool,
92 pub bool_signal: Option<String>,
93 pub bool_predicate: Option<(String, i32, String)>,
95 pub bool_not: Option<String>,
97}
98
99pub fn raw(value: &str) -> FragmentMatcher {
103 FragmentMatcher::Raw(value.to_string())
104}
105
106pub fn signal(value: &str) -> FragmentMatcher {
108 FragmentMatcher::Signal {
109 value: value.to_string(),
110 raw: false,
111 }
112}
113
114pub fn signal_raw(value: &str) -> FragmentMatcher {
116 FragmentMatcher::Signal {
117 value: value.to_string(),
118 raw: true,
119 }
120}
121
122pub fn attr(name: &str, value: &str) -> FragmentMatcher {
124 FragmentMatcher::Attribute(AttrMatcher {
125 name: name.to_string(),
126 value: Some(value.to_string()),
127 ..Default::default()
128 })
129}
130
131pub fn attr_template(name: &str, template: &str) -> FragmentMatcher {
133 FragmentMatcher::Attribute(AttrMatcher {
134 name: name.to_string(),
135 template: Some(template.to_string()),
136 ..Default::default()
137 })
138}
139
140pub fn attr_complex(name: &str, value: &str) -> FragmentMatcher {
142 FragmentMatcher::Attribute(AttrMatcher {
143 name: name.to_string(),
144 value: Some(value.to_string()),
145 complex: true,
146 ..Default::default()
147 })
148}
149
150pub fn attr_complex_start(name: &str, value: &str) -> FragmentMatcher {
152 FragmentMatcher::Attribute(AttrMatcher {
153 name: name.to_string(),
154 value: Some(value.to_string()),
155 complex: true,
156 attr_start: true,
157 ..Default::default()
158 })
159}
160
161pub fn bool_attr(name: &str, signal: &str) -> FragmentMatcher {
163 FragmentMatcher::Attribute(AttrMatcher {
164 name: name.to_string(),
165 bool_signal: Some(signal.to_string()),
166 ..Default::default()
167 })
168}
169
170pub fn bool_attr_start(name: &str, signal: &str) -> FragmentMatcher {
172 FragmentMatcher::Attribute(AttrMatcher {
173 name: name.to_string(),
174 bool_signal: Some(signal.to_string()),
175 attr_start: true,
176 ..Default::default()
177 })
178}
179
180pub fn bool_attr_predicate(name: &str, left: &str, op: i32, right: &str) -> FragmentMatcher {
182 FragmentMatcher::Attribute(AttrMatcher {
183 name: name.to_string(),
184 bool_predicate: Some((left.to_string(), op, right.to_string())),
185 ..Default::default()
186 })
187}
188
189pub fn bool_attr_not(name: &str, inner: &str) -> FragmentMatcher {
191 FragmentMatcher::Attribute(AttrMatcher {
192 name: name.to_string(),
193 bool_not: Some(inner.to_string()),
194 ..Default::default()
195 })
196}
197
198pub fn attr_raw(name: &str, value: &str) -> FragmentMatcher {
200 FragmentMatcher::Attribute(AttrMatcher {
201 name: name.to_string(),
202 value: Some(value.to_string()),
203 raw_value: true,
204 ..Default::default()
205 })
206}
207
208pub fn attr_raw_start(name: &str, value: &str) -> FragmentMatcher {
210 FragmentMatcher::Attribute(AttrMatcher {
211 name: name.to_string(),
212 value: Some(value.to_string()),
213 raw_value: true,
214 attr_start: true,
215 ..Default::default()
216 })
217}
218
219pub fn attr_skip(name: &str, value: &str) -> FragmentMatcher {
221 FragmentMatcher::Attribute(AttrMatcher {
222 name: name.to_string(),
223 value: Some(value.to_string()),
224 attr_skip: true,
225 ..Default::default()
226 })
227}
228
229pub fn attr_start(name: &str, value: &str) -> FragmentMatcher {
231 FragmentMatcher::Attribute(AttrMatcher {
232 name: name.to_string(),
233 value: Some(value.to_string()),
234 attr_start: true,
235 ..Default::default()
236 })
237}
238
239pub fn component(id: &str) -> FragmentMatcher {
241 FragmentMatcher::Component(id.to_string())
242}
243
244pub fn for_loop(item: &str, collection: &str, template: &str) -> FragmentMatcher {
246 FragmentMatcher::ForLoop {
247 item: item.to_string(),
248 collection: collection.to_string(),
249 template: template.to_string(),
250 }
251}
252
253pub fn if_cond(template: &str) -> FragmentMatcher {
255 FragmentMatcher::IfCond {
256 template: template.to_string(),
257 }
258}
259
260pub fn assert_fragment_list(
264 fragments: &[webui_protocol::WebUIFragment],
265 matchers: &[FragmentMatcher],
266) {
267 use webui_protocol::web_ui_fragment::Fragment;
268
269 assert_eq!(
270 fragments.len(),
271 matchers.len(),
272 "Fragment count mismatch: got {} fragments, expected {}\nFragments: {:#?}",
273 fragments.len(),
274 matchers.len(),
275 fragments.iter().map(format_fragment).collect::<Vec<_>>()
276 );
277
278 for (i, (frag, matcher)) in fragments.iter().zip(matchers.iter()).enumerate() {
279 match (frag.fragment.as_ref(), matcher) {
280 (Some(Fragment::Raw(r)), FragmentMatcher::Raw(expected)) => {
281 assert_eq!(r.value, *expected, "Fragment[{}]: raw value mismatch", i);
282 }
283 (Some(Fragment::Signal(s)), FragmentMatcher::Signal { value, raw }) => {
284 assert_eq!(s.value, *value, "Fragment[{}]: signal value mismatch", i);
285 assert_eq!(s.raw, *raw, "Fragment[{}]: signal raw flag mismatch", i);
286 }
287 (Some(Fragment::Attribute(a)), FragmentMatcher::Attribute(m)) => {
288 assert_eq!(a.name, m.name, "Fragment[{}]: attr name mismatch", i);
289 if let Some(ref v) = m.value {
290 assert_eq!(a.value, *v, "Fragment[{}]: attr value mismatch", i);
291 }
292 if let Some(ref t) = m.template {
293 assert_eq!(a.template, *t, "Fragment[{}]: attr template mismatch", i);
294 }
295 assert_eq!(
296 a.complex, m.complex,
297 "Fragment[{}]: attr complex mismatch",
298 i
299 );
300 assert_eq!(
301 a.attr_start, m.attr_start,
302 "Fragment[{}]: attr_start mismatch",
303 i
304 );
305 assert_eq!(
306 a.attr_skip, m.attr_skip,
307 "Fragment[{}]: attr_skip mismatch",
308 i
309 );
310 assert_eq!(
311 a.raw_value, m.raw_value,
312 "Fragment[{}]: raw_value mismatch",
313 i
314 );
315 if let Some(ref sig) = m.bool_signal {
316 let cond = a
317 .condition_tree
318 .as_ref()
319 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
320 match cond.expr.as_ref() {
321 Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
322 assert_eq!(
323 id.value, *sig,
324 "Fragment[{}]: bool attr signal mismatch",
325 i
326 );
327 }
328 other => panic!(
329 "Fragment[{}]: expected identifier condition, got {:?}",
330 i, other
331 ),
332 }
333 }
334 if let Some((ref left, op, ref right)) = m.bool_predicate {
335 let cond = a
336 .condition_tree
337 .as_ref()
338 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
339 match cond.expr.as_ref() {
340 Some(webui_protocol::condition_expr::Expr::Predicate(pred)) => {
341 assert_eq!(
342 pred.left, *left,
343 "Fragment[{}]: predicate left mismatch",
344 i
345 );
346 assert_eq!(
347 pred.operator, op,
348 "Fragment[{}]: predicate operator mismatch",
349 i
350 );
351 assert_eq!(
352 pred.right, *right,
353 "Fragment[{}]: predicate right mismatch",
354 i
355 );
356 }
357 other => panic!(
358 "Fragment[{}]: expected predicate condition, got {:?}",
359 i, other
360 ),
361 }
362 }
363 if let Some(ref inner) = m.bool_not {
364 let cond = a
365 .condition_tree
366 .as_ref()
367 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
368 match cond.expr.as_ref() {
369 Some(webui_protocol::condition_expr::Expr::Not(not_cond)) => {
370 let inner_cond = not_cond.condition.as_ref().unwrap_or_else(|| {
371 panic!("Fragment[{}]: not condition missing inner", i)
372 });
373 match inner_cond.expr.as_ref() {
374 Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
375 assert_eq!(
376 id.value, *inner,
377 "Fragment[{}]: not inner identifier mismatch",
378 i
379 );
380 }
381 other => panic!(
382 "Fragment[{}]: expected identifier inside not, got {:?}",
383 i, other
384 ),
385 }
386 }
387 other => panic!("Fragment[{}]: expected not condition, got {:?}", i, other),
388 }
389 }
390 }
391 (Some(Fragment::Component(c)), FragmentMatcher::Component(id)) => {
392 assert_eq!(c.fragment_id, *id, "Fragment[{}]: component id mismatch", i);
393 }
394 (
395 Some(Fragment::ForLoop(fl)),
396 FragmentMatcher::ForLoop {
397 item,
398 collection,
399 template,
400 },
401 ) => {
402 assert_eq!(fl.item, *item, "Fragment[{}]: for item mismatch", i);
403 assert_eq!(
404 fl.collection, *collection,
405 "Fragment[{}]: for collection mismatch",
406 i
407 );
408 assert_eq!(
409 fl.fragment_id, *template,
410 "Fragment[{}]: for template mismatch",
411 i
412 );
413 }
414 (Some(Fragment::IfCond(ic)), FragmentMatcher::IfCond { template }) => {
415 assert_eq!(
416 ic.fragment_id, *template,
417 "Fragment[{}]: if template mismatch",
418 i
419 );
420 }
421 (_actual, expected) => {
422 panic!(
423 "Fragment[{}]: type mismatch\n expected: {:?}\n actual: {}",
424 i,
425 expected,
426 format_fragment(frag)
427 );
428 }
429 }
430 }
431}
432
433fn format_fragment(frag: &webui_protocol::WebUIFragment) -> String {
434 use webui_protocol::web_ui_fragment::Fragment;
435 match frag.fragment.as_ref() {
436 Some(Fragment::Raw(r)) => format!("raw({:?})", r.value),
437 Some(Fragment::Signal(s)) => format!("signal({:?}, raw={})", s.value, s.raw),
438 Some(Fragment::Attribute(a)) => format!(
439 "attr({:?}, value={:?}, template={:?}, complex={}, start={}, skip={}, raw_value={})",
440 a.name, a.value, a.template, a.complex, a.attr_start, a.attr_skip, a.raw_value
441 ),
442 Some(Fragment::Component(c)) => format!("component({:?})", c.fragment_id),
443 Some(Fragment::ForLoop(f)) => format!(
444 "for({:?} in {:?}, template={:?})",
445 f.item, f.collection, f.fragment_id
446 ),
447 Some(Fragment::IfCond(i)) => format!("if(template={:?})", i.fragment_id),
448 Some(Fragment::Plugin(p)) => format!("plugin(data={:?})", p.data),
449 Some(Fragment::Route(r)) => {
450 format!("route(path={:?}, fragment={:?})", r.path, r.fragment_id)
451 }
452 Some(Fragment::Outlet(_)) => "outlet".to_string(),
453 None => "None".to_string(),
454 }
455}
456
457pub struct TestFileSystem {
459 files: HashMap<String, PathBuf>,
460
461 _temp_dirs: Vec<TempDir>,
463}
464
465impl Default for TestFileSystem {
466 fn default() -> Self {
467 Self::new()
468 }
469}
470
471impl TestFileSystem {
472 pub fn new() -> Self {
474 Self {
475 files: HashMap::new(),
476 _temp_dirs: Vec::new(),
477 }
478 }
479
480 pub fn add_file(&mut self, path: &str, content: &str) -> PathBuf {
482 let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
484
485 let path_parts: Vec<&str> = path.split('/').collect();
487 let filename = path_parts.last().expect("Path must contain a filename");
488
489 let file_path = temp_dir.path().join(filename);
491 fs::write(&file_path, content).expect("Failed to write content to file");
492
493 self.files.insert(path.to_string(), file_path.clone());
495 self._temp_dirs.push(temp_dir);
496
497 self.files
499 .get(path)
500 .expect("File path not found in the test file system");
501
502 file_path
504 }
505}