1pub mod plugin;
10pub mod route_handler;
11pub mod route_matcher;
12pub(crate) mod route_renderer;
13
14use plugin::HandlerPlugin;
15use serde_json::Value;
16use std::collections::{HashMap, HashSet};
17use thiserror::Error;
18use webui_expressions::{evaluate_with_resolver, ExpressionError};
19use webui_protocol::{web_ui_fragment::Fragment, WebUIFragment, WebUIProtocol};
20use webui_state::find_value_by_dotted_path;
21
22#[derive(Debug, Error)]
24pub enum HandlerError {
25 #[error("Rendering error: {0}")]
26 Rendering(String),
27
28 #[error("Missing fragment: {0}")]
29 MissingFragment(String),
30
31 #[error("Missing data field: {0}")]
32 MissingData(String),
33
34 #[error("Type error: {0}")]
35 TypeError(String),
36
37 #[error("Protocol error: {0}")]
38 Protocol(#[from] webui_protocol::ProtocolError),
39
40 #[error("Evaluation error: {0}")]
41 Evaluation(String),
42
43 #[error("I/O error: {0}")]
44 Io(#[from] std::io::Error),
45
46 #[error("Writer error: {0}")]
47 Writer(String),
48}
49
50pub type Result<T> = std::result::Result<T, HandlerError>;
51
52pub trait ResponseWriter {
54 fn write(&mut self, content: &str) -> Result<()>;
56
57 fn end(&mut self) -> Result<()>;
59}
60
61pub struct RenderOptions<'a> {
66 pub entry_id: &'a str,
68 pub request_path: &'a str,
70}
71
72impl<'a> RenderOptions<'a> {
73 #[must_use]
75 pub fn new(entry_id: &'a str, request_path: &'a str) -> Self {
76 Self {
77 entry_id,
78 request_path,
79 }
80 }
81}
82
83pub struct WebUIHandler {
88 plugin_factory: Option<fn() -> Box<dyn HandlerPlugin>>,
89}
90
91struct WebUIProcessContext<'a> {
93 protocol: &'a WebUIProtocol,
94 state: &'a Value,
95 #[allow(dead_code)]
96 depth: usize,
97 writer: &'a mut dyn ResponseWriter,
98 local_vars: HashMap<String, Value>,
99 component_attrs: HashMap<String, Value>,
101 request_path: String,
103 route_base: String,
106 rendered_components: HashSet<String>,
110 plugin: Option<Box<dyn HandlerPlugin>>,
112 route_children: Vec<webui_protocol::WebUiFragmentRoute>,
115 entry_id: String,
117}
118
119fn convert_hyphen_to_camel_case(name: &str) -> String {
121 let mut result = String::with_capacity(name.len());
122 let mut capitalize_next = false;
123 for ch in name.chars() {
124 if ch == '-' {
125 capitalize_next = true;
126 } else if capitalize_next {
127 result.extend(ch.to_uppercase());
128 capitalize_next = false;
129 } else {
130 result.push(ch);
131 }
132 }
133 result
134}
135
136fn camel_to_kebab(name: &str) -> String {
138 let mut result = String::with_capacity(name.len() + 4);
139 for ch in name.chars() {
140 if ch.is_uppercase() && !result.is_empty() {
141 result.push('-');
142 for lc in ch.to_lowercase() {
143 result.push(lc);
144 }
145 } else {
146 result.push(ch);
147 }
148 }
149 result
150}
151
152fn component_attr_name(name: &str) -> String {
154 let stripped = name.strip_prefix(':').unwrap_or(name);
155 if stripped.contains('-') {
156 convert_hyphen_to_camel_case(stripped)
157 } else {
158 stripped.to_string()
159 }
160}
161
162impl WebUIHandler {
163 pub fn new() -> Self {
165 Self {
166 plugin_factory: None,
167 }
168 }
169
170 pub fn with_plugin(factory: fn() -> Box<dyn HandlerPlugin>) -> Self {
175 Self {
176 plugin_factory: Some(factory),
177 }
178 }
179
180 pub fn handle(
185 &self,
186 protocol: &WebUIProtocol,
187 state: &Value,
188 options: &RenderOptions<'_>,
189 writer: &mut dyn ResponseWriter,
190 ) -> Result<()> {
191 if !protocol.fragments.contains_key(options.entry_id) {
192 return Err(HandlerError::MissingFragment(options.entry_id.to_string()));
193 }
194
195 let mut context = WebUIProcessContext {
196 protocol,
197 state,
198 depth: 0,
199 writer,
200 local_vars: HashMap::new(),
201 component_attrs: HashMap::new(),
202 request_path: options.request_path.to_string(),
203 route_base: "/".to_string(),
204 rendered_components: HashSet::new(),
205 plugin: self.plugin_factory.map(|f| f()),
206 route_children: Vec::new(),
207 entry_id: options.entry_id.to_string(),
208 };
209 self.process_fragment_id(options.entry_id, &mut context)?;
210
211 writer.end()?;
212
213 Ok(())
214 }
215
216 pub fn handle_as_component(
221 &self,
222 protocol: &WebUIProtocol,
223 state: &Value,
224 entry_id: &str,
225 writer: &mut dyn ResponseWriter,
226 ) -> Result<()> {
227 if !protocol.fragments.contains_key(entry_id) {
228 return Err(HandlerError::MissingFragment(entry_id.to_string()));
229 }
230
231 let mut context = WebUIProcessContext {
232 protocol,
233 state,
234 depth: 0,
235 writer,
236 local_vars: HashMap::new(),
237 component_attrs: HashMap::new(),
238 request_path: String::new(),
239 route_base: "/".to_string(),
240 rendered_components: HashSet::new(),
241 plugin: self.plugin_factory.map(|f| f()),
242 route_children: Vec::new(),
243 entry_id: entry_id.to_string(),
244 };
245
246 if let Some(p) = &mut context.plugin {
247 p.push_scope();
248 }
249
250 self.process_fragment_id(entry_id, &mut context)?;
251
252 if let Some(p) = &mut context.plugin {
253 p.pop_scope();
254 }
255
256 writer.end()?;
257
258 Ok(())
259 }
260
261 fn process_fragment_id(
266 &self,
267 fragment_id: &str,
268 context: &mut WebUIProcessContext,
269 ) -> Result<()> {
270 if let Some(fragment_list) = context.protocol.fragments.get(fragment_id) {
271 self.process_fragment(&fragment_list.fragments, context)
272 } else {
273 Err(HandlerError::MissingFragment(fragment_id.to_string()))
274 }
275 }
276
277 fn process_fragment(
282 &self,
283 fragments: &[WebUIFragment],
284 context: &mut WebUIProcessContext,
285 ) -> Result<()> {
286 let best_route = route_renderer::find_best_route_match(
290 fragments,
291 &context.request_path,
292 &context.route_base,
293 );
294
295 for item in fragments {
296 match item.fragment.as_ref() {
297 Some(Fragment::Raw(raw)) => {
298 context.writer.write(&raw.value)?;
299 }
300 Some(Fragment::Component(component)) => {
301 self.process_component(component, context)?;
302 }
303 Some(Fragment::ForLoop(for_loop)) => {
304 self.process_for_loop(for_loop, context)?;
305 }
306 Some(Fragment::Signal(signal)) => {
307 self.process_signal(signal, context)?;
308 }
309 Some(Fragment::IfCond(if_cond)) => {
310 self.process_if(if_cond, context)?;
311 }
312 Some(Fragment::Attribute(attr)) => {
313 self.process_attribute(attr, context)?;
314 }
315 Some(Fragment::Plugin(plugin_frag)) => {
316 if let Some(p) = &mut context.plugin {
317 p.on_plugin_data(&plugin_frag.data, context.writer)?;
318 }
319 }
320 Some(Fragment::Route(route_frag)) => {
321 self.process_route(route_frag, &best_route, context)?;
322 }
323 Some(Fragment::Outlet(_)) => {
324 self.process_outlet(context)?;
325 }
326 None => {}
327 }
328 }
329 Ok(())
330 }
331
332 fn process_outlet(&self, context: &mut WebUIProcessContext) -> Result<()> {
338 let mut children = std::mem::take(&mut context.route_children);
339 if children.is_empty() {
340 return Ok(());
341 }
342
343 let mut best: Option<(usize, route_matcher::RouteMatch)> = None;
345 for (idx, child) in children.iter().enumerate() {
346 let resolved = route_matcher::resolve_route_path(&child.path, &context.route_base);
347 if let Some(m) =
348 route_matcher::match_single_route(&resolved, &context.request_path, child.exact)
349 {
350 let is_better = best
351 .as_ref()
352 .is_none_or(|(_, prev)| m.specificity > prev.specificity);
353 if is_better {
354 best = Some((idx, m));
355 }
356 }
357 }
358
359 let grandchildren = if let Some((idx, _)) = &best {
363 std::mem::take(&mut children[*idx].children)
364 } else {
365 Vec::new()
366 };
367
368 if let Some((idx, ref rm)) = best {
369 let matched_child = &children[idx];
370 let comp = &matched_child.fragment_id;
371
372 if !comp.is_empty() {
373 let saved_route_base = context.route_base.clone();
374 let saved_route_children = std::mem::take(&mut context.route_children);
375
376 if rm.consumed_segments > 0 {
377 context.route_base = route_matcher::compute_route_base(
378 &context.request_path,
379 rm.consumed_segments,
380 );
381 }
382
383 context.route_children = grandchildren;
384
385 context.writer.write("<webui-route")?;
387 if !matched_child.path.is_empty() {
388 context.writer.write(" path=\"")?;
389 context.writer.write(&matched_child.path)?;
390 context.writer.write("\"")?;
391 }
392 context.writer.write(" component=\"")?;
393 context.writer.write(comp)?;
394 context.writer.write("\"")?;
395 if matched_child.exact {
396 context.writer.write(" exact")?;
397 }
398 context.writer.write(" active>")?;
399
400 context.writer.write("<")?;
401 context.writer.write(comp)?;
402 route_renderer::emit_state_attributes(context.state, context.writer)?;
403 context.writer.write(">")?;
404
405 self.process_component(
406 &webui_protocol::WebUIFragmentComponent {
407 fragment_id: comp.clone(),
408 },
409 context,
410 )?;
411
412 context.writer.write("</")?;
413 context.writer.write(comp)?;
414 context.writer.write(">")?;
415 context.writer.write("</webui-route>")?;
416
417 context.route_base = saved_route_base;
418 context.route_children = saved_route_children;
419 }
420 }
421
422 for (idx, child) in children.iter().enumerate() {
424 let is_matched = best.as_ref().is_some_and(|(bi, _)| *bi == idx);
425 if !is_matched && !child.fragment_id.is_empty() {
426 context.writer.write("<webui-route")?;
427 if !child.path.is_empty() {
428 context.writer.write(" path=\"")?;
429 context.writer.write(&child.path)?;
430 context.writer.write("\"")?;
431 }
432 context.writer.write(" component=\"")?;
433 context.writer.write(&child.fragment_id)?;
434 context
435 .writer
436 .write("\" style=\"display:none\"></webui-route>")?;
437 }
438 }
439
440 Ok(())
441 }
442
443 fn emit_css_module(
446 &self,
447 component: &webui_protocol::WebUIFragmentComponent,
448 context: &mut WebUIProcessContext,
449 ) -> Result<()> {
450 if !context.rendered_components.contains(&component.fragment_id) {
451 if let Some(css) = context
452 .protocol
453 .components
454 .get(&component.fragment_id)
455 .map(|c| c.css.as_str())
456 .filter(|s| !s.is_empty())
457 {
458 context
459 .writer
460 .write("<style type=\"module\" specifier=\"")?;
461 context.writer.write(&component.fragment_id)?;
462 context.writer.write("\">")?;
463 context.writer.write(css)?;
464 context.writer.write("</style>")?;
465 }
466 }
467 Ok(())
468 }
469
470 fn process_route(
472 &self,
473 route_frag: &webui_protocol::WebUiFragmentRoute,
474 best_route: &Option<(String, route_matcher::RouteMatch)>,
475 context: &mut WebUIProcessContext,
476 ) -> Result<()> {
477 let is_matched = best_route
478 .as_ref()
479 .is_some_and(|(best_key, _)| *best_key == route_frag.fragment_id);
480
481 context.writer.write("<webui-route")?;
482 if !route_frag.path.is_empty() {
483 context.writer.write(" path=\"")?;
484 context.writer.write(&route_frag.path)?;
485 context.writer.write("\"")?;
486 }
487 if !route_frag.fragment_id.is_empty() {
488 context.writer.write(" component=\"")?;
489 context.writer.write(&route_frag.fragment_id)?;
490 context.writer.write("\"")?;
491 }
492 if route_frag.exact {
493 context.writer.write(" exact")?;
494 }
495
496 if is_matched {
497 context.writer.write(" active>")?;
498
499 if !route_frag.fragment_id.is_empty() {
500 let saved_route_base = context.route_base.clone();
501 let saved_route_children = std::mem::take(&mut context.route_children);
502 if let Some((_, ref rm)) = best_route {
503 context.route_base = route_matcher::compute_route_base(
504 &context.request_path,
505 rm.consumed_segments,
506 );
507 }
508
509 context.route_children = route_frag.children.clone();
510
511 let comp = webui_protocol::WebUIFragmentComponent {
512 fragment_id: route_frag.fragment_id.clone(),
513 };
514
515 context.writer.write("<")?;
516 context.writer.write(&route_frag.fragment_id)?;
517 route_renderer::emit_state_attributes(context.state, context.writer)?;
518 context.writer.write(">")?;
519
520 self.process_component(&comp, context)?;
521
522 context.writer.write("</")?;
523 context.writer.write(&route_frag.fragment_id)?;
524 context.writer.write(">")?;
525
526 context.route_base = saved_route_base;
527 context.route_children = saved_route_children;
528 }
529 } else {
530 context.writer.write(" style=\"display:none\">")?;
531 }
532
533 context.writer.write("</webui-route>")?;
534 Ok(())
535 }
536
537 fn process_component(
539 &self,
540 component: &webui_protocol::WebUIFragmentComponent,
541 context: &mut WebUIProcessContext,
542 ) -> Result<()> {
543 let first_render = !context.rendered_components.contains(&component.fragment_id);
546 if first_render {
547 self.emit_css_module(component, context)?;
548 }
549
550 context
552 .rendered_components
553 .insert(component.fragment_id.clone());
554
555 let saved_local_vars = std::mem::take(&mut context.local_vars);
557 let saved_component_attrs = std::mem::take(&mut context.component_attrs);
558
559 context.local_vars = saved_component_attrs;
561
562 if let Some(p) = &mut context.plugin {
563 p.push_scope();
564 }
565
566 self.process_fragment_id(&component.fragment_id, context)?;
567
568 if let Some(p) = &mut context.plugin {
569 p.pop_scope();
570 }
571
572 context.local_vars = saved_local_vars;
574 context.component_attrs = HashMap::new();
575
576 Ok(())
577 }
578
579 fn resolve_value(&self, path: &str, context: &WebUIProcessContext) -> Option<Value> {
581 if let Some(first_part) = path.split('.').next() {
583 if let Some(local_value) = context.local_vars.get(first_part) {
584 if first_part.len() == path.len() {
585 return Some(local_value.clone());
586 }
587 let remaining = &path[first_part.len() + 1..];
588 if let Some(v) = find_value_by_dotted_path(remaining, local_value) {
589 return Some(v);
590 }
591 }
592 }
593 find_value_by_dotted_path(path, context.state)
595 }
596
597 fn evaluate_condition(
603 &self,
604 condition: &webui_protocol::ConditionExpr,
605 context: &WebUIProcessContext,
606 ) -> Result<bool> {
607 let local_vars = &context.local_vars;
608 let state = context.state;
609 match evaluate_with_resolver(condition, |path| {
610 if let Some(first_part) = path.split('.').next() {
611 if let Some(local_value) = local_vars.get(first_part) {
612 if first_part.len() == path.len() {
613 return Some(local_value.clone());
614 }
615 let remaining = &path[first_part.len() + 1..];
616 if let Some(v) = find_value_by_dotted_path(remaining, local_value) {
617 return Some(v);
618 }
619 }
620 }
621 find_value_by_dotted_path(path, state)
622 }) {
623 Ok(result) => Ok(result),
624 Err(ExpressionError::MissingValue(_)) => Ok(false),
625 Err(e) => Err(HandlerError::Evaluation(e.to_string())),
626 }
627 }
628
629 fn process_for_loop(
635 &self,
636 for_loop: &webui_protocol::WebUIFragmentFor,
637 context: &mut WebUIProcessContext,
638 ) -> Result<()> {
639 let collection_name = &for_loop.collection;
640
641 let items = match self.resolve_value(collection_name, context) {
644 Some(Value::Array(arr)) => arr,
645 Some(_) => {
646 return Err(HandlerError::TypeError(format!(
647 "Collection '{}' is not an array",
648 collection_name
649 )))
650 }
651 None => Vec::new(),
652 };
653
654 if let Some(p) = &mut context.plugin {
655 p.on_binding_start(&for_loop.fragment_id, context.writer)?;
656 }
657
658 let item_name = &for_loop.item;
659 for (i, item) in items.into_iter().enumerate() {
660 if let Some(p) = &mut context.plugin {
661 p.on_repeat_item_start(i, context.writer)?;
662 p.push_scope();
663 }
664
665 let saved_value = context.local_vars.insert(item_name.clone(), item);
667 self.process_fragment_id(&for_loop.fragment_id, context)?;
668 match saved_value {
669 Some(v) => {
670 context.local_vars.insert(item_name.clone(), v);
671 }
672 None => {
673 context.local_vars.remove(item_name.as_str());
674 }
675 }
676
677 if let Some(p) = &mut context.plugin {
678 p.pop_scope();
679 p.on_repeat_item_end(i, context.writer)?;
680 }
681 }
682
683 if let Some(p) = &mut context.plugin {
684 p.on_binding_end(&for_loop.fragment_id, context.writer)?;
685 }
686
687 Ok(())
688 }
689
690 fn process_signal(
696 &self,
697 signal: &webui_protocol::WebUIFragmentSignal,
698 context: &mut WebUIProcessContext,
699 ) -> Result<()> {
700 if signal.raw && signal.value == "head_end" {
702 let (_, inventory_hex) = crate::route_handler::get_needed_components_for_request(
703 context.protocol,
704 &context.entry_id,
705 &context.request_path,
706 "",
707 );
708 context
709 .writer
710 .write("<meta name=\"webui-inventory\" content=\"")?;
711 context.writer.write(&inventory_hex)?;
712 context.writer.write("\">")?;
713 }
714
715 if signal.raw && signal.value == "body_end" {
717 if let Some(p) = &mut context.plugin {
718 p.on_render_complete(
719 context.protocol,
720 &context.rendered_components,
721 context.writer,
722 )?;
723 }
724 }
725
726 if let Some(p) = &mut context.plugin {
727 p.on_binding_start(&signal.value, context.writer)?;
728 }
729
730 if let Some(value) = self.resolve_value(&signal.value, context) {
731 self.write_signal_value(&value, signal.raw, context.writer)?;
732 }
733
734 if let Some(p) = &mut context.plugin {
735 p.on_binding_end(&signal.value, context.writer)?;
736 }
737 Ok(())
738 }
739
740 fn write_signal_value(
743 &self,
744 value: &Value,
745 raw: bool,
746 writer: &mut dyn ResponseWriter,
747 ) -> Result<()> {
748 if raw {
749 match value {
750 Value::String(s) => writer.write(s),
751 _ => writer.write(&value.to_string()),
752 }
753 } else {
754 match value {
755 Value::String(s) => writer.write(&html_escape::encode_safe(s)),
756 _ => {
757 let s = value.to_string();
758 writer.write(&html_escape::encode_safe(&s))
759 }
760 }
761 }
762 }
763
764 fn process_if(
766 &self,
767 if_cond: &webui_protocol::WebUIFragmentIf,
768 context: &mut WebUIProcessContext,
769 ) -> Result<()> {
770 let condition = if_cond
771 .condition
772 .as_ref()
773 .ok_or_else(|| HandlerError::Rendering("If fragment missing condition".to_string()))?;
774 let condition_met = self.evaluate_condition(condition, context)?;
775
776 if let Some(p) = &mut context.plugin {
777 p.on_binding_start(&if_cond.fragment_id, context.writer)?;
778 }
779
780 if condition_met {
781 if let Some(p) = &mut context.plugin {
782 p.push_scope();
783 }
784
785 self.process_fragment_id(&if_cond.fragment_id, context)?;
786
787 if let Some(p) = &mut context.plugin {
788 p.pop_scope();
789 }
790 }
791
792 if let Some(p) = &mut context.plugin {
793 p.on_binding_end(&if_cond.fragment_id, context.writer)?;
794 }
795
796 Ok(())
797 }
798
799 fn process_attribute(
801 &self,
802 attr: &webui_protocol::WebUIFragmentAttribute,
803 context: &mut WebUIProcessContext,
804 ) -> Result<()> {
805 if attr.attr_start {
807 context.component_attrs = HashMap::new();
808 }
809
810 if let Some(condition) = &attr.condition_tree {
812 let condition_met = self.evaluate_condition(condition, context)?;
813
814 if !attr.attr_skip {
815 let name = component_attr_name(&attr.name);
816 context
817 .component_attrs
818 .insert(name, Value::Bool(condition_met));
819 }
820
821 if condition_met {
822 context.writer.write(" ")?;
823 context.writer.write(&attr.name)?;
824 }
825 return Ok(());
826 }
827
828 if !attr.template.is_empty() {
830 let raw_value = self.render_template_attr_value(&attr.template, context)?;
831 let escaped = html_escape::encode_safe(&raw_value);
832 write_attr(context.writer, &attr.name, &escaped)?;
833
834 if !attr.attr_skip {
835 let name = component_attr_name(&attr.name);
836 context
837 .component_attrs
838 .insert(name, Value::String(raw_value));
839 }
840 return Ok(());
841 }
842
843 if !attr.value.is_empty() {
845 if attr.raw_value {
846 write_attr(context.writer, &attr.name, &attr.value)?;
848 if !attr.attr_skip {
849 let name = component_attr_name(&attr.name);
850 context
851 .component_attrs
852 .insert(name, Value::String(attr.value.clone()));
853 }
854 } else if attr.complex {
855 if let Some(value) = self.resolve_value(&attr.value, context) {
857 if !attr.attr_skip {
858 let stripped = attr.name.strip_prefix(':').unwrap_or(&attr.name);
859 let name = component_attr_name(stripped);
860 context.component_attrs.insert(name, value);
861 }
862 }
863 } else {
864 let value = self.resolve_value(&attr.value, context);
866 match &value {
869 Some(Value::String(s)) => {
870 write_attr(context.writer, &attr.name, &html_escape::encode_safe(s))?;
871 }
872 Some(Value::Null) | None => {
873 write_attr(context.writer, &attr.name, "")?;
874 }
875 Some(other) => {
876 let s = other.to_string();
877 write_attr(context.writer, &attr.name, &s)?;
878 }
879 }
880
881 if !attr.attr_skip {
882 let name = component_attr_name(&attr.name);
883 context
884 .component_attrs
885 .insert(name, value.unwrap_or(Value::String(String::new())));
886 }
887 }
888 }
889
890 Ok(())
891 }
892
893 fn render_template_attr_value(
895 &self,
896 template_id: &str,
897 context: &WebUIProcessContext,
898 ) -> Result<String> {
899 let fragments = context
900 .protocol
901 .fragments
902 .get(template_id)
903 .ok_or_else(|| HandlerError::MissingFragment(template_id.to_string()))?;
904 let mut raw_value = String::new();
905 for frag in &fragments.fragments {
906 match frag.fragment.as_ref() {
907 Some(Fragment::Raw(raw)) => raw_value.push_str(&raw.value),
908 Some(Fragment::Signal(signal)) => {
909 if let Some(value) = self.resolve_value(&signal.value, context) {
910 match &value {
911 Value::String(s) => raw_value.push_str(s),
912 _ => raw_value.push_str(&value.to_string()),
913 }
914 }
915 }
916 _ => {}
917 }
918 }
919 Ok(raw_value)
920 }
921
922 pub fn render(
926 &self,
927 protocol: &WebUIProtocol,
928 state: &Value,
929 options: &RenderOptions<'_>,
930 writer: &mut dyn ResponseWriter,
931 ) -> Result<()> {
932 let mut context = WebUIProcessContext {
933 protocol,
934 state,
935 depth: 0,
936 writer,
937 local_vars: HashMap::new(),
938 component_attrs: HashMap::new(),
939 request_path: options.request_path.to_string(),
940 route_base: "/".to_string(),
941 rendered_components: HashSet::new(),
942 plugin: self.plugin_factory.map(|f| f()),
943 route_children: Vec::new(),
944 entry_id: options.entry_id.to_string(),
945 };
946
947 self.process_fragment_id(options.entry_id, &mut context)?;
948
949 Ok(())
950 }
951}
952
953impl Default for WebUIHandler {
954 fn default() -> Self {
955 Self::new()
956 }
957}
958
959fn write_attr(writer: &mut dyn ResponseWriter, name: &str, value: &str) -> Result<()> {
961 writer.write(" ")?;
962 writer.write(name)?;
963 writer.write("=\"")?;
964 writer.write(value)?;
965 writer.write("\"")
966}
967
968pub fn handle(
971 protocol: &WebUIProtocol,
972 state: &Value,
973 options: &RenderOptions<'_>,
974 writer: &mut dyn ResponseWriter,
975) -> Result<()> {
976 let handler = WebUIHandler::new();
977 handler.handle(protocol, state, options, writer)
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use std::cell::RefCell;
984 use webui_protocol::{
985 web_ui_fragment, ComparisonOperator, ConditionExpr, FragmentList, LogicalOperator,
986 WebUIFragmentAttribute,
987 };
988 use webui_test_utils::test_json;
989
990 struct TestWriter {
992 content: RefCell<String>,
993 ended: RefCell<bool>,
994 }
995
996 impl TestWriter {
997 fn new() -> Self {
998 Self {
999 content: RefCell::new(String::new()),
1000 ended: RefCell::new(false),
1001 }
1002 }
1003
1004 fn get_content(&self) -> String {
1005 self.content.borrow().clone()
1006 }
1007
1008 fn is_ended(&self) -> bool {
1009 *self.ended.borrow()
1010 }
1011 }
1012
1013 impl ResponseWriter for TestWriter {
1014 fn write(&mut self, content: &str) -> Result<()> {
1015 self.content.borrow_mut().push_str(content);
1016 Ok(())
1017 }
1018
1019 fn end(&mut self) -> Result<()> {
1020 *self.ended.borrow_mut() = true;
1021 Ok(())
1022 }
1023 }
1024
1025 #[test]
1026 fn test_handle_raw() {
1027 let mut fragments = HashMap::new();
1029 fragments.insert(
1030 "index.html".to_string(),
1031 FragmentList {
1032 fragments: vec![WebUIFragment::raw("Hello, WebUI!")],
1033 },
1034 );
1035
1036 let protocol = WebUIProtocol::new(fragments);
1037 let state = test_json!({});
1038
1039 let mut writer = TestWriter::new();
1041
1042 assert!(
1044 handle(
1045 &protocol,
1046 &state,
1047 &RenderOptions::new("index.html", "/"),
1048 &mut writer
1049 )
1050 .is_ok(),
1051 "Failed to handle raw protocol"
1052 );
1053
1054 assert_eq!(writer.get_content(), "Hello, WebUI!");
1056 assert!(writer.is_ended());
1057 }
1058
1059 #[test]
1060 fn test_handle_signal() {
1061 let mut fragments = HashMap::new();
1063 fragments.insert(
1064 "index.html".to_string(),
1065 FragmentList {
1066 fragments: vec![
1067 WebUIFragment::raw("Hello, "),
1068 WebUIFragment::signal("name", false),
1069 WebUIFragment::raw("!"),
1070 ],
1071 },
1072 );
1073
1074 let protocol = WebUIProtocol::new(fragments);
1075 let state = test_json!({"name": "WebUI"});
1076
1077 let mut writer = TestWriter::new();
1079
1080 assert!(
1082 handle(
1083 &protocol,
1084 &state,
1085 &RenderOptions::new("index.html", "/"),
1086 &mut writer
1087 )
1088 .is_ok(),
1089 "Failed to handle signal protocol"
1090 );
1091
1092 assert_eq!(writer.get_content(), "Hello, WebUI!");
1094 assert!(writer.is_ended());
1095 }
1096
1097 #[test]
1098 fn test_handle_for_loop() {
1099 let mut fragments = HashMap::new();
1101 fragments.insert(
1102 "index.html".to_string(),
1103 FragmentList {
1104 fragments: vec![
1105 WebUIFragment::raw("People: "),
1106 WebUIFragment::for_loop("person", "people", "person-item"),
1107 ],
1108 },
1109 );
1110
1111 fragments.insert(
1112 "person-item".to_string(),
1113 FragmentList {
1114 fragments: vec![
1115 WebUIFragment::signal("person.name", false),
1116 WebUIFragment::raw(", "),
1117 ],
1118 },
1119 );
1120
1121 let protocol = WebUIProtocol::new(fragments);
1122 let state = test_json!({
1123 "people": [
1124 {"name": "Alice"},
1125 {"name": "Bob"},
1126 {"name": "Charlie"}
1127 ]
1128 });
1129
1130 let mut writer = TestWriter::new();
1132
1133 assert!(
1135 handle(
1136 &protocol,
1137 &state,
1138 &RenderOptions::new("index.html", "/"),
1139 &mut writer
1140 )
1141 .is_ok(),
1142 "Failed to handle for loop protocol"
1143 );
1144
1145 assert_eq!(writer.get_content(), "People: Alice, Bob, Charlie, ");
1147 assert!(writer.is_ended());
1148 }
1149
1150 #[test]
1151 fn test_handle_if_condition() {
1152 let mut fragments = HashMap::new();
1154 fragments.insert(
1155 "index.html".to_string(),
1156 FragmentList {
1157 fragments: vec![
1158 WebUIFragment::raw("Status: "),
1159 WebUIFragment::if_cond(
1160 webui_protocol::ConditionExpr::identifier("isActive"),
1161 "active-content",
1162 ),
1163 WebUIFragment::raw("End"),
1164 ],
1165 },
1166 );
1167
1168 fragments.insert(
1169 "active-content".to_string(),
1170 FragmentList {
1171 fragments: vec![WebUIFragment::raw("Active")],
1172 },
1173 );
1174
1175 let protocol = WebUIProtocol::new(fragments);
1176
1177 let state_true = test_json!({"isActive": true});
1179 let mut writer_true = TestWriter::new();
1180 assert!(
1181 handle(
1182 &protocol,
1183 &state_true,
1184 &RenderOptions::new("index.html", "/"),
1185 &mut writer_true
1186 )
1187 .is_ok(),
1188 "Failed to handle if condition (true case)"
1189 );
1190 assert_eq!(writer_true.get_content(), "Status: ActiveEnd");
1191 assert!(writer_true.is_ended());
1192
1193 let state_false = test_json!({"isActive": false});
1195 let mut writer_false = TestWriter::new();
1196 assert!(
1197 handle(
1198 &protocol,
1199 &state_false,
1200 &RenderOptions::new("index.html", "/"),
1201 &mut writer_false
1202 )
1203 .is_ok(),
1204 "Failed to handle if condition (false case)"
1205 );
1206 assert_eq!(writer_false.get_content(), "Status: End");
1207 assert!(writer_false.is_ended());
1208 }
1209
1210 #[test]
1211 fn test_handle_component() {
1212 let mut fragments = HashMap::new();
1214 fragments.insert(
1215 "index.html".to_string(),
1216 FragmentList {
1217 fragments: vec![
1218 WebUIFragment::raw("Component: "),
1219 WebUIFragment::component("my-component"),
1220 ],
1221 },
1222 );
1223
1224 fragments.insert(
1225 "my-component".to_string(),
1226 FragmentList {
1227 fragments: vec![WebUIFragment::raw("<div>Component Content</div>")],
1228 },
1229 );
1230
1231 let protocol = WebUIProtocol::new(fragments);
1232 let state = test_json!({});
1233
1234 let mut writer = TestWriter::new();
1236
1237 assert!(
1239 handle(
1240 &protocol,
1241 &state,
1242 &RenderOptions::new("index.html", "/"),
1243 &mut writer
1244 )
1245 .is_ok(),
1246 "Failed to handle component protocol"
1247 );
1248
1249 assert_eq!(
1251 writer.get_content(),
1252 "Component: <div>Component Content</div>"
1253 );
1254 assert!(writer.is_ended());
1255 }
1256
1257 #[test]
1258 fn test_missing_fragment() {
1259 let mut fragments = HashMap::new();
1261 fragments.insert(
1262 "index.html".to_string(),
1263 FragmentList {
1264 fragments: vec![WebUIFragment::component("missing-component")],
1265 },
1266 );
1267
1268 let protocol = WebUIProtocol::new(fragments);
1269 let state = test_json!({});
1270
1271 let mut writer = TestWriter::new();
1273
1274 let result = handle(
1276 &protocol,
1277 &state,
1278 &RenderOptions::new("index.html", "/"),
1279 &mut writer,
1280 );
1281
1282 assert!(result.is_err());
1284 if let Err(HandlerError::MissingFragment(fragment_id)) = result {
1285 assert_eq!(fragment_id, "missing-component");
1286 } else {
1287 panic!("Expected MissingFragment error");
1288 }
1289 }
1290
1291 #[test]
1292 fn test_missing_signal_renders_empty() {
1293 let mut fragments = HashMap::new();
1295 fragments.insert(
1296 "index.html".to_string(),
1297 FragmentList {
1298 fragments: vec![
1299 WebUIFragment::raw("Hello, "),
1300 WebUIFragment::signal("missing_field", false),
1301 WebUIFragment::raw("!"),
1302 ],
1303 },
1304 );
1305
1306 let protocol = WebUIProtocol::new(fragments);
1307 let state = test_json!({});
1308
1309 let mut writer = TestWriter::new();
1310
1311 assert!(
1312 handle(
1313 &protocol,
1314 &state,
1315 &RenderOptions::new("index.html", "/"),
1316 &mut writer
1317 )
1318 .is_ok(),
1319 "Missing signal should not produce an error"
1320 );
1321
1322 assert_eq!(writer.get_content(), "Hello, !");
1323 assert!(writer.is_ended());
1324 }
1325
1326 #[test]
1329 fn test_boolean_attr_true() {
1330 let mut fragments = HashMap::new();
1331 fragments.insert(
1332 "index.html".to_string(),
1333 FragmentList {
1334 fragments: vec![
1335 WebUIFragment::raw("<button"),
1336 WebUIFragment::attribute_boolean(
1337 "disabled",
1338 ConditionExpr::identifier("isDisabled"),
1339 ),
1340 WebUIFragment::raw(">Click</button>"),
1341 ],
1342 },
1343 );
1344 let protocol = WebUIProtocol::new(fragments);
1345 let state = test_json!({"isDisabled": true});
1346 let mut writer = TestWriter::new();
1347 handle(
1348 &protocol,
1349 &state,
1350 &RenderOptions::new("index.html", "/"),
1351 &mut writer,
1352 )
1353 .unwrap();
1354 assert_eq!(writer.get_content(), "<button disabled>Click</button>");
1355 }
1356
1357 #[test]
1358 fn test_boolean_attr_false() {
1359 let mut fragments = HashMap::new();
1360 fragments.insert(
1361 "index.html".to_string(),
1362 FragmentList {
1363 fragments: vec![
1364 WebUIFragment::raw("<button"),
1365 WebUIFragment::attribute_boolean(
1366 "disabled",
1367 ConditionExpr::identifier("isDisabled"),
1368 ),
1369 WebUIFragment::raw(">Click</button>"),
1370 ],
1371 },
1372 );
1373 let protocol = WebUIProtocol::new(fragments);
1374 let state = test_json!({"isDisabled": false});
1375 let mut writer = TestWriter::new();
1376 handle(
1377 &protocol,
1378 &state,
1379 &RenderOptions::new("index.html", "/"),
1380 &mut writer,
1381 )
1382 .unwrap();
1383 assert_eq!(writer.get_content(), "<button>Click</button>");
1384 }
1385
1386 #[test]
1387 fn test_boolean_attr_missing() {
1388 let mut fragments = HashMap::new();
1389 fragments.insert(
1390 "index.html".to_string(),
1391 FragmentList {
1392 fragments: vec![
1393 WebUIFragment::raw("<input type=\"checkbox\""),
1394 WebUIFragment::attribute_boolean(
1395 "checked",
1396 ConditionExpr::identifier("checked"),
1397 ),
1398 WebUIFragment::raw(">"),
1399 ],
1400 },
1401 );
1402 let protocol = WebUIProtocol::new(fragments);
1403 let state = test_json!({});
1404 let mut writer = TestWriter::new();
1405 handle(
1406 &protocol,
1407 &state,
1408 &RenderOptions::new("index.html", "/"),
1409 &mut writer,
1410 )
1411 .unwrap();
1412 assert_eq!(writer.get_content(), "<input type=\"checkbox\">");
1413 }
1414
1415 #[test]
1416 fn test_boolean_attr_multiple() {
1417 let mut fragments = HashMap::new();
1418 fragments.insert(
1419 "index.html".to_string(),
1420 FragmentList {
1421 fragments: vec![
1422 WebUIFragment::raw("<input type=\"checkbox\""),
1423 WebUIFragment::attribute_boolean(
1424 "checked",
1425 ConditionExpr::identifier("checked"),
1426 ),
1427 WebUIFragment::attribute_boolean(
1428 "disabled",
1429 ConditionExpr::identifier("disabled"),
1430 ),
1431 WebUIFragment::raw(">"),
1432 ],
1433 },
1434 );
1435 let protocol = WebUIProtocol::new(fragments);
1436 let state = test_json!({"checked": true, "disabled": false});
1437 let mut writer = TestWriter::new();
1438 handle(
1439 &protocol,
1440 &state,
1441 &RenderOptions::new("index.html", "/"),
1442 &mut writer,
1443 )
1444 .unwrap();
1445 assert_eq!(writer.get_content(), "<input type=\"checkbox\" checked>");
1446 }
1447
1448 #[test]
1451 fn test_attribute_with_value() {
1452 let mut fragments = HashMap::new();
1453 fragments.insert(
1454 "index.html".to_string(),
1455 FragmentList {
1456 fragments: vec![
1457 WebUIFragment::raw("<input"),
1458 WebUIFragment::attribute("value", "inputValue"),
1459 WebUIFragment::raw(">"),
1460 ],
1461 },
1462 );
1463 let protocol = WebUIProtocol::new(fragments);
1464 let state = test_json!({"inputValue": "Hello"});
1465 let mut writer = TestWriter::new();
1466 handle(
1467 &protocol,
1468 &state,
1469 &RenderOptions::new("index.html", "/"),
1470 &mut writer,
1471 )
1472 .unwrap();
1473 assert_eq!(writer.get_content(), "<input value=\"Hello\">");
1474 }
1475
1476 #[test]
1477 fn test_attribute_with_falsy_numeric() {
1478 let mut fragments = HashMap::new();
1479 fragments.insert(
1480 "index.html".to_string(),
1481 FragmentList {
1482 fragments: vec![
1483 WebUIFragment::raw("<div name=\"test\""),
1484 WebUIFragment::attribute("handle", "number"),
1485 WebUIFragment::raw("></div>"),
1486 ],
1487 },
1488 );
1489 let protocol = WebUIProtocol::new(fragments);
1490 let state = test_json!({"number": 0});
1491 let mut writer = TestWriter::new();
1492 handle(
1493 &protocol,
1494 &state,
1495 &RenderOptions::new("index.html", "/"),
1496 &mut writer,
1497 )
1498 .unwrap();
1499 assert_eq!(
1500 writer.get_content(),
1501 "<div name=\"test\" handle=\"0\"></div>"
1502 );
1503 }
1504
1505 #[test]
1508 fn test_mixed_attribute_template() {
1509 let mut fragments = HashMap::new();
1510 fragments.insert(
1511 "index.html".to_string(),
1512 FragmentList {
1513 fragments: vec![
1514 WebUIFragment::raw("<input"),
1515 WebUIFragment::attribute_template("value", "attr-1"),
1516 WebUIFragment::raw(">"),
1517 ],
1518 },
1519 );
1520 fragments.insert(
1521 "attr-1".to_string(),
1522 FragmentList {
1523 fragments: vec![
1524 WebUIFragment::raw("hello "),
1525 WebUIFragment::signal("item", false),
1526 ],
1527 },
1528 );
1529 let protocol = WebUIProtocol::new(fragments);
1530 let state = test_json!({"item": "world"});
1531 let mut writer = TestWriter::new();
1532 handle(
1533 &protocol,
1534 &state,
1535 &RenderOptions::new("index.html", "/"),
1536 &mut writer,
1537 )
1538 .unwrap();
1539 assert_eq!(writer.get_content(), "<input value=\"hello world\">");
1540 }
1541
1542 #[test]
1545 fn test_raw_signal_not_escaped() {
1546 let mut fragments = HashMap::new();
1547 fragments.insert(
1548 "index.html".to_string(),
1549 FragmentList {
1550 fragments: vec![
1551 WebUIFragment::signal("html", false),
1552 WebUIFragment::signal("html", true),
1553 ],
1554 },
1555 );
1556 let protocol = WebUIProtocol::new(fragments);
1557 let state = test_json!({"html": "<strong>hi</strong>"});
1558 let mut writer = TestWriter::new();
1559 handle(
1560 &protocol,
1561 &state,
1562 &RenderOptions::new("index.html", "/"),
1563 &mut writer,
1564 )
1565 .unwrap();
1566 assert_eq!(
1567 writer.get_content(),
1568 "<strong>hi</strong><strong>hi</strong>"
1569 );
1570 }
1571
1572 #[test]
1575 fn test_nested_for_loop() {
1576 let mut fragments = HashMap::new();
1577 fragments.insert(
1578 "index.html".to_string(),
1579 FragmentList {
1580 fragments: vec![
1581 WebUIFragment::raw("<div>"),
1582 WebUIFragment::for_loop("outerItem", "outerItems", "outer"),
1583 WebUIFragment::raw("</div>"),
1584 ],
1585 },
1586 );
1587 fragments.insert(
1588 "outer".to_string(),
1589 FragmentList {
1590 fragments: vec![
1591 WebUIFragment::raw("<div>"),
1592 WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "inner"),
1593 WebUIFragment::raw("</div>"),
1594 ],
1595 },
1596 );
1597 fragments.insert(
1598 "inner".to_string(),
1599 FragmentList {
1600 fragments: vec![WebUIFragment::raw("<span>Inner</span>")],
1601 },
1602 );
1603 let protocol = WebUIProtocol::new(fragments);
1604 let state = test_json!({
1605 "outerItems": [
1606 {"innerItems": [{"name": "A"}, {"name": "B"}]},
1607 {"innerItems": [{"name": "C"}]}
1608 ]
1609 });
1610 let mut writer = TestWriter::new();
1611 handle(
1612 &protocol,
1613 &state,
1614 &RenderOptions::new("index.html", "/"),
1615 &mut writer,
1616 )
1617 .unwrap();
1618 assert_eq!(
1619 writer.get_content(),
1620 "<div><div><span>Inner</span><span>Inner</span></div><div><span>Inner</span></div></div>"
1621 );
1622 }
1623
1624 #[test]
1625 fn test_nested_for_with_signals() {
1626 let mut fragments = HashMap::new();
1627 fragments.insert(
1628 "index.html".to_string(),
1629 FragmentList {
1630 fragments: vec![
1631 WebUIFragment::raw("<div>"),
1632 WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
1633 WebUIFragment::raw("</div>"),
1634 ],
1635 },
1636 );
1637 fragments.insert(
1638 "outerTemplate".to_string(),
1639 FragmentList {
1640 fragments: vec![
1641 WebUIFragment::raw("<div>"),
1642 WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
1643 WebUIFragment::raw("</div>"),
1644 ],
1645 },
1646 );
1647 fragments.insert(
1648 "innerTemplate".to_string(),
1649 FragmentList {
1650 fragments: vec![
1651 WebUIFragment::raw("<span>"),
1652 WebUIFragment::signal("innerItem.name", false),
1653 WebUIFragment::raw("</span>"),
1654 ],
1655 },
1656 );
1657 let protocol = WebUIProtocol::new(fragments);
1658 let state = test_json!({
1659 "outerItems": [
1660 {"innerItems": [{"name": "Item1"}, {"name": "Item2"}]},
1661 {"innerItems": [{"name": "Item3"}, {"name": "Item4"}]}
1662 ]
1663 });
1664 let mut writer = TestWriter::new();
1665 handle(
1666 &protocol,
1667 &state,
1668 &RenderOptions::new("index.html", "/"),
1669 &mut writer,
1670 )
1671 .unwrap();
1672 assert_eq!(
1673 writer.get_content(),
1674 "<div><div><span>Item1</span><span>Item2</span></div><div><span>Item3</span><span>Item4</span></div></div>"
1675 );
1676 }
1677
1678 #[test]
1679 fn test_nested_for_with_global_state() {
1680 let mut fragments = HashMap::new();
1681 fragments.insert(
1682 "index.html".to_string(),
1683 FragmentList {
1684 fragments: vec![
1685 WebUIFragment::raw("<div>"),
1686 WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
1687 WebUIFragment::raw("</div>"),
1688 ],
1689 },
1690 );
1691 fragments.insert(
1692 "outerTemplate".to_string(),
1693 FragmentList {
1694 fragments: vec![
1695 WebUIFragment::raw("<div>"),
1696 WebUIFragment::signal("globalOuter", false),
1697 WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
1698 WebUIFragment::raw("</div>"),
1699 ],
1700 },
1701 );
1702 fragments.insert(
1703 "innerTemplate".to_string(),
1704 FragmentList {
1705 fragments: vec![
1706 WebUIFragment::raw("<span>"),
1707 WebUIFragment::signal("innerItem.name", false),
1708 WebUIFragment::signal("globalInner", false),
1709 WebUIFragment::raw("</span>"),
1710 ],
1711 },
1712 );
1713 let protocol = WebUIProtocol::new(fragments);
1714 let state = test_json!({
1715 "globalOuter": "GO",
1716 "globalInner": "GI",
1717 "outerItems": [
1718 {"innerItems": [{"name": "Item1"}, {"name": "Item2"}]},
1719 {"innerItems": [{"name": "Item3"}, {"name": "Item4"}]}
1720 ]
1721 });
1722 let mut writer = TestWriter::new();
1723 handle(
1724 &protocol,
1725 &state,
1726 &RenderOptions::new("index.html", "/"),
1727 &mut writer,
1728 )
1729 .unwrap();
1730 assert_eq!(
1731 writer.get_content(),
1732 "<div><div>GO<span>Item1GI</span><span>Item2GI</span></div><div>GO<span>Item3GI</span><span>Item4GI</span></div></div>"
1733 );
1734 }
1735
1736 #[test]
1739 fn test_if_in_for_uses_local_state() {
1740 let mut fragments = HashMap::new();
1741 fragments.insert(
1742 "index.html".to_string(),
1743 FragmentList {
1744 fragments: vec![WebUIFragment::for_loop("item", "items", "item-tpl")],
1745 },
1746 );
1747 fragments.insert(
1748 "item-tpl".to_string(),
1749 FragmentList {
1750 fragments: vec![WebUIFragment::if_cond(
1751 ConditionExpr::identifier("item.visible"),
1752 "visible-tpl",
1753 )],
1754 },
1755 );
1756 fragments.insert(
1757 "visible-tpl".to_string(),
1758 FragmentList {
1759 fragments: vec![WebUIFragment::signal("item.name", false)],
1760 },
1761 );
1762 let protocol = WebUIProtocol::new(fragments);
1763 let state = test_json!({"items": [{"name": "Show", "visible": true}, {"name": "Hide", "visible": false}]});
1764 let mut writer = TestWriter::new();
1765 handle(
1766 &protocol,
1767 &state,
1768 &RenderOptions::new("index.html", "/"),
1769 &mut writer,
1770 )
1771 .unwrap();
1772 assert_eq!(writer.get_content(), "Show");
1773 }
1774
1775 #[test]
1776 fn test_for_if_local_overrides_global() {
1777 let mut fragments = HashMap::new();
1778 fragments.insert(
1779 "index.html".to_string(),
1780 FragmentList {
1781 fragments: vec![WebUIFragment::for_loop("item", "items", "item-tpl")],
1782 },
1783 );
1784 fragments.insert(
1785 "item-tpl".to_string(),
1786 FragmentList {
1787 fragments: vec![WebUIFragment::if_cond(
1788 ConditionExpr::identifier("item.flag"),
1789 "show-tpl",
1790 )],
1791 },
1792 );
1793 fragments.insert(
1794 "show-tpl".to_string(),
1795 FragmentList {
1796 fragments: vec![WebUIFragment::raw("yes")],
1797 },
1798 );
1799 let protocol = WebUIProtocol::new(fragments);
1800 let state = test_json!({"flag": true, "items": [{"flag": true}, {"flag": false}]});
1802 let mut writer = TestWriter::new();
1803 handle(
1804 &protocol,
1805 &state,
1806 &RenderOptions::new("index.html", "/"),
1807 &mut writer,
1808 )
1809 .unwrap();
1810 assert_eq!(writer.get_content(), "yes");
1811 }
1812
1813 #[test]
1816 fn test_component_attr_state_simple() {
1817 let mut fragments = HashMap::new();
1818 fragments.insert(
1819 "index.html".to_string(),
1820 FragmentList {
1821 fragments: vec![
1822 WebUIFragment::raw("<my-comp"),
1823 WebUIFragment {
1824 fragment: Some(web_ui_fragment::Fragment::Attribute(
1825 WebUIFragmentAttribute {
1826 name: "title".into(),
1827 value: "Attribute Title".into(),
1828 attr_start: true,
1829 raw_value: true,
1830 ..Default::default()
1831 },
1832 )),
1833 },
1834 WebUIFragment::raw(">"),
1835 WebUIFragment::component("my-comp"),
1836 WebUIFragment::raw("</my-comp>"),
1837 ],
1838 },
1839 );
1840 fragments.insert(
1841 "my-comp".to_string(),
1842 FragmentList {
1843 fragments: vec![
1844 WebUIFragment::raw("<span>"),
1845 WebUIFragment::signal("title", false),
1846 WebUIFragment::raw("</span>"),
1847 ],
1848 },
1849 );
1850 let protocol = WebUIProtocol::new(fragments);
1851 let state = test_json!({"title": "Global Title"});
1852 let mut writer = TestWriter::new();
1853 handle(
1854 &protocol,
1855 &state,
1856 &RenderOptions::new("index.html", "/"),
1857 &mut writer,
1858 )
1859 .unwrap();
1860 assert_eq!(
1861 writer.get_content(),
1862 "<my-comp title=\"Attribute Title\"><span>Attribute Title</span></my-comp>"
1863 );
1864 }
1865
1866 #[test]
1867 fn test_component_attr_state_template() {
1868 let mut fragments = HashMap::new();
1869 fragments.insert(
1870 "index.html".to_string(),
1871 FragmentList {
1872 fragments: vec![
1873 WebUIFragment::raw("<my-comp"),
1874 WebUIFragment {
1875 fragment: Some(web_ui_fragment::Fragment::Attribute(
1876 WebUIFragmentAttribute {
1877 name: "title".into(),
1878 template: "title-attr".into(),
1879 attr_start: true,
1880 ..Default::default()
1881 },
1882 )),
1883 },
1884 WebUIFragment::raw(">"),
1885 WebUIFragment::component("my-comp"),
1886 WebUIFragment::raw("</my-comp>"),
1887 ],
1888 },
1889 );
1890 fragments.insert(
1891 "title-attr".to_string(),
1892 FragmentList {
1893 fragments: vec![
1894 WebUIFragment::raw("hello "),
1895 WebUIFragment::signal("item", false),
1896 ],
1897 },
1898 );
1899 fragments.insert(
1900 "my-comp".to_string(),
1901 FragmentList {
1902 fragments: vec![
1903 WebUIFragment::raw("<span>"),
1904 WebUIFragment::signal("title", false),
1905 WebUIFragment::raw("</span>"),
1906 ],
1907 },
1908 );
1909 let protocol = WebUIProtocol::new(fragments);
1910 let state = test_json!({"item": "<world>"});
1911 let mut writer = TestWriter::new();
1912 handle(
1913 &protocol,
1914 &state,
1915 &RenderOptions::new("index.html", "/"),
1916 &mut writer,
1917 )
1918 .unwrap();
1919 assert_eq!(
1920 writer.get_content(),
1921 "<my-comp title=\"hello <world>\"><span>hello <world></span></my-comp>"
1922 );
1923 }
1924
1925 #[test]
1926 fn test_component_attr_camel_case() {
1927 let mut fragments = HashMap::new();
1928 fragments.insert(
1929 "index.html".to_string(),
1930 FragmentList {
1931 fragments: vec![
1932 WebUIFragment::raw("<my-comp"),
1933 WebUIFragment {
1934 fragment: Some(web_ui_fragment::Fragment::Attribute(
1935 WebUIFragmentAttribute {
1936 name: "data-title".into(),
1937 template: "dt-attr".into(),
1938 attr_start: true,
1939 ..Default::default()
1940 },
1941 )),
1942 },
1943 WebUIFragment::raw(">"),
1944 WebUIFragment::component("my-comp"),
1945 WebUIFragment::raw("</my-comp>"),
1946 ],
1947 },
1948 );
1949 fragments.insert(
1950 "dt-attr".to_string(),
1951 FragmentList {
1952 fragments: vec![
1953 WebUIFragment::raw("prefix "),
1954 WebUIFragment::signal("item", false),
1955 ],
1956 },
1957 );
1958 fragments.insert(
1959 "my-comp".to_string(),
1960 FragmentList {
1961 fragments: vec![
1962 WebUIFragment::raw("<span>"),
1963 WebUIFragment::signal("dataTitle", false),
1964 WebUIFragment::raw("</span>"),
1965 ],
1966 },
1967 );
1968 let protocol = WebUIProtocol::new(fragments);
1969 let state = test_json!({"item": "a&b"});
1970 let mut writer = TestWriter::new();
1971 handle(
1972 &protocol,
1973 &state,
1974 &RenderOptions::new("index.html", "/"),
1975 &mut writer,
1976 )
1977 .unwrap();
1978 assert_eq!(
1979 writer.get_content(),
1980 "<my-comp data-title=\"prefix a&b\"><span>prefix a&b</span></my-comp>"
1981 );
1982 }
1983
1984 #[test]
1985 fn test_component_complex_attr() {
1986 let mut fragments = HashMap::new();
1987 fragments.insert(
1988 "index.html".to_string(),
1989 FragmentList {
1990 fragments: vec![
1991 WebUIFragment::raw("<my-comp"),
1992 WebUIFragment {
1993 fragment: Some(web_ui_fragment::Fragment::Attribute(
1994 WebUIFragmentAttribute {
1995 name: ":item".into(),
1996 value: "complexItem".into(),
1997 attr_start: true,
1998 complex: true,
1999 ..Default::default()
2000 },
2001 )),
2002 },
2003 WebUIFragment::raw(">"),
2004 WebUIFragment::component("my-comp"),
2005 WebUIFragment::raw("</my-comp>"),
2006 ],
2007 },
2008 );
2009 fragments.insert(
2010 "my-comp".to_string(),
2011 FragmentList {
2012 fragments: vec![
2013 WebUIFragment::raw("<span>"),
2014 WebUIFragment::signal("item.foo", false),
2015 WebUIFragment::raw("</span><p>"),
2016 WebUIFragment::signal("item.bar", false),
2017 WebUIFragment::raw("</p>"),
2018 ],
2019 },
2020 );
2021 let protocol = WebUIProtocol::new(fragments);
2022 let state = test_json!({"complexItem": {"foo": 1, "bar": "true"}});
2023 let mut writer = TestWriter::new();
2024 handle(
2025 &protocol,
2026 &state,
2027 &RenderOptions::new("index.html", "/"),
2028 &mut writer,
2029 )
2030 .unwrap();
2031 assert_eq!(
2032 writer.get_content(),
2033 "<my-comp><span>1</span><p>true</p></my-comp>"
2034 );
2035 }
2036
2037 #[test]
2038 fn test_component_no_parent_pollution() {
2039 let mut fragments = HashMap::new();
2040 fragments.insert(
2041 "index.html".to_string(),
2042 FragmentList {
2043 fragments: vec![
2044 WebUIFragment::raw("<parent"),
2045 WebUIFragment {
2046 fragment: Some(web_ui_fragment::Fragment::Attribute(
2047 WebUIFragmentAttribute {
2048 name: "var".into(),
2049 value: "var".into(),
2050 attr_start: true,
2051 ..Default::default()
2052 },
2053 )),
2054 },
2055 WebUIFragment::raw(">"),
2056 WebUIFragment::component("parent"),
2057 WebUIFragment::raw("</parent>"),
2058 ],
2059 },
2060 );
2061 fragments.insert(
2062 "parent".to_string(),
2063 FragmentList {
2064 fragments: vec![
2065 WebUIFragment::raw("Before: "),
2066 WebUIFragment::signal("var", false),
2067 WebUIFragment::raw("<child foo"),
2068 WebUIFragment {
2069 fragment: Some(web_ui_fragment::Fragment::Attribute(
2070 WebUIFragmentAttribute {
2071 name: "var".into(),
2072 value: "replaced".into(),
2073 raw_value: true,
2074 ..Default::default()
2075 },
2076 )),
2077 },
2078 WebUIFragment::raw(">"),
2079 WebUIFragment::component("child"),
2080 WebUIFragment::raw("Label</child>After: "),
2081 WebUIFragment::signal("var", false),
2082 ],
2083 },
2084 );
2085 fragments.insert("child".to_string(), FragmentList { fragments: vec![] });
2086 let protocol = WebUIProtocol::new(fragments);
2087 let state = test_json!({"var": "original"});
2088 let mut writer = TestWriter::new();
2089 handle(
2090 &protocol,
2091 &state,
2092 &RenderOptions::new("index.html", "/"),
2093 &mut writer,
2094 )
2095 .unwrap();
2096 assert_eq!(
2097 writer.get_content(),
2098 "<parent var=\"original\">Before: original<child foo var=\"replaced\">Label</child>After: original</parent>"
2099 );
2100 }
2101
2102 #[test]
2103 fn test_component_boolean_attr_state() {
2104 let mut fragments = HashMap::new();
2105 fragments.insert(
2106 "index.html".to_string(),
2107 FragmentList {
2108 fragments: vec![
2109 WebUIFragment::raw("<my-comp"),
2110 WebUIFragment {
2111 fragment: Some(web_ui_fragment::Fragment::Attribute(
2112 WebUIFragmentAttribute {
2113 name: "disabled".into(),
2114 attr_start: true,
2115 condition_tree: Some(ConditionExpr::identifier("isDisabled")),
2116 ..Default::default()
2117 },
2118 )),
2119 },
2120 WebUIFragment::raw(">"),
2121 WebUIFragment::component("my-comp"),
2122 WebUIFragment::raw("</my-comp>"),
2123 ],
2124 },
2125 );
2126 fragments.insert(
2127 "my-comp".to_string(),
2128 FragmentList {
2129 fragments: vec![WebUIFragment::if_cond(
2130 ConditionExpr::identifier("disabled"),
2131 "show",
2132 )],
2133 },
2134 );
2135 fragments.insert(
2136 "show".to_string(),
2137 FragmentList {
2138 fragments: vec![WebUIFragment::raw("disabled!")],
2139 },
2140 );
2141 let protocol = WebUIProtocol::new(fragments);
2142 let state = test_json!({"isDisabled": true});
2143 let mut writer = TestWriter::new();
2144 handle(
2145 &protocol,
2146 &state,
2147 &RenderOptions::new("index.html", "/"),
2148 &mut writer,
2149 )
2150 .unwrap();
2151 assert_eq!(
2152 writer.get_content(),
2153 "<my-comp disabled>disabled!</my-comp>"
2154 );
2155 }
2156
2157 fn render_signal(value: &str) -> String {
2161 let mut fragments = HashMap::new();
2162 fragments.insert(
2163 "index.html".to_string(),
2164 FragmentList {
2165 fragments: vec![WebUIFragment::signal("v", false)],
2166 },
2167 );
2168 let protocol = WebUIProtocol::new(fragments);
2169 let state = test_json!({"v": value});
2170 let mut writer = TestWriter::new();
2171 handle(
2172 &protocol,
2173 &state,
2174 &RenderOptions::new("index.html", "/"),
2175 &mut writer,
2176 )
2177 .unwrap();
2178 writer.get_content()
2179 }
2180
2181 #[test]
2182 fn test_escape_ampersand() {
2183 assert_eq!(render_signal("&"), "&");
2184 }
2185
2186 #[test]
2187 fn test_escape_less_than() {
2188 assert_eq!(render_signal("<"), "<");
2189 }
2190
2191 #[test]
2192 fn test_escape_greater_than() {
2193 assert_eq!(render_signal(">"), ">");
2194 }
2195
2196 #[test]
2197 fn test_escape_double_quote() {
2198 assert_eq!(render_signal("\""), """);
2199 }
2200
2201 #[test]
2202 fn test_escape_single_quote() {
2203 let result = render_signal("'");
2205 assert!(
2206 result == "'" || result == "'" || result == "'",
2207 "Expected escaped single quote, got: {}",
2208 result
2209 );
2210 }
2211
2212 #[test]
2213 fn test_escape_multiple_special_chars() {
2214 let result = render_signal("<script>alert('xss');</script>");
2215 assert!(
2216 result.contains("<") && result.contains(">"),
2217 "Expected escaped HTML, got: {}",
2218 result
2219 );
2220 assert!(
2221 !result.contains("<script>"),
2222 "Should not contain raw <script> tag"
2223 );
2224 }
2225
2226 #[test]
2227 fn test_escape_no_special_chars() {
2228 assert_eq!(render_signal("Hello World"), "Hello World");
2229 }
2230
2231 #[test]
2232 fn test_escape_empty_string() {
2233 assert_eq!(render_signal(""), "");
2234 }
2235
2236 #[test]
2237 fn test_escape_special_at_beginning() {
2238 let result = render_signal("<Hello");
2239 assert!(
2240 result.starts_with("<"),
2241 "Expected < at start, got: {}",
2242 result
2243 );
2244 }
2245
2246 #[test]
2247 fn test_escape_special_at_end() {
2248 let result = render_signal("Hello>");
2249 assert!(
2250 result.ends_with(">"),
2251 "Expected > at end, got: {}",
2252 result
2253 );
2254 }
2255
2256 #[test]
2257 fn test_escape_special_in_middle() {
2258 let result = render_signal("Hel&lo");
2259 assert!(
2260 result.contains("&"),
2261 "Expected & in middle, got: {}",
2262 result
2263 );
2264 }
2265
2266 #[test]
2269 fn test_boolean_attr_truthy_values() {
2270 {
2272 let mut fragments = HashMap::new();
2273 fragments.insert(
2274 "index.html".to_string(),
2275 FragmentList {
2276 fragments: vec![
2277 WebUIFragment::raw("<input"),
2278 WebUIFragment::attribute_boolean(
2279 "checked",
2280 ConditionExpr::identifier("checked"),
2281 ),
2282 WebUIFragment::raw(">"),
2283 ],
2284 },
2285 );
2286 let protocol = WebUIProtocol::new(fragments);
2287 let state = test_json!({"checked": 1});
2288 let mut writer = TestWriter::new();
2289 handle(
2290 &protocol,
2291 &state,
2292 &RenderOptions::new("index.html", "/"),
2293 &mut writer,
2294 )
2295 .unwrap();
2296 assert_eq!(writer.get_content(), "<input checked>");
2297 }
2298 {
2300 let mut fragments = HashMap::new();
2301 fragments.insert(
2302 "index.html".to_string(),
2303 FragmentList {
2304 fragments: vec![
2305 WebUIFragment::raw("<input"),
2306 WebUIFragment::attribute_boolean(
2307 "checked",
2308 ConditionExpr::identifier("checked"),
2309 ),
2310 WebUIFragment::raw(">"),
2311 ],
2312 },
2313 );
2314 let protocol = WebUIProtocol::new(fragments);
2315 let state = test_json!({"checked": "yes"});
2316 let mut writer = TestWriter::new();
2317 handle(
2318 &protocol,
2319 &state,
2320 &RenderOptions::new("index.html", "/"),
2321 &mut writer,
2322 )
2323 .unwrap();
2324 assert_eq!(writer.get_content(), "<input checked>");
2325 }
2326 {
2328 let mut fragments = HashMap::new();
2329 fragments.insert(
2330 "index.html".to_string(),
2331 FragmentList {
2332 fragments: vec![
2333 WebUIFragment::raw("<input"),
2334 WebUIFragment::attribute_boolean(
2335 "checked",
2336 ConditionExpr::identifier("checked"),
2337 ),
2338 WebUIFragment::raw(">"),
2339 ],
2340 },
2341 );
2342 let protocol = WebUIProtocol::new(fragments);
2343 let state = test_json!({"checked": {}});
2344 let mut writer = TestWriter::new();
2345 handle(
2346 &protocol,
2347 &state,
2348 &RenderOptions::new("index.html", "/"),
2349 &mut writer,
2350 )
2351 .unwrap();
2352 assert_eq!(writer.get_content(), "<input>");
2354 }
2355 {
2357 let mut fragments = HashMap::new();
2358 fragments.insert(
2359 "index.html".to_string(),
2360 FragmentList {
2361 fragments: vec![
2362 WebUIFragment::raw("<input"),
2363 WebUIFragment::attribute_boolean(
2364 "checked",
2365 ConditionExpr::identifier("checked"),
2366 ),
2367 WebUIFragment::raw(">"),
2368 ],
2369 },
2370 );
2371 let protocol = WebUIProtocol::new(fragments);
2372 let state = test_json!({"checked": "false"});
2373 let mut writer = TestWriter::new();
2374 handle(
2375 &protocol,
2376 &state,
2377 &RenderOptions::new("index.html", "/"),
2378 &mut writer,
2379 )
2380 .unwrap();
2381 assert_eq!(writer.get_content(), "<input checked>");
2382 }
2383 }
2384
2385 #[test]
2386 fn test_boolean_attr_falsy_values() {
2387 {
2389 let mut fragments = HashMap::new();
2390 fragments.insert(
2391 "index.html".to_string(),
2392 FragmentList {
2393 fragments: vec![
2394 WebUIFragment::raw("<input"),
2395 WebUIFragment::attribute_boolean(
2396 "checked",
2397 ConditionExpr::identifier("checked"),
2398 ),
2399 WebUIFragment::raw(">"),
2400 ],
2401 },
2402 );
2403 let protocol = WebUIProtocol::new(fragments);
2404 let state = test_json!({"checked": 0});
2405 let mut writer = TestWriter::new();
2406 handle(
2407 &protocol,
2408 &state,
2409 &RenderOptions::new("index.html", "/"),
2410 &mut writer,
2411 )
2412 .unwrap();
2413 assert_eq!(writer.get_content(), "<input>");
2414 }
2415 {
2417 let mut fragments = HashMap::new();
2418 fragments.insert(
2419 "index.html".to_string(),
2420 FragmentList {
2421 fragments: vec![
2422 WebUIFragment::raw("<input"),
2423 WebUIFragment::attribute_boolean(
2424 "checked",
2425 ConditionExpr::identifier("checked"),
2426 ),
2427 WebUIFragment::raw(">"),
2428 ],
2429 },
2430 );
2431 let protocol = WebUIProtocol::new(fragments);
2432 let state = test_json!({"checked": ""});
2433 let mut writer = TestWriter::new();
2434 handle(
2435 &protocol,
2436 &state,
2437 &RenderOptions::new("index.html", "/"),
2438 &mut writer,
2439 )
2440 .unwrap();
2441 assert_eq!(writer.get_content(), "<input>");
2442 }
2443 {
2445 let mut fragments = HashMap::new();
2446 fragments.insert(
2447 "index.html".to_string(),
2448 FragmentList {
2449 fragments: vec![
2450 WebUIFragment::raw("<input"),
2451 WebUIFragment::attribute_boolean(
2452 "checked",
2453 ConditionExpr::identifier("checked"),
2454 ),
2455 WebUIFragment::raw(">"),
2456 ],
2457 },
2458 );
2459 let protocol = WebUIProtocol::new(fragments);
2460 let state = test_json!({"checked": false});
2461 let mut writer = TestWriter::new();
2462 handle(
2463 &protocol,
2464 &state,
2465 &RenderOptions::new("index.html", "/"),
2466 &mut writer,
2467 )
2468 .unwrap();
2469 assert_eq!(writer.get_content(), "<input>");
2470 }
2471 {
2473 let mut fragments = HashMap::new();
2474 fragments.insert(
2475 "index.html".to_string(),
2476 FragmentList {
2477 fragments: vec![
2478 WebUIFragment::raw("<input"),
2479 WebUIFragment::attribute_boolean(
2480 "checked",
2481 ConditionExpr::identifier("checked"),
2482 ),
2483 WebUIFragment::raw(">"),
2484 ],
2485 },
2486 );
2487 let protocol = WebUIProtocol::new(fragments);
2488 let state = test_json!({});
2489 let mut writer = TestWriter::new();
2490 handle(
2491 &protocol,
2492 &state,
2493 &RenderOptions::new("index.html", "/"),
2494 &mut writer,
2495 )
2496 .unwrap();
2497 assert_eq!(writer.get_content(), "<input>");
2498 }
2499 }
2500
2501 #[test]
2502 fn test_boolean_attr_expression_true() {
2503 let mut fragments = HashMap::new();
2504 fragments.insert(
2505 "index.html".to_string(),
2506 FragmentList {
2507 fragments: vec![
2508 WebUIFragment::raw("<button"),
2509 WebUIFragment::attribute_boolean(
2510 "disabled",
2511 ConditionExpr::predicate("itemCount", ComparisonOperator::Equal, "5"),
2512 ),
2513 WebUIFragment::raw(">Click</button>"),
2514 ],
2515 },
2516 );
2517 let protocol = WebUIProtocol::new(fragments);
2518 let state = test_json!({"itemCount": 5});
2519 let mut writer = TestWriter::new();
2520 handle(
2521 &protocol,
2522 &state,
2523 &RenderOptions::new("index.html", "/"),
2524 &mut writer,
2525 )
2526 .unwrap();
2527 assert_eq!(writer.get_content(), "<button disabled>Click</button>");
2528 }
2529
2530 #[test]
2531 fn test_boolean_attr_expression_false() {
2532 let mut fragments = HashMap::new();
2533 fragments.insert(
2534 "index.html".to_string(),
2535 FragmentList {
2536 fragments: vec![
2537 WebUIFragment::raw("<button"),
2538 WebUIFragment::attribute_boolean(
2539 "disabled",
2540 ConditionExpr::predicate("itemCount", ComparisonOperator::Equal, "5"),
2541 ),
2542 WebUIFragment::raw(">Click</button>"),
2543 ],
2544 },
2545 );
2546 let protocol = WebUIProtocol::new(fragments);
2547 let state = test_json!({"itemCount": 3});
2548 let mut writer = TestWriter::new();
2549 handle(
2550 &protocol,
2551 &state,
2552 &RenderOptions::new("index.html", "/"),
2553 &mut writer,
2554 )
2555 .unwrap();
2556 assert_eq!(writer.get_content(), "<button>Click</button>");
2557 }
2558
2559 #[test]
2562 fn test_nested_component_attr_capture() {
2563 let mut fragments = HashMap::new();
2564 fragments.insert(
2565 "index.html".to_string(),
2566 FragmentList {
2567 fragments: vec![
2568 WebUIFragment::raw("<parent-component"),
2569 WebUIFragment {
2570 fragment: Some(web_ui_fragment::Fragment::Attribute(
2571 WebUIFragmentAttribute {
2572 name: "title".into(),
2573 template: "parent-title".into(),
2574 attr_start: true,
2575 ..Default::default()
2576 },
2577 )),
2578 },
2579 WebUIFragment::raw(">"),
2580 WebUIFragment::component("parent-component"),
2581 WebUIFragment::raw("</parent-component>"),
2582 ],
2583 },
2584 );
2585 fragments.insert(
2586 "parent-title".to_string(),
2587 FragmentList {
2588 fragments: vec![
2589 WebUIFragment::raw("Hello "),
2590 WebUIFragment::signal("who", false),
2591 ],
2592 },
2593 );
2594 fragments.insert(
2595 "parent-component".to_string(),
2596 FragmentList {
2597 fragments: vec![
2598 WebUIFragment::raw("<child-component"),
2599 WebUIFragment {
2600 fragment: Some(web_ui_fragment::Fragment::Attribute(
2601 WebUIFragmentAttribute {
2602 name: "title".into(),
2603 template: "child-title".into(),
2604 attr_start: true,
2605 ..Default::default()
2606 },
2607 )),
2608 },
2609 WebUIFragment::raw(">"),
2610 WebUIFragment::component("child-component"),
2611 WebUIFragment::raw("</child-component>"),
2612 ],
2613 },
2614 );
2615 fragments.insert(
2616 "child-title".to_string(),
2617 FragmentList {
2618 fragments: vec![
2619 WebUIFragment::raw("Child of "),
2620 WebUIFragment::signal("title", false),
2621 ],
2622 },
2623 );
2624 fragments.insert(
2625 "child-component".to_string(),
2626 FragmentList {
2627 fragments: vec![
2628 WebUIFragment::raw("<span>"),
2629 WebUIFragment::signal("title", false),
2630 WebUIFragment::raw("</span>"),
2631 ],
2632 },
2633 );
2634 let protocol = WebUIProtocol::new(fragments);
2635 let state = test_json!({"who": "<world>"});
2636 let mut writer = TestWriter::new();
2637 handle(
2638 &protocol,
2639 &state,
2640 &RenderOptions::new("index.html", "/"),
2641 &mut writer,
2642 )
2643 .unwrap();
2644 assert_eq!(
2645 writer.get_content(),
2646 "<parent-component title=\"Hello <world>\"><child-component title=\"Child of Hello <world>\"><span>Child of Hello <world></span></child-component></parent-component>"
2647 );
2648 }
2649
2650 #[test]
2651 fn test_grandchild_attr_propagation() {
2652 let mut fragments = HashMap::new();
2653 fragments.insert(
2654 "index.html".to_string(),
2655 FragmentList {
2656 fragments: vec![
2657 WebUIFragment::raw("<parent-component"),
2658 WebUIFragment {
2659 fragment: Some(web_ui_fragment::Fragment::Attribute(
2660 WebUIFragmentAttribute {
2661 name: "title".into(),
2662 template: "p-title".into(),
2663 attr_start: true,
2664 ..Default::default()
2665 },
2666 )),
2667 },
2668 WebUIFragment::raw(">"),
2669 WebUIFragment::component("parent-component"),
2670 WebUIFragment::raw("</parent-component>"),
2671 ],
2672 },
2673 );
2674 fragments.insert(
2675 "p-title".to_string(),
2676 FragmentList {
2677 fragments: vec![WebUIFragment::raw("P:"), WebUIFragment::signal("p", false)],
2678 },
2679 );
2680 fragments.insert(
2681 "parent-component".to_string(),
2682 FragmentList {
2683 fragments: vec![
2684 WebUIFragment::raw("<child-component"),
2685 WebUIFragment {
2686 fragment: Some(web_ui_fragment::Fragment::Attribute(
2687 WebUIFragmentAttribute {
2688 name: "title".into(),
2689 template: "c-title".into(),
2690 attr_start: true,
2691 ..Default::default()
2692 },
2693 )),
2694 },
2695 WebUIFragment::raw(">"),
2696 WebUIFragment::component("child-component"),
2697 WebUIFragment::raw("</child-component>"),
2698 ],
2699 },
2700 );
2701 fragments.insert(
2702 "c-title".to_string(),
2703 FragmentList {
2704 fragments: vec![
2705 WebUIFragment::raw("C("),
2706 WebUIFragment::signal("title", false),
2707 WebUIFragment::raw(")-"),
2708 WebUIFragment::signal("cExtra", false),
2709 ],
2710 },
2711 );
2712 fragments.insert(
2713 "child-component".to_string(),
2714 FragmentList {
2715 fragments: vec![
2716 WebUIFragment::raw("<grandchild-component"),
2717 WebUIFragment {
2718 fragment: Some(web_ui_fragment::Fragment::Attribute(
2719 WebUIFragmentAttribute {
2720 name: "title".into(),
2721 value: "title".into(),
2722 attr_start: true,
2723 ..Default::default()
2724 },
2725 )),
2726 },
2727 WebUIFragment::raw(">"),
2728 WebUIFragment::component("grandchild-component"),
2729 WebUIFragment::raw("</grandchild-component>"),
2730 ],
2731 },
2732 );
2733 fragments.insert(
2734 "grandchild-component".to_string(),
2735 FragmentList {
2736 fragments: vec![
2737 WebUIFragment::raw("<span>"),
2738 WebUIFragment::signal("title", false),
2739 WebUIFragment::raw("</span>"),
2740 ],
2741 },
2742 );
2743 let protocol = WebUIProtocol::new(fragments);
2744 let state = test_json!({"p": "<p>", "cExtra": "x&y"});
2745 let mut writer = TestWriter::new();
2746 handle(
2747 &protocol,
2748 &state,
2749 &RenderOptions::new("index.html", "/"),
2750 &mut writer,
2751 )
2752 .unwrap();
2753 assert_eq!(
2754 writer.get_content(),
2755 "<parent-component title=\"P:<p>\"><child-component title=\"C(P:<p>)-x&y\"><grandchild-component title=\"C(P:<p>)-x&y\"><span>C(P:<p>)-x&y</span></grandchild-component></child-component></parent-component>"
2756 );
2757 }
2758
2759 #[test]
2760 fn test_for_loop_component_attr() {
2761 let mut fragments = HashMap::new();
2762 fragments.insert(
2763 "index.html".to_string(),
2764 FragmentList {
2765 fragments: vec![
2766 WebUIFragment::raw("<parent-component"),
2767 WebUIFragment {
2768 fragment: Some(web_ui_fragment::Fragment::Attribute(
2769 WebUIFragmentAttribute {
2770 name: "title".into(),
2771 template: "parent-title-loop".into(),
2772 attr_start: true,
2773 ..Default::default()
2774 },
2775 )),
2776 },
2777 WebUIFragment::raw(">"),
2778 WebUIFragment::component("parent-component"),
2779 WebUIFragment::raw("</parent-component>"),
2780 ],
2781 },
2782 );
2783 fragments.insert(
2784 "parent-title-loop".to_string(),
2785 FragmentList {
2786 fragments: vec![
2787 WebUIFragment::raw("Parent:"),
2788 WebUIFragment::signal("who", false),
2789 ],
2790 },
2791 );
2792 fragments.insert(
2793 "parent-component".to_string(),
2794 FragmentList {
2795 fragments: vec![WebUIFragment::for_loop("item", "items", "child-loop")],
2796 },
2797 );
2798 fragments.insert(
2799 "child-loop".to_string(),
2800 FragmentList {
2801 fragments: vec![
2802 WebUIFragment::raw("<child-component"),
2803 WebUIFragment {
2804 fragment: Some(web_ui_fragment::Fragment::Attribute(
2805 WebUIFragmentAttribute {
2806 name: "title".into(),
2807 template: "child-title-loop".into(),
2808 attr_start: true,
2809 ..Default::default()
2810 },
2811 )),
2812 },
2813 WebUIFragment::raw(">"),
2814 WebUIFragment::component("child-component"),
2815 WebUIFragment::raw("</child-component>"),
2816 ],
2817 },
2818 );
2819 fragments.insert(
2820 "child-title-loop".to_string(),
2821 FragmentList {
2822 fragments: vec![
2823 WebUIFragment::raw("Hi "),
2824 WebUIFragment::signal("item.name", false),
2825 WebUIFragment::raw(" / "),
2826 WebUIFragment::signal("title", false),
2827 ],
2828 },
2829 );
2830 fragments.insert(
2831 "child-component".to_string(),
2832 FragmentList {
2833 fragments: vec![
2834 WebUIFragment::raw("<span>"),
2835 WebUIFragment::signal("title", false),
2836 WebUIFragment::raw("</span>"),
2837 ],
2838 },
2839 );
2840 let protocol = WebUIProtocol::new(fragments);
2841 let state = test_json!({"who": "Bob", "items": [{"name": "A<1>"}, {"name": "B&2"}]});
2842 let mut writer = TestWriter::new();
2843 handle(
2844 &protocol,
2845 &state,
2846 &RenderOptions::new("index.html", "/"),
2847 &mut writer,
2848 )
2849 .unwrap();
2850 assert_eq!(
2851 writer.get_content(),
2852 "<parent-component title=\"Parent:Bob\"><child-component title=\"Hi A<1> / Parent:Bob\"><span>Hi A<1> / Parent:Bob</span></child-component><child-component title=\"Hi B&2 / Parent:Bob\"><span>Hi B&2 / Parent:Bob</span></child-component></parent-component>"
2853 );
2854 }
2855
2856 #[test]
2857 fn test_multiple_template_attrs() {
2858 let mut fragments = HashMap::new();
2859 fragments.insert(
2860 "index.html".to_string(),
2861 FragmentList {
2862 fragments: vec![
2863 WebUIFragment::raw("<my-component"),
2864 WebUIFragment {
2865 fragment: Some(web_ui_fragment::Fragment::Attribute(
2866 WebUIFragmentAttribute {
2867 name: "title".into(),
2868 template: "attr-title".into(),
2869 attr_start: true,
2870 ..Default::default()
2871 },
2872 )),
2873 },
2874 WebUIFragment {
2875 fragment: Some(web_ui_fragment::Fragment::Attribute(
2876 WebUIFragmentAttribute {
2877 name: "data-title".into(),
2878 template: "attr-data-title".into(),
2879 attr_start: false,
2880 ..Default::default()
2881 },
2882 )),
2883 },
2884 WebUIFragment {
2885 fragment: Some(web_ui_fragment::Fragment::Attribute(
2886 WebUIFragmentAttribute {
2887 name: "aria-label".into(),
2888 template: "attr-aria-label".into(),
2889 attr_start: false,
2890 ..Default::default()
2891 },
2892 )),
2893 },
2894 WebUIFragment::raw(">"),
2895 WebUIFragment::component("my-component"),
2896 WebUIFragment::raw("</my-component>"),
2897 ],
2898 },
2899 );
2900 fragments.insert(
2901 "attr-title".to_string(),
2902 FragmentList {
2903 fragments: vec![WebUIFragment::raw("T:"), WebUIFragment::signal("t", false)],
2904 },
2905 );
2906 fragments.insert(
2907 "attr-data-title".to_string(),
2908 FragmentList {
2909 fragments: vec![WebUIFragment::raw("D:"), WebUIFragment::signal("d", false)],
2910 },
2911 );
2912 fragments.insert(
2913 "attr-aria-label".to_string(),
2914 FragmentList {
2915 fragments: vec![WebUIFragment::raw("A:"), WebUIFragment::signal("a", false)],
2916 },
2917 );
2918 fragments.insert(
2919 "my-component".to_string(),
2920 FragmentList {
2921 fragments: vec![
2922 WebUIFragment::raw("<span>"),
2923 WebUIFragment::signal("title", false),
2924 WebUIFragment::raw("|"),
2925 WebUIFragment::signal("dataTitle", false),
2926 WebUIFragment::raw("|"),
2927 WebUIFragment::signal("ariaLabel", false),
2928 WebUIFragment::raw("</span>"),
2929 ],
2930 },
2931 );
2932 let protocol = WebUIProtocol::new(fragments);
2933 let state = test_json!({"t": "<t&1>", "d": "d<2>", "a": "a&3"});
2934 let mut writer = TestWriter::new();
2935 handle(
2936 &protocol,
2937 &state,
2938 &RenderOptions::new("index.html", "/"),
2939 &mut writer,
2940 )
2941 .unwrap();
2942 assert_eq!(
2943 writer.get_content(),
2944 "<my-component title=\"T:<t&1>\" data-title=\"D:d<2>\" aria-label=\"A:a&3\"><span>T:<t&1>|D:d<2>|A:a&3</span></my-component>"
2945 );
2946 }
2947
2948 #[test]
2949 fn test_attr_priority_over_global() {
2950 let mut fragments = HashMap::new();
2951 fragments.insert(
2952 "index.html".to_string(),
2953 FragmentList {
2954 fragments: vec![
2955 WebUIFragment::raw("<my-component"),
2956 WebUIFragment {
2957 fragment: Some(web_ui_fragment::Fragment::Attribute(
2958 WebUIFragmentAttribute {
2959 name: "title".into(),
2960 value: "Attribute Title".into(),
2961 raw_value: true,
2962 attr_start: true,
2963 ..Default::default()
2964 },
2965 )),
2966 },
2967 WebUIFragment::raw(">"),
2968 WebUIFragment::component("my-component"),
2969 WebUIFragment::raw("</my-component>"),
2970 ],
2971 },
2972 );
2973 fragments.insert(
2974 "my-component".to_string(),
2975 FragmentList {
2976 fragments: vec![
2977 WebUIFragment::raw("<span>"),
2978 WebUIFragment::signal("title", false),
2979 WebUIFragment::raw("</span>"),
2980 ],
2981 },
2982 );
2983 let protocol = WebUIProtocol::new(fragments);
2984 let state = test_json!({"title": "Global Title"});
2985 let mut writer = TestWriter::new();
2986 handle(
2987 &protocol,
2988 &state,
2989 &RenderOptions::new("index.html", "/"),
2990 &mut writer,
2991 )
2992 .unwrap();
2993 assert_eq!(
2994 writer.get_content(),
2995 "<my-component title=\"Attribute Title\"><span>Attribute Title</span></my-component>"
2996 );
2997 }
2998
2999 #[test]
3000 fn test_attr_priority_over_local_and_global() {
3001 let mut fragments = HashMap::new();
3002 fragments.insert(
3003 "index.html".to_string(),
3004 FragmentList {
3005 fragments: vec![WebUIFragment::for_loop("item", "items", "loop")],
3006 },
3007 );
3008 fragments.insert(
3009 "loop".to_string(),
3010 FragmentList {
3011 fragments: vec![
3012 WebUIFragment::raw("<my-component"),
3013 WebUIFragment {
3014 fragment: Some(web_ui_fragment::Fragment::Attribute(
3015 WebUIFragmentAttribute {
3016 name: "title".into(),
3017 value: "Attribute Title".into(),
3018 raw_value: true,
3019 attr_start: true,
3020 ..Default::default()
3021 },
3022 )),
3023 },
3024 WebUIFragment::raw(">"),
3025 WebUIFragment::component("my-component"),
3026 WebUIFragment::raw("</my-component>"),
3027 ],
3028 },
3029 );
3030 fragments.insert(
3031 "my-component".to_string(),
3032 FragmentList {
3033 fragments: vec![
3034 WebUIFragment::raw("<span>"),
3035 WebUIFragment::signal("title", false),
3036 WebUIFragment::raw("</span>"),
3037 ],
3038 },
3039 );
3040 let protocol = WebUIProtocol::new(fragments);
3041 let state = test_json!({"title": "Global Title", "items": [{"title": "Local Title"}]});
3042 let mut writer = TestWriter::new();
3043 handle(
3044 &protocol,
3045 &state,
3046 &RenderOptions::new("index.html", "/"),
3047 &mut writer,
3048 )
3049 .unwrap();
3050 assert_eq!(
3051 writer.get_content(),
3052 "<my-component title=\"Attribute Title\"><span>Attribute Title</span></my-component>"
3053 );
3054 }
3055
3056 #[test]
3057 fn test_boolean_attr_first_component_attr() {
3058 let mut fragments = HashMap::new();
3059 fragments.insert(
3060 "index.html".to_string(),
3061 FragmentList {
3062 fragments: vec![
3063 WebUIFragment::raw("<my-component"),
3064 WebUIFragment {
3065 fragment: Some(web_ui_fragment::Fragment::Attribute(
3066 WebUIFragmentAttribute {
3067 name: "disabled".into(),
3068 attr_start: true,
3069 condition_tree: Some(ConditionExpr::identifier("isDisabled")),
3070 ..Default::default()
3071 },
3072 )),
3073 },
3074 WebUIFragment {
3075 fragment: Some(web_ui_fragment::Fragment::Attribute(
3076 WebUIFragmentAttribute {
3077 name: "label".into(),
3078 value: "Component Label".into(),
3079 raw_value: true,
3080 attr_start: false,
3081 ..Default::default()
3082 },
3083 )),
3084 },
3085 WebUIFragment::raw(">"),
3086 WebUIFragment::component("my-component"),
3087 WebUIFragment::raw("</my-component>"),
3088 ],
3089 },
3090 );
3091 fragments.insert(
3092 "my-component".to_string(),
3093 FragmentList {
3094 fragments: vec![
3095 WebUIFragment::raw("<template shadowrootmode=\"open\">"),
3096 WebUIFragment::if_cond(
3097 ConditionExpr::identifier("disabled"),
3098 "disabledTemplate",
3099 ),
3100 WebUIFragment::raw("<span>"),
3101 WebUIFragment::signal("label", false),
3102 WebUIFragment::raw("</span></template>"),
3103 ],
3104 },
3105 );
3106 fragments.insert(
3107 "disabledTemplate".to_string(),
3108 FragmentList {
3109 fragments: vec![WebUIFragment::raw("<div>Disabled</div>")],
3110 },
3111 );
3112 let protocol = WebUIProtocol::new(fragments);
3113 let state = test_json!({"isDisabled": true});
3114 let mut writer = TestWriter::new();
3115 handle(
3116 &protocol,
3117 &state,
3118 &RenderOptions::new("index.html", "/"),
3119 &mut writer,
3120 )
3121 .unwrap();
3122 assert_eq!(
3123 writer.get_content(),
3124 "<my-component disabled label=\"Component Label\"><template shadowrootmode=\"open\"><div>Disabled</div><span>Component Label</span></template></my-component>"
3125 );
3126 }
3127
3128 #[test]
3129 fn test_hyphenated_attr_camelcase() {
3130 let mut fragments = HashMap::new();
3131 fragments.insert(
3132 "index.html".to_string(),
3133 FragmentList {
3134 fragments: vec![
3135 WebUIFragment::raw("<my-component"),
3136 WebUIFragment {
3137 fragment: Some(web_ui_fragment::Fragment::Attribute(
3138 WebUIFragmentAttribute {
3139 name: "key-hyphen".into(),
3140 value: "Local Value".into(),
3141 raw_value: true,
3142 attr_start: true,
3143 ..Default::default()
3144 },
3145 )),
3146 },
3147 WebUIFragment::raw(">"),
3148 WebUIFragment::component("my-component"),
3149 WebUIFragment::raw("</my-component>"),
3150 ],
3151 },
3152 );
3153 fragments.insert(
3154 "my-component".to_string(),
3155 FragmentList {
3156 fragments: vec![
3157 WebUIFragment::raw("<span>"),
3158 WebUIFragment::signal("keyHyphen", false),
3159 WebUIFragment::raw("</span>"),
3160 ],
3161 },
3162 );
3163 let protocol = WebUIProtocol::new(fragments);
3164 let state = test_json!({"keyHyphen": "Global Value"});
3165 let mut writer = TestWriter::new();
3166 handle(
3167 &protocol,
3168 &state,
3169 &RenderOptions::new("index.html", "/"),
3170 &mut writer,
3171 )
3172 .unwrap();
3173 assert_eq!(
3174 writer.get_content(),
3175 "<my-component key-hyphen=\"Local Value\"><span>Local Value</span></my-component>"
3176 );
3177 }
3178
3179 #[test]
3180 fn test_skipped_component_attrs() {
3181 let mut fragments = HashMap::new();
3185 fragments.insert(
3186 "index.html".to_string(),
3187 FragmentList {
3188 fragments: vec![
3189 WebUIFragment::raw("<test-component"),
3190 WebUIFragment {
3192 fragment: Some(web_ui_fragment::Fragment::Attribute(
3193 WebUIFragmentAttribute {
3194 name: "class".into(),
3195 value: "skippedClass".into(),
3196 attr_start: true,
3197 attr_skip: true,
3198 ..Default::default()
3199 },
3200 )),
3201 },
3202 WebUIFragment {
3204 fragment: Some(web_ui_fragment::Fragment::Attribute(
3205 WebUIFragmentAttribute {
3206 name: "style".into(),
3207 value: "skippedStyle".into(),
3208 attr_skip: true,
3209 ..Default::default()
3210 },
3211 )),
3212 },
3213 WebUIFragment {
3215 fragment: Some(web_ui_fragment::Fragment::Attribute(
3216 WebUIFragmentAttribute {
3217 name: "role".into(),
3218 value: "skippedRole".into(),
3219 attr_skip: true,
3220 ..Default::default()
3221 },
3222 )),
3223 },
3224 WebUIFragment {
3226 fragment: Some(web_ui_fragment::Fragment::Attribute(
3227 WebUIFragmentAttribute {
3228 name: "data-testid".into(),
3229 value: "skippedDataTestid".into(),
3230 attr_skip: true,
3231 ..Default::default()
3232 },
3233 )),
3234 },
3235 WebUIFragment {
3237 fragment: Some(web_ui_fragment::Fragment::Attribute(
3238 WebUIFragmentAttribute {
3239 name: "aria-label".into(),
3240 value: "skippedAriaLabel".into(),
3241 attr_skip: true,
3242 ..Default::default()
3243 },
3244 )),
3245 },
3246 WebUIFragment {
3248 fragment: Some(web_ui_fragment::Fragment::Attribute(
3249 WebUIFragmentAttribute {
3250 name: "title".into(),
3251 value: "title".into(),
3252 ..Default::default()
3253 },
3254 )),
3255 },
3256 WebUIFragment::raw(">"),
3257 WebUIFragment::component("test-component"),
3258 WebUIFragment::raw("</test-component>"),
3259 ],
3260 },
3261 );
3262 fragments.insert(
3263 "test-component".to_string(),
3264 FragmentList {
3265 fragments: vec![
3266 WebUIFragment::raw("<template shadowrootmode=\"open\"><span>"),
3267 WebUIFragment::signal("title", false),
3268 WebUIFragment::raw("-"),
3269 WebUIFragment::signal("class", false),
3270 WebUIFragment::raw("-"),
3271 WebUIFragment::signal("style", false),
3272 WebUIFragment::raw("-"),
3273 WebUIFragment::signal("role", false),
3274 WebUIFragment::raw("-"),
3275 WebUIFragment::signal("dataTestid", false),
3276 WebUIFragment::raw("-"),
3277 WebUIFragment::signal("ariaLabel", false),
3278 WebUIFragment::raw("</span></template>"),
3279 ],
3280 },
3281 );
3282 let protocol = WebUIProtocol::new(fragments);
3283 let state = test_json!({
3284 "title": "Hello",
3285 "skippedClass": "my-class",
3286 "skippedStyle": "color:red",
3287 "skippedRole": "button",
3288 "skippedDataTestid": "test-id",
3289 "skippedAriaLabel": "label-text"
3290 });
3291 let mut writer = TestWriter::new();
3292 handle(
3293 &protocol,
3294 &state,
3295 &RenderOptions::new("index.html", "/"),
3296 &mut writer,
3297 )
3298 .unwrap();
3299 assert_eq!(
3303 writer.get_content(),
3304 "<test-component class=\"my-class\" style=\"color:red\" role=\"button\" data-testid=\"test-id\" aria-label=\"label-text\" title=\"Hello\"><template shadowrootmode=\"open\"><span>Hello-----</span></template></test-component>"
3305 );
3306 }
3307
3308 #[test]
3311 fn test_attr_inherit_parent_to_child() {
3312 let mut fragments = HashMap::new();
3313 fragments.insert(
3314 "index.html".to_string(),
3315 FragmentList {
3316 fragments: vec![
3317 WebUIFragment::raw("<parent-component"),
3318 WebUIFragment {
3319 fragment: Some(web_ui_fragment::Fragment::Attribute(
3320 WebUIFragmentAttribute {
3321 name: "title".into(),
3322 value: "Parent Title".into(),
3323 raw_value: true,
3324 attr_start: true,
3325 ..Default::default()
3326 },
3327 )),
3328 },
3329 WebUIFragment::raw(">"),
3330 WebUIFragment::component("parent-component"),
3331 WebUIFragment::raw("</parent-component>"),
3332 ],
3333 },
3334 );
3335 fragments.insert(
3336 "parent-component".to_string(),
3337 FragmentList {
3338 fragments: vec![
3339 WebUIFragment::raw("<h1>"),
3340 WebUIFragment::signal("title", false),
3341 WebUIFragment::raw("</h1><child-component"),
3342 WebUIFragment {
3343 fragment: Some(web_ui_fragment::Fragment::Attribute(
3344 WebUIFragmentAttribute {
3345 name: "title".into(),
3346 value: "title".into(),
3347 attr_start: true,
3348 ..Default::default()
3349 },
3350 )),
3351 },
3352 WebUIFragment::raw(">"),
3353 WebUIFragment::component("child-component"),
3354 WebUIFragment::raw("</child-component>"),
3355 ],
3356 },
3357 );
3358 fragments.insert(
3359 "child-component".to_string(),
3360 FragmentList {
3361 fragments: vec![
3362 WebUIFragment::raw("<h2>"),
3363 WebUIFragment::signal("title", false),
3364 WebUIFragment::raw("</h2>"),
3365 ],
3366 },
3367 );
3368 let protocol = WebUIProtocol::new(fragments);
3369 let state = test_json!({});
3370 let mut writer = TestWriter::new();
3371 handle(
3372 &protocol,
3373 &state,
3374 &RenderOptions::new("index.html", "/"),
3375 &mut writer,
3376 )
3377 .unwrap();
3378 assert_eq!(
3379 writer.get_content(),
3380 "<parent-component title=\"Parent Title\"><h1>Parent Title</h1><child-component title=\"Parent Title\"><h2>Parent Title</h2></child-component></parent-component>"
3381 );
3382 }
3383
3384 #[test]
3385 fn test_attr_inherit_deep() {
3386 let mut fragments = HashMap::new();
3387 fragments.insert(
3388 "index.html".to_string(),
3389 FragmentList {
3390 fragments: vec![
3391 WebUIFragment::raw("<parent-component"),
3392 WebUIFragment {
3393 fragment: Some(web_ui_fragment::Fragment::Attribute(
3394 WebUIFragmentAttribute {
3395 name: "title".into(),
3396 value: "Parent Title".into(),
3397 raw_value: true,
3398 attr_start: true,
3399 ..Default::default()
3400 },
3401 )),
3402 },
3403 WebUIFragment::raw(">"),
3404 WebUIFragment::component("parent-component"),
3405 WebUIFragment::raw("</parent-component>"),
3406 ],
3407 },
3408 );
3409 fragments.insert(
3410 "parent-component".to_string(),
3411 FragmentList {
3412 fragments: vec![
3413 WebUIFragment::raw("<child-component"),
3414 WebUIFragment {
3415 fragment: Some(web_ui_fragment::Fragment::Attribute(
3416 WebUIFragmentAttribute {
3417 name: "title".into(),
3418 value: "Child Title".into(),
3419 raw_value: true,
3420 attr_start: true,
3421 ..Default::default()
3422 },
3423 )),
3424 },
3425 WebUIFragment::raw(">"),
3426 WebUIFragment::component("child-component"),
3427 WebUIFragment::raw("</child-component>"),
3428 ],
3429 },
3430 );
3431 fragments.insert(
3432 "child-component".to_string(),
3433 FragmentList {
3434 fragments: vec![
3435 WebUIFragment::raw("<grandchild-component"),
3436 WebUIFragment {
3437 fragment: Some(web_ui_fragment::Fragment::Attribute(
3438 WebUIFragmentAttribute {
3439 name: "title".into(),
3440 value: "title".into(),
3441 attr_start: true,
3442 ..Default::default()
3443 },
3444 )),
3445 },
3446 WebUIFragment::raw(">"),
3447 WebUIFragment::component("grandchild-component"),
3448 WebUIFragment::raw("</grandchild-component>"),
3449 ],
3450 },
3451 );
3452 fragments.insert(
3453 "grandchild-component".to_string(),
3454 FragmentList {
3455 fragments: vec![
3456 WebUIFragment::raw("<h3>"),
3457 WebUIFragment::signal("title", false),
3458 WebUIFragment::raw("</h3>"),
3459 ],
3460 },
3461 );
3462 let protocol = WebUIProtocol::new(fragments);
3463 let state = test_json!({});
3464 let mut writer = TestWriter::new();
3465 handle(
3466 &protocol,
3467 &state,
3468 &RenderOptions::new("index.html", "/"),
3469 &mut writer,
3470 )
3471 .unwrap();
3472 assert_eq!(
3473 writer.get_content(),
3474 "<parent-component title=\"Parent Title\"><child-component title=\"Child Title\"><grandchild-component title=\"Child Title\"><h3>Child Title</h3></grandchild-component></child-component></parent-component>"
3475 );
3476 }
3477
3478 #[test]
3479 fn test_complex_attr_access() {
3480 let mut fragments = HashMap::new();
3481 fragments.insert(
3482 "index.html".to_string(),
3483 FragmentList {
3484 fragments: vec![
3485 WebUIFragment::raw("<my-component"),
3486 WebUIFragment {
3487 fragment: Some(web_ui_fragment::Fragment::Attribute(
3488 WebUIFragmentAttribute {
3489 name: ":item".into(),
3490 value: "complexItem".into(),
3491 attr_start: true,
3492 complex: true,
3493 ..Default::default()
3494 },
3495 )),
3496 },
3497 WebUIFragment::raw(">"),
3498 WebUIFragment::component("my-component"),
3499 WebUIFragment::raw("</my-component>"),
3500 ],
3501 },
3502 );
3503 fragments.insert(
3504 "my-component".to_string(),
3505 FragmentList {
3506 fragments: vec![
3507 WebUIFragment::raw("<span>"),
3508 WebUIFragment::signal("item.foo", false),
3509 WebUIFragment::raw("</span><p>"),
3510 WebUIFragment::signal("item.bar", false),
3511 WebUIFragment::raw("</p>"),
3512 ],
3513 },
3514 );
3515 let protocol = WebUIProtocol::new(fragments);
3516 let state = test_json!({"complexItem": {"foo": 1, "bar": "true"}});
3517 let mut writer = TestWriter::new();
3518 handle(
3519 &protocol,
3520 &state,
3521 &RenderOptions::new("index.html", "/"),
3522 &mut writer,
3523 )
3524 .unwrap();
3525 assert_eq!(
3526 writer.get_content(),
3527 "<my-component><span>1</span><p>true</p></my-component>"
3528 );
3529 }
3530
3531 #[test]
3532 fn test_complex_attr_for_loop() {
3533 let mut fragments = HashMap::new();
3534 fragments.insert(
3535 "index.html".to_string(),
3536 FragmentList {
3537 fragments: vec![WebUIFragment::for_loop(
3538 "item",
3539 "list.items",
3540 "listTemplate",
3541 )],
3542 },
3543 );
3544 fragments.insert(
3545 "listTemplate".to_string(),
3546 FragmentList {
3547 fragments: vec![
3548 WebUIFragment {
3549 fragment: Some(web_ui_fragment::Fragment::Attribute(
3550 WebUIFragmentAttribute {
3551 name: ":item".into(),
3552 value: "item".into(),
3553 attr_start: true,
3554 complex: true,
3555 ..Default::default()
3556 },
3557 )),
3558 },
3559 WebUIFragment::component("item_component"),
3560 ],
3561 },
3562 );
3563 fragments.insert(
3564 "item_component".to_string(),
3565 FragmentList {
3566 fragments: vec![
3567 WebUIFragment::raw("<span>"),
3568 WebUIFragment::signal("item.name", false),
3569 WebUIFragment::raw("</span>"),
3570 ],
3571 },
3572 );
3573 let protocol = WebUIProtocol::new(fragments);
3574 let state = test_json!({"list": {"items": [{"name": "Alice"}, {"name": "Bob"}]}});
3575 let mut writer = TestWriter::new();
3576 handle(
3577 &protocol,
3578 &state,
3579 &RenderOptions::new("index.html", "/"),
3580 &mut writer,
3581 )
3582 .unwrap();
3583 assert_eq!(writer.get_content(), "<span>Alice</span><span>Bob</span>");
3584 }
3585
3586 #[test]
3587 fn test_complex_attr_nested_for() {
3588 let mut fragments = HashMap::new();
3589 fragments.insert(
3590 "index.html".to_string(),
3591 FragmentList {
3592 fragments: vec![WebUIFragment::for_loop(
3593 "outer",
3594 "data.outer",
3595 "outerTemplate",
3596 )],
3597 },
3598 );
3599 fragments.insert(
3600 "outerTemplate".to_string(),
3601 FragmentList {
3602 fragments: vec![WebUIFragment::for_loop(
3603 "middle",
3604 "outer.middle",
3605 "middleTemplate",
3606 )],
3607 },
3608 );
3609 fragments.insert(
3610 "middleTemplate".to_string(),
3611 FragmentList {
3612 fragments: vec![WebUIFragment::for_loop(
3613 "inner",
3614 "middle.inner",
3615 "innerTemplate",
3616 )],
3617 },
3618 );
3619 fragments.insert(
3620 "innerTemplate".to_string(),
3621 FragmentList {
3622 fragments: vec![
3623 WebUIFragment::raw("<card"),
3624 WebUIFragment {
3625 fragment: Some(web_ui_fragment::Fragment::Attribute(
3626 WebUIFragmentAttribute {
3627 name: ":outer".into(),
3628 value: "outer".into(),
3629 attr_start: true,
3630 complex: true,
3631 ..Default::default()
3632 },
3633 )),
3634 },
3635 WebUIFragment {
3636 fragment: Some(web_ui_fragment::Fragment::Attribute(
3637 WebUIFragmentAttribute {
3638 name: ":middle".into(),
3639 value: "middle".into(),
3640 attr_start: false,
3641 complex: true,
3642 ..Default::default()
3643 },
3644 )),
3645 },
3646 WebUIFragment {
3647 fragment: Some(web_ui_fragment::Fragment::Attribute(
3648 WebUIFragmentAttribute {
3649 name: ":inner".into(),
3650 value: "inner".into(),
3651 attr_start: false,
3652 complex: true,
3653 ..Default::default()
3654 },
3655 )),
3656 },
3657 WebUIFragment::raw(">"),
3658 WebUIFragment::component("card_component"),
3659 WebUIFragment::raw("</card>"),
3660 ],
3661 },
3662 );
3663 fragments.insert(
3664 "card_component".to_string(),
3665 FragmentList {
3666 fragments: vec![
3667 WebUIFragment::raw("<p>"),
3668 WebUIFragment::signal("outer.label", false),
3669 WebUIFragment::raw(" / "),
3670 WebUIFragment::signal("middle.label", false),
3671 WebUIFragment::raw(" / "),
3672 WebUIFragment::signal("inner.label", false),
3673 WebUIFragment::raw("</p>"),
3674 ],
3675 },
3676 );
3677 let protocol = WebUIProtocol::new(fragments);
3678 let state = test_json!({"data": {"outer": [
3679 {"label": "Outer1", "middle": [{"label": "Middle1", "inner": [{"label": "Inner1A"}, {"label": "Inner1B"}]}]},
3680 {"label": "Outer2", "middle": [{"label": "Middle2", "inner": [{"label": "Inner2A"}]}]}
3681 ]}});
3682 let mut writer = TestWriter::new();
3683 handle(
3684 &protocol,
3685 &state,
3686 &RenderOptions::new("index.html", "/"),
3687 &mut writer,
3688 )
3689 .unwrap();
3690 assert_eq!(
3691 writer.get_content(),
3692 "<card><p>Outer1 / Middle1 / Inner1A</p></card><card><p>Outer1 / Middle1 / Inner1B</p></card><card><p>Outer2 / Middle2 / Inner2A</p></card>"
3693 );
3694 }
3695
3696 #[test]
3699 fn test_bool_component_state_true() {
3700 let mut fragments = HashMap::new();
3701 fragments.insert(
3702 "index.html".to_string(),
3703 FragmentList {
3704 fragments: vec![
3705 WebUIFragment::raw("<my-component"),
3706 WebUIFragment {
3707 fragment: Some(web_ui_fragment::Fragment::Attribute(
3708 WebUIFragmentAttribute {
3709 name: "disabled".into(),
3710 attr_start: true,
3711 condition_tree: Some(ConditionExpr::identifier("isDisabled")),
3712 ..Default::default()
3713 },
3714 )),
3715 },
3716 WebUIFragment::raw(">"),
3717 WebUIFragment::component("my-component"),
3718 WebUIFragment::raw("</my-component>"),
3719 ],
3720 },
3721 );
3722 fragments.insert(
3723 "my-component".to_string(),
3724 FragmentList {
3725 fragments: vec![
3726 WebUIFragment::if_cond(
3727 ConditionExpr::identifier("disabled"),
3728 "disabledTemplate",
3729 ),
3730 WebUIFragment::if_cond(
3731 ConditionExpr::negated(ConditionExpr::identifier("disabled")),
3732 "enabledTemplate",
3733 ),
3734 ],
3735 },
3736 );
3737 fragments.insert(
3738 "disabledTemplate".to_string(),
3739 FragmentList {
3740 fragments: vec![WebUIFragment::raw("<span>Disabled</span>")],
3741 },
3742 );
3743 fragments.insert(
3744 "enabledTemplate".to_string(),
3745 FragmentList {
3746 fragments: vec![WebUIFragment::raw("<span>Enabled</span>")],
3747 },
3748 );
3749 let protocol = WebUIProtocol::new(fragments);
3750 let state = test_json!({"isDisabled": true});
3751 let mut writer = TestWriter::new();
3752 handle(
3753 &protocol,
3754 &state,
3755 &RenderOptions::new("index.html", "/"),
3756 &mut writer,
3757 )
3758 .unwrap();
3759 assert_eq!(
3760 writer.get_content(),
3761 "<my-component disabled><span>Disabled</span></my-component>"
3762 );
3763 }
3764
3765 #[test]
3766 fn test_bool_component_state_false() {
3767 let mut fragments = HashMap::new();
3768 fragments.insert(
3769 "index.html".to_string(),
3770 FragmentList {
3771 fragments: vec![
3772 WebUIFragment::raw("<my-component"),
3773 WebUIFragment {
3774 fragment: Some(web_ui_fragment::Fragment::Attribute(
3775 WebUIFragmentAttribute {
3776 name: "disabled".into(),
3777 attr_start: true,
3778 condition_tree: Some(ConditionExpr::identifier("isDisabled")),
3779 ..Default::default()
3780 },
3781 )),
3782 },
3783 WebUIFragment::raw(">"),
3784 WebUIFragment::component("my-component"),
3785 WebUIFragment::raw("</my-component>"),
3786 ],
3787 },
3788 );
3789 fragments.insert(
3790 "my-component".to_string(),
3791 FragmentList {
3792 fragments: vec![
3793 WebUIFragment::if_cond(
3794 ConditionExpr::identifier("disabled"),
3795 "disabledTemplate",
3796 ),
3797 WebUIFragment::if_cond(
3798 ConditionExpr::negated(ConditionExpr::identifier("disabled")),
3799 "enabledTemplate",
3800 ),
3801 ],
3802 },
3803 );
3804 fragments.insert(
3805 "disabledTemplate".to_string(),
3806 FragmentList {
3807 fragments: vec![WebUIFragment::raw("<span>Disabled</span>")],
3808 },
3809 );
3810 fragments.insert(
3811 "enabledTemplate".to_string(),
3812 FragmentList {
3813 fragments: vec![WebUIFragment::raw("<span>Enabled</span>")],
3814 },
3815 );
3816 let protocol = WebUIProtocol::new(fragments);
3817 let state = test_json!({"isDisabled": false});
3818 let mut writer = TestWriter::new();
3819 handle(
3820 &protocol,
3821 &state,
3822 &RenderOptions::new("index.html", "/"),
3823 &mut writer,
3824 )
3825 .unwrap();
3826 assert_eq!(
3827 writer.get_content(),
3828 "<my-component><span>Enabled</span></my-component>"
3829 );
3830 }
3831
3832 #[test]
3833 fn test_bool_component_state_forward() {
3834 let mut fragments = HashMap::new();
3835 fragments.insert(
3836 "index.html".to_string(),
3837 FragmentList {
3838 fragments: vec![
3839 WebUIFragment::raw("<parent-component"),
3840 WebUIFragment {
3841 fragment: Some(web_ui_fragment::Fragment::Attribute(
3842 WebUIFragmentAttribute {
3843 name: "disabled".into(),
3844 attr_start: true,
3845 condition_tree: Some(ConditionExpr::identifier("isDisabled")),
3846 ..Default::default()
3847 },
3848 )),
3849 },
3850 WebUIFragment::raw(">"),
3851 WebUIFragment::component("parent-component"),
3852 WebUIFragment::raw("</parent-component>"),
3853 ],
3854 },
3855 );
3856 fragments.insert(
3857 "parent-component".to_string(),
3858 FragmentList {
3859 fragments: vec![
3860 WebUIFragment::if_cond(
3861 ConditionExpr::identifier("disabled"),
3862 "parentDisabledTemplate",
3863 ),
3864 WebUIFragment::raw("<child-component"),
3865 WebUIFragment {
3866 fragment: Some(web_ui_fragment::Fragment::Attribute(
3867 WebUIFragmentAttribute {
3868 name: "disabled".into(),
3869 attr_start: true,
3870 condition_tree: Some(ConditionExpr::identifier("disabled")),
3871 ..Default::default()
3872 },
3873 )),
3874 },
3875 WebUIFragment::raw(">"),
3876 WebUIFragment::component("child-component"),
3877 WebUIFragment::raw("</child-component>"),
3878 ],
3879 },
3880 );
3881 fragments.insert(
3882 "parentDisabledTemplate".to_string(),
3883 FragmentList {
3884 fragments: vec![WebUIFragment::raw("<div>Parent Disabled</div>")],
3885 },
3886 );
3887 fragments.insert(
3888 "child-component".to_string(),
3889 FragmentList {
3890 fragments: vec![
3891 WebUIFragment::if_cond(
3892 ConditionExpr::identifier("disabled"),
3893 "childDisabledTemplate",
3894 ),
3895 WebUIFragment::if_cond(
3896 ConditionExpr::negated(ConditionExpr::identifier("disabled")),
3897 "childEnabledTemplate",
3898 ),
3899 ],
3900 },
3901 );
3902 fragments.insert(
3903 "childDisabledTemplate".to_string(),
3904 FragmentList {
3905 fragments: vec![WebUIFragment::raw("<div>Child Disabled</div>")],
3906 },
3907 );
3908 fragments.insert(
3909 "childEnabledTemplate".to_string(),
3910 FragmentList {
3911 fragments: vec![WebUIFragment::raw("<div>Child Enabled</div>")],
3912 },
3913 );
3914
3915 {
3917 let protocol = WebUIProtocol::new(fragments.clone());
3918 let state = test_json!({"isDisabled": true});
3919 let mut writer = TestWriter::new();
3920 handle(
3921 &protocol,
3922 &state,
3923 &RenderOptions::new("index.html", "/"),
3924 &mut writer,
3925 )
3926 .unwrap();
3927 assert_eq!(
3928 writer.get_content(),
3929 "<parent-component disabled><div>Parent Disabled</div><child-component disabled><div>Child Disabled</div></child-component></parent-component>"
3930 );
3931 }
3932
3933 {
3935 let protocol = WebUIProtocol::new(fragments.clone());
3936 let state = test_json!({"isDisabled": false});
3937 let mut writer = TestWriter::new();
3938 handle(
3939 &protocol,
3940 &state,
3941 &RenderOptions::new("index.html", "/"),
3942 &mut writer,
3943 )
3944 .unwrap();
3945 assert_eq!(
3946 writer.get_content(),
3947 "<parent-component><child-component><div>Child Enabled</div></child-component></parent-component>"
3948 );
3949 }
3950 }
3951
3952 #[test]
3959 fn test_component_with_template() {
3960 let mut fragments = HashMap::new();
3961 fragments.insert(
3962 "index.html".to_string(),
3963 FragmentList {
3964 fragments: vec![
3965 WebUIFragment::raw("<custom-element>"),
3966 WebUIFragment::component("custom-element"),
3967 WebUIFragment::raw("</custom-element>"),
3968 ],
3969 },
3970 );
3971 fragments.insert(
3972 "custom-element".to_string(),
3973 FragmentList {
3974 fragments: vec![WebUIFragment::raw(
3975 "<template shadowrootmode=\"open\"><div>Custom Element</div></template>",
3976 )],
3977 },
3978 );
3979 let protocol = WebUIProtocol::new(fragments);
3980 let state = test_json!({});
3981 let mut writer = TestWriter::new();
3982 handle(
3983 &protocol,
3984 &state,
3985 &RenderOptions::new("index.html", "/"),
3986 &mut writer,
3987 )
3988 .unwrap();
3989 assert_eq!(
3990 writer.get_content(),
3991 "<custom-element><template shadowrootmode=\"open\"><div>Custom Element</div></template></custom-element>"
3992 );
3993 assert!(writer.is_ended());
3994 }
3995
3996 #[test]
3997 fn test_component_with_slots() {
3998 let mut fragments = HashMap::new();
3999 fragments.insert(
4000 "index.html".to_string(),
4001 FragmentList {
4002 fragments: vec![
4003 WebUIFragment::raw("<custom-element appearance=\"subtle\">"),
4004 WebUIFragment::component("custom-element"),
4005 WebUIFragment::raw("Hello World</custom-element>"),
4006 ],
4007 },
4008 );
4009 fragments.insert(
4010 "custom-element".to_string(),
4011 FragmentList {
4012 fragments: vec![WebUIFragment::raw(
4013 "<template shadowrootmode=\"open\"><slot></slot></template>",
4014 )],
4015 },
4016 );
4017 let protocol = WebUIProtocol::new(fragments);
4018 let state = test_json!({});
4019 let mut writer = TestWriter::new();
4020 handle(
4021 &protocol,
4022 &state,
4023 &RenderOptions::new("index.html", "/"),
4024 &mut writer,
4025 )
4026 .unwrap();
4027 assert_eq!(
4028 writer.get_content(),
4029 "<custom-element appearance=\"subtle\"><template shadowrootmode=\"open\"><slot></slot></template>Hello World</custom-element>"
4030 );
4031 assert!(writer.is_ended());
4032 }
4033
4034 #[test]
4035 fn test_multiple_nested_components() {
4036 let mut fragments = HashMap::new();
4037 fragments.insert(
4038 "index.html".to_string(),
4039 FragmentList {
4040 fragments: vec![
4041 WebUIFragment::raw("<div>"),
4042 WebUIFragment::for_loop("item", "items", "templateRepeat"),
4043 WebUIFragment::raw("</div>"),
4044 ],
4045 },
4046 );
4047 fragments.insert(
4048 "custom-button".to_string(),
4049 FragmentList {
4050 fragments: vec![WebUIFragment::raw(
4051 "<template shadowrootmode=\"open\"><slot></slot></template>",
4052 )],
4053 },
4054 );
4055 fragments.insert(
4056 "custom-element".to_string(),
4057 FragmentList {
4058 fragments: vec![
4059 WebUIFragment::raw("<template shadowrootmode=\"open\"><custom-child>"),
4060 WebUIFragment::component("custom-child"),
4061 WebUIFragment::raw("</custom-child><slot></slot></template>"),
4062 ],
4063 },
4064 );
4065 fragments.insert(
4066 "custom-child".to_string(),
4067 FragmentList {
4068 fragments: vec![WebUIFragment::raw(
4069 "<template shadowrootmode=\"open\"><h1>Hello World!</h1></template>",
4070 )],
4071 },
4072 );
4073 fragments.insert(
4074 "templateRepeat".to_string(),
4075 FragmentList {
4076 fragments: vec![
4077 WebUIFragment::raw("<custom-element>"),
4078 WebUIFragment::component("custom-element"),
4079 WebUIFragment::raw("<custom-button>"),
4080 WebUIFragment::component("custom-button"),
4081 WebUIFragment::raw("Ok</custom-button></custom-element>"),
4082 ],
4083 },
4084 );
4085 let protocol = WebUIProtocol::new(fragments);
4086 let state = test_json!({"items": [{"name": "Item1"}]});
4087 let mut writer = TestWriter::new();
4088 handle(
4089 &protocol,
4090 &state,
4091 &RenderOptions::new("index.html", "/"),
4092 &mut writer,
4093 )
4094 .unwrap();
4095 assert_eq!(
4096 writer.get_content(),
4097 "<div><custom-element><template shadowrootmode=\"open\"><custom-child><template shadowrootmode=\"open\"><h1>Hello World!</h1></template></custom-child><slot></slot></template><custom-button><template shadowrootmode=\"open\"><slot></slot></template>Ok</custom-button></custom-element></div>"
4098 );
4099 assert!(writer.is_ended());
4100 }
4101
4102 #[test]
4105 fn test_if_with_binary_expression() {
4106 let mut fragments = HashMap::new();
4107 fragments.insert(
4108 "index.html".to_string(),
4109 FragmentList {
4110 fragments: vec![
4111 WebUIFragment::raw("<div>"),
4112 WebUIFragment::if_cond(
4113 ConditionExpr::predicate("x", ComparisonOperator::GreaterThan, "5"),
4114 "if-1",
4115 ),
4116 WebUIFragment::raw("</div>"),
4117 ],
4118 },
4119 );
4120 fragments.insert(
4121 "if-1".to_string(),
4122 FragmentList {
4123 fragments: vec![WebUIFragment::raw("<span>If 1</span>")],
4124 },
4125 );
4126 let protocol = WebUIProtocol::new(fragments);
4127
4128 let state_true = test_json!({"x": 10});
4130 let mut writer_true = TestWriter::new();
4131 handle(
4132 &protocol,
4133 &state_true,
4134 &RenderOptions::new("index.html", "/"),
4135 &mut writer_true,
4136 )
4137 .unwrap();
4138 assert_eq!(writer_true.get_content(), "<div><span>If 1</span></div>");
4139
4140 let state_false = test_json!({"x": 1});
4142 let mut writer_false = TestWriter::new();
4143 handle(
4144 &protocol,
4145 &state_false,
4146 &RenderOptions::new("index.html", "/"),
4147 &mut writer_false,
4148 )
4149 .unwrap();
4150 assert_eq!(writer_false.get_content(), "<div></div>");
4151 }
4152
4153 #[test]
4154 fn test_for_if_overlapping_local_state() {
4155 let mut fragments = HashMap::new();
4156 fragments.insert(
4157 "index.html".to_string(),
4158 FragmentList {
4159 fragments: vec![
4160 WebUIFragment::raw("<div>"),
4161 WebUIFragment::for_loop("item", "items", "template1"),
4162 WebUIFragment::raw("</div>"),
4163 ],
4164 },
4165 );
4166 fragments.insert(
4167 "template1".to_string(),
4168 FragmentList {
4169 fragments: vec![
4170 WebUIFragment::raw("<div>"),
4171 WebUIFragment::if_cond(ConditionExpr::identifier("item.flag"), "ifBlock"),
4172 WebUIFragment::raw("</div>"),
4173 ],
4174 },
4175 );
4176 fragments.insert(
4177 "ifBlock".to_string(),
4178 FragmentList {
4179 fragments: vec![
4180 WebUIFragment::raw("<span>"),
4181 WebUIFragment::signal("item.label", false),
4182 WebUIFragment::raw("</span>"),
4183 ],
4184 },
4185 );
4186 let protocol = WebUIProtocol::new(fragments);
4187 let state = test_json!({
4188 "flag": false,
4189 "items": [
4190 {"label": "A", "flag": true},
4191 {"label": "B", "flag": false},
4192 {"label": "C", "flag": true}
4193 ]
4194 });
4195 let mut writer = TestWriter::new();
4196 handle(
4197 &protocol,
4198 &state,
4199 &RenderOptions::new("index.html", "/"),
4200 &mut writer,
4201 )
4202 .unwrap();
4203 assert_eq!(
4204 writer.get_content(),
4205 "<div><div><span>A</span></div><div></div><div><span>C</span></div></div>"
4206 );
4207 }
4208
4209 #[test]
4210 fn test_for_if_global_flag_no_effect() {
4211 let mut fragments = HashMap::new();
4212 fragments.insert(
4213 "index.html".to_string(),
4214 FragmentList {
4215 fragments: vec![
4216 WebUIFragment::raw("<div>"),
4217 WebUIFragment::for_loop("item", "items", "template1"),
4218 WebUIFragment::raw("</div>"),
4219 ],
4220 },
4221 );
4222 fragments.insert(
4223 "template1".to_string(),
4224 FragmentList {
4225 fragments: vec![
4226 WebUIFragment::raw("<div>"),
4227 WebUIFragment::if_cond(ConditionExpr::identifier("item.flag"), "ifBlock"),
4228 WebUIFragment::raw("</div>"),
4229 ],
4230 },
4231 );
4232 fragments.insert(
4233 "ifBlock".to_string(),
4234 FragmentList {
4235 fragments: vec![
4236 WebUIFragment::raw("<span>"),
4237 WebUIFragment::signal("item.label", false),
4238 WebUIFragment::raw("</span>"),
4239 ],
4240 },
4241 );
4242 let protocol = WebUIProtocol::new(fragments);
4243 let state = test_json!({
4244 "item": {"flag": true},
4245 "items": [
4246 {"label": "A", "flag": false},
4247 {"label": "B", "flag": true},
4248 {"label": "C", "flag": false}
4249 ]
4250 });
4251 let mut writer = TestWriter::new();
4252 handle(
4253 &protocol,
4254 &state,
4255 &RenderOptions::new("index.html", "/"),
4256 &mut writer,
4257 )
4258 .unwrap();
4259 assert_eq!(
4260 writer.get_content(),
4261 "<div><div></div><div><span>B</span></div><div></div></div>"
4262 );
4263 }
4264
4265 #[test]
4268 fn test_recursive_template_refs() {
4269 let mut fragments = HashMap::new();
4270 fragments.insert(
4271 "index.html".to_string(),
4272 FragmentList {
4273 fragments: vec![WebUIFragment::for_loop("item", "items", "static")],
4274 },
4275 );
4276 fragments.insert(
4277 "static".to_string(),
4278 FragmentList {
4279 fragments: vec![
4280 WebUIFragment::raw("<div expanded=\""),
4281 WebUIFragment::signal("item.expanded", false),
4282 WebUIFragment::raw("\" class=\""),
4283 WebUIFragment::signal("testScenario", false),
4284 WebUIFragment::raw("\"><span>"),
4285 WebUIFragment::signal("item.name", false),
4286 WebUIFragment::raw("</span>"),
4287 WebUIFragment::for_loop("item", "item.children", "static"),
4288 WebUIFragment::raw("</div>"),
4289 ],
4290 },
4291 );
4292 let protocol = WebUIProtocol::new(fragments);
4293 let state = test_json!({
4294 "testScenario": "RecursiveTemplatesWithGlobalState",
4295 "items": [
4296 {"name": "A", "expanded": "false", "children": []},
4297 {"name": "B", "expanded": "true", "children": [
4298 {"name": "C", "expanded": "false"},
4299 {"name": "D", "expanded": "false"}
4300 ]},
4301 {"name": "E", "expanded": "false"}
4302 ]
4303 });
4304 let mut writer = TestWriter::new();
4305 handle(
4306 &protocol,
4307 &state,
4308 &RenderOptions::new("index.html", "/"),
4309 &mut writer,
4310 )
4311 .unwrap();
4312 assert_eq!(
4313 writer.get_content(),
4314 "<div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>A</span></div><div expanded=\"true\" class=\"RecursiveTemplatesWithGlobalState\"><span>B</span><div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>C</span></div><div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>D</span></div></div><div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>E</span></div>"
4315 );
4316 }
4317
4318 #[test]
4321 fn test_component_in_for_no_local_access() {
4322 let mut fragments = HashMap::new();
4323 fragments.insert(
4324 "index.html".to_string(),
4325 FragmentList {
4326 fragments: vec![
4327 WebUIFragment::raw("<div>"),
4328 WebUIFragment::for_loop("item", "items", "templateComponent"),
4329 WebUIFragment::raw("</div>"),
4330 ],
4331 },
4332 );
4333 fragments.insert(
4334 "templateComponent".to_string(),
4335 FragmentList {
4336 fragments: vec![
4337 WebUIFragment::raw("<component-tag>"),
4338 WebUIFragment::component("my-component"),
4339 WebUIFragment::raw("</component-tag>"),
4340 ],
4341 },
4342 );
4343 fragments.insert(
4344 "my-component".to_string(),
4345 FragmentList {
4346 fragments: vec![
4347 WebUIFragment::raw("<span>"),
4348 WebUIFragment::signal("name", false),
4349 WebUIFragment::raw("</span>"),
4350 ],
4351 },
4352 );
4353 let protocol = WebUIProtocol::new(fragments);
4354 let state = test_json!({"items": [{"name": "Item1"}, {"name": "Item2"}]});
4355 let mut writer = TestWriter::new();
4356 handle(
4357 &protocol,
4358 &state,
4359 &RenderOptions::new("index.html", "/"),
4360 &mut writer,
4361 )
4362 .unwrap();
4363 assert_eq!(
4364 writer.get_content(),
4365 "<div><component-tag><span></span></component-tag><component-tag><span></span></component-tag></div>"
4366 );
4367 }
4368
4369 #[test]
4370 fn test_nested_for_hierarchical_state() {
4371 let mut fragments = HashMap::new();
4372 fragments.insert(
4373 "index.html".to_string(),
4374 FragmentList {
4375 fragments: vec![
4376 WebUIFragment::raw("<div>"),
4377 WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
4378 WebUIFragment::raw("</div>"),
4379 ],
4380 },
4381 );
4382 fragments.insert(
4383 "outerTemplate".to_string(),
4384 FragmentList {
4385 fragments: vec![
4386 WebUIFragment::raw("<section>"),
4387 WebUIFragment::signal("globalPrefix", false),
4388 WebUIFragment::signal("outerItem.outerLabel", false),
4389 WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
4390 WebUIFragment::raw("</section>"),
4391 ],
4392 },
4393 );
4394 fragments.insert(
4395 "innerTemplate".to_string(),
4396 FragmentList {
4397 fragments: vec![
4398 WebUIFragment::raw("<p>"),
4399 WebUIFragment::signal("globalPrefix", false),
4400 WebUIFragment::signal("outerItem.outerLabel", false),
4401 WebUIFragment::raw(": "),
4402 WebUIFragment::signal("innerItem.innerLabel", false),
4403 WebUIFragment::raw("</p>"),
4404 ],
4405 },
4406 );
4407 let protocol = WebUIProtocol::new(fragments);
4408 let state = test_json!({
4409 "globalPrefix": "Prefix: ",
4410 "outerItems": [
4411 {"outerLabel": "O1", "innerItems": [{"innerLabel": "I1"}, {"innerLabel": "I2"}]},
4412 {"outerLabel": "O2", "innerItems": [{"innerLabel": "I3"}]}
4413 ]
4414 });
4415 let mut writer = TestWriter::new();
4416 handle(
4417 &protocol,
4418 &state,
4419 &RenderOptions::new("index.html", "/"),
4420 &mut writer,
4421 )
4422 .unwrap();
4423 assert_eq!(
4424 writer.get_content(),
4425 "<div><section>Prefix: O1<p>Prefix: O1: I1</p><p>Prefix: O1: I2</p></section><section>Prefix: O2<p>Prefix: O2: I3</p></section></div>"
4426 );
4427 }
4428
4429 #[test]
4430 fn test_component_in_for_global_only() {
4431 let mut fragments = HashMap::new();
4432 fragments.insert(
4433 "index.html".to_string(),
4434 FragmentList {
4435 fragments: vec![
4436 WebUIFragment::raw("<div>"),
4437 WebUIFragment::for_loop("item", "items", "templateComponent"),
4438 WebUIFragment::raw("</div>"),
4439 ],
4440 },
4441 );
4442 fragments.insert(
4443 "templateComponent".to_string(),
4444 FragmentList {
4445 fragments: vec![
4446 WebUIFragment::raw("<component-tag>"),
4447 WebUIFragment::component("my-component"),
4448 WebUIFragment::raw("</component-tag>"),
4449 ],
4450 },
4451 );
4452 fragments.insert(
4453 "my-component".to_string(),
4454 FragmentList {
4455 fragments: vec![
4456 WebUIFragment::raw("<span>"),
4457 WebUIFragment::signal("name", false),
4458 WebUIFragment::raw("-"),
4459 WebUIFragment::signal("globalSuffix", false),
4460 WebUIFragment::raw("</span>"),
4461 ],
4462 },
4463 );
4464 let protocol = WebUIProtocol::new(fragments);
4465 let state =
4466 test_json!({"globalSuffix": "Global", "items": [{"name": "Item1"}, {"name": "Item2"}]});
4467 let mut writer = TestWriter::new();
4468 handle(
4469 &protocol,
4470 &state,
4471 &RenderOptions::new("index.html", "/"),
4472 &mut writer,
4473 )
4474 .unwrap();
4475 assert_eq!(
4476 writer.get_content(),
4477 "<div><component-tag><span>-Global</span></component-tag><component-tag><span>-Global</span></component-tag></div>"
4478 );
4479 }
4480
4481 #[test]
4482 fn test_component_no_item_moniker() {
4483 let mut fragments = HashMap::new();
4484 fragments.insert(
4485 "index.html".to_string(),
4486 FragmentList {
4487 fragments: vec![
4488 WebUIFragment::raw("<div>"),
4489 WebUIFragment::for_loop("item", "items", "templateComponent"),
4490 WebUIFragment::raw("</div>"),
4491 ],
4492 },
4493 );
4494 fragments.insert(
4495 "templateComponent".to_string(),
4496 FragmentList {
4497 fragments: vec![
4498 WebUIFragment::raw("<component-tag>"),
4499 WebUIFragment::component("my-component"),
4500 WebUIFragment::raw("</component-tag>"),
4501 ],
4502 },
4503 );
4504 fragments.insert(
4505 "my-component".to_string(),
4506 FragmentList {
4507 fragments: vec![
4508 WebUIFragment::raw("<span>"),
4509 WebUIFragment::signal("item.name", false),
4510 WebUIFragment::raw("-"),
4511 WebUIFragment::signal("globalSuffix", false),
4512 WebUIFragment::raw("</span>"),
4513 ],
4514 },
4515 );
4516 let protocol = WebUIProtocol::new(fragments);
4517 let state =
4518 test_json!({"globalSuffix": "Global", "items": [{"name": "Item1"}, {"name": "Item2"}]});
4519 let mut writer = TestWriter::new();
4520 handle(
4521 &protocol,
4522 &state,
4523 &RenderOptions::new("index.html", "/"),
4524 &mut writer,
4525 )
4526 .unwrap();
4527 assert_eq!(
4528 writer.get_content(),
4529 "<div><component-tag><span>-Global</span></component-tag><component-tag><span>-Global</span></component-tag></div>"
4530 );
4531 }
4532
4533 #[test]
4534 fn test_for_nonqualified_uses_global() {
4535 let mut fragments = HashMap::new();
4536 fragments.insert(
4537 "index.html".to_string(),
4538 FragmentList {
4539 fragments: vec![
4540 WebUIFragment::raw("<div>"),
4541 WebUIFragment::for_loop("item", "items", "template1"),
4542 WebUIFragment::raw("</div>"),
4543 ],
4544 },
4545 );
4546 fragments.insert(
4547 "template1".to_string(),
4548 FragmentList {
4549 fragments: vec![
4550 WebUIFragment::raw("<span>"),
4551 WebUIFragment::signal("name", false),
4552 WebUIFragment::raw("</span>"),
4553 ],
4554 },
4555 );
4556 let protocol = WebUIProtocol::new(fragments);
4557 let state = test_json!({"name": "GlobalName", "items": [{"name": "LocalName1"}, {"name": "LocalName2"}]});
4558 let mut writer = TestWriter::new();
4559 handle(
4560 &protocol,
4561 &state,
4562 &RenderOptions::new("index.html", "/"),
4563 &mut writer,
4564 )
4565 .unwrap();
4566 assert_eq!(
4567 writer.get_content(),
4568 "<div><span>GlobalName</span><span>GlobalName</span></div>"
4569 );
4570 }
4571
4572 #[test]
4573 fn test_nested_for_if_interleaved() {
4574 let mut fragments = HashMap::new();
4575 fragments.insert(
4576 "index.html".to_string(),
4577 FragmentList {
4578 fragments: vec![
4579 WebUIFragment::raw("<div>"),
4580 WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
4581 WebUIFragment::raw("</div>"),
4582 ],
4583 },
4584 );
4585 fragments.insert(
4586 "outerTemplate".to_string(),
4587 FragmentList {
4588 fragments: vec![
4589 WebUIFragment::raw("<section>"),
4590 WebUIFragment::signal("globalPrefix", false),
4591 WebUIFragment::signal("outerItem.outerLabel", false),
4592 WebUIFragment::if_cond(
4593 ConditionExpr::identifier("outerItem.include"),
4594 "ifTemplate",
4595 ),
4596 WebUIFragment::raw("</section>"),
4597 ],
4598 },
4599 );
4600 fragments.insert(
4601 "ifTemplate".to_string(),
4602 FragmentList {
4603 fragments: vec![
4604 WebUIFragment::raw("<div>"),
4605 WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
4606 WebUIFragment::raw("</div>"),
4607 ],
4608 },
4609 );
4610 fragments.insert(
4611 "innerTemplate".to_string(),
4612 FragmentList {
4613 fragments: vec![
4614 WebUIFragment::raw("<p>"),
4615 WebUIFragment::signal("globalSuffix", false),
4616 WebUIFragment::raw(": "),
4617 WebUIFragment::signal("innerItem.innerLabel", false),
4618 WebUIFragment::raw("</p>"),
4619 ],
4620 },
4621 );
4622 let protocol = WebUIProtocol::new(fragments);
4623 let state = test_json!({
4624 "globalPrefix": "Prefix: ",
4625 "globalSuffix": "Suffix",
4626 "outerItems": [
4627 {"outerLabel": "O1", "include": true, "innerItems": [{"innerLabel": "I1"}, {"innerLabel": "I2"}]},
4628 {"outerLabel": "O2", "include": false, "innerItems": [{"innerLabel": "Iignored"}]},
4629 {"outerLabel": "O3", "include": true, "innerItems": [{"innerLabel": "I3"}]}
4630 ]
4631 });
4632 let mut writer = TestWriter::new();
4633 handle(
4634 &protocol,
4635 &state,
4636 &RenderOptions::new("index.html", "/"),
4637 &mut writer,
4638 )
4639 .unwrap();
4640 assert_eq!(
4641 writer.get_content(),
4642 "<div><section>Prefix: O1<div><p>Suffix: I1</p><p>Suffix: I2</p></div></section><section>Prefix: O2</section><section>Prefix: O3<div><p>Suffix: I3</p></div></section></div>"
4643 );
4644 }
4645
4646 #[test]
4647 fn test_nested_for_if_outer_state() {
4648 let mut fragments = HashMap::new();
4649 fragments.insert(
4650 "index.html".to_string(),
4651 FragmentList {
4652 fragments: vec![
4653 WebUIFragment::raw("<div>"),
4654 WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
4655 WebUIFragment::raw("</div>"),
4656 ],
4657 },
4658 );
4659 fragments.insert(
4660 "outerTemplate".to_string(),
4661 FragmentList {
4662 fragments: vec![
4663 WebUIFragment::raw("<section>"),
4664 WebUIFragment::signal("globalPrefix", false),
4665 WebUIFragment::signal("outerItem.label", false),
4666 WebUIFragment::for_loop(
4667 "middleItem",
4668 "outerItem.middleItems",
4669 "middleTemplate",
4670 ),
4671 WebUIFragment::raw("</section>"),
4672 ],
4673 },
4674 );
4675 fragments.insert(
4676 "middleTemplate".to_string(),
4677 FragmentList {
4678 fragments: vec![
4679 WebUIFragment::raw("<div>"),
4680 WebUIFragment::if_cond(
4681 ConditionExpr::identifier("outerItem.active"),
4682 "ifTemplate",
4683 ),
4684 WebUIFragment::raw("</div>"),
4685 ],
4686 },
4687 );
4688 fragments.insert(
4689 "ifTemplate".to_string(),
4690 FragmentList {
4691 fragments: vec![
4692 WebUIFragment::raw("<p>"),
4693 WebUIFragment::signal("middleItem.value", false),
4694 WebUIFragment::raw("</p>"),
4695 ],
4696 },
4697 );
4698 let protocol = WebUIProtocol::new(fragments);
4699 let state = test_json!({
4700 "globalPrefix": "GP-",
4701 "outerItems": [
4702 {"label": "O1", "active": true, "middleItems": [{"value": "M1"}, {"value": "M2"}]},
4703 {"label": "O2", "active": false, "middleItems": [{"value": "M3"}]},
4704 {"label": "O3", "active": true, "middleItems": [{"value": "M4"}]}
4705 ]
4706 });
4707 let mut writer = TestWriter::new();
4708 handle(
4709 &protocol,
4710 &state,
4711 &RenderOptions::new("index.html", "/"),
4712 &mut writer,
4713 )
4714 .unwrap();
4715 assert_eq!(
4716 writer.get_content(),
4717 "<div><section>GP-O1<div><p>M1</p></div><div><p>M2</p></div></section><section>GP-O2<div></div></section><section>GP-O3<div><p>M4</p></div></section></div>"
4718 );
4719 }
4720
4721 #[test]
4722 fn test_nested_for_if_inner_state() {
4723 let mut fragments = HashMap::new();
4724 fragments.insert(
4725 "index.html".to_string(),
4726 FragmentList {
4727 fragments: vec![
4728 WebUIFragment::raw("<div>"),
4729 WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
4730 WebUIFragment::raw("</div>"),
4731 ],
4732 },
4733 );
4734 fragments.insert(
4735 "outerTemplate".to_string(),
4736 FragmentList {
4737 fragments: vec![
4738 WebUIFragment::raw("<section>"),
4739 WebUIFragment::signal("outerItem.label", false),
4740 WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
4741 WebUIFragment::raw("</section>"),
4742 ],
4743 },
4744 );
4745 fragments.insert(
4746 "innerTemplate".to_string(),
4747 FragmentList {
4748 fragments: vec![
4749 WebUIFragment::raw("<article>"),
4750 WebUIFragment::if_cond(
4751 ConditionExpr::identifier("innerItem.show"),
4752 "ifTemplate",
4753 ),
4754 WebUIFragment::raw("</article>"),
4755 ],
4756 },
4757 );
4758 fragments.insert(
4759 "ifTemplate".to_string(),
4760 FragmentList {
4761 fragments: vec![
4762 WebUIFragment::raw("<p>"),
4763 WebUIFragment::signal("innerItem.detail", false),
4764 WebUIFragment::raw("</p>"),
4765 ],
4766 },
4767 );
4768 let protocol = WebUIProtocol::new(fragments);
4769 let state = test_json!({
4770 "outerItems": [
4771 {"label": "Outer1", "innerItems": [{"detail": "Detail1", "show": true}, {"detail": "Detail2", "show": false}]},
4772 {"label": "Outer2", "innerItems": [{"detail": "Detail3", "show": true}]}
4773 ]
4774 });
4775 let mut writer = TestWriter::new();
4776 handle(
4777 &protocol,
4778 &state,
4779 &RenderOptions::new("index.html", "/"),
4780 &mut writer,
4781 )
4782 .unwrap();
4783 assert_eq!(
4784 writer.get_content(),
4785 "<div><section>Outer1<article><p>Detail1</p></article><article></article></section><section>Outer2<article><p>Detail3</p></article></section></div>"
4786 );
4787 }
4788
4789 #[test]
4790 fn test_for_merge_local_global_monikers() {
4791 let mut fragments = HashMap::new();
4792 fragments.insert(
4793 "index.html".to_string(),
4794 FragmentList {
4795 fragments: vec![
4796 WebUIFragment::raw("<div>"),
4797 WebUIFragment::for_loop("item", "items", "template1"),
4798 WebUIFragment::raw("</div>"),
4799 ],
4800 },
4801 );
4802 fragments.insert(
4803 "template1".to_string(),
4804 FragmentList {
4805 fragments: vec![
4806 WebUIFragment::raw("<span>"),
4807 WebUIFragment::signal("item.name", false),
4808 WebUIFragment::raw("-"),
4809 WebUIFragment::signal("item.globalValue", false),
4810 WebUIFragment::raw("-"),
4811 WebUIFragment::signal("item.localOnly", false),
4812 WebUIFragment::raw("-"),
4813 WebUIFragment::signal("item.otherVal", false),
4814 WebUIFragment::raw("</span>"),
4815 ],
4816 },
4817 );
4818 let protocol = WebUIProtocol::new(fragments);
4819 let state = test_json!({
4820 "item": {"globalValue": "GLOBAL", "otherVal": "other"},
4821 "items": [
4822 {"name": "Local1", "globalValue": "LOCAL", "localOnly": "Only1"},
4823 {"name": "Local2", "localOnly": "Only2"}
4824 ]
4825 });
4826 let mut writer = TestWriter::new();
4827 handle(
4828 &protocol,
4829 &state,
4830 &RenderOptions::new("index.html", "/"),
4831 &mut writer,
4832 )
4833 .unwrap();
4834 assert_eq!(
4835 writer.get_content(),
4836 "<div><span>Local1-LOCAL-Only1-other</span><span>Local2-GLOBAL-Only2-other</span></div>"
4837 );
4838 }
4839
4840 #[test]
4841 fn test_component_in_for_global_moniker_shadow() {
4842 let mut fragments = HashMap::new();
4843 fragments.insert(
4844 "index.html".to_string(),
4845 FragmentList {
4846 fragments: vec![
4847 WebUIFragment::raw("<div>"),
4848 WebUIFragment::for_loop("item", "items", "templateComponent"),
4849 WebUIFragment::raw("</div>"),
4850 ],
4851 },
4852 );
4853 fragments.insert(
4854 "templateComponent".to_string(),
4855 FragmentList {
4856 fragments: vec![
4857 WebUIFragment::raw("<component-tag>"),
4858 WebUIFragment::component("my-component"),
4859 WebUIFragment::raw("</component-tag>"),
4860 ],
4861 },
4862 );
4863 fragments.insert(
4864 "my-component".to_string(),
4865 FragmentList {
4866 fragments: vec![
4867 WebUIFragment::raw("<span>"),
4868 WebUIFragment::signal("name", false),
4869 WebUIFragment::raw("-"),
4870 WebUIFragment::signal("item.globalValue", false),
4871 WebUIFragment::raw("-"),
4872 WebUIFragment::signal("localOnly", false),
4873 WebUIFragment::raw("-"),
4874 WebUIFragment::signal("item.otherVal", false),
4875 WebUIFragment::raw("</span>"),
4876 ],
4877 },
4878 );
4879 let protocol = WebUIProtocol::new(fragments);
4880 let state = test_json!({
4881 "item": {"globalValue": "GLOBAL", "otherVal": "other"},
4882 "items": [
4883 {"name": "Local1", "globalValue": "LOCAL", "localOnly": "Only1"},
4884 {"name": "Local2", "localOnly": "Only2"}
4885 ]
4886 });
4887 let mut writer = TestWriter::new();
4888 handle(
4889 &protocol,
4890 &state,
4891 &RenderOptions::new("index.html", "/"),
4892 &mut writer,
4893 )
4894 .unwrap();
4895 assert_eq!(
4896 writer.get_content(),
4897 "<div><component-tag><span>-GLOBAL--other</span></component-tag><component-tag><span>-GLOBAL--other</span></component-tag></div>"
4898 );
4899 }
4900
4901 #[test]
4902 fn test_if_in_nested_for_local_flag() {
4903 let mut fragments = HashMap::new();
4904 fragments.insert(
4905 "index.html".to_string(),
4906 FragmentList {
4907 fragments: vec![
4908 WebUIFragment::raw("<div>"),
4909 WebUIFragment::for_loop("outer", "list.outer_items", "outerTemplate"),
4910 WebUIFragment::raw("</div>"),
4911 ],
4912 },
4913 );
4914 fragments.insert(
4915 "outerTemplate".to_string(),
4916 FragmentList {
4917 fragments: vec![
4918 WebUIFragment::raw("<section>"),
4919 WebUIFragment::for_loop("inner_item", "outer.inner_items", "innerTemplate"),
4920 WebUIFragment::raw("</section>"),
4921 ],
4922 },
4923 );
4924 fragments.insert(
4925 "innerTemplate".to_string(),
4926 FragmentList {
4927 fragments: vec![WebUIFragment::if_cond(
4928 ConditionExpr::identifier("inner_item.flag"),
4929 "ifInner",
4930 )],
4931 },
4932 );
4933 fragments.insert(
4934 "ifInner".to_string(),
4935 FragmentList {
4936 fragments: vec![
4937 WebUIFragment::raw("<p>"),
4938 WebUIFragment::signal("inner_item.value", false),
4939 WebUIFragment::raw("</p>"),
4940 ],
4941 },
4942 );
4943 let protocol = WebUIProtocol::new(fragments);
4944 let state = test_json!({
4945 "list": {"outer_items": [{"inner_items": [{"flag": true, "value": "X"}, {"flag": false, "value": "Y"}]}]},
4946 "inner_item": {"flag": false}
4947 });
4948 let mut writer = TestWriter::new();
4949 handle(
4950 &protocol,
4951 &state,
4952 &RenderOptions::new("index.html", "/"),
4953 &mut writer,
4954 )
4955 .unwrap();
4956 assert_eq!(
4957 writer.get_content(),
4958 "<div><section><p>X</p></section></div>"
4959 );
4960 }
4961
4962 #[test]
4963 fn test_if_in_nested_for_global_fallback() {
4964 let mut fragments = HashMap::new();
4965 fragments.insert(
4966 "index.html".to_string(),
4967 FragmentList {
4968 fragments: vec![
4969 WebUIFragment::raw("<div>"),
4970 WebUIFragment::for_loop("outer", "list.outer_items", "outerTemplate"),
4971 WebUIFragment::raw("</div>"),
4972 ],
4973 },
4974 );
4975 fragments.insert(
4976 "outerTemplate".to_string(),
4977 FragmentList {
4978 fragments: vec![
4979 WebUIFragment::raw("<section>"),
4980 WebUIFragment::for_loop("inner_item", "outer.inner_items", "innerTemplate"),
4981 WebUIFragment::raw("</section>"),
4982 ],
4983 },
4984 );
4985 fragments.insert(
4986 "innerTemplate".to_string(),
4987 FragmentList {
4988 fragments: vec![WebUIFragment::if_cond(
4989 ConditionExpr::identifier("inner_item.flag"),
4990 "ifInner",
4991 )],
4992 },
4993 );
4994 fragments.insert(
4995 "ifInner".to_string(),
4996 FragmentList {
4997 fragments: vec![
4998 WebUIFragment::raw("<p>"),
4999 WebUIFragment::signal("inner_item.value", false),
5000 WebUIFragment::raw("</p>"),
5001 ],
5002 },
5003 );
5004 let protocol = WebUIProtocol::new(fragments);
5005 let state = test_json!({
5006 "list": {"outer_items": [{"inner_items": [{"value": "X"}, {"value": "Y"}]}]},
5007 "inner_item": {"flag": true}
5008 });
5009 let mut writer = TestWriter::new();
5010 handle(
5011 &protocol,
5012 &state,
5013 &RenderOptions::new("index.html", "/"),
5014 &mut writer,
5015 )
5016 .unwrap();
5017 assert_eq!(
5018 writer.get_content(),
5019 "<div><section><p>X</p><p>Y</p></section></div>"
5020 );
5021 }
5022
5023 #[test]
5024 fn test_if_mixed_for_monikers() {
5025 let mut fragments = HashMap::new();
5026 fragments.insert(
5027 "index.html".to_string(),
5028 FragmentList {
5029 fragments: vec![
5030 WebUIFragment::raw("<div>"),
5031 WebUIFragment::for_loop("outer", "list.outerItems", "outerTemplate"),
5032 WebUIFragment::raw("</div>"),
5033 ],
5034 },
5035 );
5036 fragments.insert(
5037 "outerTemplate".to_string(),
5038 FragmentList {
5039 fragments: vec![
5040 WebUIFragment::raw("<section>"),
5041 WebUIFragment::signal("outer.outerLabel", false),
5042 WebUIFragment::for_loop("inner", "outer.innerItems", "innerTemplate"),
5043 WebUIFragment::raw("</section>"),
5044 ],
5045 },
5046 );
5047 fragments.insert(
5048 "innerTemplate".to_string(),
5049 FragmentList {
5050 fragments: vec![WebUIFragment::if_cond(
5051 ConditionExpr::compound(
5052 ConditionExpr::identifier("outer.active"),
5053 LogicalOperator::And,
5054 ConditionExpr::predicate(
5055 "inner.value",
5056 ComparisonOperator::GreaterThan,
5057 "globalLimit",
5058 ),
5059 ),
5060 "ifInner",
5061 )],
5062 },
5063 );
5064 fragments.insert(
5065 "ifInner".to_string(),
5066 FragmentList {
5067 fragments: vec![
5068 WebUIFragment::raw("<p>"),
5069 WebUIFragment::signal("inner.value", false),
5070 WebUIFragment::raw("</p>"),
5071 ],
5072 },
5073 );
5074 let protocol = WebUIProtocol::new(fragments);
5075 let state = test_json!({
5076 "globalLimit": 10,
5077 "list": {"outerItems": [
5078 {"outerLabel": "O1", "active": true, "innerItems": [{"value": 15}, {"value": 8}]},
5079 {"outerLabel": "O2", "active": false, "innerItems": [{"value": 20}]},
5080 {"outerLabel": "O3", "active": true, "innerItems": [{"value": 5}]}
5081 ]}
5082 });
5083 let mut writer = TestWriter::new();
5084 handle(
5085 &protocol,
5086 &state,
5087 &RenderOptions::new("index.html", "/"),
5088 &mut writer,
5089 )
5090 .unwrap();
5091 assert_eq!(
5092 writer.get_content(),
5093 "<div><section>O1<p>15</p></section><section>O2</section><section>O3</section></div>"
5094 );
5095 }
5096
5097 fn make_route_protocol() -> WebUIProtocol {
5100 use webui_protocol::WebUiFragmentRoute;
5101
5102 let mut fragments = HashMap::new();
5103
5104 fragments.insert(
5106 "index.html".to_string(),
5107 FragmentList {
5108 fragments: vec![
5109 WebUIFragment::raw("<h1>Shell</h1>"),
5110 WebUIFragment::route_from(WebUiFragmentRoute {
5111 path: "/".into(),
5112 fragment_id: "dash-page".into(),
5113 exact: true,
5114 ..Default::default()
5115 }),
5116 WebUIFragment::route_from(WebUiFragmentRoute {
5117 path: "/contacts/:id".into(),
5118 fragment_id: "detail-page".into(),
5119 exact: true,
5120 ..Default::default()
5121 }),
5122 ],
5123 },
5124 );
5125
5126 fragments.insert(
5128 "dash-page".to_string(),
5129 FragmentList {
5130 fragments: vec![WebUIFragment::raw("<p>Dashboard</p>")],
5131 },
5132 );
5133
5134 fragments.insert(
5136 "detail-page".to_string(),
5137 FragmentList {
5138 fragments: vec![WebUIFragment::raw("<p>Detail</p>")],
5139 },
5140 );
5141
5142 WebUIProtocol::new(fragments)
5143 }
5144
5145 #[test]
5146 fn test_route_renders_shell_always() {
5147 let protocol = make_route_protocol();
5148 let state = test_json!({});
5149 let mut writer = TestWriter::new();
5150 handle(
5151 &protocol,
5152 &state,
5153 &RenderOptions::new("index.html", "/"),
5154 &mut writer,
5155 )
5156 .unwrap();
5157 let html = writer.get_content();
5158
5159 assert!(html.contains("<h1>Shell</h1>"), "shell should render");
5161 assert!(html.contains(" active>"), "matched route should be active");
5163 assert!(
5165 html.contains("style=\"display:none\""),
5166 "non-matched routes should be hidden"
5167 );
5168 }
5169
5170 #[test]
5171 fn test_route_matched_renders_visible() {
5172 let protocol = make_route_protocol();
5173 let state = test_json!({});
5174 let mut writer = TestWriter::new();
5175 handle(
5176 &protocol,
5177 &state,
5178 &RenderOptions::new("index.html", "/"),
5179 &mut writer,
5180 )
5181 .unwrap();
5182 let html = writer.get_content();
5183
5184 assert!(
5186 html.contains("<webui-route path=\"/\""),
5187 "dashboard route should exist"
5188 );
5189 assert!(
5190 html.contains("active>") && html.contains("<dash-page>"),
5191 "matched route should be active with component tag: {html}"
5192 );
5193 assert!(
5194 html.contains("<p>Dashboard</p>"),
5195 "matched route should have content"
5196 );
5197 }
5198
5199 #[test]
5200 fn test_route_non_matched_renders_hidden_empty() {
5201 let protocol = make_route_protocol();
5202 let state = test_json!({});
5203 let mut writer = TestWriter::new();
5204 handle(
5205 &protocol,
5206 &state,
5207 &RenderOptions::new("index.html", "/"),
5208 &mut writer,
5209 )
5210 .unwrap();
5211 let html = writer.get_content();
5212
5213 assert!(
5215 html.contains("<webui-route path=\"/contacts/:id\""),
5216 "detail route element should exist"
5217 );
5218 let detail_start = html.find("path=\"/contacts/:id\"").expect("detail route");
5220 let after_detail = &html[detail_start..];
5221 assert!(
5222 after_detail.contains("style=\"display:none\">")
5223 && !after_detail.starts_with(&format!("path=\"/contacts/:id\"{}detail-page>", "")),
5224 "non-matched route should be hidden: {after_detail}"
5225 );
5226 let detail_end = after_detail.find("</webui-route>").expect("closing tag");
5228 let detail_body = &after_detail[..detail_end];
5229 assert!(
5230 !detail_body.contains("<detail-page>"),
5231 "non-matched route should not render component content: {detail_body}"
5232 );
5233 }
5234
5235 #[test]
5236 fn test_route_parameterized_match() {
5237 let protocol = make_route_protocol();
5238 let state = test_json!({});
5239 let mut writer = TestWriter::new();
5240 handle(
5241 &protocol,
5242 &state,
5243 &RenderOptions::new("index.html", "/contacts/42"),
5244 &mut writer,
5245 )
5246 .unwrap();
5247 let html = writer.get_content();
5248
5249 assert!(
5251 html.contains("active>") && html.contains("<detail-page>"),
5252 "detail route should be active: {html}"
5253 );
5254 assert!(html.contains("<p>Detail</p>"), "detail should have content");
5255 let dash_start = html
5257 .find("component=\"dash-page\"")
5258 .expect("dashboard route");
5259 let after_dash = &html[dash_start..];
5260 assert!(
5261 after_dash.contains("style=\"display:none\">"),
5262 "dashboard should be hidden when detail matches: {after_dash}"
5263 );
5264 let dash_end = after_dash.find("</webui-route>").expect("closing tag");
5265 let dash_body = &after_dash[..dash_end];
5266 assert!(
5267 !dash_body.contains("<dash-page>"),
5268 "dashboard should not render component content: {dash_body}"
5269 );
5270 }
5271
5272 #[test]
5273 fn test_route_no_match_all_hidden_empty() {
5274 let protocol = make_route_protocol();
5275 let state = test_json!({});
5276 let mut writer = TestWriter::new();
5277 handle(
5278 &protocol,
5279 &state,
5280 &RenderOptions::new("index.html", "/nonexistent"),
5281 &mut writer,
5282 )
5283 .unwrap();
5284 let html = writer.get_content();
5285
5286 assert!(html.contains("<h1>Shell</h1>"));
5288 assert!(
5290 !html.contains("<p>Dashboard</p>"),
5291 "no route content when nothing matches"
5292 );
5293 assert!(
5294 !html.contains("<p>Detail</p>"),
5295 "no route content when nothing matches"
5296 );
5297 }
5298
5299 #[test]
5300 fn test_route_component_attr_emitted() {
5301 let protocol = make_route_protocol();
5302 let state = test_json!({});
5303 let mut writer = TestWriter::new();
5304 handle(
5305 &protocol,
5306 &state,
5307 &RenderOptions::new("index.html", "/"),
5308 &mut writer,
5309 )
5310 .unwrap();
5311 let html = writer.get_content();
5312 assert!(
5314 html.contains("component=\"dash-page\""),
5315 "component attr should be on webui-route: {html}"
5316 );
5317 assert!(
5318 html.contains("component=\"detail-page\""),
5319 "component attr should be on webui-route: {html}"
5320 );
5321 }
5322
5323 #[test]
5324 fn test_route_state_attributes_escape_scalars_and_data_state() {
5325 let protocol = make_route_protocol();
5326 let state = test_json!({
5327 "title": "Fish & Chips <\"special\">",
5328 "cartOpen": true,
5329 "items": [{"name": "A&B"}]
5330 });
5331 let mut writer = TestWriter::new();
5332
5333 handle(
5334 &protocol,
5335 &state,
5336 &RenderOptions::new("index.html", "/"),
5337 &mut writer,
5338 )
5339 .unwrap();
5340
5341 let html = writer.get_content();
5342 assert!(
5343 html.contains(r#"title="Fish & Chips <"special">""#),
5344 "escaped title should be emitted: {html}"
5345 );
5346 assert!(
5347 html.contains(r#"cart-open="true""#),
5348 "bool attrs should render: {html}"
5349 );
5350 assert!(
5351 html.contains(r#"data-state=""#),
5352 "complex state should be emitted as data-state: {html}"
5353 );
5354 assert!(
5355 html.contains(r#""name":"A&B""#),
5356 "complex state values should be escaped inside data-state: {html}"
5357 );
5358 }
5359
5360 fn make_nested_route_protocol() -> WebUIProtocol {
5364 use webui_protocol::WebUiFragmentRoute;
5365
5366 let mut fragments = HashMap::new();
5367
5368 fragments.insert(
5369 "index.html".to_string(),
5370 FragmentList {
5371 fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
5372 path: "/".into(),
5373 fragment_id: "app-shell".into(),
5374 exact: false,
5375 children: vec![WebUiFragmentRoute {
5376 path: "sections/:id".into(),
5377 fragment_id: "section-comp".into(),
5378 exact: false,
5379 children: vec![WebUiFragmentRoute {
5380 path: "topics/:topicId".into(),
5381 fragment_id: "topic-comp".into(),
5382 exact: true,
5383 children: vec![],
5384 }],
5385 }],
5386 })],
5387 },
5388 );
5389
5390 fragments.insert(
5391 "app-shell".to_string(),
5392 FragmentList {
5393 fragments: vec![
5394 WebUIFragment::raw("<h1>Shell</h1>"),
5395 WebUIFragment::outlet(),
5396 ],
5397 },
5398 );
5399
5400 fragments.insert(
5401 "section-comp".to_string(),
5402 FragmentList {
5403 fragments: vec![
5404 WebUIFragment::raw("<h2>Section</h2>"),
5405 WebUIFragment::outlet(),
5406 ],
5407 },
5408 );
5409
5410 fragments.insert(
5411 "topic-comp".to_string(),
5412 FragmentList {
5413 fragments: vec![WebUIFragment::raw("<p>Topic content</p>")],
5414 },
5415 );
5416
5417 WebUIProtocol::new(fragments)
5418 }
5419
5420 #[test]
5421 fn test_nested_routes_render_webui_route_as_light_dom() {
5422 let protocol = make_nested_route_protocol();
5423 let state = test_json!({"title": "Test"});
5424 let handler = WebUIHandler::new();
5425 let mut writer = TestWriter::new();
5426
5427 handler
5428 .handle(
5429 &protocol,
5430 &state,
5431 &RenderOptions::new("index.html", "/sections/frontend"),
5432 &mut writer,
5433 )
5434 .expect("render failed");
5435
5436 let html = writer.get_content();
5437
5438 assert!(
5439 html.contains("component=\"app-shell\"") && html.contains("active>"),
5440 "root route should be active: {html}"
5441 );
5442 assert!(
5444 !html.contains("<webui-route path=\"/\"")
5445 || !html
5446 .contains("<webui-route path=\"/\" component=\"app-shell\" active><template shadowrootmode"),
5447 "webui-route should be light DOM (no shadow template): {html}"
5448 );
5449 }
5450
5451 #[test]
5452 fn test_nested_routes_render_outlet_as_light_dom() {
5453 let protocol = make_nested_route_protocol();
5454 let state = test_json!({"title": "Test"});
5455 let handler = WebUIHandler::new();
5456 let mut writer = TestWriter::new();
5457
5458 handler
5459 .handle(
5460 &protocol,
5461 &state,
5462 &RenderOptions::new("index.html", "/sections/frontend"),
5463 &mut writer,
5464 )
5465 .expect("render failed");
5466
5467 let html = writer.get_content();
5468
5469 assert!(
5471 !html.contains("<webui-outlet>"),
5472 "should not contain webui-outlet wrapper: {html}"
5473 );
5474 assert!(
5476 html.contains("<webui-route"),
5477 "should contain webui-route elements: {html}"
5478 );
5479 }
5480
5481 #[test]
5482 fn test_nested_routes_match_child_at_outlet() {
5483 let protocol = make_nested_route_protocol();
5484 let state = test_json!({"title": "Test"});
5485 let handler = WebUIHandler::new();
5486 let mut writer = TestWriter::new();
5487
5488 handler
5489 .handle(
5490 &protocol,
5491 &state,
5492 &RenderOptions::new("index.html", "/sections/frontend"),
5493 &mut writer,
5494 )
5495 .expect("render failed");
5496
5497 let html = writer.get_content();
5498
5499 assert!(
5500 html.contains("component=\"section-comp\"") && html.contains("active>"),
5501 "section route should be active: {html}"
5502 );
5503 assert!(
5504 html.contains("<h2>Section</h2>"),
5505 "section content should be present: {html}"
5506 );
5507 }
5508
5509 #[test]
5510 fn test_nested_routes_three_levels_deep() {
5511 let protocol = make_nested_route_protocol();
5512 let state = test_json!({"title": "Test"});
5513 let handler = WebUIHandler::new();
5514 let mut writer = TestWriter::new();
5515
5516 handler
5517 .handle(
5518 &protocol,
5519 &state,
5520 &RenderOptions::new("index.html", "/sections/frontend/topics/react"),
5521 &mut writer,
5522 )
5523 .expect("render failed");
5524
5525 let html = writer.get_content();
5526
5527 assert!(
5528 html.contains("component=\"app-shell\"") && html.contains("active>"),
5529 "root active: {html}"
5530 );
5531 assert!(
5532 html.contains("component=\"section-comp\"") && html.contains("active>"),
5533 "section active: {html}"
5534 );
5535 assert!(
5536 html.contains("component=\"topic-comp\"") && html.contains("exact active>"),
5537 "topic active: {html}"
5538 );
5539 assert!(
5540 html.contains("<p>Topic content</p>"),
5541 "leaf content present: {html}"
5542 );
5543 }
5544
5545 #[test]
5546 fn test_nested_routes_nonmatched_siblings_hidden() {
5547 let protocol = make_nested_route_protocol();
5548 let state = test_json!({"title": "Test"});
5549 let handler = WebUIHandler::new();
5550 let mut writer = TestWriter::new();
5551
5552 handler
5553 .handle(
5554 &protocol,
5555 &state,
5556 &RenderOptions::new("index.html", "/sections/frontend"),
5557 &mut writer,
5558 )
5559 .expect("render failed");
5560
5561 let html = writer.get_content();
5562
5563 assert!(
5564 html.contains(r#"component="topic-comp" style="display:none">"#),
5565 "topic should be hidden: {html}"
5566 );
5567 }
5568
5569 #[test]
5570 fn test_nested_routes_root_only() {
5571 let protocol = make_nested_route_protocol();
5572 let state = test_json!({"title": "Test"});
5573 let handler = WebUIHandler::new();
5574 let mut writer = TestWriter::new();
5575
5576 handler
5577 .handle(
5578 &protocol,
5579 &state,
5580 &RenderOptions::new("index.html", "/"),
5581 &mut writer,
5582 )
5583 .expect("render failed");
5584
5585 let html = writer.get_content();
5586
5587 assert!(
5588 html.contains("component=\"app-shell\"") && html.contains("active>"),
5589 "root active at /: {html}"
5590 );
5591 assert!(
5592 html.contains("<h1>Shell</h1>"),
5593 "shell renders at /: {html}"
5594 );
5595 assert!(
5596 html.contains(r#"component="section-comp" style="display:none">"#),
5597 "section hidden at /: {html}"
5598 );
5599 }
5600
5601 #[test]
5604 fn test_css_module_emitted_once_before_component() {
5605 let template = r#"<template shadowrootmode="open" shadowrootadoptedstylesheets="my-card"><p><slot></slot></p></template>"#;
5608
5609 let mut fragments = HashMap::new();
5610 fragments.insert(
5611 "index.html".to_string(),
5612 FragmentList {
5613 fragments: vec![
5614 WebUIFragment::raw("<div>".to_string()),
5615 WebUIFragment::component("my-card"),
5616 WebUIFragment::raw("A".to_string()),
5617 WebUIFragment::component("my-card"),
5618 WebUIFragment::raw("B</div>".to_string()),
5619 ],
5620 },
5621 );
5622 fragments.insert(
5623 "my-card".to_string(),
5624 FragmentList {
5625 fragments: vec![WebUIFragment::raw(template.to_string())],
5626 },
5627 );
5628
5629 let mut protocol = WebUIProtocol::new(fragments);
5630 protocol
5631 .components
5632 .entry("my-card".to_string())
5633 .or_default()
5634 .css = "p{color:red}".to_string();
5635 let state = test_json!({});
5636 let mut writer = TestWriter::new();
5637
5638 handle(
5639 &protocol,
5640 &state,
5641 &RenderOptions::new("index.html", "/"),
5642 &mut writer,
5643 )
5644 .unwrap();
5645
5646 let html = writer.get_content();
5647
5648 let count = html
5650 .matches(r#"<style type="module" specifier="my-card">"#)
5651 .count();
5652 assert_eq!(
5653 count, 1,
5654 "CSS module should be emitted once, got {count} in: {html}"
5655 );
5656
5657 let tmpl_count = html
5659 .matches(r#"shadowrootadoptedstylesheets="my-card""#)
5660 .count();
5661 assert_eq!(
5662 tmpl_count, 2,
5663 "Template should render twice, got {tmpl_count} in: {html}"
5664 );
5665
5666 let css_pos = html
5668 .find(r#"<style type="module""#)
5669 .expect("CSS module missing");
5670 let tmpl_pos = html
5671 .find(r#"shadowrootadoptedstylesheets"#)
5672 .expect("template missing");
5673 assert!(
5674 css_pos < tmpl_pos,
5675 "CSS module should precede first component: {html}"
5676 );
5677 }
5678
5679 #[test]
5680 fn test_component_without_css_renders_normally() {
5681 let mut fragments = HashMap::new();
5683 fragments.insert(
5684 "index.html".to_string(),
5685 FragmentList {
5686 fragments: vec![WebUIFragment::component("my-card")],
5687 },
5688 );
5689 fragments.insert(
5690 "my-card".to_string(),
5691 FragmentList {
5692 fragments: vec![WebUIFragment::raw(
5693 r#"<template shadowrootmode="open"><p>hello</p></template>"#.to_string(),
5694 )],
5695 },
5696 );
5697
5698 let protocol = WebUIProtocol::new(fragments);
5699 let state = test_json!({});
5700 let mut writer = TestWriter::new();
5701
5702 handle(
5703 &protocol,
5704 &state,
5705 &RenderOptions::new("index.html", "/"),
5706 &mut writer,
5707 )
5708 .unwrap();
5709
5710 let html = writer.get_content();
5711 assert!(
5712 html.contains("<p>hello</p>"),
5713 "Non-module component should render normally: {html}"
5714 );
5715 }
5716
5717 #[test]
5718 fn test_non_module_strategy_no_css_in_head() {
5719 let template = r#"<template shadowrootmode="open"><p>hello</p></template>"#;
5722
5723 let mut fragments = HashMap::new();
5724 fragments.insert(
5725 "index.html".to_string(),
5726 FragmentList {
5727 fragments: vec![
5728 WebUIFragment::raw("<html><head>".to_string()),
5729 WebUIFragment::signal("head_end", true),
5730 WebUIFragment::raw("</head><body>".to_string()),
5731 WebUIFragment::component("my-card"),
5732 WebUIFragment::raw("</body></html>".to_string()),
5733 ],
5734 },
5735 );
5736 fragments.insert(
5737 "my-card".to_string(),
5738 FragmentList {
5739 fragments: vec![WebUIFragment::raw(template.to_string())],
5740 },
5741 );
5742
5743 let protocol = WebUIProtocol::new(fragments);
5745 let state = test_json!({});
5746 let mut writer = TestWriter::new();
5747
5748 handle(
5749 &protocol,
5750 &state,
5751 &RenderOptions::new("index.html", "/"),
5752 &mut writer,
5753 )
5754 .unwrap();
5755
5756 let html = writer.get_content();
5757
5758 assert!(
5759 !html.contains(r#"<style type="module""#),
5760 "Non-module strategy should not emit CSS modules in <head>: {html}"
5761 );
5762 assert!(
5763 html.contains("<p>hello</p>"),
5764 "Component should still render: {html}"
5765 );
5766 }
5767
5768 #[test]
5769 fn test_css_module_emitted_in_component_light_dom() {
5770 let mut fragments = HashMap::new();
5774 fragments.insert(
5775 "index.html".to_string(),
5776 FragmentList {
5777 fragments: vec![
5778 WebUIFragment::raw("<my-card>".to_string()),
5779 WebUIFragment::component("my-card"),
5780 WebUIFragment::raw("</my-card>".to_string()),
5781 ],
5782 },
5783 );
5784 fragments.insert(
5785 "my-card".to_string(),
5786 FragmentList {
5787 fragments: vec![WebUIFragment::raw(
5788 r#"<template shadowrootmode="open" shadowrootadoptedstylesheets="my-card"><p>hi</p></template>"#.to_string(),
5789 )],
5790 },
5791 );
5792
5793 let mut protocol = WebUIProtocol::new(fragments);
5794 protocol
5795 .components
5796 .entry("my-card".to_string())
5797 .or_default()
5798 .css = "p{color:red}".to_string();
5799 let state = test_json!({});
5800 let mut writer = TestWriter::new();
5801
5802 handle(
5803 &protocol,
5804 &state,
5805 &RenderOptions::new("index.html", "/"),
5806 &mut writer,
5807 )
5808 .unwrap();
5809
5810 let html = writer.get_content();
5811
5812 let tag_open = html.find("<my-card>").expect("<my-card> missing");
5814 let css_pos = html
5815 .find(r#"<style type="module""#)
5816 .expect("CSS module missing");
5817 let tag_close = html.rfind("</my-card>").expect("</my-card> missing");
5818 assert!(
5819 css_pos > tag_open && css_pos < tag_close,
5820 "CSS module should be inside component light DOM: {html}"
5821 );
5822 }
5823
5824 #[test]
5825 fn test_css_module_emitted_for_route_components() {
5826 let template = r#"<template shadowrootmode="open" shadowrootadoptedstylesheets="dash-page"><h1>Dashboard</h1></template>"#;
5828
5829 let mut fragments = HashMap::new();
5830 fragments.insert(
5831 "index.html".to_string(),
5832 FragmentList {
5833 fragments: vec![WebUIFragment::route("/", "dash-page")],
5834 },
5835 );
5836 fragments.insert(
5837 "dash-page".to_string(),
5838 FragmentList {
5839 fragments: vec![WebUIFragment::raw(template.to_string())],
5840 },
5841 );
5842
5843 let mut protocol = WebUIProtocol::new(fragments);
5844 protocol
5845 .components
5846 .entry("dash-page".to_string())
5847 .or_default()
5848 .css = "h1{font-size:2rem}".to_string();
5849 let state = test_json!({});
5850 let mut writer = TestWriter::new();
5851
5852 handle(
5853 &protocol,
5854 &state,
5855 &RenderOptions::new("index.html", "/"),
5856 &mut writer,
5857 )
5858 .unwrap();
5859
5860 let html = writer.get_content();
5861
5862 assert!(
5863 html.contains(
5864 r#"<style type="module" specifier="dash-page">h1{font-size:2rem}</style>"#
5865 ),
5866 "Route component should have CSS module: {html}"
5867 );
5868 assert!(
5869 html.contains("<h1>Dashboard</h1>"),
5870 "Route component should render content: {html}"
5871 );
5872 }
5873}