mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-26 12:27:22 +00:00
932 lines
28 KiB
Rust
932 lines
28 KiB
Rust
use dioxus::prelude::*;
|
|
use dioxus_core::{AttributeValue, ElementId, Mutation};
|
|
use pretty_assertions::assert_eq;
|
|
use std::future::poll_fn;
|
|
use std::task::Poll;
|
|
|
|
async fn poll_three_times() {
|
|
// Poll each task 3 times
|
|
let mut count = 0;
|
|
poll_fn(|cx| {
|
|
println!("polling... {}", count);
|
|
if count < 3 {
|
|
count += 1;
|
|
cx.waker().wake_by_ref();
|
|
Poll::Pending
|
|
} else {
|
|
Poll::Ready(())
|
|
}
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[test]
|
|
fn suspense_resolves_ssr() {
|
|
// wait just a moment, not enough time for the boundary to resolve
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
dom.rebuild_in_place();
|
|
dom.wait_for_suspense().await;
|
|
dom.render_immediate(&mut dioxus_core::NoOpMutations);
|
|
let out = dioxus_ssr::render(&dom);
|
|
|
|
assert_eq!(out, "<div>Waiting for... child</div>");
|
|
});
|
|
}
|
|
|
|
fn app() -> Element {
|
|
rsx!(
|
|
div {
|
|
"Waiting for... "
|
|
SuspenseBoundary {
|
|
fallback: |_| rsx! { "fallback" },
|
|
suspended_child {}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
fn suspended_child() -> Element {
|
|
let mut val = use_signal(|| 0);
|
|
|
|
// Tasks that are not suspended should never be polled
|
|
spawn(async move {
|
|
panic!("Non-suspended task was polled");
|
|
});
|
|
|
|
// Memos should still work like normal
|
|
let memo = use_memo(move || val * 2);
|
|
assert_eq!(memo, val * 2);
|
|
|
|
if val() < 3 {
|
|
let task = spawn(async move {
|
|
poll_three_times().await;
|
|
println!("waiting... {}", val);
|
|
val += 1;
|
|
});
|
|
suspend(task)?;
|
|
}
|
|
|
|
rsx!("child")
|
|
}
|
|
|
|
/// When switching from a suspense fallback to the real child, the state of that component must be kept
|
|
#[test]
|
|
fn suspense_keeps_state() {
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_time()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
|
dom.render_suspense_immediate().await;
|
|
|
|
let out = dioxus_ssr::render(&dom);
|
|
|
|
assert_eq!(out, "fallback");
|
|
|
|
dom.wait_for_suspense().await;
|
|
let out = dioxus_ssr::render(&dom);
|
|
|
|
assert_eq!(out, "<div>child with future resolved</div>");
|
|
});
|
|
|
|
fn app() -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: |_| rsx! { "fallback" },
|
|
Child {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn Child() -> Element {
|
|
let mut future_resolved = use_signal(|| false);
|
|
|
|
let task = use_hook(|| {
|
|
spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
future_resolved.set(true);
|
|
})
|
|
});
|
|
if !future_resolved() {
|
|
suspend(task)?;
|
|
}
|
|
|
|
println!("future resolved: {future_resolved:?}");
|
|
|
|
if future_resolved() {
|
|
rsx! {
|
|
div { "child with future resolved" }
|
|
}
|
|
} else {
|
|
rsx! {
|
|
div { "this should never be rendered" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// spawn doesn't run in suspense
|
|
#[test]
|
|
fn suspense_does_not_poll_spawn() {
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_time()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
|
|
|
dom.wait_for_suspense().await;
|
|
let out = dioxus_ssr::render(&dom);
|
|
|
|
assert_eq!(out, "<div>child with future resolved</div>");
|
|
});
|
|
|
|
fn app() -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: |_| rsx! { "fallback" },
|
|
Child {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn Child() -> Element {
|
|
let mut future_resolved = use_signal(|| false);
|
|
|
|
// futures that are spawned, but not suspended should never be polled
|
|
use_hook(|| {
|
|
spawn(async move {
|
|
panic!("Non-suspended task was polled");
|
|
});
|
|
});
|
|
|
|
let task = use_hook(|| {
|
|
spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
future_resolved.set(true);
|
|
})
|
|
});
|
|
if !future_resolved() {
|
|
suspend(task)?;
|
|
}
|
|
|
|
rsx! {
|
|
div { "child with future resolved" }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// suspended nodes are not mounted, so they should not run effects
|
|
#[test]
|
|
fn suspended_nodes_dont_trigger_effects() {
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_time()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
|
|
|
let work = async move {
|
|
loop {
|
|
dom.wait_for_work().await;
|
|
dom.render_immediate(&mut dioxus_core::NoOpMutations);
|
|
}
|
|
};
|
|
tokio::select! {
|
|
_ = work => {},
|
|
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
|
|
}
|
|
});
|
|
|
|
fn app() -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: |_| rsx! { "fallback" },
|
|
Child {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn RerendersFrequently() -> Element {
|
|
let mut count = use_signal(|| 0);
|
|
|
|
use_future(move || async move {
|
|
for _ in 0..100 {
|
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
count.set(count() + 1);
|
|
}
|
|
});
|
|
|
|
rsx! {
|
|
div { "rerenders frequently" }
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn Child() -> Element {
|
|
let mut future_resolved = use_signal(|| false);
|
|
|
|
use_effect(|| panic!("effects should not run during suspense"));
|
|
|
|
let task = use_hook(|| {
|
|
spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
|
future_resolved.set(true);
|
|
})
|
|
});
|
|
if !future_resolved() {
|
|
suspend(task)?;
|
|
}
|
|
|
|
rsx! {
|
|
div { "child with future resolved" }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Make sure we keep any state of components when we switch from a resolved future to a suspended future
|
|
#[test]
|
|
fn resolved_to_suspended() {
|
|
static SUSPENDED: GlobalSignal<bool> = Signal::global(|| false);
|
|
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_time()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
|
|
|
let out = dioxus_ssr::render(&dom);
|
|
|
|
assert_eq!(out, "rendered 1 times");
|
|
|
|
dom.in_runtime(|| ScopeId::APP.in_runtime(|| *SUSPENDED.write() = true));
|
|
|
|
dom.render_suspense_immediate().await;
|
|
let out = dioxus_ssr::render(&dom);
|
|
|
|
assert_eq!(out, "fallback");
|
|
|
|
dom.wait_for_suspense().await;
|
|
let out = dioxus_ssr::render(&dom);
|
|
|
|
assert_eq!(out, "rendered 3 times");
|
|
});
|
|
|
|
fn app() -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: |_| rsx! { "fallback" },
|
|
Child {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn Child() -> Element {
|
|
let mut render_count = use_signal(|| 0);
|
|
render_count += 1;
|
|
|
|
let mut task = use_hook(|| CopyValue::new(None));
|
|
|
|
tracing::info!("render_count: {}", render_count.peek());
|
|
|
|
if SUSPENDED() {
|
|
if task().is_none() {
|
|
task.set(Some(spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
tracing::info!("task finished");
|
|
*SUSPENDED.write() = false;
|
|
})));
|
|
}
|
|
suspend(task().unwrap())?;
|
|
}
|
|
|
|
rsx! {
|
|
"rendered {render_count.peek()} times"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Make sure suspense tells the renderer that a suspense boundary was resolved
|
|
#[test]
|
|
fn suspense_tracks_resolved() {
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_time()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
|
|
|
dom.render_suspense_immediate().await;
|
|
dom.wait_for_suspense_work().await;
|
|
assert_eq!(
|
|
dom.render_suspense_immediate().await,
|
|
vec![ScopeId(ScopeId::APP.0 + 1)]
|
|
);
|
|
});
|
|
|
|
fn app() -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: |_| rsx! { "fallback" },
|
|
Child {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn Child() -> Element {
|
|
let mut resolved = use_signal(|| false);
|
|
let task = use_hook(|| {
|
|
spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
tracing::info!("task finished");
|
|
resolved.set(true);
|
|
})
|
|
});
|
|
|
|
if resolved() {
|
|
println!("suspense is resolved");
|
|
} else {
|
|
println!("suspense is not resolved");
|
|
suspend(task)?;
|
|
}
|
|
|
|
rsx! {
|
|
"child"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regression test for https://github.com/DioxusLabs/dioxus/issues/2783
|
|
#[test]
|
|
fn toggle_suspense() {
|
|
use dioxus::prelude::*;
|
|
|
|
fn app() -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: |_| rsx! { "fallback" },
|
|
if generation() % 2 == 0 {
|
|
Page {}
|
|
} else {
|
|
Home {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
pub fn Home() -> Element {
|
|
let _calculation = use_resource(|| async move {
|
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
1 + 1
|
|
})
|
|
.suspend()?;
|
|
rsx! {
|
|
"hello world"
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
pub fn Page() -> Element {
|
|
rsx! {
|
|
"goodbye world"
|
|
}
|
|
}
|
|
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_time()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
let mutations = dom.rebuild_to_vec();
|
|
|
|
// First create goodbye world
|
|
println!("{:#?}", mutations);
|
|
assert_eq!(
|
|
mutations.edits,
|
|
[
|
|
Mutation::LoadTemplate { index: 0, id: ElementId(1) },
|
|
Mutation::AppendChildren { id: ElementId(0), m: 1 }
|
|
]
|
|
);
|
|
|
|
dom.mark_dirty(ScopeId::APP);
|
|
let mutations = dom.render_immediate_to_vec();
|
|
|
|
// Then replace that with nothing
|
|
println!("{:#?}", mutations);
|
|
assert_eq!(
|
|
mutations.edits,
|
|
[
|
|
Mutation::CreatePlaceholder { id: ElementId(2) },
|
|
Mutation::ReplaceWith { id: ElementId(1), m: 1 },
|
|
]
|
|
);
|
|
|
|
dom.wait_for_work().await;
|
|
let mutations = dom.render_immediate_to_vec();
|
|
|
|
// Then replace it with a placeholder
|
|
println!("{:#?}", mutations);
|
|
assert_eq!(
|
|
mutations.edits,
|
|
[
|
|
Mutation::LoadTemplate { index: 0, id: ElementId(1) },
|
|
Mutation::ReplaceWith { id: ElementId(2), m: 1 },
|
|
]
|
|
);
|
|
|
|
dom.wait_for_work().await;
|
|
let mutations = dom.render_immediate_to_vec();
|
|
|
|
// Then replace it with the resolved node
|
|
println!("{:#?}", mutations);
|
|
assert_eq!(
|
|
mutations.edits,
|
|
[
|
|
Mutation::CreatePlaceholder { id: ElementId(2,) },
|
|
Mutation::ReplaceWith { id: ElementId(1,), m: 1 },
|
|
Mutation::LoadTemplate { index: 0, id: ElementId(1) },
|
|
Mutation::ReplaceWith { id: ElementId(2), m: 1 },
|
|
]
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn nested_suspense_resolves_client() {
|
|
use Mutation::*;
|
|
|
|
async fn poll_three_times() {
|
|
// Poll each task 3 times
|
|
let mut count = 0;
|
|
poll_fn(|cx| {
|
|
println!("polling... {}", count);
|
|
if count < 3 {
|
|
count += 1;
|
|
cx.waker().wake_by_ref();
|
|
Poll::Pending
|
|
} else {
|
|
Poll::Ready(())
|
|
}
|
|
})
|
|
.await;
|
|
}
|
|
|
|
fn app() -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: move |_| rsx! {},
|
|
LoadTitle {}
|
|
}
|
|
MessageWithLoader { id: 0 }
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn MessageWithLoader(id: usize) -> Element {
|
|
rsx! {
|
|
SuspenseBoundary {
|
|
fallback: move |_| rsx! {
|
|
"Loading {id}..."
|
|
},
|
|
Message { id }
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn LoadTitle() -> Element {
|
|
let title = use_resource(move || async_content(0)).suspend()?();
|
|
|
|
rsx! {
|
|
document::Title { "{title.title}" }
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn Message(id: usize) -> Element {
|
|
let message = use_resource(move || async_content(id)).suspend()?();
|
|
|
|
rsx! {
|
|
h2 {
|
|
id: "title-{id}",
|
|
"{message.title}"
|
|
}
|
|
p {
|
|
id: "body-{id}",
|
|
"{message.body}"
|
|
}
|
|
div {
|
|
id: "children-{id}",
|
|
padding: "10px",
|
|
for child in message.children {
|
|
MessageWithLoader { id: child }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Content {
|
|
title: String,
|
|
body: String,
|
|
children: Vec<usize>,
|
|
}
|
|
|
|
async fn async_content(id: usize) -> Content {
|
|
let content_tree = [
|
|
Content {
|
|
title: "The robot says hello world".to_string(),
|
|
body: "The robot becomes sentient and says hello world".to_string(),
|
|
children: vec![1, 2],
|
|
},
|
|
Content {
|
|
title: "The world says hello back".to_string(),
|
|
body: "In a stunning turn of events, the world collectively unites and says hello back"
|
|
.to_string(),
|
|
children: vec![],
|
|
},
|
|
Content {
|
|
title: "Goodbye Robot".to_string(),
|
|
body: "The robot says goodbye".to_string(),
|
|
children: vec![3],
|
|
},
|
|
Content {
|
|
title: "Goodbye Robot again".to_string(),
|
|
body: "The robot says goodbye again".to_string(),
|
|
children: vec![],
|
|
},
|
|
];
|
|
poll_three_times().await;
|
|
content_tree[id].clone()
|
|
}
|
|
|
|
// wait just a moment, not enough time for the boundary to resolve
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async {
|
|
let mut dom = VirtualDom::new(app);
|
|
let mutations = dom.rebuild_to_vec();
|
|
// Initial loading message and loading title
|
|
assert_eq!(
|
|
mutations.edits,
|
|
vec![
|
|
CreatePlaceholder { id: ElementId(1,) },
|
|
CreateTextNode { value: "Loading 0...".to_string(), id: ElementId(2,) },
|
|
AppendChildren { id: ElementId(0,), m: 2 },
|
|
]
|
|
);
|
|
|
|
dom.wait_for_work().await;
|
|
// DOM STATE:
|
|
// placeholder // ID: 1
|
|
// "Loading 0..." // ID: 2
|
|
let mutations = dom.render_immediate_to_vec();
|
|
// Fill in the contents of the initial message and start loading the nested suspense
|
|
// The title also finishes loading
|
|
assert_eq!(
|
|
mutations.edits,
|
|
vec![
|
|
// Creating and swapping these placeholders doesn't do anything
|
|
// It is just extra work that we are forced to do because mutations are not
|
|
// reversible. We start rendering the children and then realize it is suspended.
|
|
// Then we need to replace what we just rendered with the suspense placeholder
|
|
CreatePlaceholder { id: ElementId(3,) },
|
|
ReplaceWith { id: ElementId(1,), m: 1 },
|
|
|
|
// Replace the pending placeholder with the title placeholder
|
|
CreatePlaceholder { id: ElementId(1,) },
|
|
ReplaceWith { id: ElementId(3,), m: 1 },
|
|
|
|
// Replace loading... with a placeholder for us to fill in later
|
|
CreatePlaceholder { id: ElementId(3,) },
|
|
ReplaceWith { id: ElementId(2,), m: 1 },
|
|
|
|
// Load the title
|
|
LoadTemplate { index: 0, id: ElementId(2,) },
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("title-0".to_string()),
|
|
id: ElementId(2,),
|
|
},
|
|
CreateTextNode { value: "The robot says hello world".to_string(), id: ElementId(4,) },
|
|
ReplacePlaceholder { path: &[0,], m: 1 },
|
|
|
|
// Then load the body
|
|
LoadTemplate { index: 1, id: ElementId(5,) },
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("body-0".to_string()),
|
|
id: ElementId(5,),
|
|
},
|
|
CreateTextNode { value: "The robot becomes sentient and says hello world".to_string(), id: ElementId(6,) },
|
|
ReplacePlaceholder { path: &[0,], m: 1 },
|
|
|
|
// Then load the suspended children
|
|
LoadTemplate { index: 2, id: ElementId(7,) },
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("children-0".to_string()),
|
|
id: ElementId(7,),
|
|
},
|
|
CreateTextNode { value: "Loading 1...".to_string(), id: ElementId(8,) },
|
|
CreateTextNode { value: "Loading 2...".to_string(), id: ElementId(9,) },
|
|
ReplacePlaceholder { path: &[0,], m: 2 },
|
|
|
|
// Finally replace the loading placeholder in the body with the resolved children
|
|
ReplaceWith { id: ElementId(3,), m: 3 },
|
|
]
|
|
);
|
|
|
|
dom.wait_for_work().await;
|
|
// DOM STATE:
|
|
// placeholder // ID: 1
|
|
// h2 // ID: 2
|
|
// p // ID: 5
|
|
// div // ID: 7
|
|
// "Loading 1..." // ID: 8
|
|
// "Loading 2..." // ID: 9
|
|
let mutations = dom.render_immediate_to_vec();
|
|
assert_eq!(
|
|
mutations.edits,
|
|
vec![
|
|
// Replace the first loading placeholder with a placeholder for us to fill in later
|
|
CreatePlaceholder {
|
|
id: ElementId(
|
|
3,
|
|
),
|
|
},
|
|
ReplaceWith {
|
|
id: ElementId(
|
|
8,
|
|
),
|
|
m: 1,
|
|
},
|
|
|
|
// Load the nested suspense
|
|
LoadTemplate {
|
|
|
|
index: 0,
|
|
id: ElementId(
|
|
8,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("title-1".to_string()),
|
|
id: ElementId(
|
|
8,
|
|
),
|
|
},
|
|
CreateTextNode { value: "The world says hello back".to_string(), id: ElementId(10,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0,
|
|
],
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
index: 1,
|
|
id: ElementId(
|
|
11,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("body-1".to_string()),
|
|
id: ElementId(
|
|
11,
|
|
),
|
|
},
|
|
CreateTextNode { value: "In a stunning turn of events, the world collectively unites and says hello back".to_string(), id: ElementId(12,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0,
|
|
],
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
index: 2,
|
|
id: ElementId(
|
|
13,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("children-1".to_string()),
|
|
id: ElementId(
|
|
13,
|
|
),
|
|
},
|
|
CreatePlaceholder { id: ElementId(14,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0,
|
|
],
|
|
m: 1,
|
|
},
|
|
ReplaceWith {
|
|
id: ElementId(
|
|
3,
|
|
),
|
|
m: 3,
|
|
},
|
|
|
|
// Replace the second loading placeholder with a placeholder for us to fill in later
|
|
CreatePlaceholder {
|
|
id: ElementId(
|
|
3,
|
|
),
|
|
},
|
|
ReplaceWith {
|
|
id: ElementId(
|
|
9,
|
|
),
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
index: 0,
|
|
id: ElementId(
|
|
9,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("title-2".to_string()),
|
|
id: ElementId(
|
|
9,
|
|
),
|
|
},
|
|
CreateTextNode { value: "Goodbye Robot".to_string(), id: ElementId(15,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0,
|
|
],
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
index: 1,
|
|
id: ElementId(
|
|
16,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("body-2".to_string()),
|
|
id: ElementId(
|
|
16,
|
|
),
|
|
},
|
|
CreateTextNode { value: "The robot says goodbye".to_string(), id: ElementId(17,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0,
|
|
],
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
|
|
index: 2,
|
|
id: ElementId(
|
|
18,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("children-2".to_string()),
|
|
id: ElementId(
|
|
18,
|
|
),
|
|
},
|
|
// Create a placeholder for the resolved children
|
|
CreateTextNode { value: "Loading 3...".to_string(), id: ElementId(19,) },
|
|
ReplacePlaceholder { path: &[0,], m: 1 },
|
|
|
|
// Replace the loading placeholder with the resolved children
|
|
ReplaceWith {
|
|
id: ElementId(
|
|
3,
|
|
),
|
|
m: 3,
|
|
},
|
|
]
|
|
);
|
|
|
|
dom.wait_for_work().await;
|
|
let mutations = dom.render_immediate_to_vec();
|
|
assert_eq!(
|
|
mutations.edits,
|
|
vec![
|
|
CreatePlaceholder {
|
|
id: ElementId(
|
|
3,
|
|
),
|
|
},
|
|
ReplaceWith {
|
|
id: ElementId(
|
|
19,
|
|
),
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
|
|
index: 0,
|
|
id: ElementId(
|
|
19,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("title-3".to_string()),
|
|
id: ElementId(
|
|
19,
|
|
),
|
|
},
|
|
CreateTextNode { value: "Goodbye Robot again".to_string(), id: ElementId(20,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0,
|
|
],
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
index: 1,
|
|
id: ElementId(
|
|
21,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("body-3".to_string()),
|
|
id: ElementId(
|
|
21,
|
|
),
|
|
},
|
|
CreateTextNode { value: "The robot says goodbye again".to_string(), id: ElementId(22,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0,
|
|
],
|
|
m: 1,
|
|
},
|
|
LoadTemplate {
|
|
index: 2,
|
|
id: ElementId(
|
|
23,
|
|
),
|
|
},
|
|
SetAttribute {
|
|
name: "id",
|
|
ns: None,
|
|
value: AttributeValue::Text("children-3".to_string()),
|
|
id: ElementId(
|
|
23,
|
|
),
|
|
},
|
|
CreatePlaceholder { id: ElementId(24,) },
|
|
ReplacePlaceholder {
|
|
path: &[
|
|
0
|
|
],
|
|
m: 1,
|
|
},
|
|
ReplaceWith {
|
|
id: ElementId(
|
|
3,
|
|
),
|
|
m: 3,
|
|
},
|
|
]
|
|
)
|
|
});
|
|
}
|