1use std::collections::{HashMap, HashSet};
10
11use plushie_core::protocol::{PatchOp, TreeNode};
12use plushie_widget_sdk::shared_state::MAX_TREE_DEPTH;
13
14#[derive(Debug, Default)]
25pub struct Tree {
26 root: Option<TreeNode>,
27 id_index: HashMap<String, Vec<usize>>,
28}
29
30impl Tree {
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn snapshot(&mut self, root: TreeNode) -> Result<(), Vec<String>> {
42 self.root = Some(root);
43 self.rebuild_id_index();
44 if let Some(root) = self.root.as_ref() {
47 validate_unique_ids(root)
48 } else {
49 Ok(())
50 }
51 }
52
53 pub fn root(&self) -> Option<&TreeNode> {
55 self.root.as_ref()
56 }
57
58 pub fn root_mut(&mut self) -> Option<&mut TreeNode> {
64 self.root.as_mut()
65 }
66
67 pub fn find_window(&self, window_id: &str) -> Option<&TreeNode> {
69 let root = self.root.as_ref()?;
70 find_window_recursive(root, window_id, 0)
71 }
72
73 pub fn window_ids(&self) -> Vec<String> {
75 let Some(root) = self.root.as_ref() else {
76 return Vec::new();
77 };
78 let mut ids = Vec::new();
79 collect_window_ids_recursive(root, &mut ids, 0);
80 ids
81 }
82
83 pub fn find_by_id(&self, node_id: &str) -> Option<&TreeNode> {
86 let root = self.root.as_ref()?;
87 let path = self.id_index.get(node_id)?;
88 navigate(root, path).ok()
89 }
90
91 pub fn find_by_type(&self, node_id: &str) -> Option<&str> {
93 self.find_by_id(node_id).map(|n| n.type_name.as_str())
94 }
95
96 fn rebuild_id_index(&mut self) {
97 self.id_index.clear();
98 if let Some(root) = self.root.as_ref() {
99 let mut path = Vec::new();
100 collect_id_index(root, &mut path, &mut self.id_index, 0);
101 }
102 }
103
104 pub fn validate_patch_order(ops: &[PatchOp]) -> Result<(), String> {
110 validate_patch_order(ops)
111 }
112
113 pub fn apply_patch(&mut self, ops: Vec<PatchOp>) -> Vec<(String, usize, TreeNode)> {
123 log::debug!("applying patch: {} ops", ops.len());
124 let mut exit_nodes = Vec::new();
125 for op in ops {
126 if op.op == "remove_child"
128 && let Some(root) = self.root.as_ref()
129 && let Ok(parent) = navigate(root, &op.path)
130 {
131 let index = op
132 .rest
133 .get("index")
134 .and_then(|v| v.as_u64())
135 .unwrap_or(u64::MAX) as usize;
136 if index < parent.children.len() {
137 let child = &parent.children[index];
138 if child.props.get("exit").is_some() {
139 exit_nodes.push((parent.id.clone(), index, child.clone()));
140 }
141 }
142 }
143 if let Err(e) = self.apply_op(&op) {
144 if matches!(e, PatchApplyError::NoTree) {
145 log::debug!("failed to apply patch op {:?}: {}", op.op, e);
146 } else {
147 log::warn!("failed to apply patch op {:?}: {}", op.op, e);
148 }
149 }
150 }
151 self.rebuild_id_index();
156 exit_nodes
157 }
158
159 fn apply_op(&mut self, op: &PatchOp) -> Result<(), PatchApplyError> {
160 let root = self.root.as_mut().ok_or(PatchApplyError::NoTree)?;
161
162 match op.op.as_str() {
163 "replace_node" => {
164 let node = op.rest.get("node").ok_or_else(|| {
165 PatchApplyError::invalid("replace_node: missing 'node' field")
166 })?;
167 let new_node: TreeNode = serde_json::from_value(node.clone()).map_err(|e| {
168 PatchApplyError::invalid(format!("replace_node: invalid node: {e}"))
169 })?;
170
171 if op.path.is_empty() {
172 *root = new_node;
174 } else {
175 let parent = navigate_mut(root, &op.path[..op.path.len() - 1])
176 .map_err(PatchApplyError::invalid)?;
177 let idx = *op.path.last().unwrap();
178 if idx < parent.children.len() {
179 parent.children[idx] = new_node;
180 } else {
181 return Err(PatchApplyError::invalid(format!(
182 "replace_node: index {idx} out of bounds"
183 )));
184 }
185 }
186 Ok(())
187 }
188 "update_props" => {
189 let target = navigate_mut(root, &op.path).map_err(PatchApplyError::invalid)?;
190 let props = op.rest.get("props").ok_or_else(|| {
191 PatchApplyError::invalid("update_props: missing 'props' field")
192 })?;
193
194 if !props.is_object() {
195 log::warn!("update_props: patch props is not an object: {}", props);
196 return Ok(());
197 }
198 let target_map = target.props.as_prop_map_mut();
199 let patch_map = props.as_object().unwrap();
200 for (k, v) in patch_map {
201 if v.is_null() {
202 target_map.remove(k);
203 } else {
204 target_map.insert(
205 k.clone(),
206 plushie_core::protocol::PropValue::from(v.clone()),
207 );
208 }
209 }
210 Ok(())
211 }
212 "insert_child" => {
213 let parent = navigate_mut(root, &op.path).map_err(PatchApplyError::invalid)?;
214 let index = op
215 .rest
216 .get("index")
217 .and_then(|v| v.as_u64())
218 .ok_or_else(|| {
219 PatchApplyError::invalid("insert_child: missing or invalid 'index'")
220 })? as usize;
221 let node = op.rest.get("node").ok_or_else(|| {
222 PatchApplyError::invalid("insert_child: missing 'node' field")
223 })?;
224 let new_node: TreeNode = serde_json::from_value(node.clone()).map_err(|e| {
225 PatchApplyError::invalid(format!("insert_child: invalid node: {e}"))
226 })?;
227
228 if index <= parent.children.len() {
229 parent.children.insert(index, new_node);
230 } else {
231 log::warn!(
232 "insert_child: index {index} is beyond children length {}, appending instead",
233 parent.children.len()
234 );
235 parent.children.push(new_node);
236 }
237 Ok(())
238 }
239 "remove_child" => {
240 let parent = navigate_mut(root, &op.path).map_err(PatchApplyError::invalid)?;
241 let index = op
242 .rest
243 .get("index")
244 .and_then(|v| v.as_u64())
245 .ok_or_else(|| {
246 PatchApplyError::invalid("remove_child: missing or invalid 'index'")
247 })? as usize;
248
249 if index < parent.children.len() {
250 parent.children.remove(index);
251 Ok(())
252 } else {
253 Err(PatchApplyError::invalid(format!(
254 "remove_child: index {index} out of bounds (len={})",
255 parent.children.len()
256 )))
257 }
258 }
259 other => {
260 plushie_core::diagnostics::error(plushie_core::Diagnostic::UnknownPatchOp {
261 op: other.to_string(),
262 payload: patch_op_payload(op),
263 });
264 Ok(())
265 }
266 }
267 }
268}
269
270#[derive(Debug, PartialEq, Eq)]
271enum PatchApplyError {
272 NoTree,
273 Invalid(String),
274}
275
276impl PatchApplyError {
277 fn invalid(message: impl Into<String>) -> Self {
278 Self::Invalid(message.into())
279 }
280}
281
282impl std::fmt::Display for PatchApplyError {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 match self {
285 Self::NoTree => f.write_str("no tree to patch"),
286 Self::Invalid(message) => f.write_str(message),
287 }
288 }
289}
290
291fn patch_op_payload(op: &PatchOp) -> serde_json::Value {
292 serde_json::to_value(op).unwrap_or_else(|_| {
293 serde_json::json!({
294 "op": op.op,
295 "path": op.path,
296 "rest": op.rest
297 })
298 })
299}
300
301#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
302enum PatchPhase {
303 Remove,
304 Middle,
305 Insert,
306}
307
308enum PatchOrderOp<'a> {
309 Remove { path: &'a [usize], index: usize },
310 Middle { parent_path: &'a [usize] },
311 Insert { path: &'a [usize], index: usize },
312}
313
314#[derive(Debug)]
315struct ParentPatchOrder {
316 phase: PatchPhase,
317 last_remove: Option<usize>,
318 last_insert: Option<usize>,
319}
320
321impl Default for ParentPatchOrder {
322 fn default() -> Self {
323 Self {
324 phase: PatchPhase::Remove,
325 last_remove: None,
326 last_insert: None,
327 }
328 }
329}
330
331fn validate_patch_order(ops: &[PatchOp]) -> Result<(), String> {
332 let mut parent_orders: HashMap<Vec<usize>, ParentPatchOrder> = HashMap::new();
333
334 for (op_index, op) in ops.iter().enumerate() {
335 let Some(order_op) = classify_patch_order_op(op) else {
336 continue;
337 };
338
339 match order_op {
340 PatchOrderOp::Remove { path, index } => {
341 let order = parent_orders.entry(path.to_vec()).or_default();
342 if order.phase > PatchPhase::Remove {
343 return Err(format!(
344 "patch op at index {op_index} is remove_child, but removes must appear before update_props, replace_node, and insert_child"
345 ));
346 }
347 if let Some(previous) = order.last_remove
348 && index > previous
349 {
350 return Err(format!(
351 "patch op at index {op_index} removes child {index} from parent path {path:?}, but remove_child ops for the same parent must not increase child indices"
352 ));
353 }
354 order.last_remove = Some(index);
355 }
356 PatchOrderOp::Middle { parent_path } => {
357 let order = parent_orders.entry(parent_path.to_vec()).or_default();
358 if order.phase == PatchPhase::Insert {
359 return Err(format!(
360 "patch op at index {op_index} is update_props or replace_node, but updates and replacements must appear before insert_child"
361 ));
362 }
363 order.phase = PatchPhase::Middle;
364 }
365 PatchOrderOp::Insert { path, index } => {
366 let order = parent_orders.entry(path.to_vec()).or_default();
367 order.phase = PatchPhase::Insert;
368 if let Some(previous) = order.last_insert
369 && index < previous
370 {
371 return Err(format!(
372 "patch op at index {op_index} inserts child {index} into parent path {path:?}, but insert_child ops for the same parent must not decrease child indices"
373 ));
374 }
375 order.last_insert = Some(index);
376 }
377 }
378 }
379
380 Ok(())
381}
382
383fn classify_patch_order_op(op: &PatchOp) -> Option<PatchOrderOp<'_>> {
384 match op.op.as_str() {
385 "remove_child" => op
386 .rest
387 .get("index")
388 .and_then(|value| value.as_u64())
389 .map(|index| PatchOrderOp::Remove {
390 path: &op.path,
391 index: index as usize,
392 }),
393 "update_props" if op.rest.get("props").is_some_and(|props| props.is_object()) => op
394 .path
395 .split_last()
396 .map(|(_, parent_path)| PatchOrderOp::Middle { parent_path }),
397 "replace_node"
398 if op
399 .rest
400 .get("node")
401 .is_some_and(|node| serde_json::from_value::<TreeNode>(node.clone()).is_ok()) =>
402 {
403 op.path
404 .split_last()
405 .map(|(_, parent_path)| PatchOrderOp::Middle { parent_path })
406 }
407 "insert_child" => op
408 .rest
409 .get("index")
410 .and_then(|value| value.as_u64())
411 .and_then(|index| {
412 op.rest.get("node").and_then(|node| {
413 serde_json::from_value::<TreeNode>(node.clone())
414 .is_ok()
415 .then_some(PatchOrderOp::Insert {
416 path: &op.path,
417 index: index as usize,
418 })
419 })
420 }),
421 _ => None,
422 }
423}
424
425fn collect_id_index(
430 node: &TreeNode,
431 path: &mut Vec<usize>,
432 out: &mut HashMap<String, Vec<usize>>,
433 depth: usize,
434) {
435 if depth > MAX_TREE_DEPTH {
436 return;
437 }
438 if !node.id.is_empty() {
439 out.entry(node.id.clone()).or_insert_with(|| path.clone());
440 }
441 for (idx, child) in node.children.iter().enumerate() {
442 path.push(idx);
443 collect_id_index(child, path, out, depth + 1);
444 path.pop();
445 }
446}
447
448fn find_window_recursive<'a>(
449 node: &'a TreeNode,
450 window_id: &str,
451 depth: usize,
452) -> Option<&'a TreeNode> {
453 if depth > MAX_TREE_DEPTH {
454 plushie_core::diagnostics::warn(plushie_core::Diagnostic::TreeDepthExceeded {
455 id: node.id.clone(),
456 max_depth: MAX_TREE_DEPTH,
457 });
458 return None;
459 }
460 if node.type_name == "window" && node.id == window_id {
461 return Some(node);
462 }
463 for child in &node.children {
464 if let Some(found) = find_window_recursive(child, window_id, depth + 1) {
465 return Some(found);
466 }
467 }
468 None
469}
470
471fn collect_window_ids_recursive(node: &TreeNode, ids: &mut Vec<String>, depth: usize) {
472 if depth > MAX_TREE_DEPTH {
473 plushie_core::diagnostics::warn(plushie_core::Diagnostic::TreeDepthExceeded {
474 id: node.id.clone(),
475 max_depth: MAX_TREE_DEPTH,
476 });
477 return;
478 }
479 if node.type_name == "window" {
480 ids.push(node.id.clone());
481 }
482 for child in &node.children {
483 collect_window_ids_recursive(child, ids, depth + 1);
484 }
485}
486
487const MAX_DUPLICATE_IDS: usize = 100;
496
497fn validate_unique_ids(root: &TreeNode) -> Result<(), Vec<String>> {
507 let mut seen = HashSet::new();
508 let mut duplicates = Vec::new();
509 let mut summary_emitted = false;
510 collect_duplicate_ids(root, &mut seen, &mut duplicates, &mut summary_emitted, 0);
511 if duplicates.is_empty() {
512 Ok(())
513 } else {
514 Err(duplicates)
515 }
516}
517
518fn collect_duplicate_ids(
519 node: &TreeNode,
520 seen: &mut HashSet<String>,
521 duplicates: &mut Vec<String>,
522 summary_emitted: &mut bool,
523 depth: usize,
524) {
525 if depth > MAX_TREE_DEPTH {
526 return;
527 }
528 if duplicates.len() >= MAX_DUPLICATE_IDS {
529 if !*summary_emitted {
530 let diag = plushie_core::Diagnostic::TooManyDuplicates {
531 limit: MAX_DUPLICATE_IDS,
532 };
533 duplicates.push(diag.to_string());
534 *summary_emitted = true;
535 }
536 return;
537 }
538 if !node.id.is_empty() && !seen.insert(node.id.clone()) {
539 duplicates.push(format!("{} ({})", node.id, node.type_name));
540 }
541 for child in &node.children {
542 collect_duplicate_ids(child, seen, duplicates, summary_emitted, depth + 1);
543 }
544}
545
546fn navigate<'a>(root: &'a TreeNode, path: &[usize]) -> Result<&'a TreeNode, String> {
548 let mut current = root;
549 for &idx in path {
550 if idx < current.children.len() {
551 current = ¤t.children[idx];
552 } else {
553 return Err(format!(
554 "path navigation: index {idx} out of bounds (len={})",
555 current.children.len()
556 ));
557 }
558 }
559 Ok(current)
560}
561
562fn navigate_mut<'a>(root: &'a mut TreeNode, path: &[usize]) -> Result<&'a mut TreeNode, String> {
563 let mut current = root;
564 for &idx in path {
565 if idx < current.children.len() {
566 current = &mut current.children[idx];
567 } else {
568 return Err(format!(
569 "path navigation: index {idx} out of bounds (len={})",
570 current.children.len()
571 ));
572 }
573 }
574 Ok(current)
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use plushie_core::protocol::PatchOp;
581 use plushie_widget_sdk::testing::{node, node_with_children, node_with_props};
582 use serde_json::json;
583
584 fn make_patch_op(op: &str, path: Vec<usize>, rest: serde_json::Value) -> PatchOp {
585 let mut obj = serde_json::Map::new();
587 obj.insert("op".to_string(), json!(op));
588 obj.insert("path".to_string(), json!(path));
589 if let Some(map) = rest.as_object() {
590 for (k, v) in map {
591 obj.insert(k.clone(), v.clone());
592 }
593 }
594 serde_json::from_value(serde_json::Value::Object(obj)).unwrap()
595 }
596
597 #[test]
602 fn new_tree_is_empty() {
603 let tree = Tree::new();
604 assert!(tree.root().is_none());
605 }
606
607 #[test]
608 fn default_tree_is_empty() {
609 let tree = Tree::default();
610 assert!(tree.root().is_none());
611 }
612
613 #[test]
614 fn snapshot_sets_root() {
615 let mut tree = Tree::new();
616 let _ = tree.snapshot(node("root", "column"));
617 assert!(tree.root().is_some());
618 assert_eq!(tree.root().unwrap().id, "root");
619 assert_eq!(tree.root().unwrap().type_name, "column");
620 }
621
622 #[test]
623 fn snapshot_replaces_previous_root() {
624 let mut tree = Tree::new();
625 let _ = tree.snapshot(node("first", "column"));
626 let _ = tree.snapshot(node("second", "row"));
627 assert_eq!(tree.root().unwrap().id, "second");
628 assert_eq!(tree.root().unwrap().type_name, "row");
629 }
630
631 #[test]
632 fn snapshot_preserves_children() {
633 let mut tree = Tree::new();
634 let root = node_with_children(
635 "root",
636 "column",
637 vec![node("a", "text"), node("b", "button")],
638 );
639 let _ = tree.snapshot(root);
640 assert_eq!(tree.root().unwrap().children.len(), 2);
641 assert_eq!(tree.root().unwrap().children[0].id, "a");
642 assert_eq!(tree.root().unwrap().children[1].id, "b");
643 }
644
645 #[test]
650 fn find_window_at_root() {
651 let mut tree = Tree::new();
652 let _ = tree.snapshot(node("main", "window"));
653 let found = tree.find_window("main");
654 assert!(found.is_some());
655 assert_eq!(found.unwrap().id, "main");
656 assert_eq!(found.unwrap().type_name, "window");
657 }
658
659 #[test]
660 fn find_window_root_wrong_id() {
661 let mut tree = Tree::new();
662 let _ = tree.snapshot(node("main", "window"));
663 assert!(tree.find_window("other").is_none());
664 }
665
666 #[test]
667 fn find_window_in_children() {
668 let mut tree = Tree::new();
669 let root = node_with_children(
670 "root",
671 "column",
672 vec![node("win1", "window"), node("win2", "window")],
673 );
674 let _ = tree.snapshot(root);
675 assert!(tree.find_window("win1").is_some());
676 assert!(tree.find_window("win2").is_some());
677 assert_eq!(tree.find_window("win1").unwrap().id, "win1");
678 }
679
680 #[test]
681 fn find_window_not_found() {
682 let mut tree = Tree::new();
683 let _ = tree.snapshot(node("root", "column"));
684 assert!(tree.find_window("nope").is_none());
685 }
686
687 #[test]
688 fn find_window_on_empty_tree() {
689 let tree = Tree::new();
690 assert!(tree.find_window("anything").is_none());
691 }
692
693 #[test]
694 fn find_window_ignores_non_window_children() {
695 let mut tree = Tree::new();
696 let root = node_with_children(
697 "root",
698 "column",
699 vec![
700 node("btn", "button"),
701 node("win", "window"),
702 node("txt", "text"),
703 ],
704 );
705 let _ = tree.snapshot(root);
706 assert!(tree.find_window("btn").is_none());
707 assert!(tree.find_window("txt").is_none());
708 assert!(tree.find_window("win").is_some());
709 }
710
711 #[test]
712 fn find_window_searches_grandchildren() {
713 let mut tree = Tree::new();
714 let root = node_with_children(
715 "root",
716 "column",
717 vec![node_with_children(
718 "inner",
719 "row",
720 vec![node("deep_win", "window")],
721 )],
722 );
723 let _ = tree.snapshot(root);
724 let found = tree.find_window("deep_win");
725 assert!(found.is_some());
726 assert_eq!(found.unwrap().id, "deep_win");
727 }
728
729 #[test]
730 fn find_window_deeply_nested() {
731 let mut tree = Tree::new();
732 let root = node_with_children(
733 "root",
734 "column",
735 vec![node_with_children(
736 "l1",
737 "row",
738 vec![node_with_children(
739 "l2",
740 "column",
741 vec![node_with_children(
742 "l3",
743 "row",
744 vec![node("buried_win", "window")],
745 )],
746 )],
747 )],
748 );
749 let _ = tree.snapshot(root);
750 let found = tree.find_window("buried_win");
751 assert!(found.is_some());
752 assert_eq!(found.unwrap().id, "buried_win");
753 }
754
755 #[test]
756 fn window_ids_finds_nested_windows() {
757 let mut tree = Tree::new();
758 let root = node_with_children(
759 "root",
760 "column",
761 vec![
762 node("w1", "window"),
763 node_with_children("inner", "row", vec![node("w2", "window")]),
764 ],
765 );
766 let _ = tree.snapshot(root);
767 let ids = tree.window_ids();
768 assert_eq!(ids.len(), 2);
769 assert!(ids.contains(&"w1".to_string()));
770 assert!(ids.contains(&"w2".to_string()));
771 }
772
773 #[test]
778 fn window_ids_when_root_is_window() {
779 let mut tree = Tree::new();
780 let _ = tree.snapshot(node("main", "window"));
781 let ids = tree.window_ids();
782 assert_eq!(ids, vec!["main".to_string()]);
783 }
784
785 #[test]
786 fn window_ids_collects_child_windows() {
787 let mut tree = Tree::new();
788 let root = node_with_children(
789 "root",
790 "column",
791 vec![
792 node("w1", "window"),
793 node("w2", "window"),
794 node("w3", "window"),
795 ],
796 );
797 let _ = tree.snapshot(root);
798 let ids = tree.window_ids();
799 assert_eq!(ids.len(), 3);
800 assert!(ids.contains(&"w1".to_string()));
801 assert!(ids.contains(&"w2".to_string()));
802 assert!(ids.contains(&"w3".to_string()));
803 }
804
805 #[test]
806 fn window_ids_skips_non_windows() {
807 let mut tree = Tree::new();
808 let root = node_with_children(
809 "root",
810 "column",
811 vec![
812 node("w1", "window"),
813 node("btn", "button"),
814 node("w2", "window"),
815 ],
816 );
817 let _ = tree.snapshot(root);
818 let ids = tree.window_ids();
819 assert_eq!(ids.len(), 2);
820 assert!(!ids.contains(&"btn".to_string()));
821 }
822
823 #[test]
824 fn window_ids_empty_when_no_windows() {
825 let mut tree = Tree::new();
826 let _ = tree.snapshot(node("root", "column"));
827 assert!(tree.window_ids().is_empty());
828 }
829
830 #[test]
831 fn window_ids_empty_on_empty_tree() {
832 let tree = Tree::new();
833 assert!(tree.window_ids().is_empty());
834 }
835
836 #[test]
841 fn patch_replace_root() {
842 let mut tree = Tree::new();
843 let _ = tree.snapshot(node("old", "column"));
844 let op = make_patch_op(
845 "replace_node",
846 vec![],
847 json!({
848 "node": {"id": "new", "type": "row", "props": {}, "children": []}
849 }),
850 );
851 tree.apply_patch(vec![op]);
852 assert_eq!(tree.root().unwrap().id, "new");
853 assert_eq!(tree.root().unwrap().type_name, "row");
854 }
855
856 #[test]
857 fn patch_replace_child() {
858 let mut tree = Tree::new();
859 let root = node_with_children(
860 "root",
861 "column",
862 vec![node("a", "text"), node("b", "button")],
863 );
864 let _ = tree.snapshot(root);
865 let op = make_patch_op(
866 "replace_node",
867 vec![1],
868 json!({
869 "node": {"id": "c", "type": "text", "props": {"content": "replaced"}, "children": []}
870 }),
871 );
872 tree.apply_patch(vec![op]);
873 assert_eq!(tree.root().unwrap().children[1].id, "c");
874 assert_eq!(
875 tree.root().unwrap().children[1].props.to_value()["content"],
876 "replaced"
877 );
878 }
879
880 #[test]
881 fn patch_replace_nested_child() {
882 let mut tree = Tree::new();
883 let root = node_with_children(
884 "root",
885 "column",
886 vec![node_with_children(
887 "row",
888 "row",
889 vec![node("inner", "text")],
890 )],
891 );
892 let _ = tree.snapshot(root);
893 let op = make_patch_op(
894 "replace_node",
895 vec![0, 0],
896 json!({
897 "node": {"id": "replaced", "type": "button", "props": {}, "children": []}
898 }),
899 );
900 tree.apply_patch(vec![op]);
901 assert_eq!(tree.root().unwrap().children[0].children[0].id, "replaced");
902 assert_eq!(
903 tree.root().unwrap().children[0].children[0].type_name,
904 "button"
905 );
906 }
907
908 #[test]
909 fn patch_replace_out_of_bounds_does_not_panic() {
910 let mut tree = Tree::new();
911 let _ = tree.snapshot(node("root", "column"));
912 let op = make_patch_op(
913 "replace_node",
914 vec![5],
915 json!({
916 "node": {"id": "x", "type": "text", "props": {}, "children": []}
917 }),
918 );
919 tree.apply_patch(vec![op]);
921 assert_eq!(tree.root().unwrap().id, "root");
923 }
924
925 #[test]
930 fn patch_update_props_on_root() {
931 let mut tree = Tree::new();
932 let _ = tree.snapshot(node_with_props("root", "column", json!({"spacing": 5})));
933 let op = make_patch_op(
934 "update_props",
935 vec![],
936 json!({
937 "props": {"spacing": 10, "padding": 20}
938 }),
939 );
940 tree.apply_patch(vec![op]);
941 assert_eq!(tree.root().unwrap().props.to_value()["spacing"], 10);
942 assert_eq!(tree.root().unwrap().props.to_value()["padding"], 20);
943 }
944
945 #[test]
946 fn patch_update_props_removes_null_keys() {
947 let mut tree = Tree::new();
948 let _ = tree.snapshot(node_with_props(
949 "root",
950 "text",
951 json!({"content": "hi", "size": 14}),
952 ));
953 let op = make_patch_op(
954 "update_props",
955 vec![],
956 json!({
957 "props": {"size": null}
958 }),
959 );
960 tree.apply_patch(vec![op]);
961 assert_eq!(tree.root().unwrap().props.to_value()["content"], "hi");
962 assert!(tree.root().unwrap().props.get("size").is_none());
963 }
964
965 #[test]
966 fn patch_update_props_on_child() {
967 let mut tree = Tree::new();
968 let root = node_with_children(
969 "root",
970 "column",
971 vec![node_with_props("txt", "text", json!({"content": "old"}))],
972 );
973 let _ = tree.snapshot(root);
974 let op = make_patch_op(
975 "update_props",
976 vec![0],
977 json!({
978 "props": {"content": "new"}
979 }),
980 );
981 tree.apply_patch(vec![op]);
982 assert_eq!(
983 tree.root().unwrap().children[0].props.to_value()["content"],
984 "new"
985 );
986 }
987
988 #[test]
989 fn patch_update_props_non_object_target_props_does_not_panic() {
990 let mut tree = Tree::new();
991 let _ = tree.snapshot(node_with_props("root", "text", json!("not an object")));
994 let op = make_patch_op(
995 "update_props",
996 vec![],
997 json!({
998 "props": {"content": "new"}
999 }),
1000 );
1001 tree.apply_patch(vec![op]);
1002 assert_eq!(tree.root().unwrap().props.get_str("content"), Some("new"));
1003 }
1004
1005 #[test]
1006 fn patch_update_props_non_object_patch_props_does_not_panic() {
1007 let mut tree = Tree::new();
1008 let _ = tree.snapshot(node_with_props("root", "text", json!({"content": "hi"})));
1009 let op = make_patch_op(
1011 "update_props",
1012 vec![],
1013 json!({
1014 "props": "not an object"
1015 }),
1016 );
1017 tree.apply_patch(vec![op]);
1018 assert_eq!(tree.root().unwrap().props.to_value()["content"], "hi");
1020 }
1021
1022 #[test]
1027 fn patch_insert_child_at_beginning() {
1028 let mut tree = Tree::new();
1029 let root = node_with_children("root", "column", vec![node("a", "text")]);
1030 let _ = tree.snapshot(root);
1031 let op = make_patch_op(
1032 "insert_child",
1033 vec![],
1034 json!({
1035 "index": 0,
1036 "node": {"id": "b", "type": "button", "props": {}, "children": []}
1037 }),
1038 );
1039 tree.apply_patch(vec![op]);
1040 assert_eq!(tree.root().unwrap().children.len(), 2);
1041 assert_eq!(tree.root().unwrap().children[0].id, "b");
1042 assert_eq!(tree.root().unwrap().children[1].id, "a");
1043 }
1044
1045 #[test]
1046 fn patch_insert_child_at_end() {
1047 let mut tree = Tree::new();
1048 let root = node_with_children("root", "column", vec![node("a", "text")]);
1049 let _ = tree.snapshot(root);
1050 let op = make_patch_op(
1051 "insert_child",
1052 vec![],
1053 json!({
1054 "index": 1,
1055 "node": {"id": "b", "type": "button", "props": {}, "children": []}
1056 }),
1057 );
1058 tree.apply_patch(vec![op]);
1059 assert_eq!(tree.root().unwrap().children.len(), 2);
1060 assert_eq!(tree.root().unwrap().children[1].id, "b");
1061 }
1062
1063 #[test]
1064 fn patch_insert_child_beyond_length_appends() {
1065 let mut tree = Tree::new();
1066 let _ = tree.snapshot(node("root", "column"));
1067 let op = make_patch_op(
1068 "insert_child",
1069 vec![],
1070 json!({
1071 "index": 99,
1072 "node": {"id": "x", "type": "text", "props": {}, "children": []}
1073 }),
1074 );
1075 tree.apply_patch(vec![op]);
1076 assert_eq!(tree.root().unwrap().children.len(), 1);
1077 assert_eq!(tree.root().unwrap().children[0].id, "x");
1078 }
1079
1080 #[test]
1081 fn patch_insert_child_into_nested_parent() {
1082 let mut tree = Tree::new();
1083 let root = node_with_children(
1084 "root",
1085 "column",
1086 vec![node_with_children(
1087 "row",
1088 "row",
1089 vec![node("existing", "text")],
1090 )],
1091 );
1092 let _ = tree.snapshot(root);
1093 let op = make_patch_op(
1094 "insert_child",
1095 vec![0],
1096 json!({
1097 "index": 0,
1098 "node": {"id": "new", "type": "button", "props": {}, "children": []}
1099 }),
1100 );
1101 tree.apply_patch(vec![op]);
1102 let row = &tree.root().unwrap().children[0];
1103 assert_eq!(row.children.len(), 2);
1104 assert_eq!(row.children[0].id, "new");
1105 assert_eq!(row.children[1].id, "existing");
1106 }
1107
1108 #[test]
1113 fn patch_remove_child() {
1114 let mut tree = Tree::new();
1115 let root = node_with_children(
1116 "root",
1117 "column",
1118 vec![node("a", "text"), node("b", "button"), node("c", "text")],
1119 );
1120 let _ = tree.snapshot(root);
1121 let op = make_patch_op("remove_child", vec![], json!({"index": 1}));
1122 tree.apply_patch(vec![op]);
1123 assert_eq!(tree.root().unwrap().children.len(), 2);
1124 assert_eq!(tree.root().unwrap().children[0].id, "a");
1125 assert_eq!(tree.root().unwrap().children[1].id, "c");
1126 }
1127
1128 #[test]
1129 fn patch_remove_child_first() {
1130 let mut tree = Tree::new();
1131 let root = node_with_children(
1132 "root",
1133 "column",
1134 vec![node("a", "text"), node("b", "button")],
1135 );
1136 let _ = tree.snapshot(root);
1137 let op = make_patch_op("remove_child", vec![], json!({"index": 0}));
1138 tree.apply_patch(vec![op]);
1139 assert_eq!(tree.root().unwrap().children.len(), 1);
1140 assert_eq!(tree.root().unwrap().children[0].id, "b");
1141 }
1142
1143 #[test]
1144 fn patch_remove_child_last() {
1145 let mut tree = Tree::new();
1146 let root = node_with_children(
1147 "root",
1148 "column",
1149 vec![node("a", "text"), node("b", "button")],
1150 );
1151 let _ = tree.snapshot(root);
1152 let op = make_patch_op("remove_child", vec![], json!({"index": 1}));
1153 tree.apply_patch(vec![op]);
1154 assert_eq!(tree.root().unwrap().children.len(), 1);
1155 assert_eq!(tree.root().unwrap().children[0].id, "a");
1156 }
1157
1158 #[test]
1159 fn patch_remove_child_out_of_bounds_does_not_panic() {
1160 let mut tree = Tree::new();
1161 let _ = tree.snapshot(node("root", "column"));
1162 let op = make_patch_op("remove_child", vec![], json!({"index": 0}));
1163 tree.apply_patch(vec![op]);
1165 assert!(tree.root().unwrap().children.is_empty());
1166 }
1167
1168 #[test]
1173 fn patch_unknown_op_does_not_panic() {
1174 let mut tree = Tree::new();
1175 let mut root = node("root", "column");
1176 root.children.push(node("existing", "text"));
1177 let _ = tree.snapshot(root);
1178 let unknown = make_patch_op("frobnicate", vec![], json!({}));
1179 let valid = make_patch_op(
1180 "insert_child",
1181 vec![],
1182 json!({
1183 "index": 1,
1184 "node": node("child", "text")
1185 }),
1186 );
1187
1188 tree.apply_patch(vec![unknown, valid]);
1189
1190 assert_eq!(tree.root().unwrap().id, "root");
1191 assert_eq!(tree.root().unwrap().children.len(), 2);
1192 assert_eq!(tree.root().unwrap().children[0].id, "existing");
1193 assert_eq!(tree.root().unwrap().children[1].id, "child");
1194 }
1195
1196 #[test]
1197 fn unknown_patch_payload_preserves_flattened_fields() {
1198 let unknown = make_patch_op(
1199 "frobnicate",
1200 vec![1, 2],
1201 json!({
1202 "index": 3,
1203 "extra": {"answer": 42}
1204 }),
1205 );
1206
1207 let payload = patch_op_payload(&unknown);
1208
1209 assert_eq!(payload["op"], "frobnicate");
1210 assert_eq!(payload["path"], json!([1, 2]));
1211 assert_eq!(payload["index"], 3);
1212 assert_eq!(payload["extra"], json!({"answer": 42}));
1213 }
1214
1215 #[test]
1220 fn patch_multiple_ops_applied_in_order() {
1221 let mut tree = Tree::new();
1222 let _ = tree.snapshot(node("root", "column"));
1223
1224 let ops = vec![
1225 make_patch_op(
1226 "insert_child",
1227 vec![],
1228 json!({
1229 "index": 0,
1230 "node": {"id": "a", "type": "text", "props": {}, "children": []}
1231 }),
1232 ),
1233 make_patch_op(
1234 "insert_child",
1235 vec![],
1236 json!({
1237 "index": 1,
1238 "node": {"id": "b", "type": "text", "props": {}, "children": []}
1239 }),
1240 ),
1241 make_patch_op(
1242 "insert_child",
1243 vec![],
1244 json!({
1245 "index": 1,
1246 "node": {"id": "c", "type": "text", "props": {}, "children": []}
1247 }),
1248 ),
1249 ];
1250 tree.apply_patch(ops);
1251 let children = &tree.root().unwrap().children;
1252 assert_eq!(children.len(), 3);
1253 assert_eq!(children[0].id, "a");
1254 assert_eq!(children[1].id, "c");
1255 assert_eq!(children[2].id, "b");
1256 }
1257
1258 #[test]
1263 fn patch_on_empty_tree_does_not_panic() {
1264 let mut tree = Tree::new();
1265 let op = make_patch_op(
1266 "replace_node",
1267 vec![],
1268 json!({
1269 "node": {"id": "x", "type": "text", "props": {}, "children": []}
1270 }),
1271 );
1272 tree.apply_patch(vec![op]);
1273 assert!(tree.root().is_none());
1275 }
1276
1277 #[test]
1282 fn patch_deep_path_navigation() {
1283 let mut tree = Tree::new();
1284 let root = node_with_children(
1285 "root",
1286 "column",
1287 vec![node_with_children(
1288 "r0",
1289 "row",
1290 vec![node_with_children(
1291 "r0c0",
1292 "column",
1293 vec![node("deep", "text")],
1294 )],
1295 )],
1296 );
1297 let _ = tree.snapshot(root);
1298 let op = make_patch_op(
1299 "update_props",
1300 vec![0, 0, 0],
1301 json!({
1302 "props": {"content": "updated deep"}
1303 }),
1304 );
1305 tree.apply_patch(vec![op]);
1306 let deep = &tree.root().unwrap().children[0].children[0].children[0];
1307 assert_eq!(deep.props.to_value()["content"], "updated deep");
1308 }
1309
1310 #[test]
1311 fn patch_invalid_path_does_not_panic() {
1312 let mut tree = Tree::new();
1313 let _ = tree.snapshot(node("root", "column"));
1314 let op = make_patch_op(
1315 "update_props",
1316 vec![0, 1, 2],
1317 json!({
1318 "props": {"x": 1}
1319 }),
1320 );
1321 tree.apply_patch(vec![op]);
1322 assert_eq!(tree.root().unwrap().id, "root");
1324 }
1325
1326 #[test]
1331 fn patch_replace_node_missing_node_field_does_not_panic() {
1332 let mut tree = Tree::new();
1333 let _ = tree.snapshot(node("root", "column"));
1334 let op = make_patch_op("replace_node", vec![], json!({}));
1336 tree.apply_patch(vec![op]);
1337 assert_eq!(tree.root().unwrap().id, "root");
1339 }
1340
1341 #[test]
1342 fn patch_replace_node_invalid_node_json_does_not_panic() {
1343 let mut tree = Tree::new();
1344 let _ = tree.snapshot(node("root", "column"));
1345 let op = make_patch_op("replace_node", vec![], json!({"node": {"garbage": true}}));
1347 tree.apply_patch(vec![op]);
1348 assert_eq!(tree.root().unwrap().id, "root");
1349 }
1350
1351 #[test]
1352 fn patch_update_props_missing_props_field_does_not_panic() {
1353 let mut tree = Tree::new();
1354 let _ = tree.snapshot(node_with_props("root", "text", json!({"content": "hi"})));
1355 let op = make_patch_op("update_props", vec![], json!({}));
1356 tree.apply_patch(vec![op]);
1357 assert_eq!(tree.root().unwrap().props.to_value()["content"], "hi");
1359 }
1360
1361 #[test]
1362 fn patch_insert_child_missing_index_does_not_panic() {
1363 let mut tree = Tree::new();
1364 let _ = tree.snapshot(node("root", "column"));
1365 let op = make_patch_op(
1366 "insert_child",
1367 vec![],
1368 json!({
1369 "node": {"id": "x", "type": "text", "props": {}, "children": []}
1370 }),
1371 );
1372 tree.apply_patch(vec![op]);
1373 assert!(tree.root().unwrap().children.is_empty());
1375 }
1376
1377 #[test]
1378 fn patch_insert_child_missing_node_does_not_panic() {
1379 let mut tree = Tree::new();
1380 let _ = tree.snapshot(node("root", "column"));
1381 let op = make_patch_op("insert_child", vec![], json!({"index": 0}));
1382 tree.apply_patch(vec![op]);
1383 assert!(tree.root().unwrap().children.is_empty());
1384 }
1385
1386 #[test]
1387 fn patch_remove_child_missing_index_does_not_panic() {
1388 let mut tree = Tree::new();
1389 let root = node_with_children("root", "column", vec![node("a", "text")]);
1390 let _ = tree.snapshot(root);
1391 let op = make_patch_op("remove_child", vec![], json!({}));
1392 tree.apply_patch(vec![op]);
1393 assert_eq!(tree.root().unwrap().children.len(), 1);
1395 }
1396
1397 #[test]
1402 fn patch_multi_op_mixed_types() {
1403 let mut tree = Tree::new();
1404 let root = node_with_children(
1405 "root",
1406 "column",
1407 vec![
1408 node_with_props("a", "text", json!({"content": "hello"})),
1409 node("b", "button"),
1410 ],
1411 );
1412 let _ = tree.snapshot(root);
1413
1414 let ops = vec![
1415 make_patch_op(
1417 "insert_child",
1418 vec![],
1419 json!({
1420 "index": 2,
1421 "node": {"id": "c", "type": "text", "props": {"content": "new"}, "children": []}
1422 }),
1423 ),
1424 make_patch_op("remove_child", vec![], json!({"index": 0})),
1426 make_patch_op(
1428 "update_props",
1429 vec![0],
1430 json!({"props": {"label": "updated"}}),
1431 ),
1432 ];
1433 tree.apply_patch(ops);
1434
1435 let children = &tree.root().unwrap().children;
1436 assert_eq!(children.len(), 2);
1437 assert_eq!(children[0].id, "b");
1438 assert_eq!(children[0].props.to_value()["label"], "updated");
1439 assert_eq!(children[1].id, "c");
1440 }
1441
1442 #[test]
1443 fn patch_remove_shifts_indices() {
1444 let mut tree = Tree::new();
1445 let root = node_with_children(
1446 "root",
1447 "column",
1448 vec![
1449 node("first", "text"),
1450 node("second", "button"),
1451 node("third", "text"),
1452 ],
1453 );
1454 let _ = tree.snapshot(root);
1455
1456 let ops = vec![
1457 make_patch_op("remove_child", vec![], json!({"index": 0})),
1459 make_patch_op(
1461 "replace_node",
1462 vec![0],
1463 json!({
1464 "node": {"id": "replaced", "type": "row", "props": {}, "children": []}
1465 }),
1466 ),
1467 ];
1468 tree.apply_patch(ops);
1469
1470 let children = &tree.root().unwrap().children;
1471 assert_eq!(children.len(), 2);
1472 assert_eq!(children[0].id, "replaced");
1473 assert_eq!(children[0].type_name, "row");
1474 assert_eq!(children[1].id, "third");
1475 }
1476
1477 #[test]
1478 fn patch_bad_middle_op_continues() {
1479 let mut tree = Tree::new();
1480 let root = node_with_children(
1481 "root",
1482 "column",
1483 vec![
1484 node_with_props("a", "text", json!({"content": "original"})),
1485 node("b", "button"),
1486 ],
1487 );
1488 let _ = tree.snapshot(root);
1489
1490 let ops = vec![
1491 make_patch_op(
1493 "update_props",
1494 vec![0],
1495 json!({"props": {"content": "changed"}}),
1496 ),
1497 make_patch_op(
1499 "update_props",
1500 vec![99, 0],
1501 json!({"props": {"content": "nope"}}),
1502 ),
1503 make_patch_op(
1505 "update_props",
1506 vec![1],
1507 json!({"props": {"label": "click me"}}),
1508 ),
1509 ];
1510 tree.apply_patch(ops);
1511
1512 let children = &tree.root().unwrap().children;
1513 assert_eq!(children[0].props.to_value()["content"], "changed");
1514 assert_eq!(children[1].props.to_value()["label"], "click me");
1515 }
1516
1517 #[test]
1522 fn snapshot_unique_ids_returns_ok() {
1523 let mut tree = Tree::new();
1524 let root = node_with_children(
1525 "root",
1526 "column",
1527 vec![node("a", "text"), node("b", "button")],
1528 );
1529 assert!(tree.snapshot(root).is_ok());
1530 }
1531
1532 #[test]
1533 fn snapshot_duplicate_ids_returns_err() {
1534 let mut tree = Tree::new();
1535 let root = node_with_children(
1536 "root",
1537 "column",
1538 vec![node("dupe", "text"), node("dupe", "button")],
1539 );
1540 let result = tree.snapshot(root);
1541 assert!(result.is_err());
1542 let dupes = result.unwrap_err();
1543 assert_eq!(dupes.len(), 1);
1544 assert!(dupes[0].contains("dupe"));
1545 }
1546
1547 #[test]
1548 fn snapshot_duplicate_ids_still_accepts_tree() {
1549 let mut tree = Tree::new();
1550 let root = node_with_children(
1551 "root",
1552 "column",
1553 vec![node("dupe", "text"), node("dupe", "button")],
1554 );
1555 let _ = tree.snapshot(root);
1556 assert!(tree.root().is_some());
1558 assert_eq!(tree.root().unwrap().children.len(), 2);
1559 }
1560
1561 #[test]
1562 fn snapshot_multiple_duplicate_ids() {
1563 let mut tree = Tree::new();
1564 let root = node_with_children(
1565 "root",
1566 "column",
1567 vec![
1568 node("a", "text"),
1569 node("a", "text"),
1570 node("b", "button"),
1571 node("b", "button"),
1572 ],
1573 );
1574 let result = tree.snapshot(root);
1575 assert!(result.is_err());
1576 let dupes = result.unwrap_err();
1577 assert_eq!(dupes.len(), 2);
1578 }
1579
1580 #[test]
1581 fn snapshot_empty_ids_are_ignored() {
1582 let mut tree = Tree::new();
1583 let root = node_with_children("root", "column", vec![node("", "text"), node("", "text")]);
1584 assert!(tree.snapshot(root).is_ok());
1586 }
1587
1588 #[test]
1589 fn duplicate_collection_short_circuits_past_cap() {
1590 let over = super::MAX_DUPLICATE_IDS + 50;
1595 let mut children = Vec::with_capacity(over * 2);
1596 for _ in 0..over {
1597 children.push(node("shared", "text"));
1598 children.push(node("shared", "text"));
1599 }
1600 let root = node_with_children("root", "column", children);
1601 let mut tree = Tree::new();
1602 let dupes = tree.snapshot(root).unwrap_err();
1603
1604 assert_eq!(dupes.len(), super::MAX_DUPLICATE_IDS + 1);
1607 assert!(
1608 dupes
1609 .last()
1610 .is_some_and(|s| s.contains("too_many_duplicates")),
1611 "expected summary entry, got {:?}",
1612 dupes.last()
1613 );
1614 }
1615
1616 #[test]
1617 fn find_window_returns_none_beyond_max_depth() {
1618 let mut deepest = node("deep_win", "window");
1620 for i in 0..MAX_TREE_DEPTH + 10 {
1621 deepest = node_with_children(&format!("n{i}"), "column", vec![deepest]);
1622 }
1623 let mut tree = Tree::new();
1624 let _ = tree.snapshot(deepest);
1625
1626 assert!(
1628 tree.find_window("deep_win").is_none(),
1629 "window beyond MAX_TREE_DEPTH should not be reachable"
1630 );
1631 }
1632
1633 #[test]
1634 fn window_ids_skips_windows_beyond_max_depth() {
1635 let mut deepest = node("deep_win", "window");
1636 for i in 0..MAX_TREE_DEPTH + 10 {
1637 deepest = node_with_children(&format!("n{i}"), "column", vec![deepest]);
1638 }
1639 let mut tree = Tree::new();
1640 let _ = tree.snapshot(deepest);
1641
1642 let ids = tree.window_ids();
1643 assert!(
1644 !ids.contains(&"deep_win".to_string()),
1645 "window beyond MAX_TREE_DEPTH should not appear in window_ids"
1646 );
1647 }
1648
1649 #[test]
1654 fn find_by_id_finds_root() {
1655 let mut tree = Tree::new();
1656 let _ = tree.snapshot(node("root", "column"));
1657 let found = tree.find_by_id("root").unwrap();
1658 assert_eq!(found.id, "root");
1659 }
1660
1661 #[test]
1662 fn find_by_id_finds_nested_descendant() {
1663 let mut tree = Tree::new();
1664 let root = node_with_children(
1665 "root",
1666 "column",
1667 vec![node_with_children("row", "row", vec![node("deep", "text")])],
1668 );
1669 let _ = tree.snapshot(root);
1670 let found = tree.find_by_id("deep").unwrap();
1671 assert_eq!(found.id, "deep");
1672 assert_eq!(found.type_name, "text");
1673 }
1674
1675 #[test]
1676 fn find_by_id_returns_none_for_missing() {
1677 let mut tree = Tree::new();
1678 let _ = tree.snapshot(node("root", "column"));
1679 assert!(tree.find_by_id("ghost").is_none());
1680 }
1681
1682 #[test]
1683 fn find_by_id_skips_empty_ids() {
1684 let mut tree = Tree::new();
1685 let root = node_with_children("root", "column", vec![node("", "text"), node("a", "text")]);
1686 let _ = tree.snapshot(root);
1687 assert!(tree.find_by_id("").is_none());
1689 assert!(tree.find_by_id("a").is_some());
1690 }
1691
1692 #[test]
1693 fn find_by_id_first_match_wins_under_duplicates() {
1694 let mut tree = Tree::new();
1695 let root = node_with_children(
1699 "root",
1700 "column",
1701 vec![
1702 node_with_props("dupe", "text", json!({"content": "first"})),
1703 node_with_props("dupe", "button", json!({"content": "second"})),
1704 ],
1705 );
1706 let _ = tree.snapshot(root);
1707 let found = tree.find_by_id("dupe").unwrap();
1708 assert_eq!(found.type_name, "text");
1709 assert_eq!(found.props.to_value()["content"], "first");
1710 }
1711
1712 #[test]
1713 fn id_index_reflects_inserts_and_removes() {
1714 let mut tree = Tree::new();
1715 let _ = tree.snapshot(node("root", "column"));
1716 assert!(tree.find_by_id("root").is_some());
1717
1718 tree.apply_patch(vec![make_patch_op(
1721 "insert_child",
1722 vec![],
1723 json!({
1724 "index": 0,
1725 "node": {"id": "a", "type": "text", "props": {}, "children": []}
1726 }),
1727 )]);
1728 assert_eq!(tree.find_by_id("a").unwrap().id, "a");
1729
1730 tree.apply_patch(vec![make_patch_op(
1733 "insert_child",
1734 vec![],
1735 json!({
1736 "index": 0,
1737 "node": {"id": "b", "type": "text", "props": {}, "children": []}
1738 }),
1739 )]);
1740 assert_eq!(tree.find_by_id("a").unwrap().id, "a");
1741 assert_eq!(tree.find_by_id("b").unwrap().id, "b");
1742
1743 tree.apply_patch(vec![make_patch_op(
1746 "update_props",
1747 vec![1],
1748 json!({"props": {"content": "updated"}}),
1749 )]);
1750 assert_eq!(
1751 tree.find_by_id("a").unwrap().props.to_value()["content"],
1752 "updated"
1753 );
1754
1755 tree.apply_patch(vec![make_patch_op(
1758 "remove_child",
1759 vec![],
1760 json!({"index": 0}),
1761 )]);
1762 assert!(tree.find_by_id("b").is_none());
1763 let a = tree.find_by_id("a").unwrap();
1764 assert_eq!(a.id, "a");
1765 assert_eq!(a.props.to_value()["content"], "updated");
1766
1767 tree.apply_patch(vec![make_patch_op(
1770 "replace_node",
1771 vec![0],
1772 json!({
1773 "node": {"id": "c", "type": "button", "props": {}, "children": []}
1774 }),
1775 )]);
1776 assert!(tree.find_by_id("a").is_none());
1777 assert_eq!(tree.find_by_id("c").unwrap().type_name, "button");
1778 }
1779
1780 #[test]
1781 fn id_index_indexes_descendants_added_via_replace() {
1782 let mut tree = Tree::new();
1783 let _ = tree.snapshot(node("root", "column"));
1784 tree.apply_patch(vec![make_patch_op(
1787 "replace_node",
1788 vec![],
1789 json!({
1790 "node": {
1791 "id": "new_root",
1792 "type": "column",
1793 "props": {},
1794 "children": [
1795 {"id": "kid_a", "type": "text", "props": {}, "children": []},
1796 {
1797 "id": "kid_b",
1798 "type": "row",
1799 "props": {},
1800 "children": [
1801 {"id": "grand", "type": "text", "props": {}, "children": []}
1802 ]
1803 }
1804 ]
1805 }
1806 }),
1807 )]);
1808 assert!(tree.find_by_id("root").is_none());
1809 assert_eq!(tree.find_by_id("new_root").unwrap().type_name, "column");
1810 assert_eq!(tree.find_by_id("kid_a").unwrap().type_name, "text");
1811 assert_eq!(tree.find_by_id("kid_b").unwrap().type_name, "row");
1812 assert_eq!(tree.find_by_id("grand").unwrap().type_name, "text");
1813 }
1814}