From 9c4abcbea0e63a9dfc5f20b110f9153a44973a49 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 22 Nov 2022 21:32:26 -0800 Subject: [PATCH] wip: more tests! --- packages/core-macro/src/props/mod.rs | 2 +- packages/core/src/create.rs | 25 ++++- packages/core/src/factory.rs | 12 ++- packages/core/src/mutations.rs | 19 ++++ packages/core/src/nodes.rs | 21 +++- packages/core/src/scope_arena.rs | 2 +- packages/core/src/scopes.rs | 21 +++- packages/core/tests/passthru.rs | 101 ++++++++++++++++++ packages/core/tests/{element.rs => safety.rs} | 22 ++-- packages/rsx/src/component.rs | 4 +- 10 files changed, 203 insertions(+), 26 deletions(-) create mode 100644 packages/core/tests/passthru.rs rename packages/core/tests/{element.rs => safety.rs} (52%) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 56a3407f7..b614acb43 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -194,7 +194,7 @@ mod field_info { // children field is automatically defaulted to None if name == "children" { builder_attr.default = - Some(syn::parse(quote!(Default::default()).into()).unwrap()); + Some(syn::parse(quote!(::dioxus::core::VNode::empty()).into()).unwrap()); } // auto detect optional diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index baa78a75b..988d3b4b3 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -157,6 +157,28 @@ impl<'b: 'static> VirtualDom { /// Insert a new template into the VirtualDom's template registry fn register_template(&mut self, template: &'b VNode<'b>) { + // First, make sure we mark the template as seen, regardless if we process it + self.templates + .insert(template.template.id, template.template); + + // If it's all dynamic nodes, then we don't need to register it + // Quickly run through and see if it's all just dynamic nodes + let dynamic_roots = template + .template + .roots + .iter() + .filter(|root| { + matches!( + root, + TemplateNode::Dynamic(_) | TemplateNode::DynamicText(_) + ) + }) + .count(); + + if dynamic_roots == template.template.roots.len() { + return; + } + for node in template.template.roots { self.create_static_node(template, node); } @@ -165,9 +187,6 @@ impl<'b: 'static> VirtualDom { name: template.template.id, m: template.template.roots.len(), }); - - self.templates - .insert(template.template.id, template.template); } pub(crate) fn create_static_node( diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index c70404757..d92899f3d 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -155,8 +155,7 @@ impl<'a, 'b> IntoDynNode<'a> for () { } impl<'a, 'b> IntoDynNode<'a> for VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - // DynamicNode::Fragment { nodes: cx., inner: () } - todo!() + DynamicNode::Fragment(VFragment::NonEmpty(_cx.bump().alloc([self]))) } } @@ -169,6 +168,15 @@ impl<'a, 'b, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { } } +impl<'a> IntoDynNode<'a> for &Element<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self.as_ref() { + Ok(val) => val.clone().into_vnode(_cx), + _ => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), + } + } +} + impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(VFragment::NonEmpty(cx.bump().alloc([self.call(cx)]))) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 7427da69b..66d95cfd0 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -15,6 +15,25 @@ impl<'a> Mutations<'a> { template_mutations: Vec::new(), } } + + /// A useful tool for testing mutations + /// + /// Rewrites IDs to just be "template", so you can compare the mutations + pub fn santize(mut self) -> Self { + for edit in self + .template_mutations + .iter_mut() + .chain(self.edits.iter_mut()) + { + match edit { + Mutation::LoadTemplate { name, .. } => *name = "template", + Mutation::SaveTemplate { name, .. } => *name = "template", + _ => {} + } + } + + self + } } impl<'a> std::ops::Deref for Mutations<'a> { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1ab579117..54cb47df6 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,4 +1,4 @@ -use crate::{any_props::AnyProps, arena::ElementId, ScopeId, ScopeState, UiEvent}; +use crate::{any_props::AnyProps, arena::ElementId, Element, ScopeId, ScopeState, UiEvent}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, @@ -10,7 +10,7 @@ pub type TemplateId = &'static str; /// /// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping /// static parts of the template. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct VNode<'a> { // The ID assigned for the root of this template pub node_id: Cell, @@ -47,6 +47,23 @@ impl<'a> VNode<'a> { .unwrap() } + pub fn empty() -> Element<'a> { + Ok(VNode { + node_id: Cell::new(ElementId(0)), + key: None, + parent: None, + root_ids: &[], + dynamic_nodes: &[], + dynamic_attrs: &[], + template: Template { + id: "dioxus-empty", + roots: &[], + node_paths: &[], + attr_paths: &[], + }, + }) + } + pub fn template_from_dynamic_node( cx: &'a ScopeState, node: DynamicNode<'a>, diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index b206fbe7c..9259a9f56 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -20,7 +20,7 @@ impl VirtualDom { pub(super) fn new_scope(&mut self, props: *const dyn AnyProps<'static>) -> &mut ScopeState { let parent = self.acquire_current_scope_raw(); let entry = self.scopes.vacant_entry(); - let height = unsafe { parent.map(|f| (*f).height).unwrap_or(0) + 1 }; + let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) }; let id = ScopeId(entry.key()); entry.insert(ScopeState { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index a1a35f69d..14bea6ccc 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -120,8 +120,27 @@ impl ScopeState { /// Get a handle to the currently active head node arena for this Scope /// /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. + /// + /// Panics if the tree has not been built yet. pub fn root_node<'a>(&'a self) -> &'a RenderReturn<'a> { - let r: &RenderReturn = unsafe { &*self.current_frame().node.get() }; + self.try_root_node() + .expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.") + } + + /// Try to get a handle to the currently active head node arena for this Scope + /// + /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. + /// + /// Returns [`None`] if the tree has not been built yet. + pub fn try_root_node<'a>(&'a self) -> Option<&'a RenderReturn<'a>> { + let ptr = self.current_frame().node.get(); + + if ptr.is_null() { + return None; + } + + let r: &RenderReturn = unsafe { &*ptr }; + unsafe { std::mem::transmute(r) } } diff --git a/packages/core/tests/passthru.rs b/packages/core/tests/passthru.rs new file mode 100644 index 000000000..6cd9146bd --- /dev/null +++ b/packages/core/tests/passthru.rs @@ -0,0 +1,101 @@ +use dioxus::core::Mutation::*; +use dioxus::prelude::*; +use dioxus_core::ElementId; + +/// Should push the text node onto the stack and modify it +#[test] +fn nested_passthru_creates() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + Child { + Child { + Child { + div { + "hi" + } + } + } + } + }) + } + + #[inline_props] + fn Child<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { + cx.render(rsx! { children }) + } + + let mut dom = VirtualDom::new(app); + let edits = dom.rebuild().santize(); + + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0 }, + AppendChildren { m: 1 }, + ] + ) +} + +/// Should load all the templates and append them +#[test] +fn nested_passthru_creates_add() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + child_comp { + "1" + child_comp { + "2" + child_comp { + "3" + div { + "hi" + } + } + } + } + }) + } + + #[inline_props] + fn child_comp<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { children }) + } + + let mut dom = VirtualDom::new(app); + + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0 }, + LoadTemplate { name: "template", index: 0 }, + LoadTemplate { name: "template", index: 0 }, + LoadTemplate { name: "template", index: 1 }, + AppendChildren { m: 4 }, + ] + ); +} + +#[test] +fn dynamic_node_as_root() { + fn app(cx: Scope) -> Element { + let a = 123; + let b = 456; + cx.render(rsx! { "{a}" "{b}" }) + } + + let mut dom = VirtualDom::new(app); + let edits = dom.rebuild().santize(); + + // Since the roots were all dynamic, they should not cause any template muations + assert_eq!(edits.template_mutations, []); + + // The root node is text, so we just create it on the spot + assert_eq!( + edits.edits, + [ + CreateTextNode { value: "123", id: ElementId(1) }, + CreateTextNode { value: "456", id: ElementId(2) }, + AppendChildren { m: 2 } + ] + ) +} diff --git a/packages/core/tests/element.rs b/packages/core/tests/safety.rs similarity index 52% rename from packages/core/tests/element.rs rename to packages/core/tests/safety.rs index b1622c85d..318e1088d 100644 --- a/packages/core/tests/element.rs +++ b/packages/core/tests/safety.rs @@ -1,15 +1,21 @@ +//! Tests related to safety of the library. + use dioxus::prelude::*; use dioxus_core::SuspenseContext; -/// Ensure no issues with not building the virtualdom before +/// Ensure no issues with not calling rebuild #[test] fn root_node_isnt_null() { let dom = VirtualDom::new(|cx| render!("Hello world!")); let scope = dom.base_scope(); - // The root should be a valid pointer - assert_ne!(scope.root_node() as *const _, std::ptr::null_mut()); + // We haven't built the tree, so trying to get out the root node should fail + assert!(scope.try_root_node().is_none()); + + // There should be no way to gain an invalid pointer + assert!(scope.current_frame().node.get().is_null()); + assert!(scope.previous_frame().node.get().is_null()); // The height should be 0 assert_eq!(scope.height(), 0); @@ -18,13 +24,3 @@ fn root_node_isnt_null() { // todo: there should also be a default error boundary assert!(scope.has_context::().is_some()); } - -#[test] -fn elements() { - let mut dom = VirtualDom::new(|cx| { - // - cx.render(rsx!( div { "Hello world!" } )) - }); - - let muts = dom.rebuild(); -} diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index e00515e4f..d1132fdae 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -171,9 +171,7 @@ impl ToTokens for Component { toks.append_all(quote! { .children( - Some({ - #renderer - }) + Ok({ #renderer }) ) }); }