diff --git a/packages/rsx_interpreter/Cargo.toml b/packages/rsx_interpreter/Cargo.toml index 2734ceebe..0b1f79a5d 100644 --- a/packages/rsx_interpreter/Cargo.toml +++ b/packages/rsx_interpreter/Cargo.toml @@ -16,4 +16,8 @@ dioxus-rsx = { path = "../rsx", default-features = false } dioxus-ssr = { path = "../ssr" } dioxus-core = { path = "../core" } dioxus-html = { path = "../html" } -dioxus-hooks = { path = "../hooks"} \ No newline at end of file +dioxus-hooks = { path = "../hooks"} + +[dev-dependencies] +dioxus-core-macro = { path = "../core-macro" } +bumpalo = { version = "3.6", features = ["collections", "boxed"] } diff --git a/packages/rsx_interpreter/src/interperter.rs b/packages/rsx_interpreter/src/interperter.rs index ad02fc448..fd0f02bdc 100644 --- a/packages/rsx_interpreter/src/interperter.rs +++ b/packages/rsx_interpreter/src/interperter.rs @@ -53,7 +53,12 @@ pub fn build<'a>( for child in rsx.roots { children_built.push(build_node(child, &mut ctx, factory)?); } - Ok(factory.fragment_from_iter(children_built.iter())) + + if children_built.len() == 1 { + return Ok(children_built.pop().unwrap()); + } else { + Ok(factory.fragment_from_iter(children_built.iter())) + } } fn build_node<'a>( diff --git a/packages/rsx_interpreter/tests/render.rs b/packages/rsx_interpreter/tests/render.rs new file mode 100644 index 000000000..29117be60 --- /dev/null +++ b/packages/rsx_interpreter/tests/render.rs @@ -0,0 +1,356 @@ +use std::cell::{Cell, RefCell}; + +use bumpalo::boxed::Box; +use dioxus_core::{prelude::*, AnyEvent}; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; +use dioxus_rsx_interpreter::{ + captuered_context::{CapturedContext, FormattedArg, IfmtArgs}, + CodeLocation, ErrorHandler, +}; + +#[test] +#[allow(non_snake_case)] +fn render_basic() { + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + let dom = VirtualDom::new(Base); + let static_vnodes = rsx!(div{"hello world"}); + let location = CodeLocation { + file: String::new(), + line: 0, + column: 0, + }; + let empty_context = CapturedContext { + captured: IfmtArgs { + named_args: Vec::new(), + }, + components: Vec::new(), + iterators: Vec::new(), + expressions: Vec::new(), + listeners: Vec::new(), + location: location.clone(), + }; + let interperted_vnodes = LazyNodes::new(|factory| { + dioxus_rsx_interpreter::resolve_scope( + location, + "div{\"hello world\"}", + empty_context, + factory, + ) + }); + + let interperted_vnodes = dom.render_vnodes(interperted_vnodes); + let static_vnodes = dom.render_vnodes(static_vnodes); + assert!(check_eq(interperted_vnodes, static_vnodes)); +} + +#[test] +#[allow(non_snake_case)] +fn render_nested() { + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + let dom = VirtualDom::new(Base); + let static_vnodes = rsx! { + div { + p { "hello world" } + div { + p { "hello world" } + } + } + }; + let location = CodeLocation { + file: String::new(), + line: 1, + column: 0, + }; + let empty_context = CapturedContext { + captured: IfmtArgs { + named_args: Vec::new(), + }, + components: Vec::new(), + iterators: Vec::new(), + expressions: Vec::new(), + listeners: Vec::new(), + location: location.clone(), + }; + let interperted_vnodes = LazyNodes::new(|factory| { + dioxus_rsx_interpreter::resolve_scope( + location, + r#"div { + p { "hello world" } + div { + p { "hello world" } + } + }"#, + empty_context, + factory, + ) + }); + + let interperted_vnodes = dom.render_vnodes(interperted_vnodes); + let static_vnodes = dom.render_vnodes(static_vnodes); + assert!(check_eq(interperted_vnodes, static_vnodes)); +} + +#[test] +#[allow(non_snake_case)] +fn render_component() { + fn Comp(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + let dom = VirtualDom::new(Base); + let static_vnodes = rsx! { + div { + Comp {} + } + }; + let location = CodeLocation { + file: String::new(), + line: 2, + column: 0, + }; + + let interperted_vnodes = LazyNodes::new(|factory| { + let context = CapturedContext { + captured: IfmtArgs { + named_args: Vec::new(), + }, + components: vec![( + r#"__cx.component(Comp, fc_to_builder(Comp).build(), None, "Comp")"#, + factory.component(Comp, (), None, "Comp"), + )], + iterators: Vec::new(), + expressions: Vec::new(), + listeners: Vec::new(), + location: location.clone(), + }; + dioxus_rsx_interpreter::resolve_scope( + location, + r#"div { + Comp {} + }"#, + context, + factory, + ) + }); + + let interperted_vnodes = dom.render_vnodes(interperted_vnodes); + let static_vnodes = dom.render_vnodes(static_vnodes); + println!("{:#?}", interperted_vnodes); + println!("{:#?}", static_vnodes); + assert!(check_eq(interperted_vnodes, static_vnodes)); +} + +#[test] +#[allow(non_snake_case)] +fn render_iterator() { + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + let dom = VirtualDom::new(Base); + let iter = (0..10).map(|i| dom.render_vnodes(rsx! {"{i}"})); + let static_vnodes = rsx! { + div { + iter + } + }; + let location = CodeLocation { + file: String::new(), + line: 3, + column: 0, + }; + + let interperted_vnodes = LazyNodes::new(|factory| { + let context = CapturedContext { + captured: IfmtArgs { + named_args: Vec::new(), + }, + components: Vec::new(), + iterators: vec![( + r#" + (0..10).map(|i| dom.render_vnodes(rsx!{"{i}"}))"#, + factory.fragment_from_iter((0..10).map(|i| factory.text(format_args!("{i}")))), + )], + expressions: Vec::new(), + listeners: Vec::new(), + location: location.clone(), + }; + dioxus_rsx_interpreter::resolve_scope( + location, + r#"div { + (0..10).map(|i| dom.render_vnodes(rsx!{"{i}"})) + }"#, + context, + factory, + ) + }); + + let interperted_vnodes = dom.render_vnodes(interperted_vnodes); + let static_vnodes = dom.render_vnodes(static_vnodes); + println!("{:#?}", interperted_vnodes); + println!("{:#?}", static_vnodes); + assert!(check_eq(interperted_vnodes, static_vnodes)); +} + +#[test] +#[allow(non_snake_case)] +fn render_captured_variable() { + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + let dom = VirtualDom::new(Base); + + let x = 10; + let static_vnodes = rsx! { + div { + "{x}" + } + }; + let location = CodeLocation { + file: String::new(), + line: 4, + column: 0, + }; + + let interperted_vnodes = LazyNodes::new(|factory| { + let context = CapturedContext { + captured: IfmtArgs { + named_args: vec![FormattedArg { + expr: "x", + format_args: "", + result: x.to_string(), + }], + }, + components: Vec::new(), + iterators: Vec::new(), + expressions: Vec::new(), + listeners: Vec::new(), + location: location.clone(), + }; + dioxus_rsx_interpreter::resolve_scope( + location, + r#"div { + "{x}" + }"#, + context, + factory, + ) + }); + + let interperted_vnodes = dom.render_vnodes(interperted_vnodes); + let static_vnodes = dom.render_vnodes(static_vnodes); + println!("{:#?}", interperted_vnodes); + println!("{:#?}", static_vnodes); + assert!(check_eq(interperted_vnodes, static_vnodes)); +} + +#[test] +#[allow(non_snake_case)] +fn render_listener() { + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + let dom = VirtualDom::new(Base); + let static_vnodes = rsx! { + div { + onclick: |_| println!("clicked") + } + }; + let location = CodeLocation { + file: String::new(), + line: 5, + column: 0, + }; + + let interperted_vnodes = LazyNodes::new(|factory| { + let f = |_| println!("clicked"); + let f = factory.bump().alloc(f); + let context = CapturedContext { + captured: IfmtArgs { + named_args: Vec::new(), + }, + components: Vec::new(), + iterators: Vec::new(), + expressions: Vec::new(), + listeners: vec![( + r#"dioxus_elements::on::onclick(__cx, |_| println!("clicked"))"#, + dioxus_elements::on::onclick(factory, f), + )], + location: location.clone(), + }; + dioxus_rsx_interpreter::resolve_scope( + location, + r#"div { + onclick: |_| println!("clicked") + }"#, + context, + factory, + ) + }); + + let interperted_vnodes = dom.render_vnodes(interperted_vnodes); + let static_vnodes = dom.render_vnodes(static_vnodes); + println!("{:#?}", interperted_vnodes); + println!("{:#?}", static_vnodes); + assert!(check_eq(interperted_vnodes, static_vnodes)); +} + +fn check_eq<'a>(a: &'a VNode<'a>, b: &'a VNode<'a>) -> bool { + match (a, b) { + (VNode::Text(t_a), VNode::Text(t_b)) => t_a.text == t_b.text, + (VNode::Element(e_a), VNode::Element(e_b)) => { + e_a.attributes + .iter() + .zip(e_b.attributes.iter()) + .all(|(a, b)| { + a.is_static == b.is_static + && a.is_volatile == b.is_volatile + && a.name == b.name + && a.value == b.value + && a.namespace == b.namespace + }) + && e_a + .children + .iter() + .zip(e_b.children.iter()) + .all(|(a, b)| check_eq(a, b)) + && e_a.key == e_b.key + && e_a.tag == e_b.tag + && e_a.namespace == e_b.namespace + && e_a + .listeners + .iter() + .zip(e_b.listeners.iter()) + .all(|(a, b)| a.event == b.event) + } + (VNode::Fragment(f_a), VNode::Fragment(f_b)) => { + f_a.key == f_b.key + && f_a + .children + .iter() + .zip(f_b.children.iter()) + .all(|(a, b)| check_eq(a, b)) + } + (VNode::Component(c_a), VNode::Component(c_b)) => { + c_a.can_memoize == c_b.can_memoize + && c_a.key == c_b.key + && c_a.fn_name == c_b.fn_name + && c_a.user_fc == c_b.user_fc + } + (VNode::Placeholder(_), VNode::Placeholder(_)) => true, + _ => false, + } +}