suspense.rs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. use dioxus::prelude::*;
  2. use dioxus_core::{AttributeValue, ElementId, Mutation};
  3. use pretty_assertions::assert_eq;
  4. use std::future::poll_fn;
  5. use std::task::Poll;
  6. async fn poll_three_times() {
  7. // Poll each task 3 times
  8. let mut count = 0;
  9. poll_fn(|cx| {
  10. println!("polling... {}", count);
  11. if count < 3 {
  12. count += 1;
  13. cx.waker().wake_by_ref();
  14. Poll::Pending
  15. } else {
  16. Poll::Ready(())
  17. }
  18. })
  19. .await;
  20. }
  21. #[test]
  22. fn suspense_resolves_ssr() {
  23. // wait just a moment, not enough time for the boundary to resolve
  24. tokio::runtime::Builder::new_current_thread()
  25. .build()
  26. .unwrap()
  27. .block_on(async {
  28. let mut dom = VirtualDom::new(app);
  29. dom.rebuild_in_place();
  30. dom.wait_for_suspense().await;
  31. dom.render_immediate(&mut dioxus_core::NoOpMutations);
  32. let out = dioxus_ssr::render(&dom);
  33. assert_eq!(out, "<div>Waiting for... child</div>");
  34. });
  35. }
  36. fn app() -> Element {
  37. rsx!(
  38. div {
  39. "Waiting for... "
  40. SuspenseBoundary {
  41. fallback: |_| rsx! { "fallback" },
  42. suspended_child {}
  43. }
  44. }
  45. )
  46. }
  47. fn suspended_child() -> Element {
  48. let mut val = use_signal(|| 0);
  49. // Tasks that are not suspended should never be polled
  50. spawn(async move {
  51. panic!("Non-suspended task was polled");
  52. });
  53. // Memos should still work like normal
  54. let memo = use_memo(move || val * 2);
  55. assert_eq!(memo, val * 2);
  56. if val() < 3 {
  57. let task = spawn(async move {
  58. poll_three_times().await;
  59. println!("waiting... {}", val);
  60. val += 1;
  61. });
  62. suspend(task)?;
  63. }
  64. rsx!("child")
  65. }
  66. /// When switching from a suspense fallback to the real child, the state of that component must be kept
  67. #[test]
  68. fn suspense_keeps_state() {
  69. tokio::runtime::Builder::new_current_thread()
  70. .enable_time()
  71. .build()
  72. .unwrap()
  73. .block_on(async {
  74. let mut dom = VirtualDom::new(app);
  75. dom.rebuild(&mut dioxus_core::NoOpMutations);
  76. dom.render_suspense_immediate().await;
  77. let out = dioxus_ssr::render(&dom);
  78. assert_eq!(out, "fallback");
  79. dom.wait_for_suspense().await;
  80. let out = dioxus_ssr::render(&dom);
  81. assert_eq!(out, "<div>child with future resolved</div>");
  82. });
  83. fn app() -> Element {
  84. rsx! {
  85. SuspenseBoundary {
  86. fallback: |_| rsx! { "fallback" },
  87. Child {}
  88. }
  89. }
  90. }
  91. #[component]
  92. fn Child() -> Element {
  93. let mut future_resolved = use_signal(|| false);
  94. let task = use_hook(|| {
  95. spawn(async move {
  96. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  97. future_resolved.set(true);
  98. })
  99. });
  100. if !future_resolved() {
  101. suspend(task)?;
  102. }
  103. println!("future resolved: {future_resolved:?}");
  104. if future_resolved() {
  105. rsx! {
  106. div { "child with future resolved" }
  107. }
  108. } else {
  109. rsx! {
  110. div { "this should never be rendered" }
  111. }
  112. }
  113. }
  114. }
  115. /// spawn doesn't run in suspense
  116. #[test]
  117. fn suspense_does_not_poll_spawn() {
  118. tokio::runtime::Builder::new_current_thread()
  119. .enable_time()
  120. .build()
  121. .unwrap()
  122. .block_on(async {
  123. let mut dom = VirtualDom::new(app);
  124. dom.rebuild(&mut dioxus_core::NoOpMutations);
  125. dom.wait_for_suspense().await;
  126. let out = dioxus_ssr::render(&dom);
  127. assert_eq!(out, "<div>child with future resolved</div>");
  128. });
  129. fn app() -> Element {
  130. rsx! {
  131. SuspenseBoundary {
  132. fallback: |_| rsx! { "fallback" },
  133. Child {}
  134. }
  135. }
  136. }
  137. #[component]
  138. fn Child() -> Element {
  139. let mut future_resolved = use_signal(|| false);
  140. // futures that are spawned, but not suspended should never be polled
  141. use_hook(|| {
  142. spawn(async move {
  143. panic!("Non-suspended task was polled");
  144. });
  145. });
  146. let task = use_hook(|| {
  147. spawn(async move {
  148. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  149. future_resolved.set(true);
  150. })
  151. });
  152. if !future_resolved() {
  153. suspend(task)?;
  154. }
  155. rsx! {
  156. div { "child with future resolved" }
  157. }
  158. }
  159. }
  160. /// suspended nodes are not mounted, so they should not run effects
  161. #[test]
  162. fn suspended_nodes_dont_trigger_effects() {
  163. tokio::runtime::Builder::new_current_thread()
  164. .enable_time()
  165. .build()
  166. .unwrap()
  167. .block_on(async {
  168. let mut dom = VirtualDom::new(app);
  169. dom.rebuild(&mut dioxus_core::NoOpMutations);
  170. let work = async move {
  171. loop {
  172. dom.wait_for_work().await;
  173. dom.render_immediate(&mut dioxus_core::NoOpMutations);
  174. }
  175. };
  176. tokio::select! {
  177. _ = work => {},
  178. _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
  179. }
  180. });
  181. fn app() -> Element {
  182. rsx! {
  183. SuspenseBoundary {
  184. fallback: |_| rsx! { "fallback" },
  185. Child {}
  186. }
  187. }
  188. }
  189. #[component]
  190. fn RerendersFrequently() -> Element {
  191. let mut count = use_signal(|| 0);
  192. use_future(move || async move {
  193. for _ in 0..100 {
  194. tokio::time::sleep(std::time::Duration::from_millis(10)).await;
  195. count.set(count() + 1);
  196. }
  197. });
  198. rsx! {
  199. div { "rerenders frequently" }
  200. }
  201. }
  202. #[component]
  203. fn Child() -> Element {
  204. let mut future_resolved = use_signal(|| false);
  205. use_effect(|| panic!("effects should not run during suspense"));
  206. let task = use_hook(|| {
  207. spawn(async move {
  208. tokio::time::sleep(std::time::Duration::from_millis(500)).await;
  209. future_resolved.set(true);
  210. })
  211. });
  212. if !future_resolved() {
  213. suspend(task)?;
  214. }
  215. rsx! {
  216. div { "child with future resolved" }
  217. }
  218. }
  219. }
  220. /// Make sure we keep any state of components when we switch from a resolved future to a suspended future
  221. #[test]
  222. fn resolved_to_suspended() {
  223. static SUSPENDED: GlobalSignal<bool> = Signal::global(|| false);
  224. tokio::runtime::Builder::new_current_thread()
  225. .enable_time()
  226. .build()
  227. .unwrap()
  228. .block_on(async {
  229. let mut dom = VirtualDom::new(app);
  230. dom.rebuild(&mut dioxus_core::NoOpMutations);
  231. let out = dioxus_ssr::render(&dom);
  232. assert_eq!(out, "rendered 1 times");
  233. dom.in_runtime(|| ScopeId::APP.in_runtime(|| *SUSPENDED.write() = true));
  234. dom.render_suspense_immediate().await;
  235. let out = dioxus_ssr::render(&dom);
  236. assert_eq!(out, "fallback");
  237. dom.wait_for_suspense().await;
  238. let out = dioxus_ssr::render(&dom);
  239. assert_eq!(out, "rendered 3 times");
  240. });
  241. fn app() -> Element {
  242. rsx! {
  243. SuspenseBoundary {
  244. fallback: |_| rsx! { "fallback" },
  245. Child {}
  246. }
  247. }
  248. }
  249. #[component]
  250. fn Child() -> Element {
  251. let mut render_count = use_signal(|| 0);
  252. render_count += 1;
  253. let mut task = use_hook(|| CopyValue::new(None));
  254. tracing::info!("render_count: {}", render_count.peek());
  255. if SUSPENDED() {
  256. if task().is_none() {
  257. task.set(Some(spawn(async move {
  258. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  259. tracing::info!("task finished");
  260. *SUSPENDED.write() = false;
  261. })));
  262. }
  263. suspend(task().unwrap())?;
  264. }
  265. rsx! {
  266. "rendered {render_count.peek()} times"
  267. }
  268. }
  269. }
  270. /// Make sure suspense tells the renderer that a suspense boundary was resolved
  271. #[test]
  272. fn suspense_tracks_resolved() {
  273. tokio::runtime::Builder::new_current_thread()
  274. .enable_time()
  275. .build()
  276. .unwrap()
  277. .block_on(async {
  278. let mut dom = VirtualDom::new(app);
  279. dom.rebuild(&mut dioxus_core::NoOpMutations);
  280. dom.render_suspense_immediate().await;
  281. dom.wait_for_suspense_work().await;
  282. assert_eq!(
  283. dom.render_suspense_immediate().await,
  284. vec![ScopeId(ScopeId::APP.0 + 1)]
  285. );
  286. });
  287. fn app() -> Element {
  288. rsx! {
  289. SuspenseBoundary {
  290. fallback: |_| rsx! { "fallback" },
  291. Child {}
  292. }
  293. }
  294. }
  295. #[component]
  296. fn Child() -> Element {
  297. let mut resolved = use_signal(|| false);
  298. let task = use_hook(|| {
  299. spawn(async move {
  300. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  301. tracing::info!("task finished");
  302. resolved.set(true);
  303. })
  304. });
  305. if resolved() {
  306. println!("suspense is resolved");
  307. } else {
  308. println!("suspense is not resolved");
  309. suspend(task)?;
  310. }
  311. rsx! {
  312. "child"
  313. }
  314. }
  315. }
  316. // Regression test for https://github.com/DioxusLabs/dioxus/issues/2783
  317. #[test]
  318. fn toggle_suspense() {
  319. use dioxus::prelude::*;
  320. fn app() -> Element {
  321. rsx! {
  322. SuspenseBoundary {
  323. fallback: |_| rsx! { "fallback" },
  324. if generation() % 2 == 0 {
  325. Page {}
  326. } else {
  327. Home {}
  328. }
  329. }
  330. }
  331. }
  332. #[component]
  333. pub fn Home() -> Element {
  334. let _calculation = use_resource(|| async move {
  335. tokio::time::sleep(std::time::Duration::from_secs(1)).await;
  336. 1 + 1
  337. })
  338. .suspend()?;
  339. rsx! {
  340. "hello world"
  341. }
  342. }
  343. #[component]
  344. pub fn Page() -> Element {
  345. rsx! {
  346. "goodbye world"
  347. }
  348. }
  349. tokio::runtime::Builder::new_current_thread()
  350. .enable_time()
  351. .build()
  352. .unwrap()
  353. .block_on(async {
  354. let mut dom = VirtualDom::new(app);
  355. let mutations = dom.rebuild_to_vec();
  356. // First create goodbye world
  357. println!("{:#?}", mutations);
  358. assert_eq!(
  359. mutations.edits,
  360. [
  361. Mutation::LoadTemplate { index: 0, id: ElementId(1) },
  362. Mutation::AppendChildren { id: ElementId(0), m: 1 }
  363. ]
  364. );
  365. dom.mark_dirty(ScopeId::APP);
  366. let mutations = dom.render_immediate_to_vec();
  367. // Then replace that with nothing
  368. println!("{:#?}", mutations);
  369. assert_eq!(
  370. mutations.edits,
  371. [
  372. Mutation::CreatePlaceholder { id: ElementId(2) },
  373. Mutation::ReplaceWith { id: ElementId(1), m: 1 },
  374. ]
  375. );
  376. dom.wait_for_work().await;
  377. let mutations = dom.render_immediate_to_vec();
  378. // Then replace it with a placeholder
  379. println!("{:#?}", mutations);
  380. assert_eq!(
  381. mutations.edits,
  382. [
  383. Mutation::LoadTemplate { index: 0, id: ElementId(1) },
  384. Mutation::ReplaceWith { id: ElementId(2), m: 1 },
  385. ]
  386. );
  387. dom.wait_for_work().await;
  388. let mutations = dom.render_immediate_to_vec();
  389. // Then replace it with the resolved node
  390. println!("{:#?}", mutations);
  391. assert_eq!(
  392. mutations.edits,
  393. [
  394. Mutation::CreatePlaceholder { id: ElementId(2,) },
  395. Mutation::ReplaceWith { id: ElementId(1,), m: 1 },
  396. Mutation::LoadTemplate { index: 0, id: ElementId(1) },
  397. Mutation::ReplaceWith { id: ElementId(2), m: 1 },
  398. ]
  399. );
  400. });
  401. }
  402. #[test]
  403. fn nested_suspense_resolves_client() {
  404. use Mutation::*;
  405. async fn poll_three_times() {
  406. // Poll each task 3 times
  407. let mut count = 0;
  408. poll_fn(|cx| {
  409. println!("polling... {}", count);
  410. if count < 3 {
  411. count += 1;
  412. cx.waker().wake_by_ref();
  413. Poll::Pending
  414. } else {
  415. Poll::Ready(())
  416. }
  417. })
  418. .await;
  419. }
  420. fn app() -> Element {
  421. rsx! {
  422. SuspenseBoundary {
  423. fallback: move |_| rsx! {},
  424. LoadTitle {}
  425. }
  426. MessageWithLoader { id: 0 }
  427. }
  428. }
  429. #[component]
  430. fn MessageWithLoader(id: usize) -> Element {
  431. rsx! {
  432. SuspenseBoundary {
  433. fallback: move |_| rsx! {
  434. "Loading {id}..."
  435. },
  436. Message { id }
  437. }
  438. }
  439. }
  440. #[component]
  441. fn LoadTitle() -> Element {
  442. let title = use_resource(move || async_content(0)).suspend()?();
  443. rsx! {
  444. document::Title { "{title.title}" }
  445. }
  446. }
  447. #[component]
  448. fn Message(id: usize) -> Element {
  449. let message = use_resource(move || async_content(id)).suspend()?();
  450. rsx! {
  451. h2 {
  452. id: "title-{id}",
  453. "{message.title}"
  454. }
  455. p {
  456. id: "body-{id}",
  457. "{message.body}"
  458. }
  459. div {
  460. id: "children-{id}",
  461. padding: "10px",
  462. for child in message.children {
  463. MessageWithLoader { id: child }
  464. }
  465. }
  466. }
  467. }
  468. #[derive(Clone)]
  469. pub struct Content {
  470. title: String,
  471. body: String,
  472. children: Vec<usize>,
  473. }
  474. async fn async_content(id: usize) -> Content {
  475. let content_tree = [
  476. Content {
  477. title: "The robot says hello world".to_string(),
  478. body: "The robot becomes sentient and says hello world".to_string(),
  479. children: vec![1, 2],
  480. },
  481. Content {
  482. title: "The world says hello back".to_string(),
  483. body: "In a stunning turn of events, the world collectively unites and says hello back"
  484. .to_string(),
  485. children: vec![],
  486. },
  487. Content {
  488. title: "Goodbye Robot".to_string(),
  489. body: "The robot says goodbye".to_string(),
  490. children: vec![3],
  491. },
  492. Content {
  493. title: "Goodbye Robot again".to_string(),
  494. body: "The robot says goodbye again".to_string(),
  495. children: vec![],
  496. },
  497. ];
  498. poll_three_times().await;
  499. content_tree[id].clone()
  500. }
  501. // wait just a moment, not enough time for the boundary to resolve
  502. tokio::runtime::Builder::new_current_thread()
  503. .build()
  504. .unwrap()
  505. .block_on(async {
  506. let mut dom = VirtualDom::new(app);
  507. let mutations = dom.rebuild_to_vec();
  508. // Initial loading message and loading title
  509. assert_eq!(
  510. mutations.edits,
  511. vec![
  512. CreatePlaceholder { id: ElementId(1,) },
  513. CreateTextNode { value: "Loading 0...".to_string(), id: ElementId(2,) },
  514. AppendChildren { id: ElementId(0,), m: 2 },
  515. ]
  516. );
  517. dom.wait_for_work().await;
  518. // DOM STATE:
  519. // placeholder // ID: 1
  520. // "Loading 0..." // ID: 2
  521. let mutations = dom.render_immediate_to_vec();
  522. // Fill in the contents of the initial message and start loading the nested suspense
  523. // The title also finishes loading
  524. assert_eq!(
  525. mutations.edits,
  526. vec![
  527. // Creating and swapping these placeholders doesn't do anything
  528. // It is just extra work that we are forced to do because mutations are not
  529. // reversible. We start rendering the children and then realize it is suspended.
  530. // Then we need to replace what we just rendered with the suspense placeholder
  531. CreatePlaceholder { id: ElementId(3,) },
  532. ReplaceWith { id: ElementId(1,), m: 1 },
  533. // Replace the pending placeholder with the title placeholder
  534. CreatePlaceholder { id: ElementId(1,) },
  535. ReplaceWith { id: ElementId(3,), m: 1 },
  536. // Replace loading... with a placeholder for us to fill in later
  537. CreatePlaceholder { id: ElementId(3,) },
  538. ReplaceWith { id: ElementId(2,), m: 1 },
  539. // Load the title
  540. LoadTemplate { index: 0, id: ElementId(2,) },
  541. CreateTextNode { value: "The robot says hello world".to_string(), id: ElementId(4,) },
  542. ReplacePlaceholder { path: &[0,], m: 1 },
  543. SetAttribute {
  544. name: "id",
  545. ns: None,
  546. value: AttributeValue::Text("title-0".to_string()),
  547. id: ElementId(2,),
  548. },
  549. // Then load the body
  550. LoadTemplate { index: 1, id: ElementId(5,) },
  551. CreateTextNode { value: "The robot becomes sentient and says hello world".to_string(), id: ElementId(6,) },
  552. ReplacePlaceholder { path: &[0,], m: 1 },
  553. SetAttribute {
  554. name: "id",
  555. ns: None,
  556. value: AttributeValue::Text("body-0".to_string()),
  557. id: ElementId(5,),
  558. },
  559. // Then load the suspended children
  560. LoadTemplate { index: 2, id: ElementId(7,) },
  561. CreateTextNode { value: "Loading 1...".to_string(), id: ElementId(8,) },
  562. CreateTextNode { value: "Loading 2...".to_string(), id: ElementId(9,) },
  563. ReplacePlaceholder { path: &[0,], m: 2 },
  564. SetAttribute {
  565. name: "id",
  566. ns: None,
  567. value: AttributeValue::Text("children-0".to_string()),
  568. id: ElementId(7,),
  569. },
  570. // Finally replace the loading placeholder in the body with the resolved children
  571. ReplaceWith { id: ElementId(3,), m: 3 },
  572. ]
  573. );
  574. dom.wait_for_work().await;
  575. // DOM STATE:
  576. // placeholder // ID: 1
  577. // h2 // ID: 2
  578. // p // ID: 5
  579. // div // ID: 7
  580. // "Loading 1..." // ID: 8
  581. // "Loading 2..." // ID: 9
  582. let mutations = dom.render_immediate_to_vec();
  583. assert_eq!(
  584. mutations.edits,
  585. vec![
  586. // Replace the first loading placeholder with a placeholder for us to fill in later
  587. CreatePlaceholder {
  588. id: ElementId(
  589. 3,
  590. ),
  591. },
  592. ReplaceWith {
  593. id: ElementId(
  594. 8,
  595. ),
  596. m: 1,
  597. },
  598. // Load the nested suspense
  599. LoadTemplate {
  600. index: 0,
  601. id: ElementId(
  602. 8,
  603. ),
  604. },
  605. CreateTextNode { value: "The world says hello back".to_string(), id: ElementId(10,) },
  606. ReplacePlaceholder {
  607. path: &[
  608. 0,
  609. ],
  610. m: 1,
  611. },
  612. SetAttribute {
  613. name: "id",
  614. ns: None,
  615. value: AttributeValue::Text("title-1".to_string()),
  616. id: ElementId(
  617. 8,
  618. ),
  619. },
  620. LoadTemplate {
  621. index: 1,
  622. id: ElementId(
  623. 11,
  624. ),
  625. },
  626. CreateTextNode { value: "In a stunning turn of events, the world collectively unites and says hello back".to_string(), id: ElementId(12,) },
  627. ReplacePlaceholder {
  628. path: &[
  629. 0,
  630. ],
  631. m: 1,
  632. },
  633. SetAttribute {
  634. name: "id",
  635. ns: None,
  636. value: AttributeValue::Text("body-1".to_string()),
  637. id: ElementId(
  638. 11,
  639. ),
  640. },
  641. LoadTemplate {
  642. index: 2,
  643. id: ElementId(
  644. 13,
  645. ),
  646. },
  647. CreatePlaceholder { id: ElementId(14,) },
  648. ReplacePlaceholder {
  649. path: &[
  650. 0,
  651. ],
  652. m: 1,
  653. },
  654. SetAttribute {
  655. name: "id",
  656. ns: None,
  657. value: AttributeValue::Text("children-1".to_string()),
  658. id: ElementId(
  659. 13,
  660. ),
  661. },
  662. ReplaceWith {
  663. id: ElementId(
  664. 3,
  665. ),
  666. m: 3,
  667. },
  668. // Replace the second loading placeholder with a placeholder for us to fill in later
  669. CreatePlaceholder {
  670. id: ElementId(
  671. 3,
  672. ),
  673. },
  674. ReplaceWith {
  675. id: ElementId(
  676. 9,
  677. ),
  678. m: 1,
  679. },
  680. LoadTemplate {
  681. index: 0,
  682. id: ElementId(
  683. 9,
  684. ),
  685. },
  686. CreateTextNode { value: "Goodbye Robot".to_string(), id: ElementId(15,) },
  687. ReplacePlaceholder {
  688. path: &[
  689. 0,
  690. ],
  691. m: 1,
  692. },
  693. SetAttribute {
  694. name: "id",
  695. ns: None,
  696. value: AttributeValue::Text("title-2".to_string()),
  697. id: ElementId(
  698. 9,
  699. ),
  700. },
  701. LoadTemplate {
  702. index: 1,
  703. id: ElementId(
  704. 16,
  705. ),
  706. },
  707. CreateTextNode { value: "The robot says goodbye".to_string(), id: ElementId(17,) },
  708. ReplacePlaceholder {
  709. path: &[
  710. 0,
  711. ],
  712. m: 1,
  713. },
  714. SetAttribute {
  715. name: "id",
  716. ns: None,
  717. value: AttributeValue::Text("body-2".to_string()),
  718. id: ElementId(
  719. 16,
  720. ),
  721. },
  722. LoadTemplate {
  723. index: 2,
  724. id: ElementId(
  725. 18,
  726. ),
  727. },
  728. // Create a placeholder for the resolved children
  729. CreateTextNode { value: "Loading 3...".to_string(), id: ElementId(19,) },
  730. ReplacePlaceholder { path: &[0,], m: 1 },
  731. SetAttribute {
  732. name: "id",
  733. ns: None,
  734. value: AttributeValue::Text("children-2".to_string()),
  735. id: ElementId(
  736. 18,
  737. ),
  738. },
  739. // Replace the loading placeholder with the resolved children
  740. ReplaceWith {
  741. id: ElementId(
  742. 3,
  743. ),
  744. m: 3,
  745. },
  746. ]
  747. );
  748. dom.wait_for_work().await;
  749. let mutations = dom.render_immediate_to_vec();
  750. assert_eq!(
  751. mutations.edits,
  752. vec![
  753. CreatePlaceholder {
  754. id: ElementId(
  755. 3,
  756. ),
  757. },
  758. ReplaceWith {
  759. id: ElementId(
  760. 19,
  761. ),
  762. m: 1,
  763. },
  764. LoadTemplate {
  765. index: 0,
  766. id: ElementId(
  767. 19,
  768. ),
  769. },
  770. CreateTextNode { value: "Goodbye Robot again".to_string(), id: ElementId(20,) },
  771. ReplacePlaceholder {
  772. path: &[
  773. 0,
  774. ],
  775. m: 1,
  776. },
  777. SetAttribute {
  778. name: "id",
  779. ns: None,
  780. value: AttributeValue::Text("title-3".to_string()),
  781. id: ElementId(
  782. 19,
  783. ),
  784. },
  785. LoadTemplate {
  786. index: 1,
  787. id: ElementId(
  788. 21,
  789. ),
  790. },
  791. CreateTextNode { value: "The robot says goodbye again".to_string(), id: ElementId(22,) },
  792. ReplacePlaceholder {
  793. path: &[
  794. 0,
  795. ],
  796. m: 1,
  797. },
  798. SetAttribute {
  799. name: "id",
  800. ns: None,
  801. value: AttributeValue::Text("body-3".to_string()),
  802. id: ElementId(
  803. 21,
  804. ),
  805. },
  806. LoadTemplate {
  807. index: 2,
  808. id: ElementId(
  809. 23,
  810. ),
  811. },
  812. CreatePlaceholder { id: ElementId(24,) },
  813. ReplacePlaceholder {
  814. path: &[
  815. 0
  816. ],
  817. m: 1,
  818. },
  819. SetAttribute {
  820. name: "id",
  821. ns: None,
  822. value: AttributeValue::Text("children-3".to_string()),
  823. id: ElementId(
  824. 23,
  825. ),
  826. },
  827. ReplaceWith {
  828. id: ElementId(
  829. 3,
  830. ),
  831. m: 3,
  832. },
  833. ]
  834. )
  835. });
  836. }