wip: migrate all tests to core

This commit is contained in:
Jonathan Kelley committed 2022-11-27 00:56:49 -05:00
1 parent d0554b9ed6
commit f614cbb401
12 files changed
+104 -273

No files matched your search

+1 -1
View File
@@ -235,7 +235,7 @@ impl<'b: 'static> VirtualDom {
TemplateNode::DynamicText { .. } => self
.mutations
.template_mutations
.push(CreateStaticText { value: "d" }),
.push(CreateTextPlaceholder),
TemplateNode::Element {
attrs,
+1
View File
@@ -81,6 +81,7 @@ pub enum Mutation<'a> {
id: ElementId,
},
CreateStaticPlaceholder,
CreateTextPlaceholder,
CreateStaticText {
value: &'a str,
},
+53
View File
@@ -0,0 +1,53 @@
# Testing of Dioxus core
NodeFactory
- [] rsx, html, NodeFactory generate the same structures
Diffing
- [x] create elements
- [x] create text
- [x] create fragments
- [x] create empty fragments (placeholders)
- [x] diff elements
- [x] diff from element/text to fragment
- [x] diff from element/text to empty fragment
- [x] diff to element with children works too
- [x] replace with works forward
- [x] replace with works backward
- [x] un-keyed diffing
- [x] keyed diffing
- [x] keyed diffing out of order
- [x] keyed diffing with prefix/suffix
- [x] suspended nodes work
Lifecycle
- [] Components mount properly
- [] Components create new child components
- [] Replaced components unmount old components and mount new
- [] Post-render effects are called
- []
Shared Context
- [] Shared context propagates downwards
- [] unwrapping shared context if it doesn't exist works too
Suspense
- [] use_suspense generates suspended nodes
Hooks
- [] Drop order is maintained
- [] Shared hook state is okay
- [] use_hook works
- [] use_ref works
- [] use_noderef works
- [] use_provide_state
- [] use_consume_state
VirtualDOM API
- [] work
- [] rebuild
- [] change props
+831
View File
@@ -0,0 +1,831 @@
#![allow(unused, non_upper_case_globals)]
//! Diffing Tests
//!
//! These tests only verify that the diffing algorithm works properly for single components.
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use dioxus::{core_macro::rsx_without_templates, prelude::*};
fn new_dom() -> VirtualDom {
VirtualDom::new(|cx| cx.render(rsx_without_templates!("hi")))
}
use dioxus_core::DomEdit::*;
/// Should push the text node onto the stack and modify it
#[test]
fn html_and_rsx_generate_the_same_output() {
let dom = new_dom();
let (create, change) = dom.diff_lazynodes(
rsx_without_templates! ( div { "Hello world" } ),
rsx_without_templates! ( div { "Goodbye world" } ),
);
assert_eq!(
create.edits,
[
CreateElement { root: Some(1,), tag: "div", children: 0 },
CreateTextNode { root: Some(2,), text: "Hello world" },
AppendChildren { root: Some(1,), children: vec![2] },
AppendChildren { root: Some(0,), children: vec![1] },
]
);
assert_eq!(
change.edits,
[SetText { root: Some(2,), text: "Goodbye world" },]
);
}
/// Should result in 3 elements on the stack
#[test]
fn fragments_create_properly() {
let dom = new_dom();
let create = dom.create_vnodes(rsx_without_templates! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
});
assert_eq!(
create.edits,
[
CreateElement { root: Some(1,), tag: "div", children: 0 },
CreateTextNode { root: Some(2,), text: "Hello a" },
AppendChildren { root: Some(1,), children: vec![2,] },
CreateElement { root: Some(3,), tag: "div", children: 0 },
CreateTextNode { root: Some(4,), text: "Hello b" },
AppendChildren { root: Some(3,), children: vec![4,] },
CreateElement { root: Some(5,), tag: "div", children: 0 },
CreateTextNode { root: Some(6,), text: "Hello c" },
AppendChildren { root: Some(5,), children: vec![6,] },
AppendChildren { root: Some(0,), children: vec![1, 3, 5,] },
]
);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith
#[test]
fn empty_fragments_create_anchors() {
let dom = new_dom();
let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) });
let right = rsx_without_templates!({ (0..1).map(|_f| rsx_without_templates! { div {}}) });
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
CreatePlaceholder { root: Some(1,) },
AppendChildren { root: Some(0,), children: vec![1,] },
]
);
assert_eq!(
change.edits,
[
CreateElement { root: Some(2,), tag: "div", children: 0 },
ReplaceWith { root: Some(1,), nodes: vec![2,] },
]
);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5
#[test]
fn empty_fragments_create_many_anchors() {
let dom = new_dom();
let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) });
let right = rsx_without_templates!({ (0..5).map(|_f| rsx_without_templates! { div {}}) });
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
CreatePlaceholder { root: Some(1,) },
AppendChildren { root: Some(0,), children: vec![1,] },
]
);
assert_eq!(
change.edits,
[
CreateElement { root: Some(2,), tag: "div", children: 0 },
CreateElement { root: Some(3,), tag: "div", children: 0 },
CreateElement { root: Some(4,), tag: "div", children: 0 },
CreateElement { root: Some(5,), tag: "div", children: 0 },
CreateElement { root: Some(6,), tag: "div", children: 0 },
ReplaceWith { root: Some(1,), nodes: vec![2, 3, 4, 5, 6,] },
]
);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith
/// Includes child nodes inside the fragment
#[test]
fn empty_fragments_create_anchors_with_many_children() {
let dom = new_dom();
let left = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) });
let right = rsx_without_templates!({
(0..3).map(|f| {
rsx_without_templates! { div { "hello: {f}" }}
})
});
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
CreatePlaceholder { root: Some(1,) },
AppendChildren { root: Some(0,), children: vec![1,] },
]
);
assert_eq!(
change.edits,
[
CreateElement { root: Some(2,), tag: "div", children: 0 },
CreateTextNode { root: Some(3,), text: "hello: 0" },
AppendChildren { root: Some(2,), children: vec![3,] },
CreateElement { root: Some(4,), tag: "div", children: 0 },
CreateTextNode { root: Some(5,), text: "hello: 1" },
AppendChildren { root: Some(4,), children: vec![5,] },
CreateElement { root: Some(6,), tag: "div", children: 0 },
CreateTextNode { root: Some(7,), text: "hello: 2" },
AppendChildren { root: Some(6,), children: vec![7,] },
ReplaceWith { root: Some(1,), nodes: vec![2, 4, 6,] },
]
);
}
/// Should result in every node being pushed and then replaced with an anchor
#[test]
fn many_items_become_fragment() {
let dom = new_dom();
let left = rsx_without_templates!({
(0..2).map(|_| {
rsx_without_templates! { div { "hello" }}
})
});
let right = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) });
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
CreateElement { root: Some(1,), tag: "div", children: 0 },
CreateTextNode { root: Some(2,), text: "hello" },
AppendChildren { root: Some(1,), children: vec![2,] },
CreateElement { root: Some(3,), tag: "div", children: 0 },
CreateTextNode { root: Some(4,), text: "hello" },
AppendChildren { root: Some(3,), children: vec![4,] },
AppendChildren { root: Some(0,), children: vec![1, 3,] },
]
);
assert_eq!(
change.edits,
[
CreatePlaceholder { root: Some(5,) },
ReplaceWith { root: Some(1,), nodes: vec![5,] },
Remove { root: Some(3,) },
]
);
}
/// Should result in no edits
#[test]
fn two_equal_fragments_are_equal() {
let dom = new_dom();
let left = rsx_without_templates!({
(0..2).map(|_| {
rsx_without_templates! { div { "hello" }}
})
});
let right = rsx_without_templates!({
(0..2).map(|_| {
rsx_without_templates! { div { "hello" }}
})
});
let (_create, change) = dom.diff_lazynodes(left, right);
assert!(change.edits.is_empty());
}
/// Should result the creation of more nodes appended after the old last node
#[test]
fn two_fragments_with_differrent_elements_are_differet() {
let dom = new_dom();
let left = rsx_without_templates!(
{ (0..2).map(|_| rsx_without_templates! { div { }} ) }
p {}
);
let right = rsx_without_templates!(
{ (0..5).map(|_| rsx_without_templates! (h1 { }) ) }
p {}
);
let (_create, changes) = dom.diff_lazynodes(left, right);
assert_eq!(
changes.edits,
[
CreateElement { root: Some(4,), tag: "h1", children: 0 },
CreateElement { root: Some(5,), tag: "h1", children: 0 },
CreateElement { root: Some(6,), tag: "h1", children: 0 },
InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] },
CreateElement { root: Some(7,), tag: "h1", children: 0 },
ReplaceWith { root: Some(1,), nodes: vec![7,] }, // notice how 1 gets re-used
CreateElement { root: Some(1,), tag: "h1", children: 0 },
ReplaceWith { root: Some(2,), nodes: vec![1,] },
]
);
}
/// Should result in multiple nodes destroyed - with changes to the first nodes
#[test]
fn two_fragments_with_differrent_elements_are_differet_shorter() {
let dom = new_dom();
let left = rsx_without_templates!(
{(0..5).map(|f| {rsx_without_templates! { div { }}})}
p {}
);
let right = rsx_without_templates!(
{(0..2).map(|f| {rsx_without_templates! { h1 { }}})}
p {}
);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
CreateElement { root: Some(1,), tag: "div", children: 0 },
CreateElement { root: Some(2,), tag: "div", children: 0 },
CreateElement { root: Some(3,), tag: "div", children: 0 },
CreateElement { root: Some(4,), tag: "div", children: 0 },
CreateElement { root: Some(5,), tag: "div", children: 0 },
CreateElement { root: Some(6,), tag: "p", children: 0 },
AppendChildren { root: Some(0,), children: vec![1, 2, 3, 4, 5, 6,] },
]
);
// note: key reuse is always the last node that got used
// slab maintains a linked list, essentially
assert_eq!(
change.edits,
[
Remove { root: Some(3,) },
Remove { root: Some(4,) },
Remove { root: Some(5,) },
CreateElement { root: Some(5,), tag: "h1", children: 0 }, // 5 gets reused
ReplaceWith { root: Some(1,), nodes: vec![5,] }, // 1 gets deleted
CreateElement { root: Some(1,), tag: "h1", children: 0 }, // 1 gets reused
ReplaceWith { root: Some(2,), nodes: vec![1,] },
]
);
}
/// Should result in multiple nodes destroyed - with no changes
#[test]
fn two_fragments_with_same_elements_are_differet() {
let dom = new_dom();
let left = rsx_without_templates!(
{(0..2).map(|f| rsx_without_templates! { div { }})}
p {}
);
let right = rsx_without_templates!(
{(0..5).map(|f| rsx_without_templates! { div { }})}
p {}
);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
CreateElement { root: Some(1,), tag: "div", children: 0 },
CreateElement { root: Some(2,), tag: "div", children: 0 },
CreateElement { root: Some(3,), tag: "p", children: 0 },
AppendChildren { root: Some(0,), children: vec![1, 2, 3,] },
]
);
assert_eq!(
change.edits,
[
CreateElement { root: Some(4,), tag: "div", children: 0 },
CreateElement { root: Some(5,), tag: "div", children: 0 },
CreateElement { root: Some(6,), tag: "div", children: 0 },
InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] },
]
);
}
/// should result in the removal of elements
#[test]
fn keyed_diffing_order() {
let dom = new_dom();
let left = rsx_without_templates!(
{(0..5).map(|f| {rsx_without_templates! { div { key: "{f}" }}})}
p {"e"}
);
let right = rsx_without_templates!(
{(0..2).map(|f| rsx_without_templates! { div { key: "{f}" }})}
p {"e"}
);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[
Remove { root: Some(3,) },
Remove { root: Some(4,) },
Remove { root: Some(5,) },
]
);
}
/// Should result in moves, but not removals or additions
#[test]
fn keyed_diffing_out_of_order() {
let dom = new_dom();
let left = rsx_without_templates!({
[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, changes) = dom.diff_lazynodes(left, right);
assert_eq!(
changes.edits,
[InsertBefore { root: Some(5,), nodes: vec![7,] },]
);
}
/// Should result in moves only
#[test]
fn keyed_diffing_out_of_order_adds() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[InsertBefore { root: Some(1,), nodes: vec![5, 4,] },]
);
}
/// Should result in moves only
#[test]
fn keyed_diffing_out_of_order_adds_2() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[InsertBefore { root: Some(1,), nodes: vec![4, 5,] },]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_3() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[InsertBefore { root: Some(2,), nodes: vec![5, 4,] },]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_4() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[InsertBefore { root: Some(3), nodes: vec![5, 4,] },]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_5() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[InsertBefore { root: Some(4), nodes: vec![5] }]
);
}
#[test]
fn keyed_diffing_additions() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[
CreateElement { root: Some(6,), tag: "div", children: 0 },
CreateElement { root: Some(7,), tag: "div", children: 0 },
InsertAfter { root: Some(5,), nodes: vec![6, 7,] },
]
);
}
#[test]
fn keyed_diffing_additions_and_moves_on_ends() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, change) = dom.diff_lazynodes(left, right);
println!("{:?}", change);
assert_eq!(
change.edits,
[
// create 11, 12
CreateElement { root: Some(5), tag: "div", children: 0 },
CreateElement { root: Some(6), tag: "div", children: 0 },
InsertAfter { root: Some(3), nodes: vec![5, 6] },
// // move 7 to the front
InsertBefore { root: Some(1), nodes: vec![4] }
]
);
}
#[test]
fn keyed_diffing_additions_and_moves_in_middle() {
let dom = new_dom();
let left = rsx_without_templates!({
[/**/ 1, 2, 3, 4 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
// LIS: 4, 5, 6
let (_, change) = dom.diff_lazynodes(left, right);
println!("{:#?}", change);
assert_eq!(
change.edits,
[
// create 5, 6
CreateElement { root: Some(5,), tag: "div", children: 0 },
CreateElement { root: Some(6,), tag: "div", children: 0 },
InsertBefore { root: Some(3,), nodes: vec![5, 6,] },
// create 7, 8
CreateElement { root: Some(7,), tag: "div", children: 0 },
CreateElement { root: Some(8,), tag: "div", children: 0 },
InsertBefore { root: Some(2,), nodes: vec![7, 8,] },
// move 7
InsertBefore { root: Some(1,), nodes: vec![4,] },
]
);
}
#[test]
fn controlled_keyed_diffing_out_of_order() {
let dom = new_dom();
let left = rsx_without_templates!({
[4, 5, 6, 7].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[0, 5, 9, 6, 4].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
// LIS: 5, 6
let (_, changes) = dom.diff_lazynodes(left, right);
println!("{:#?}", &changes);
assert_eq!(
changes.edits,
[
// remove 7
Remove { root: Some(4,) },
// move 4 to after 6
InsertAfter { root: Some(3,), nodes: vec![1,] },
// create 9 and insert before 6
CreateElement { root: Some(4,), tag: "div", children: 0 },
InsertBefore { root: Some(3,), nodes: vec![4,] },
// create 0 and insert before 5
CreateElement { root: Some(5,), tag: "div", children: 0 },
InsertBefore { root: Some(2,), nodes: vec![5,] },
]
);
}
#[test]
fn controlled_keyed_diffing_out_of_order_max_test() {
let dom = new_dom();
let left = rsx_without_templates!({
[0, 1, 2, 3, 4].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx_without_templates!({
[3, 0, 1, 10, 2].iter().map(|f| {
rsx_without_templates! { div { key: "{f}" }}
})
});
let (_, changes) = dom.diff_lazynodes(left, right);
println!("{:#?}", &changes);
assert_eq!(
changes.edits,
[
Remove { root: Some(5,) },
CreateElement { root: Some(5,), tag: "div", children: 0 },
InsertBefore { root: Some(3,), nodes: vec![5,] },
InsertBefore { root: Some(1,), nodes: vec![4,] },
]
);
}
// noticed some weird behavior in the desktop interpreter
// just making sure it doesnt happen in the core implementation
#[test]
fn remove_list() {
let dom = new_dom();
let left = rsx_without_templates!({
(0..10).rev().take(5).map(|i| {
rsx_without_templates! { Fragment { key: "{i}", "{i}" }}
})
});
let right = rsx_without_templates!({
(0..10).rev().take(2).map(|i| {
rsx_without_templates! { Fragment { key: "{i}", "{i}" }}
})
});
let (create, changes) = dom.diff_lazynodes(left, right);
// dbg!(create);
// dbg!(changes);
assert_eq!(
changes.edits,
// remove 5, 4, 3
[
Remove { root: Some(3) },
Remove { root: Some(4) },
Remove { root: Some(5) }
]
);
}
// noticed some weird behavior in the desktop interpreter
// just making sure it doesnt happen in the core implementation
#[test]
fn remove_list_nokeyed() {
let dom = new_dom();
let left = rsx_without_templates!({
(0..10).rev().take(5).map(|i| {
rsx_without_templates! { Fragment { "{i}" }}
})
});
let right = rsx_without_templates!({
(0..10).rev().take(2).map(|i| {
rsx_without_templates! { Fragment { "{i}" }}
})
});
let (create, changes) = dom.diff_lazynodes(left, right);
assert_eq!(
changes.edits,
[
// remove 5, 4, 3
Remove { root: Some(3) },
Remove { root: Some(4) },
Remove { root: Some(5) },
]
);
}
#[test]
fn add_nested_elements() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx_without_templates! {
div{}
},
rsx_without_templates! {
div{
div{}
}
},
);
assert_eq!(
change.edits,
[
CreateElement { root: Some(2), tag: "div", children: 0 },
AppendChildren { root: Some(1), children: vec![2] },
]
);
}
#[test]
fn add_listeners() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx_without_templates! {
div{}
},
rsx_without_templates! {
div{
onkeyup: |_| {},
onkeydown: |_| {},
}
},
);
assert_eq!(
change.edits,
[
NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) },
NewEventListener { event_name: "keydown", scope: ScopeId(0), root: Some(1) },
]
);
}
#[test]
fn remove_listeners() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx_without_templates! {
div{
onkeyup: |_| {},
onkeydown: |_| {},
}
},
rsx_without_templates! {
div{}
},
);
assert_eq!(
change.edits,
[
RemoveEventListener { event: "keyup", root: Some(1) },
RemoveEventListener { event: "keydown", root: Some(1) },
]
);
}
#[test]
fn diff_listeners() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx_without_templates! {
div{
onkeydown: |_| {},
}
},
rsx_without_templates! {
div{
onkeyup: |_| {},
}
},
);
assert_eq!(
change.edits,
[
RemoveEventListener { root: Some(1), event: "keydown" },
NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) }
]
);
}
+78
View File
@@ -0,0 +1,78 @@
use dioxus::core::{ElementId, Mutation};
use dioxus::prelude::*;
fn basic_syntax_is_a_template(cx: Scope) -> Element {
let asd = 123;
let var = 123;
cx.render(rsx! {
div { key: "12345",
class: "asd",
class: "{asd}",
onclick: move |_| {},
div { "{var}" }
div {
h1 { "var" }
p { "you're great!" }
div { background_color: "red",
h1 { "var" }
div { b { "asd" } "not great" }
}
p { "you're great!" }
}
}
})
}
#[test]
fn dual_stream() {
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
let edits = dom.rebuild().santize();
use Mutation::*;
assert_eq!(
edits.template_mutations,
[
CreateElement { name: "div" },
SetStaticAttribute { name: "class", value: "asd", ns: None },
CreateElement { name: "div" },
CreateTextPlaceholder,
AppendChildren { m: 1 },
CreateElement { name: "div" },
CreateElement { name: "h1" },
CreateStaticText { value: "var" },
AppendChildren { m: 1 },
CreateElement { name: "p" },
CreateStaticText { value: "you're great!" },
AppendChildren { m: 1 },
CreateElement { name: "div" },
SetStaticAttribute { name: "background-color", value: "red", ns: Some("style") },
CreateElement { name: "h1" },
CreateStaticText { value: "var" },
AppendChildren { m: 1 },
CreateElement { name: "div" },
CreateElement { name: "b" },
CreateStaticText { value: "asd" },
AppendChildren { m: 1 },
CreateStaticText { value: "not great" },
AppendChildren { m: 2 },
AppendChildren { m: 2 },
CreateElement { name: "p" },
CreateStaticText { value: "you're great!" },
AppendChildren { m: 1 },
AppendChildren { m: 4 },
AppendChildren { m: 2 },
SaveTemplate { name: "template", m: 1 }
],
);
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
NewEventListener { event_name: "click", scope: ScopeId(0), id: ElementId(1) },
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
AppendChildren { m: 1 }
],
);
}
+264
View File
@@ -0,0 +1,264 @@
#![allow(unused, non_upper_case_globals)]
#![allow(non_snake_case)]
//! Tests for the lifecycle of components.
use dioxus::{core_macro::rsx_without_templates, prelude::*};
use dioxus_core::DomEdit::*;
use std::sync::{Arc, Mutex};
type Shared<T> = Arc<Mutex<T>>;
#[test]
fn manual_diffing() {
struct AppProps {
value: Shared<&'static str>,
}
static App: Component<AppProps> = |cx| {
let val = cx.props.value.lock().unwrap();
cx.render(rsx_without_templates! { div { "{val}" } })
};
let value = Arc::new(Mutex::new("Hello"));
let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() });
let _ = dom.rebuild();
*value.lock().unwrap() = "goodbye";
let edits = dom.rebuild();
println!("edits: {:?}", edits);
}
#[test]
fn events_generate() {
fn app(cx: Scope) -> Element {
let count = cx.use_hook(|| 0);
let inner = match *count {
0 => {
rsx_without_templates! {
div {
onclick: move |_| *count += 1,
div {
"nested"
}
"Click me!"
}
}
}
_ => todo!(),
};
cx.render(inner)
};
let mut dom = VirtualDom::new(app);
let mut channel = dom.get_scheduler_channel();
assert!(dom.has_work());
let edits = dom.rebuild();
assert_eq!(
edits.edits,
[
CreateElement { root: Some(1), tag: "div", children: 0 },
NewEventListener { event_name: "click", scope: ScopeId(0), root: Some(1) },
CreateElement { root: Some(2), tag: "div", children: 0 },
CreateTextNode { root: Some(3), text: "nested" },
AppendChildren { root: Some(2), children: vec![3] },
CreateTextNode { root: Some(4), text: "Click me!" },
AppendChildren { root: Some(1), children: vec![2, 4] },
AppendChildren { root: Some(0), children: vec![1] }
]
)
}
#[test]
fn components_generate() {
fn app(cx: Scope) -> Element {
let render_phase = cx.use_hook(|| 0);
*render_phase += 1;
cx.render(match *render_phase {
1 => rsx_without_templates!("Text0"),
2 => rsx_without_templates!(div {}),
3 => rsx_without_templates!("Text2"),
4 => rsx_without_templates!(Child {}),
5 => rsx_without_templates!({ None as Option<()> }),
6 => rsx_without_templates!("text 3"),
7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }),
8 => rsx_without_templates!(Child {}),
_ => todo!(),
})
};
fn Child(cx: Scope) -> Element {
println!("Running child");
cx.render(rsx_without_templates! {
h1 {}
})
}
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild();
assert_eq!(
edits.edits,
[
CreateTextNode { root: Some(1), text: "Text0" },
AppendChildren { root: Some(0), children: vec![1] }
]
);
assert_eq!(
dom.hard_diff(ScopeId(0)).edits,
[
CreateElement { root: Some(2), tag: "div", children: 0 },
ReplaceWith { root: Some(1), nodes: vec![2] }
]
);
assert_eq!(
dom.hard_diff(ScopeId(0)).edits,
[
CreateTextNode { root: Some(1), text: "Text2" },
ReplaceWith { root: Some(2), nodes: vec![1] }
]
);
// child {}
assert_eq!(
dom.hard_diff(ScopeId(0)).edits,
[
CreateElement { root: Some(2), tag: "h1", children: 0 },
ReplaceWith { root: Some(1), nodes: vec![2] }
]
);
// placeholder
assert_eq!(
dom.hard_diff(ScopeId(0)).edits,
[
CreatePlaceholder { root: Some(1) },
ReplaceWith { root: Some(2), nodes: vec![1] }
]
);
assert_eq!(
dom.hard_diff(ScopeId(0)).edits,
[
CreateTextNode { root: Some(2), text: "text 3" },
ReplaceWith { root: Some(1), nodes: vec![2] }
]
);
assert_eq!(
dom.hard_diff(ScopeId(0)).edits,
[
CreateTextNode { text: "text 0", root: Some(1) },
CreateTextNode { text: "text 1", root: Some(3) },
ReplaceWith { root: Some(2), nodes: vec![1, 3] },
]
);
assert_eq!(
dom.hard_diff(ScopeId(0)).edits,
[
CreateElement { tag: "h1", root: Some(2), children: 0 },
ReplaceWith { root: Some(1), nodes: vec![2] },
Remove { root: Some(3) },
]
);
}
#[test]
fn component_swap() {
fn app(cx: Scope) -> Element {
let render_phase = cx.use_hook(|| 0);
*render_phase += 1;
cx.render(match *render_phase {
0 => rsx_without_templates!(
div {
NavBar {}
Dashboard {}
}
),
1 => rsx_without_templates!(
div {
NavBar {}
Results {}
}
),
2 => rsx_without_templates!(
div {
NavBar {}
Dashboard {}
}
),
3 => rsx_without_templates!(
div {
NavBar {}
Results {}
}
),
4 => rsx_without_templates!(
div {
NavBar {}
Dashboard {}
}
),
_ => rsx_without_templates!("blah"),
})
};
static NavBar: Component = |cx| {
println!("running navbar");
cx.render(rsx_without_templates! {
h1 {
"NavBar"
{(0..3).map(|f| rsx_without_templates!(NavLink {}))}
}
})
};
static NavLink: Component = |cx| {
println!("running navlink");
cx.render(rsx_without_templates! {
h1 {
"NavLink"
}
})
};
static Dashboard: Component = |cx| {
println!("running dashboard");
cx.render(rsx_without_templates! {
div {
"dashboard"
}
})
};
static Results: Component = |cx| {
println!("running results");
cx.render(rsx_without_templates! {
div {
"results"
}
})
};
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild();
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
}
+413
View File
@@ -0,0 +1,413 @@
#![allow(non_snake_case)]
/*
Stress Miri as much as possible.
Prove that we don't leak memory and that our methods are safe.
Specifically:
- [ ] VirtualDom drops memory safely
- [ ] Borrowed components don't expose invalid pointers
- [ ] Async isn't busted
*/
use dioxus::prelude::*;
use dioxus_core::SchedulerMsg;
fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
VirtualDom::new_with_props(app, props)
}
/// This test ensures that if a component aborts early, it is replaced with a placeholder.
/// In debug, this should also toss a warning.
#[test]
#[ignore]
fn test_memory_leak() {
fn app(cx: Scope) -> Element {
let val = cx.use_hook(|| 0);
*val += 1;
if *val == 2 || *val == 4 {
return None;
}
let name = cx.use_hook(|| String::from("asd"));
cx.render(rsx!(
div { "Hello, world!" }
Child {}
Child {}
Child {}
Child {}
Child {}
Child {}
BorrowedChild { na: name }
BorrowedChild { na: name }
BorrowedChild { na: name }
BorrowedChild { na: name }
BorrowedChild { na: name }
))
}
#[derive(Props)]
struct BorrowedProps<'a> {
na: &'a str,
}
fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element {
render!(div {
"goodbye {cx.props.na}"
Child {}
Child {}
})
}
fn Child(cx: Scope) -> Element {
render!(div { "goodbye world" })
}
let mut dom = new_dom(app, ());
dom.rebuild();
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
}
#[test]
fn memo_works_properly() {
fn app(cx: Scope) -> Element {
let val = cx.use_hook(|| 0);
*val += 1;
if *val == 2 || *val == 4 {
return None;
}
let name = cx.use_hook(|| String::from("asd"));
cx.render(rsx!(
div { "Hello, world! {name}" }
Child { na: "asdfg".to_string() }
))
}
#[derive(PartialEq, Props)]
struct ChildProps {
na: String,
}
fn Child(cx: Scope<ChildProps>) -> Element {
render!(div { "goodbye world" })
}
let mut dom = new_dom(app, ());
dom.rebuild();
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
}
#[test]
fn free_works_on_root_props() {
fn app(cx: Scope<Custom>) -> Element {
cx.render(rsx! {
Child { a: "alpha"}
Child { a: "beta"}
Child { a: "gamma"}
Child { a: "delta"}
})
}
#[derive(Props, PartialEq)]
struct ChildProps {
a: &'static str,
}
fn Child(cx: Scope<ChildProps>) -> Element {
render!("child {cx.props.a}")
}
struct Custom {
val: String,
}
impl Drop for Custom {
fn drop(&mut self) {
dbg!("dropped! {}", &self.val);
}
}
let mut dom = new_dom(app, Custom { val: String::from("asd") });
dom.rebuild();
}
#[test]
fn free_works_on_borrowed() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Child { a: "alpha", b: "asd".to_string() }
})
}
#[derive(Props)]
struct ChildProps<'a> {
a: &'a str,
b: String,
}
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
dbg!("rendering child");
render!("child {cx.props.a}, {cx.props.b}")
}
impl Drop for ChildProps<'_> {
fn drop(&mut self) {
dbg!("dropped child!");
}
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
}
#[test]
fn free_works_on_root_hooks() {
/*
On Drop, scopearena drops all the hook contents.
*/
struct Droppable<T>(T);
impl<T> Drop for Droppable<T> {
fn drop(&mut self) {
dbg!("dropping droppable");
}
}
fn app(cx: Scope) -> Element {
let name = cx.use_hook(|| Droppable(String::from("asd")));
render!(div { "{name.0}" })
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
}
#[test]
fn old_props_arent_stale() {
fn app(cx: Scope) -> Element {
dbg!("rendering parent");
let cnt = cx.use_hook(|| 0);
*cnt += 1;
if *cnt == 1 {
render!(div { Child { a: "abcdef".to_string() } })
} else {
render!(div { Child { a: "abcdef".to_string() } })
}
}
#[derive(Props, PartialEq)]
struct ChildProps {
a: String,
}
fn Child(cx: Scope<ChildProps>) -> Element {
dbg!("rendering child", &cx.props.a);
render!(div { "child {cx.props.a}" })
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dbg!("forcing update to child");
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
}
#[test]
fn basic() {
fn app(cx: Scope) -> Element {
render!(div {
Child { a: "abcdef".to_string() }
})
}
#[derive(Props, PartialEq)]
struct ChildProps {
a: String,
}
fn Child(cx: Scope<ChildProps>) -> Element {
dbg!("rendering child", &cx.props.a);
render!(div { "child {cx.props.a}" })
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
}
#[test]
fn leak_thru_children() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Child {
name: "asd".to_string(),
}
});
cx.render(rsx! {
div {}
})
}
#[inline_props]
fn Child(cx: Scope, name: String) -> Element {
render!(div { "child {name}" })
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
}
#[test]
fn test_pass_thru() {
#[inline_props]
fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! {
header {
nav { children }
}
})
}
fn NavMenu(cx: Scope) -> Element {
render!( NavBrand {}
div {
NavStart {}
NavEnd {}
}
)
}
fn NavBrand(cx: Scope) -> Element {
render!(div {})
}
fn NavStart(cx: Scope) -> Element {
render!(div {})
}
fn NavEnd(cx: Scope) -> Element {
render!(div {})
}
#[inline_props]
fn MainContainer<'a>(
cx: Scope,
nav: Element<'a>,
body: Element<'a>,
footer: Element<'a>,
) -> Element {
cx.render(rsx! {
div {
class: "columns is-mobile",
div {
class: "column is-full",
nav,
body,
footer,
}
}
})
}
fn app(cx: Scope) -> Element {
let nav = cx.render(rsx! {
NavContainer {
NavMenu {}
}
});
let body = cx.render(rsx! {
div {}
});
let footer = cx.render(rsx! {
div {}
});
cx.render(rsx! {
MainContainer {
nav: nav,
body: body,
footer: footer,
}
})
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
for _ in 0..40 {
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
dom.work_with_deadline(|| false);
}
}
@@ -2,6 +2,7 @@ use dioxus::core::ElementId;
use dioxus::core::{Mutation::*, SuspenseBoundary};
use dioxus::prelude::*;
use dioxus_core::SuspenseContext;
use std::future::IntoFuture;
use std::{rc::Rc, time::Duration};
#[tokio::test]
@@ -63,5 +64,27 @@ async fn async_child(cx: Scope<'_>) -> Element {
}
async fn async_text(cx: Scope<'_>) -> Element {
cx.render(rsx!("async_text"))
let username = use_future!(cx, || async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
"async child 1"
});
let age = use_future!(cx, || async {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
println!("long future completed");
1234
});
let (_user, _age) = use_future!(cx, || async {
tokio::join!(
tokio::time::sleep(std::time::Duration::from_secs(1)),
tokio::time::sleep(std::time::Duration::from_secs(2))
);
("async child 1", 1234)
})
.await;
let (username, age) = tokio::join!(username.into_future(), age.into_future());
cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } ))
}