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_skip_raw(name: &str, value: &str) -> FragmentMatcher {
231 FragmentMatcher::Attribute(AttrMatcher {
232 name: name.to_string(),
233 raw_value: true,
234 value: Some(value.to_string()),
235 attr_skip: true,
236 ..Default::default()
237 })
238}
239
240pub fn attr_skip_template(name: &str, template: &str) -> FragmentMatcher {
242 FragmentMatcher::Attribute(AttrMatcher {
243 name: name.to_string(),
244 template: Some(template.to_string()),
245 attr_skip: true,
246 ..Default::default()
247 })
248}
249
250pub fn attr_start(name: &str, value: &str) -> FragmentMatcher {
252 FragmentMatcher::Attribute(AttrMatcher {
253 name: name.to_string(),
254 value: Some(value.to_string()),
255 attr_start: true,
256 ..Default::default()
257 })
258}
259
260pub fn component(id: &str) -> FragmentMatcher {
262 FragmentMatcher::Component(id.to_string())
263}
264
265pub fn for_loop(item: &str, collection: &str, template: &str) -> FragmentMatcher {
267 FragmentMatcher::ForLoop {
268 item: item.to_string(),
269 collection: collection.to_string(),
270 template: template.to_string(),
271 }
272}
273
274pub fn if_cond(template: &str) -> FragmentMatcher {
276 FragmentMatcher::IfCond {
277 template: template.to_string(),
278 }
279}
280
281pub fn assert_fragment_list(
285 fragments: &[webui_protocol::WebUIFragment],
286 matchers: &[FragmentMatcher],
287) {
288 use webui_protocol::web_ui_fragment::Fragment;
289
290 assert_eq!(
291 fragments.len(),
292 matchers.len(),
293 "Fragment count mismatch: got {} fragments, expected {}\nFragments: {:#?}",
294 fragments.len(),
295 matchers.len(),
296 fragments.iter().map(format_fragment).collect::<Vec<_>>()
297 );
298
299 for (i, (frag, matcher)) in fragments.iter().zip(matchers.iter()).enumerate() {
300 match (frag.fragment.as_ref(), matcher) {
301 (Some(Fragment::Raw(r)), FragmentMatcher::Raw(expected)) => {
302 assert_eq!(r.value, *expected, "Fragment[{}]: raw value mismatch", i);
303 }
304 (Some(Fragment::Signal(s)), FragmentMatcher::Signal { value, raw }) => {
305 assert_eq!(s.value, *value, "Fragment[{}]: signal value mismatch", i);
306 assert_eq!(s.raw, *raw, "Fragment[{}]: signal raw flag mismatch", i);
307 }
308 (Some(Fragment::Attribute(a)), FragmentMatcher::Attribute(m)) => {
309 assert_eq!(a.name, m.name, "Fragment[{}]: attr name mismatch", i);
310 if let Some(ref v) = m.value {
311 assert_eq!(a.value, *v, "Fragment[{}]: attr value mismatch", i);
312 }
313 if let Some(ref t) = m.template {
314 assert_eq!(a.template, *t, "Fragment[{}]: attr template mismatch", i);
315 }
316 assert_eq!(
317 a.complex, m.complex,
318 "Fragment[{}]: attr complex mismatch",
319 i
320 );
321 assert_eq!(
322 a.attr_start, m.attr_start,
323 "Fragment[{}]: attr_start mismatch",
324 i
325 );
326 assert_eq!(
327 a.attr_skip, m.attr_skip,
328 "Fragment[{}]: attr_skip mismatch",
329 i
330 );
331 assert_eq!(
332 a.raw_value, m.raw_value,
333 "Fragment[{}]: raw_value mismatch",
334 i
335 );
336 if let Some(ref sig) = m.bool_signal {
337 let cond = a
338 .condition_tree
339 .as_ref()
340 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
341 match cond.expr.as_ref() {
342 Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
343 assert_eq!(
344 id.value, *sig,
345 "Fragment[{}]: bool attr signal mismatch",
346 i
347 );
348 }
349 other => panic!(
350 "Fragment[{}]: expected identifier condition, got {:?}",
351 i, other
352 ),
353 }
354 }
355 if let Some((ref left, op, ref right)) = m.bool_predicate {
356 let cond = a
357 .condition_tree
358 .as_ref()
359 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
360 match cond.expr.as_ref() {
361 Some(webui_protocol::condition_expr::Expr::Predicate(pred)) => {
362 assert_eq!(
363 pred.left, *left,
364 "Fragment[{}]: predicate left mismatch",
365 i
366 );
367 assert_eq!(
368 pred.operator, op,
369 "Fragment[{}]: predicate operator mismatch",
370 i
371 );
372 assert_eq!(
373 pred.right, *right,
374 "Fragment[{}]: predicate right mismatch",
375 i
376 );
377 }
378 other => panic!(
379 "Fragment[{}]: expected predicate condition, got {:?}",
380 i, other
381 ),
382 }
383 }
384 if let Some(ref inner) = m.bool_not {
385 let cond = a
386 .condition_tree
387 .as_ref()
388 .unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
389 match cond.expr.as_ref() {
390 Some(webui_protocol::condition_expr::Expr::Not(not_cond)) => {
391 let inner_cond = not_cond.condition.as_ref().unwrap_or_else(|| {
392 panic!("Fragment[{}]: not condition missing inner", i)
393 });
394 match inner_cond.expr.as_ref() {
395 Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
396 assert_eq!(
397 id.value, *inner,
398 "Fragment[{}]: not inner identifier mismatch",
399 i
400 );
401 }
402 other => panic!(
403 "Fragment[{}]: expected identifier inside not, got {:?}",
404 i, other
405 ),
406 }
407 }
408 other => panic!("Fragment[{}]: expected not condition, got {:?}", i, other),
409 }
410 }
411 }
412 (Some(Fragment::Component(c)), FragmentMatcher::Component(id)) => {
413 assert_eq!(c.fragment_id, *id, "Fragment[{}]: component id mismatch", i);
414 }
415 (
416 Some(Fragment::ForLoop(fl)),
417 FragmentMatcher::ForLoop {
418 item,
419 collection,
420 template,
421 },
422 ) => {
423 assert_eq!(fl.item, *item, "Fragment[{}]: for item mismatch", i);
424 assert_eq!(
425 fl.collection, *collection,
426 "Fragment[{}]: for collection mismatch",
427 i
428 );
429 assert_eq!(
430 fl.fragment_id, *template,
431 "Fragment[{}]: for template mismatch",
432 i
433 );
434 }
435 (Some(Fragment::IfCond(ic)), FragmentMatcher::IfCond { template }) => {
436 assert_eq!(
437 ic.fragment_id, *template,
438 "Fragment[{}]: if template mismatch",
439 i
440 );
441 }
442 (_actual, expected) => {
443 panic!(
444 "Fragment[{}]: type mismatch\n expected: {:?}\n actual: {}",
445 i,
446 expected,
447 format_fragment(frag)
448 );
449 }
450 }
451 }
452}
453
454fn format_fragment(frag: &webui_protocol::WebUIFragment) -> String {
455 use webui_protocol::web_ui_fragment::Fragment;
456 match frag.fragment.as_ref() {
457 Some(Fragment::Raw(r)) => format!("raw({:?})", r.value),
458 Some(Fragment::Signal(s)) => format!("signal({:?}, raw={})", s.value, s.raw),
459 Some(Fragment::Attribute(a)) => format!(
460 "attr({:?}, value={:?}, template={:?}, complex={}, start={}, skip={}, raw_value={})",
461 a.name, a.value, a.template, a.complex, a.attr_start, a.attr_skip, a.raw_value
462 ),
463 Some(Fragment::Component(c)) => format!("component({:?})", c.fragment_id),
464 Some(Fragment::ForLoop(f)) => format!(
465 "for({:?} in {:?}, template={:?})",
466 f.item, f.collection, f.fragment_id
467 ),
468 Some(Fragment::IfCond(i)) => format!("if(template={:?})", i.fragment_id),
469 Some(Fragment::Plugin(p)) => format!("plugin(data={:?})", p.data),
470 Some(Fragment::Route(r)) => {
471 format!("route(path={:?}, fragment={:?})", r.path, r.fragment_id)
472 }
473 Some(Fragment::Outlet(_)) => "outlet".to_string(),
474 None => "None".to_string(),
475 }
476}
477
478pub struct TestFileSystem {
480 files: HashMap<String, PathBuf>,
481
482 _temp_dirs: Vec<TempDir>,
484}
485
486impl Default for TestFileSystem {
487 fn default() -> Self {
488 Self::new()
489 }
490}
491
492impl TestFileSystem {
493 pub fn new() -> Self {
495 Self {
496 files: HashMap::new(),
497 _temp_dirs: Vec::new(),
498 }
499 }
500
501 #[allow(clippy::disallowed_methods)] pub fn add_file(&mut self, path: &str, content: &str) -> PathBuf {
504 let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
506
507 let path_parts: Vec<&str> = path.split('/').collect();
509 let filename = path_parts.last().expect("Path must contain a filename");
510
511 let file_path = temp_dir.path().join(filename);
513 fs::write(&file_path, content).expect("Failed to write content to file");
514
515 self.files.insert(path.to_string(), file_path.clone());
517 self._temp_dirs.push(temp_dir);
518
519 self.files
521 .get(path)
522 .expect("File path not found in the test file system");
523
524 file_path
526 }
527}