mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 20:23:09 +00:00
463e67cd12
* fix imports, migrate over rsx-hotreload
1263 lines
30 KiB
Rust
1263 lines
30 KiB
Rust
#![allow(unused)]
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use dioxus_core::{
|
|
internal::{
|
|
FmtSegment, FmtedSegments, HotReloadAttributeValue, HotReloadDynamicAttribute,
|
|
HotReloadDynamicNode, HotReloadLiteral, HotReloadedTemplate, NamedAttribute,
|
|
},
|
|
prelude::{Template, TemplateNode},
|
|
TemplateAttribute, VNode,
|
|
};
|
|
use dioxus_core_types::HotReloadingContext;
|
|
use dioxus_rsx::CallBody;
|
|
use dioxus_rsx_hotreload::{self, diff_rsx, ChangedRsx, HotReloadResult};
|
|
use proc_macro2::TokenStream;
|
|
use quote::{quote, ToTokens};
|
|
use syn::{parse::Parse, spanned::Spanned, token::Token, File};
|
|
|
|
#[derive(Debug)]
|
|
struct Mock;
|
|
|
|
impl HotReloadingContext for Mock {
|
|
fn map_attribute(
|
|
element_name_rust: &str,
|
|
attribute_name_rust: &str,
|
|
) -> Option<(&'static str, Option<&'static str>)> {
|
|
match element_name_rust {
|
|
"svg" => match attribute_name_rust {
|
|
"width" => Some(("width", Some("style"))),
|
|
"height" => Some(("height", Some("style"))),
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
|
|
match element_name_rust {
|
|
"svg" => Some(("svg", Some("svg"))),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn hot_reload_from_tokens(
|
|
old: TokenStream,
|
|
new: TokenStream,
|
|
) -> Option<HashMap<usize, HotReloadedTemplate>> {
|
|
let old: CallBody = syn::parse2(old).unwrap();
|
|
let new: CallBody = syn::parse2(new).unwrap();
|
|
|
|
hotreload_callbody::<Mock>(&old, &new)
|
|
}
|
|
|
|
fn can_hotreload(old: TokenStream, new: TokenStream) -> bool {
|
|
hot_reload_from_tokens(old, new).is_some()
|
|
}
|
|
|
|
fn hotreload_callbody<Ctx: HotReloadingContext>(
|
|
old: &CallBody,
|
|
new: &CallBody,
|
|
) -> Option<HashMap<usize, HotReloadedTemplate>> {
|
|
let results = HotReloadResult::new::<Ctx>(&old.body, &new.body, Default::default())?;
|
|
Some(results.templates)
|
|
}
|
|
|
|
fn callbody_to_template<Ctx: HotReloadingContext>(
|
|
old: &CallBody,
|
|
location: &'static str,
|
|
) -> Option<HotReloadedTemplate> {
|
|
let mut results = HotReloadResult::new::<Ctx>(&old.body, &old.body, Default::default())?;
|
|
Some(results.templates.remove(&0).unwrap())
|
|
}
|
|
|
|
fn base_stream() -> TokenStream {
|
|
quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div { "asasddasdasd" }
|
|
}
|
|
for item in vec![4, 5, 6] {
|
|
div { "asasddasdasd" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn base() -> CallBody {
|
|
syn::parse2(base_stream()).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn simple_for_loop() {
|
|
let old = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div { "asasddasdasd" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let new_valid = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div { "asasddasdasd" }
|
|
div { "123" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let new_invalid = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3, 4] {
|
|
div { "asasddasdasd" }
|
|
div { "123" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let location = "file:line:col:0";
|
|
let old: CallBody = syn::parse2(old).unwrap();
|
|
let new_valid: CallBody = syn::parse2(new_valid).unwrap();
|
|
let new_invalid: CallBody = syn::parse2(new_invalid).unwrap();
|
|
|
|
assert!(hotreload_callbody::<Mock>(&old, &new_valid).is_some());
|
|
assert!(hotreload_callbody::<Mock>(&old, &new_invalid).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn valid_reorder() {
|
|
let old = base();
|
|
let new_valid = quote! {
|
|
div {
|
|
for item in vec![4, 5, 6] {
|
|
span { "asasddasdasd" }
|
|
span { "123" }
|
|
}
|
|
for item in vec![1, 2, 3] {
|
|
div { "asasddasdasd" }
|
|
div { "123" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let new: CallBody = syn::parse2(new_valid).unwrap();
|
|
|
|
let valid = hotreload_callbody::<Mock>(&old, &new);
|
|
assert!(valid.is_some());
|
|
let templates = valid.unwrap();
|
|
|
|
// Currently we return all the templates, even if they didn't change
|
|
assert_eq!(templates.len(), 3);
|
|
|
|
let template = &templates[&0];
|
|
|
|
// It's an inversion, so we should get them in reverse
|
|
assert_eq!(
|
|
template.roots,
|
|
&[TemplateNode::Element {
|
|
tag: "div",
|
|
namespace: None,
|
|
attrs: &[],
|
|
children: &[
|
|
TemplateNode::Dynamic { id: 0 },
|
|
TemplateNode::Dynamic { id: 1 }
|
|
]
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
template.dynamic_nodes,
|
|
&[
|
|
HotReloadDynamicNode::Dynamic(1),
|
|
HotReloadDynamicNode::Dynamic(0)
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_new_node() {
|
|
// Adding a new dynamic node should be hot reloadable as long as the text was present in the old version
|
|
// of the rsx block
|
|
let old = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div { "item is {item}" }
|
|
}
|
|
}
|
|
};
|
|
let new = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div { "item is {item}" }
|
|
div { "item is also {item}" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let templates = hot_reload_from_tokens(old, new).unwrap();
|
|
|
|
// Currently we return all the templates, even if they didn't change
|
|
assert_eq!(templates.len(), 2);
|
|
|
|
let template = &templates[&1];
|
|
|
|
// The new dynamic node should be created from the formatted segments pool
|
|
assert_eq!(
|
|
template.dynamic_nodes,
|
|
&[
|
|
HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
|
|
FmtSegment::Literal { value: "item is " },
|
|
FmtSegment::Dynamic { id: 0 }
|
|
],)),
|
|
HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
|
|
FmtSegment::Literal {
|
|
value: "item is also "
|
|
},
|
|
FmtSegment::Dynamic { id: 0 }
|
|
],)),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_new_dynamic_attribute() {
|
|
// Adding a new dynamic attribute should be hot reloadable as long as the text was present in the old version
|
|
// of the rsx block
|
|
let old = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div {
|
|
class: "item is {item}"
|
|
}
|
|
}
|
|
}
|
|
};
|
|
let new = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div {
|
|
class: "item is {item}"
|
|
}
|
|
div {
|
|
class: "item is also {item}"
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let templates = hot_reload_from_tokens(old, new).unwrap();
|
|
|
|
// Currently we return all the templates, even if they didn't change
|
|
assert_eq!(templates.len(), 2);
|
|
|
|
let template = &templates[&1];
|
|
|
|
// We should have a new dynamic attribute
|
|
assert_eq!(
|
|
template.roots,
|
|
&[
|
|
TemplateNode::Element {
|
|
tag: "div",
|
|
namespace: None,
|
|
attrs: &[TemplateAttribute::Dynamic { id: 0 }],
|
|
children: &[]
|
|
},
|
|
TemplateNode::Element {
|
|
tag: "div",
|
|
namespace: None,
|
|
attrs: &[TemplateAttribute::Dynamic { id: 1 }],
|
|
children: &[]
|
|
}
|
|
]
|
|
);
|
|
|
|
// The new dynamic attribute should be created from the formatted segments pool
|
|
assert_eq!(
|
|
template.dynamic_attributes,
|
|
&[
|
|
HotReloadDynamicAttribute::Named(NamedAttribute::new(
|
|
"class",
|
|
None,
|
|
HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(FmtedSegments::new(
|
|
vec![
|
|
FmtSegment::Literal { value: "item is " },
|
|
FmtSegment::Dynamic { id: 0 }
|
|
],
|
|
)))
|
|
)),
|
|
HotReloadDynamicAttribute::Named(NamedAttribute::new(
|
|
"class",
|
|
None,
|
|
HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(FmtedSegments::new(
|
|
vec![
|
|
FmtSegment::Literal {
|
|
value: "item is also "
|
|
},
|
|
FmtSegment::Dynamic { id: 0 }
|
|
],
|
|
)))
|
|
)),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_move_dynamic_segment_between_nodes() {
|
|
// Hot reloading should let you move around a dynamic formatted segment between nodes
|
|
let old = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div {
|
|
class: "item is {item}"
|
|
}
|
|
}
|
|
}
|
|
};
|
|
let new = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
"item is {item}"
|
|
}
|
|
}
|
|
};
|
|
|
|
let templates = hot_reload_from_tokens(old, new).unwrap();
|
|
|
|
// Currently we return all the templates, even if they didn't change
|
|
assert_eq!(templates.len(), 2);
|
|
|
|
let template = &templates[&1];
|
|
|
|
// We should have a new dynamic node and no attributes
|
|
assert_eq!(template.roots, &[TemplateNode::Dynamic { id: 0 }]);
|
|
|
|
// The new dynamic node should be created from the formatted segments pool
|
|
assert_eq!(
|
|
template.dynamic_nodes,
|
|
&[HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
|
|
FmtSegment::Literal { value: "item is " },
|
|
FmtSegment::Dynamic { id: 0 }
|
|
])),]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_keys() {
|
|
let a = quote! {
|
|
div {
|
|
key: "{value}",
|
|
}
|
|
};
|
|
|
|
// we can clone dynamic nodes to hot reload them
|
|
let b = quote! {
|
|
div {
|
|
key: "{value}-1234",
|
|
}
|
|
};
|
|
|
|
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
|
|
|
|
assert_eq!(hot_reload.len(), 1);
|
|
|
|
let template = &hot_reload[&0];
|
|
|
|
assert_eq!(
|
|
template.key,
|
|
Some(FmtedSegments::new(vec![
|
|
FmtSegment::Dynamic { id: 0 },
|
|
FmtSegment::Literal { value: "-1234" }
|
|
]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_cases() {
|
|
let new_invalid = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3, 4] {
|
|
div { "asasddasdasd" }
|
|
div { "123" }
|
|
}
|
|
for item in vec![4, 5, 6] {
|
|
span { "asasddasdasd" }
|
|
span { "123" }
|
|
}
|
|
}
|
|
};
|
|
|
|
// just remove an entire for loop
|
|
let new_valid_removed = quote! {
|
|
div {
|
|
for item in vec![4, 5, 6] {
|
|
span { "asasddasdasd" }
|
|
span { "123" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let new_invalid_new_dynamic_internal = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div { "asasddasdasd" }
|
|
div { "123" }
|
|
}
|
|
for item in vec![4, 5, 6] {
|
|
span { "asasddasdasd" }
|
|
|
|
// this is a new dynamic node, and thus can't be hot reloaded
|
|
// Eventually we might be able to do a format like this, but not right now
|
|
span { "123 {item}" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let new_invalid_added = quote! {
|
|
div {
|
|
for item in vec![1, 2, 3] {
|
|
div { "asasddasdasd" }
|
|
div { "123" }
|
|
}
|
|
for item in vec![4, 5, 6] {
|
|
span { "asasddasdasd" }
|
|
span { "123" }
|
|
}
|
|
|
|
for item in vec![7, 8, 9] {
|
|
span { "asasddasdasd" }
|
|
span { "123" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let location = "file:line:col:0";
|
|
let old = base();
|
|
|
|
let new_invalid: CallBody = syn::parse2(new_invalid).unwrap();
|
|
let new_valid_removed: CallBody = syn::parse2(new_valid_removed).unwrap();
|
|
let new_invalid_new_dynamic_internal: CallBody =
|
|
syn::parse2(new_invalid_new_dynamic_internal).unwrap();
|
|
let new_invalid_added: CallBody = syn::parse2(new_invalid_added).unwrap();
|
|
|
|
assert!(hotreload_callbody::<Mock>(&old, &new_invalid).is_none());
|
|
assert!(hotreload_callbody::<Mock>(&old, &new_invalid_new_dynamic_internal).is_none());
|
|
|
|
let templates = hotreload_callbody::<Mock>(&old, &new_valid_removed).unwrap();
|
|
|
|
// we don't get the removed template back
|
|
assert_eq!(templates.len(), 2);
|
|
let template = &templates.get(&0).unwrap();
|
|
|
|
// We just completely removed the dynamic node, so it should be a "dud" path and then the placement
|
|
assert_eq!(
|
|
template.roots,
|
|
&[TemplateNode::Element {
|
|
tag: "div",
|
|
namespace: None,
|
|
attrs: &[],
|
|
children: &[TemplateNode::Dynamic { id: 0 }]
|
|
}]
|
|
);
|
|
assert_eq!(template.dynamic_nodes, &[HotReloadDynamicNode::Dynamic(1)]);
|
|
|
|
// Adding a new dynamic node should not be hot reloadable
|
|
let added = hotreload_callbody::<Mock>(&old, &new_invalid_added);
|
|
assert!(added.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn attributes_reload() {
|
|
let old = quote! {
|
|
div {
|
|
class: "{class}",
|
|
id: "{id}",
|
|
name: "name",
|
|
}
|
|
};
|
|
|
|
// Same order, just different contents
|
|
let new_valid_internal = quote! {
|
|
div {
|
|
id: "{id}",
|
|
name: "name",
|
|
class: "{class}"
|
|
}
|
|
};
|
|
|
|
let templates = hot_reload_from_tokens(old, new_valid_internal).unwrap();
|
|
|
|
dbg!(templates);
|
|
}
|
|
|
|
#[test]
|
|
fn template_generates() {
|
|
let old = quote! {
|
|
svg {
|
|
width: 100,
|
|
height: "100px",
|
|
"width2": 100,
|
|
"height2": "100px",
|
|
p { "hello world" }
|
|
{(0..10).map(|i| rsx! {"{i}"})}
|
|
}
|
|
div {
|
|
width: 120,
|
|
div {
|
|
height: "100px",
|
|
"width2": 130,
|
|
"height2": "100px",
|
|
for i in 0..10 {
|
|
div {
|
|
"asdasd"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let old: CallBody = syn::parse2(old).unwrap();
|
|
let template = callbody_to_template::<Mock>(&old, "file:line:col:0");
|
|
}
|
|
|
|
#[test]
|
|
fn diffs_complex() {
|
|
#[allow(unused, non_snake_case)]
|
|
fn Comp() -> dioxus_core::Element {
|
|
VNode::empty()
|
|
}
|
|
|
|
let old = quote! {
|
|
svg {
|
|
width: 100,
|
|
height: "100px",
|
|
"width2": 100,
|
|
"height2": "100px",
|
|
p { "hello world" }
|
|
{(0..10).map(|i| rsx! {"{i}"})},
|
|
{(0..10).map(|i| rsx! {"{i}"})},
|
|
{(0..11).map(|i| rsx! {"{i}"})},
|
|
Comp {}
|
|
}
|
|
};
|
|
|
|
// scrambling the attributes should not cause a full rebuild
|
|
let new = quote! {
|
|
div {
|
|
width: 100,
|
|
height: "100px",
|
|
"width2": 100,
|
|
"height2": "100px",
|
|
p { "hello world" }
|
|
Comp {}
|
|
{(0..10).map(|i| rsx! {"{i}"})},
|
|
{(0..10).map(|i| rsx! {"{i}"})},
|
|
{(0..11).map(|i| rsx! {"{i}"})},
|
|
}
|
|
};
|
|
|
|
let old: CallBody = syn::parse2(old).unwrap();
|
|
let new: CallBody = syn::parse2(new).unwrap();
|
|
|
|
let templates = hotreload_callbody::<Mock>(&old, &new).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn remove_node() {
|
|
let valid = hot_reload_from_tokens(
|
|
quote! {
|
|
svg {
|
|
Comp {}
|
|
{(0..10).map(|i| rsx! {"{i}"})},
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
{(0..10).map(|i| rsx! {"{i}"})},
|
|
}
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
dbg!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn if_chains() {
|
|
let valid = hot_reload_from_tokens(
|
|
quote! {
|
|
if cond {
|
|
"foo"
|
|
}
|
|
},
|
|
quote! {
|
|
if cond {
|
|
"baz"
|
|
}
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
let very_complex_chain = hot_reload_from_tokens(
|
|
quote! {
|
|
if cond {
|
|
if second_cond {
|
|
"foo"
|
|
}
|
|
} else if othercond {
|
|
"bar"
|
|
} else {
|
|
"baz"
|
|
}
|
|
},
|
|
quote! {
|
|
if cond {
|
|
if second_cond {
|
|
span { "asasddasdasd 789" }
|
|
}
|
|
} else if othercond {
|
|
span { "asasddasdasd 123" }
|
|
} else {
|
|
span { "asasddasdas 456" }
|
|
}
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
dbg!(very_complex_chain);
|
|
}
|
|
|
|
#[test]
|
|
fn component_bodies() {
|
|
let valid = can_hotreload(
|
|
quote! {
|
|
Comp {
|
|
"foo"
|
|
}
|
|
},
|
|
quote! {
|
|
Comp {
|
|
"baz"
|
|
}
|
|
},
|
|
);
|
|
|
|
assert!(valid);
|
|
}
|
|
|
|
// We currently don't track aliasing which means we can't allow dynamic nodes/formatted segments to be moved between scopes
|
|
#[test]
|
|
fn moving_between_scopes() {
|
|
let valid = can_hotreload(
|
|
quote! {
|
|
for x in 0..10 {
|
|
for y in 0..10 {
|
|
div { "x is {x}" }
|
|
}
|
|
}
|
|
},
|
|
quote! {
|
|
for x in 0..10 {
|
|
div { "x is {x}" }
|
|
}
|
|
},
|
|
);
|
|
|
|
assert!(!valid);
|
|
}
|
|
|
|
/// Everything reloads!
|
|
#[test]
|
|
fn kitch_sink_of_reloadability() {
|
|
let valid = hot_reload_from_tokens(
|
|
quote! {
|
|
div {
|
|
for i in 0..10 {
|
|
div { "123" }
|
|
Comp {
|
|
"foo"
|
|
}
|
|
if cond {
|
|
"foo"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
"hi!"
|
|
for i in 0..10 {
|
|
div { "456" }
|
|
Comp { "bar" }
|
|
if cond {
|
|
"baz"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
dbg!(valid);
|
|
}
|
|
|
|
/// Moving nodes inbetween multiple rsx! calls currently doesn't work
|
|
/// Sad. Needs changes to core to work, and is technically flawed?
|
|
#[test]
|
|
fn entire_kitchen_sink() {
|
|
let valid = hot_reload_from_tokens(
|
|
quote! {
|
|
div {
|
|
for i in 0..10 {
|
|
div { "123" }
|
|
}
|
|
Comp {
|
|
"foo"
|
|
}
|
|
if cond {
|
|
"foo"
|
|
}
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
"hi!"
|
|
Comp {
|
|
for i in 0..10 {
|
|
div { "456" }
|
|
}
|
|
"bar"
|
|
if cond {
|
|
"baz"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
assert!(valid.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn tokenstreams_and_locations() {
|
|
let valid = hot_reload_from_tokens(
|
|
quote! {
|
|
div { "hhi" }
|
|
div {
|
|
{rsx! { "hi again!" }},
|
|
for i in 0..2 {
|
|
"first"
|
|
div { "hi {i}" }
|
|
}
|
|
|
|
for i in 0..3 {
|
|
"Second"
|
|
div { "hi {i}" }
|
|
}
|
|
|
|
if false {
|
|
div { "hi again!?" }
|
|
} else if true {
|
|
div { "its cool?" }
|
|
} else {
|
|
div { "not nice !" }
|
|
}
|
|
}
|
|
},
|
|
quote! {
|
|
div { "hhi" }
|
|
div {
|
|
{rsx! { "hi again!" }},
|
|
for i in 0..2 {
|
|
"first"
|
|
div { "hi {i}" }
|
|
}
|
|
|
|
for i in 0..3 {
|
|
"Second"
|
|
div { "hi {i}" }
|
|
}
|
|
|
|
if false {
|
|
div { "hi again?" }
|
|
} else if true {
|
|
div { "cool?" }
|
|
} else {
|
|
div { "nice !" }
|
|
}
|
|
}
|
|
|
|
},
|
|
);
|
|
|
|
dbg!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn ide_testcase() {
|
|
let valid = hot_reload_from_tokens(
|
|
quote! {
|
|
div {
|
|
div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
|
|
for x in 0..5 {
|
|
h3 { "For loop contents" }
|
|
}
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
|
|
for x in 0..5 {
|
|
h3 { "For loop contents" }
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
dbg!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn assigns_ids() {
|
|
let toks = quote! {
|
|
div {
|
|
div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
|
|
for x in 0..5 {
|
|
h3 { "For loop contents" }
|
|
}
|
|
}
|
|
};
|
|
|
|
let parsed = syn::parse2::<CallBody>(toks).unwrap();
|
|
|
|
let node = parsed.body.get_dyn_node(&[0, 1]);
|
|
dbg!(node);
|
|
}
|
|
|
|
#[test]
|
|
fn simple_start() {
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! {
|
|
div {
|
|
class: "Some {one}",
|
|
id: "Something {two}",
|
|
"One"
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
id: "Something {two}",
|
|
class: "Some {one}",
|
|
"One"
|
|
}
|
|
},
|
|
);
|
|
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn complex_cases() {
|
|
let valid = can_hotreload(
|
|
quote! {
|
|
div {
|
|
class: "Some {one}",
|
|
id: "Something {two}",
|
|
"One"
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
class: "Some {one}",
|
|
id: "Something else {two}",
|
|
"One"
|
|
}
|
|
},
|
|
);
|
|
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_cases() {
|
|
let valid = can_hotreload(
|
|
quote! {
|
|
div {
|
|
class: "Some {one}",
|
|
id: "Something {two}",
|
|
"One"
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
id: "Something {two}",
|
|
"One"
|
|
}
|
|
},
|
|
);
|
|
assert!(valid);
|
|
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! { div { class: 123 } },
|
|
quote! { div { class: 456 } },
|
|
);
|
|
assert!(valid);
|
|
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! { div { class: 123.0 } },
|
|
quote! { div { class: 456.0 } },
|
|
);
|
|
assert!(valid);
|
|
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! { div { class: "asd {123}", } },
|
|
quote! { div { class: "def", } },
|
|
);
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn text_node_cases() {
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! { div { "hello {world}" } },
|
|
quote! { div { "world {world}" } },
|
|
);
|
|
assert!(valid);
|
|
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! { div { "hello {world}" } },
|
|
quote! { div { "world" } },
|
|
);
|
|
assert!(valid);
|
|
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! { div { "hello {world}" } },
|
|
quote! { div { "world {world} {world}" } },
|
|
);
|
|
assert!(valid);
|
|
|
|
let valid = can_hotreload(
|
|
//
|
|
quote! { div { "hello" } },
|
|
quote! { div { "world {world}" } },
|
|
);
|
|
assert!(!valid);
|
|
}
|
|
|
|
#[test]
|
|
fn simple_carry() {
|
|
let a = quote! {
|
|
// start with
|
|
"thing {abc} {def}" // 1, 1, 1
|
|
"thing {def}" // 1, 0, 1
|
|
"other {hij}" // 1, 1, 1
|
|
};
|
|
|
|
let b = quote! {
|
|
// end with
|
|
"thing {def}"
|
|
"thing {abc}"
|
|
"thing {hij}"
|
|
};
|
|
|
|
let valid = can_hotreload(a, b);
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn complex_carry_text() {
|
|
let a = quote! {
|
|
// start with
|
|
"thing {abc} {def}" // 1, 1, 1
|
|
"thing {abc}" // 1, 0, 1
|
|
"other {abc} {def} {hij}" // 1, 1, 1
|
|
};
|
|
|
|
let b = quote! {
|
|
// end with
|
|
"thing {abc}"
|
|
"thing {hij}"
|
|
};
|
|
|
|
let valid = can_hotreload(a, b);
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn complex_carry() {
|
|
let a = quote! {
|
|
Component {
|
|
class: "thing {abc}",
|
|
other: "other {abc} {def}",
|
|
}
|
|
Component {
|
|
class: "thing {abc}",
|
|
other: "other",
|
|
}
|
|
};
|
|
|
|
let b = quote! {
|
|
// how about shuffling components, for, if, etc
|
|
Component {
|
|
class: "thing {abc}",
|
|
other: "other {abc} {def}",
|
|
}
|
|
Component {
|
|
class: "thing",
|
|
other: "other",
|
|
}
|
|
};
|
|
|
|
let valid = can_hotreload(a, b);
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn component_with_lits() {
|
|
let a = quote! {
|
|
Component {
|
|
class: 123,
|
|
id: 456.789,
|
|
other: true,
|
|
blah: "hello {world}",
|
|
}
|
|
};
|
|
|
|
// changing lit values
|
|
let b = quote! {
|
|
Component {
|
|
class: 456,
|
|
id: 789.456,
|
|
other: false,
|
|
blah: "goodbye {world}",
|
|
}
|
|
};
|
|
|
|
let valid = can_hotreload(a, b);
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn component_with_handlers() {
|
|
let a = quote! {
|
|
Component {
|
|
class: 123,
|
|
id: 456.789,
|
|
other: true,
|
|
blah: "hello {world}",
|
|
onclick: |e| { println!("clicked") },
|
|
}
|
|
};
|
|
|
|
// changing lit values
|
|
let b = quote! {
|
|
Component {
|
|
class: 456,
|
|
id: 789.456,
|
|
other: false,
|
|
blah: "goodbye {world}",
|
|
onclick: |e| { println!("clicked") },
|
|
}
|
|
};
|
|
|
|
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
|
|
let template = hot_reload.get(&0).unwrap();
|
|
assert_eq!(
|
|
template.component_values,
|
|
&[
|
|
HotReloadLiteral::Int(456),
|
|
HotReloadLiteral::Float(789.456),
|
|
HotReloadLiteral::Bool(false),
|
|
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
|
|
FmtSegment::Literal { value: "goodbye " },
|
|
FmtSegment::Dynamic { id: 0 }
|
|
])),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn component_remove_key() {
|
|
let a = quote! {
|
|
Component {
|
|
key: "{key}",
|
|
class: 123,
|
|
id: 456.789,
|
|
other: true,
|
|
dynamic1,
|
|
dynamic2,
|
|
blah: "hello {world}",
|
|
onclick: |e| { println!("clicked") },
|
|
}
|
|
};
|
|
|
|
// changing lit values
|
|
let b = quote! {
|
|
Component {
|
|
class: 456,
|
|
id: 789.456,
|
|
other: false,
|
|
dynamic1,
|
|
dynamic2,
|
|
blah: "goodbye {world}",
|
|
onclick: |e| { println!("clicked") },
|
|
}
|
|
};
|
|
|
|
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
|
|
let template = hot_reload.get(&0).unwrap();
|
|
assert_eq!(
|
|
template.component_values,
|
|
&[
|
|
HotReloadLiteral::Int(456),
|
|
HotReloadLiteral::Float(789.456),
|
|
HotReloadLiteral::Bool(false),
|
|
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
|
|
FmtSegment::Literal { value: "goodbye " },
|
|
FmtSegment::Dynamic { id: 1 }
|
|
]))
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn component_modify_key() {
|
|
let a = quote! {
|
|
Component {
|
|
key: "{key}",
|
|
class: 123,
|
|
id: 456.789,
|
|
other: true,
|
|
dynamic1,
|
|
dynamic2,
|
|
blah1: "hello {world123}",
|
|
blah2: "hello {world}",
|
|
onclick: |e| { println!("clicked") },
|
|
}
|
|
};
|
|
|
|
// changing lit values
|
|
let b = quote! {
|
|
Component {
|
|
key: "{key}-{world}",
|
|
class: 456,
|
|
id: 789.456,
|
|
other: false,
|
|
dynamic1,
|
|
dynamic2,
|
|
blah1: "hello {world123}",
|
|
blah2: "hello {world}",
|
|
onclick: |e| { println!("clicked") },
|
|
}
|
|
};
|
|
|
|
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
|
|
let template = hot_reload.get(&0).unwrap();
|
|
assert_eq!(
|
|
template.key,
|
|
Some(FmtedSegments::new(vec![
|
|
FmtSegment::Dynamic { id: 0 },
|
|
FmtSegment::Literal { value: "-" },
|
|
FmtSegment::Dynamic { id: 2 },
|
|
]))
|
|
);
|
|
assert_eq!(
|
|
template.component_values,
|
|
&[
|
|
HotReloadLiteral::Int(456),
|
|
HotReloadLiteral::Float(789.456),
|
|
HotReloadLiteral::Bool(false),
|
|
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
|
|
FmtSegment::Literal { value: "hello " },
|
|
FmtSegment::Dynamic { id: 1 }
|
|
])),
|
|
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
|
|
FmtSegment::Literal { value: "hello " },
|
|
FmtSegment::Dynamic { id: 2 }
|
|
]))
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicating_dynamic_nodes() {
|
|
let a = quote! {
|
|
div {
|
|
{some_expr}
|
|
}
|
|
};
|
|
|
|
// we can clone dynamic nodes to hot reload them
|
|
let b = quote! {
|
|
div {
|
|
{some_expr}
|
|
{some_expr}
|
|
}
|
|
};
|
|
|
|
let valid = can_hotreload(a, b);
|
|
assert!(valid);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicating_dynamic_attributes() {
|
|
let a = quote! {
|
|
div {
|
|
width: value,
|
|
}
|
|
};
|
|
|
|
// we can clone dynamic nodes to hot reload them
|
|
let b = quote! {
|
|
div {
|
|
width: value,
|
|
height: value,
|
|
}
|
|
};
|
|
|
|
let valid = can_hotreload(a, b);
|
|
assert!(valid);
|
|
}
|
|
|
|
// We should be able to fill in empty nodes
|
|
#[test]
|
|
fn valid_fill_empty() {
|
|
let valid = can_hotreload(
|
|
quote! {},
|
|
quote! {
|
|
div { "x is 123" }
|
|
},
|
|
);
|
|
|
|
assert!(valid);
|
|
}
|
|
|
|
// We should be able to hot reload spreads
|
|
#[test]
|
|
fn valid_spread() {
|
|
let valid = can_hotreload(
|
|
quote! {
|
|
div {
|
|
..spread
|
|
}
|
|
},
|
|
quote! {
|
|
div {
|
|
"hello world"
|
|
}
|
|
h1 {
|
|
..spread
|
|
}
|
|
},
|
|
);
|
|
|
|
assert!(valid);
|
|
}
|