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]
43macro_rules! assert_fragments {
44 ($fragments:expr, [ $($matcher:expr),* $(,)? ]) => {{
45 let matchers: Vec<$crate::FragmentMatcher> = vec![$($matcher),*];
46 $crate::assert_fragment_list(&$fragments, &matchers);
47 }};
48}
49
50#[macro_export]
52macro_rules! assert_stream {
53 ($records:expr, $stream_id:expr, [ $($matcher:expr),* $(,)? ]) => {{
54 let stream = $records.get($stream_id)
55 .unwrap_or_else(|| panic!("Missing stream: {}", $stream_id));
56 let matchers: Vec<$crate::FragmentMatcher> = vec![$($matcher),*];
57 $crate::assert_fragment_list(&stream.fragments, &matchers);
58 }};
59}
60
61#[derive(Debug)]
65pub enum FragmentMatcher {
66 Raw(String),
67 Signal {
68 value: String,
69 raw: bool,
70 raw_text_context: bool,
71 },
72 Attribute(AttrMatcher),
73 Component(String),
74 ForLoop {
75 item: String,
76 collection: String,
77 template: String,
78 },
79 IfCond {
80 template: String,
81 },
82 Boundary {
83 name: String,
84 declaration: u32,
85 },
86 BoundaryEnd {
87 declaration: u32,
88 },
89}
90
91#[derive(Debug, Default)]
93pub struct AttrMatcher {
94 pub name: String,
95 pub value: Option<String>,
96 pub template: Option<String>,
97 pub complex: bool,
98 pub attr_start: bool,
99 pub attr_skip: bool,
100 pub raw_value: bool,
101 pub bool_signal: Option<String>,
102 pub bool_predicate: Option<(String, i32, String)>,
104 pub bool_not: Option<String>,
106}
107
108pub fn raw(value: &str) -> FragmentMatcher {
112 FragmentMatcher::Raw(value.to_string())
113}
114
115pub fn signal(value: &str) -> FragmentMatcher {
117 FragmentMatcher::Signal {
118 value: value.to_string(),
119 raw: false,
120 raw_text_context: false,
121 }
122}
123
124pub fn signal_raw(value: &str) -> FragmentMatcher {
126 FragmentMatcher::Signal {
127 value: value.to_string(),
128 raw: true,
129 raw_text_context: false,
130 }
131}
132
133pub fn raw_text_signal(value: &str, raw: bool) -> FragmentMatcher {
135 FragmentMatcher::Signal {
136 value: value.to_string(),
137 raw,
138 raw_text_context: true,
139 }
140}
141
142pub fn attr(name: &str, value: &str) -> FragmentMatcher {
144 FragmentMatcher::Attribute(AttrMatcher {
145 name: name.to_string(),
146 value: Some(value.to_string()),
147 ..Default::default()
148 })
149}
150
151pub fn attr_template(name: &str, template: &str) -> FragmentMatcher {
153 FragmentMatcher::Attribute(AttrMatcher {
154 name: name.to_string(),
155 template: Some(template.to_string()),
156 ..Default::default()
157 })
158}
159
160pub fn attr_complex(name: &str, value: &str) -> FragmentMatcher {
162 FragmentMatcher::Attribute(AttrMatcher {
163 name: name.to_string(),
164 value: Some(value.to_string()),
165 complex: true,
166 ..Default::default()
167 })
168}
169
170pub fn attr_complex_start(name: &str, value: &str) -> FragmentMatcher {
172 FragmentMatcher::Attribute(AttrMatcher {
173 name: name.to_string(),
174 value: Some(value.to_string()),
175 complex: true,
176 attr_start: true,
177 ..Default::default()
178 })
179}
180
181pub fn bool_attr(name: &str, signal: &str) -> FragmentMatcher {
183 FragmentMatcher::Attribute(AttrMatcher {
184 name: name.to_string(),
185 bool_signal: Some(signal.to_string()),
186 ..Default::default()
187 })
188}
189
190pub fn bool_attr_start(name: &str, signal: &str) -> FragmentMatcher {
192 FragmentMatcher::Attribute(AttrMatcher {
193 name: name.to_string(),
194 bool_signal: Some(signal.to_string()),
195 attr_start: true,
196 ..Default::default()
197 })
198}
199
200pub fn bool_attr_predicate(name: &str, left: &str, op: i32, right: &str) -> FragmentMatcher {
202 FragmentMatcher::Attribute(AttrMatcher {
203 name: name.to_string(),
204 bool_predicate: Some((left.to_string(), op, right.to_string())),
205 ..Default::default()
206 })
207}
208
209pub fn bool_attr_not(name: &str, inner: &str) -> FragmentMatcher {
211 FragmentMatcher::Attribute(AttrMatcher {
212 name: name.to_string(),
213 bool_not: Some(inner.to_string()),
214 ..Default::default()
215 })
216}
217
218pub fn attr_raw(name: &str, value: &str) -> FragmentMatcher {
220 FragmentMatcher::Attribute(AttrMatcher {
221 name: name.to_string(),
222 value: Some(value.to_string()),
223 raw_value: true,
224 ..Default::default()
225 })
226}
227
228pub fn attr_raw_start(name: &str, value: &str) -> FragmentMatcher {
230 FragmentMatcher::Attribute(AttrMatcher {
231 name: name.to_string(),
232 value: Some(value.to_string()),
233 raw_value: true,
234 attr_start: true,
235 ..Default::default()
236 })
237}
238
239pub fn attr_skip(name: &str, value: &str) -> FragmentMatcher {
241 FragmentMatcher::Attribute(AttrMatcher {
242 name: name.to_string(),
243 value: Some(value.to_string()),
244 attr_skip: true,
245 ..Default::default()
246 })
247}
248
249pub fn attr_skip_raw(name: &str, value: &str) -> FragmentMatcher {
251 FragmentMatcher::Attribute(AttrMatcher {
252 name: name.to_string(),
253 raw_value: true,
254 value: Some(value.to_string()),
255 attr_skip: true,
256 ..Default::default()
257 })
258}
259
260pub fn attr_skip_template(name: &str, template: &str) -> FragmentMatcher {
262 FragmentMatcher::Attribute(AttrMatcher {
263 name: name.to_string(),
264 template: Some(template.to_string()),
265 attr_skip: true,
266 ..Default::default()
267 })
268}
269
270pub fn attr_start(name: &str, value: &str) -> FragmentMatcher {
272 FragmentMatcher::Attribute(AttrMatcher {
273 name: name.to_string(),
274 value: Some(value.to_string()),
275 attr_start: true,
276 ..Default::default()
277 })
278}
279
280pub fn component(id: &str) -> FragmentMatcher {
282 FragmentMatcher::Component(id.to_string())
283}
284
285pub fn for_loop(item: &str, collection: &str, template: &str) -> FragmentMatcher {
287 FragmentMatcher::ForLoop {
288 item: item.to_string(),
289 collection: collection.to_string(),
290 template: template.to_string(),
291 }
292}
293
294pub fn if_cond(template: &str) -> FragmentMatcher {
296 FragmentMatcher::IfCond {
297 template: template.to_string(),
298 }
299}
300
301pub fn boundary(name: &str, declaration: u32) -> FragmentMatcher {
303 FragmentMatcher::Boundary {
304 name: name.to_string(),
305 declaration,
306 }
307}
308
309#[must_use]
311pub fn boundary_end(declaration: u32) -> FragmentMatcher {
312 FragmentMatcher::BoundaryEnd { declaration }
313}
314
315pub fn assert_fragment_list(
319 fragments: &[webui_protocol::WebUIFragment],
320 matchers: &[FragmentMatcher],
321) {
322 use webui_protocol::web_ui_fragment::Fragment;
323
324 assert_eq!(
325 fragments.len(),
326 matchers.len(),
327 "Fragment count mismatch: got {} fragments, expected {}\nFragments: {:#?}",
328 fragments.len(),
329 matchers.len(),
330 fragments.iter().map(format_fragment).collect::<Vec<_>>()
331 );
332
333 for (i, (frag, matcher)) in fragments.iter().zip(matchers.iter()).enumerate() {
334 match (frag.fragment.as_ref(), matcher) {
335 (Some(Fragment::Raw(r)), FragmentMatcher::Raw(expected)) => {
336 assert_eq!(r.value, *expected, "Fragment[{}]: raw value mismatch", i);
337 }
338 (
339 Some(Fragment::Signal(s)),
340 FragmentMatcher::Signal {
341 value,
342 raw,
343 raw_text_context,
344 },
345 ) => {
346 assert_eq!(s.value, *value, "Fragment[{}]: signal value mismatch", i);
347 assert_eq!(s.raw, *raw, "Fragment[{}]: signal raw flag mismatch", i);
348 assert_eq!(
349 s.raw_text_context, *raw_text_context,
350 "Fragment[{i}]: signal raw-text context mismatch"
351 );
352 }
353 (Some(Fragment::Attribute(a)), FragmentMatcher::Attribute(m)) => {
354 assert_eq!(a.name, m.name, "Fragment[{}]: attr name mismatch", i);
355 if let Some(ref v) = m.value {
356 assert_eq!(a.value, *v, "Fragment[{}]: attr value mismatch", i);
357 }
358 if let Some(ref t) = m.template {
359 assert_eq!(a.template, *t, "Fragment[{}]: attr template mismatch", i);
360 }
361 assert_eq!(
362 a.complex, m.complex,
363 "Fragment[{}]: attr complex mismatch",
364 i
365 );
366 assert_eq!(
367 a.attr_start, m.attr_start,
368 "Fragment[{}]: attr_start mismatch",
369 i
370 );
371 assert_eq!(
372 a.attr_skip, m.attr_skip,
373 "Fragment[{}]: attr_skip mismatch",
374 i
375 );
376 assert_eq!(
377 a.raw_value, m.raw_value,
378 "Fragment[{}]: raw_value mismatch",
379 i
380 );
381 if let Some(ref sig) = m.bool_signal {
382 let cond = a
383 .condition_tree
384 .as_ref()
385 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
386 match cond.expr.as_ref() {
387 Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
388 assert_eq!(
389 id.value, *sig,
390 "Fragment[{}]: bool attr signal mismatch",
391 i
392 );
393 }
394 other => panic!(
395 "Fragment[{}]: expected identifier condition, got {:?}",
396 i, other
397 ),
398 }
399 }
400 if let Some((ref left, op, ref right)) = m.bool_predicate {
401 let cond = a
402 .condition_tree
403 .as_ref()
404 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
405 match cond.expr.as_ref() {
406 Some(webui_protocol::condition_expr::Expr::Predicate(pred)) => {
407 assert_eq!(
408 pred.left, *left,
409 "Fragment[{}]: predicate left mismatch",
410 i
411 );
412 assert_eq!(
413 pred.operator, op,
414 "Fragment[{}]: predicate operator mismatch",
415 i
416 );
417 assert_eq!(
418 pred.right, *right,
419 "Fragment[{}]: predicate right mismatch",
420 i
421 );
422 }
423 other => panic!(
424 "Fragment[{}]: expected predicate condition, got {:?}",
425 i, other
426 ),
427 }
428 }
429 if let Some(ref inner) = m.bool_not {
430 let cond = a
431 .condition_tree
432 .as_ref()
433 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
434 match cond.expr.as_ref() {
435 Some(webui_protocol::condition_expr::Expr::Not(not_cond)) => {
436 let inner_cond = not_cond.condition.as_ref().unwrap_or_else(|| {
437 panic!("Fragment[{}]: not condition missing inner", i)
438 });
439 match inner_cond.expr.as_ref() {
440 Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
441 assert_eq!(
442 id.value, *inner,
443 "Fragment[{}]: not inner identifier mismatch",
444 i
445 );
446 }
447 other => panic!(
448 "Fragment[{}]: expected identifier inside not, got {:?}",
449 i, other
450 ),
451 }
452 }
453 other => panic!("Fragment[{}]: expected not condition, got {:?}", i, other),
454 }
455 }
456 }
457 (Some(Fragment::Component(c)), FragmentMatcher::Component(id)) => {
458 assert_eq!(c.fragment_id, *id, "Fragment[{}]: component id mismatch", i);
459 }
460 (
461 Some(Fragment::ForLoop(fl)),
462 FragmentMatcher::ForLoop {
463 item,
464 collection,
465 template,
466 },
467 ) => {
468 assert_eq!(fl.item, *item, "Fragment[{}]: for item mismatch", i);
469 assert_eq!(
470 fl.collection, *collection,
471 "Fragment[{}]: for collection mismatch",
472 i
473 );
474 assert_eq!(
475 fl.fragment_id, *template,
476 "Fragment[{}]: for template mismatch",
477 i
478 );
479 }
480 (Some(Fragment::IfCond(ic)), FragmentMatcher::IfCond { template }) => {
481 assert_eq!(
482 ic.fragment_id, *template,
483 "Fragment[{}]: if template mismatch",
484 i
485 );
486 }
487 (
488 Some(Fragment::Boundary(boundary)),
489 FragmentMatcher::Boundary { name, declaration },
490 ) => {
491 assert_eq!(
492 boundary.phase(),
493 webui_protocol::BoundaryPhase::Start,
494 "Fragment[{}]: expected a boundary start marker",
495 i
496 );
497 assert_eq!(
498 boundary.name, *name,
499 "Fragment[{}]: boundary name mismatch",
500 i
501 );
502 assert_eq!(
503 boundary.declaration_id, *declaration,
504 "Fragment[{}]: boundary declaration mismatch",
505 i
506 );
507 }
508 (Some(Fragment::Boundary(boundary)), FragmentMatcher::BoundaryEnd { declaration }) => {
509 assert_eq!(
510 boundary.phase(),
511 webui_protocol::BoundaryPhase::End,
512 "Fragment[{}]: expected a boundary end marker",
513 i
514 );
515 assert_eq!(
516 boundary.declaration_id, *declaration,
517 "Fragment[{}]: boundary declaration mismatch",
518 i
519 );
520 }
521 (_actual, expected) => {
522 panic!(
523 "Fragment[{}]: type mismatch\n expected: {:?}\n actual: {}",
524 i,
525 expected,
526 format_fragment(frag)
527 );
528 }
529 }
530 }
531}
532
533fn format_fragment(frag: &webui_protocol::WebUIFragment) -> String {
534 use webui_protocol::web_ui_fragment::Fragment;
535 match frag.fragment.as_ref() {
536 Some(Fragment::Raw(r)) => format!("raw({:?})", r.value),
537 Some(Fragment::Signal(s)) if s.raw_text_context => {
538 format!("raw_text_signal({:?}, raw={})", s.value, s.raw)
539 }
540 Some(Fragment::Signal(s)) => format!("signal({:?}, raw={})", s.value, s.raw),
541 Some(Fragment::Attribute(a)) => format!(
542 "attr({:?}, value={:?}, template={:?}, complex={}, start={}, skip={}, raw_value={})",
543 a.name, a.value, a.template, a.complex, a.attr_start, a.attr_skip, a.raw_value
544 ),
545 Some(Fragment::Component(c)) => format!("component({:?})", c.fragment_id),
546 Some(Fragment::ForLoop(f)) => format!(
547 "for({:?} in {:?}, template={:?})",
548 f.item, f.collection, f.fragment_id
549 ),
550 Some(Fragment::IfCond(i)) => format!("if(template={:?})", i.fragment_id),
551 Some(Fragment::Plugin(p)) => format!("plugin(data={:?})", p.data),
552 Some(Fragment::Route(r)) => {
553 format!("route(path={:?}, fragment={:?})", r.path, r.fragment_id)
554 }
555 Some(Fragment::Outlet(_)) => "outlet".to_string(),
556 Some(Fragment::Boundary(boundary)) => format!(
557 "boundary(name={:?}, phase={:?}, declaration={})",
558 boundary.name,
559 boundary.phase(),
560 boundary.declaration_id
561 ),
562 None => "None".to_string(),
563 }
564}
565
566pub struct TestFileSystem {
568 files: HashMap<String, PathBuf>,
569
570 _temp_dirs: Vec<TempDir>,
572}
573
574impl Default for TestFileSystem {
575 fn default() -> Self {
576 Self::new()
577 }
578}
579
580impl TestFileSystem {
581 pub fn new() -> Self {
583 Self {
584 files: HashMap::new(),
585 _temp_dirs: Vec::new(),
586 }
587 }
588
589 #[allow(clippy::disallowed_methods)] pub fn add_file(&mut self, path: &str, content: &str) -> PathBuf {
592 let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
594
595 let path_parts: Vec<&str> = path.split('/').collect();
597 let filename = path_parts.last().expect("Path must contain a filename");
598
599 let file_path = temp_dir.path().join(filename);
601 fs::write(&file_path, content).expect("Failed to write content to file");
602
603 self.files.insert(path.to_string(), file_path.clone());
605 self._temp_dirs.push(temp_dir);
606
607 self.files
609 .get(path)
610 .expect("File path not found in the test file system");
611
612 file_path
614 }
615}