1use crate::protocol::{PatchOp, TreeNode};
10
11#[derive(Debug, Default)]
14pub struct Tree {
15 root: Option<TreeNode>,
16}
17
18impl Tree {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn snapshot(&mut self, root: TreeNode) {
25 self.root = Some(root);
26 }
27
28 pub fn root(&self) -> Option<&TreeNode> {
30 self.root.as_ref()
31 }
32
33 pub fn find_window(&self, toddy_id: &str) -> Option<&TreeNode> {
35 let root = self.root.as_ref()?;
36 find_window_recursive(root, toddy_id)
37 }
38
39 pub fn window_ids(&self) -> Vec<String> {
41 let Some(root) = self.root.as_ref() else {
42 return Vec::new();
43 };
44 let mut ids = Vec::new();
45 collect_window_ids_recursive(root, &mut ids);
46 ids
47 }
48
49 pub fn apply_patch(&mut self, ops: Vec<PatchOp>) {
57 for op in ops {
58 if let Err(e) = self.apply_op(&op) {
59 log::error!("failed to apply patch op {:?}: {}", op.op, e);
60 }
61 }
62 }
63
64 fn apply_op(&mut self, op: &PatchOp) -> Result<(), String> {
65 let root = self.root.as_mut().ok_or("no tree to patch")?;
66
67 match op.op.as_str() {
68 "replace_node" => {
69 let node = op
70 .rest
71 .get("node")
72 .ok_or("replace_node: missing 'node' field")?;
73 let new_node: TreeNode = serde_json::from_value(node.clone())
74 .map_err(|e| format!("replace_node: invalid node: {e}"))?;
75
76 if op.path.is_empty() {
77 *root = new_node;
79 } else {
80 let parent = navigate_mut(root, &op.path[..op.path.len() - 1])?;
81 let idx = *op.path.last().unwrap();
82 if idx < parent.children.len() {
83 parent.children[idx] = new_node;
84 } else {
85 return Err(format!("replace_node: index {idx} out of bounds"));
86 }
87 }
88 Ok(())
89 }
90 "update_props" => {
91 let target = navigate_mut(root, &op.path)?;
92 let props = op
93 .rest
94 .get("props")
95 .ok_or("update_props: missing 'props' field")?;
96
97 if !target.props.is_object() {
98 log::error!(
99 "update_props: target node '{}' props is not an object: {}",
100 target.id,
101 target.props
102 );
103 return Ok(());
104 }
105 if !props.is_object() {
106 log::error!("update_props: patch props is not an object: {}", props);
107 return Ok(());
108 }
109 let target_map = target.props.as_object_mut().unwrap();
110 let patch_map = props.as_object().unwrap();
111 for (k, v) in patch_map {
112 if v.is_null() {
113 target_map.remove(k);
114 } else {
115 target_map.insert(k.clone(), v.clone());
116 }
117 }
118 Ok(())
119 }
120 "insert_child" => {
121 let parent = navigate_mut(root, &op.path)?;
122 let index = op
123 .rest
124 .get("index")
125 .and_then(|v| v.as_u64())
126 .ok_or("insert_child: missing or invalid 'index'")?
127 as usize;
128 let node = op
129 .rest
130 .get("node")
131 .ok_or("insert_child: missing 'node' field")?;
132 let new_node: TreeNode = serde_json::from_value(node.clone())
133 .map_err(|e| format!("insert_child: invalid node: {e}"))?;
134
135 if index <= parent.children.len() {
136 parent.children.insert(index, new_node);
137 } else {
138 log::error!(
139 "insert_child: index {index} is beyond children length {}, appending instead",
140 parent.children.len()
141 );
142 parent.children.push(new_node);
143 }
144 Ok(())
145 }
146 "remove_child" => {
147 let parent = navigate_mut(root, &op.path)?;
148 let index = op
149 .rest
150 .get("index")
151 .and_then(|v| v.as_u64())
152 .ok_or("remove_child: missing or invalid 'index'")?
153 as usize;
154
155 if index < parent.children.len() {
156 parent.children.remove(index);
157 Ok(())
158 } else {
159 Err(format!(
160 "remove_child: index {index} out of bounds (len={})",
161 parent.children.len()
162 ))
163 }
164 }
165 other => {
166 log::warn!("unknown patch op: {other}");
167 Ok(())
168 }
169 }
170 }
171}
172
173fn find_window_recursive<'a>(node: &'a TreeNode, toddy_id: &str) -> Option<&'a TreeNode> {
174 if node.type_name == "window" && node.id == toddy_id {
175 return Some(node);
176 }
177 for child in &node.children {
178 if let Some(found) = find_window_recursive(child, toddy_id) {
179 return Some(found);
180 }
181 }
182 None
183}
184
185fn collect_window_ids_recursive(node: &TreeNode, ids: &mut Vec<String>) {
186 if node.type_name == "window" {
187 ids.push(node.id.clone());
188 }
189 for child in &node.children {
190 collect_window_ids_recursive(child, ids);
191 }
192}
193
194fn navigate_mut<'a>(root: &'a mut TreeNode, path: &[usize]) -> Result<&'a mut TreeNode, String> {
196 let mut current = root;
197 for &idx in path {
198 if idx < current.children.len() {
199 current = &mut current.children[idx];
200 } else {
201 return Err(format!(
202 "path navigation: index {idx} out of bounds (len={})",
203 current.children.len()
204 ));
205 }
206 }
207 Ok(current)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::protocol::PatchOp;
214 use crate::testing::{node, node_with_children, node_with_props};
215 use serde_json::json;
216
217 fn make_patch_op(op: &str, path: Vec<usize>, rest: serde_json::Value) -> PatchOp {
218 let mut obj = serde_json::Map::new();
220 obj.insert("op".to_string(), json!(op));
221 obj.insert("path".to_string(), json!(path));
222 if let Some(map) = rest.as_object() {
223 for (k, v) in map {
224 obj.insert(k.clone(), v.clone());
225 }
226 }
227 serde_json::from_value(serde_json::Value::Object(obj)).unwrap()
228 }
229
230 #[test]
235 fn new_tree_is_empty() {
236 let tree = Tree::new();
237 assert!(tree.root().is_none());
238 }
239
240 #[test]
241 fn default_tree_is_empty() {
242 let tree = Tree::default();
243 assert!(tree.root().is_none());
244 }
245
246 #[test]
247 fn snapshot_sets_root() {
248 let mut tree = Tree::new();
249 tree.snapshot(node("root", "column"));
250 assert!(tree.root().is_some());
251 assert_eq!(tree.root().unwrap().id, "root");
252 assert_eq!(tree.root().unwrap().type_name, "column");
253 }
254
255 #[test]
256 fn snapshot_replaces_previous_root() {
257 let mut tree = Tree::new();
258 tree.snapshot(node("first", "column"));
259 tree.snapshot(node("second", "row"));
260 assert_eq!(tree.root().unwrap().id, "second");
261 assert_eq!(tree.root().unwrap().type_name, "row");
262 }
263
264 #[test]
265 fn snapshot_preserves_children() {
266 let mut tree = Tree::new();
267 let root = node_with_children(
268 "root",
269 "column",
270 vec![node("a", "text"), node("b", "button")],
271 );
272 tree.snapshot(root);
273 assert_eq!(tree.root().unwrap().children.len(), 2);
274 assert_eq!(tree.root().unwrap().children[0].id, "a");
275 assert_eq!(tree.root().unwrap().children[1].id, "b");
276 }
277
278 #[test]
283 fn find_window_at_root() {
284 let mut tree = Tree::new();
285 tree.snapshot(node("main", "window"));
286 let found = tree.find_window("main");
287 assert!(found.is_some());
288 assert_eq!(found.unwrap().id, "main");
289 assert_eq!(found.unwrap().type_name, "window");
290 }
291
292 #[test]
293 fn find_window_root_wrong_id() {
294 let mut tree = Tree::new();
295 tree.snapshot(node("main", "window"));
296 assert!(tree.find_window("other").is_none());
297 }
298
299 #[test]
300 fn find_window_in_children() {
301 let mut tree = Tree::new();
302 let root = node_with_children(
303 "root",
304 "column",
305 vec![node("win1", "window"), node("win2", "window")],
306 );
307 tree.snapshot(root);
308 assert!(tree.find_window("win1").is_some());
309 assert!(tree.find_window("win2").is_some());
310 assert_eq!(tree.find_window("win1").unwrap().id, "win1");
311 }
312
313 #[test]
314 fn find_window_not_found() {
315 let mut tree = Tree::new();
316 tree.snapshot(node("root", "column"));
317 assert!(tree.find_window("nope").is_none());
318 }
319
320 #[test]
321 fn find_window_on_empty_tree() {
322 let tree = Tree::new();
323 assert!(tree.find_window("anything").is_none());
324 }
325
326 #[test]
327 fn find_window_ignores_non_window_children() {
328 let mut tree = Tree::new();
329 let root = node_with_children(
330 "root",
331 "column",
332 vec![
333 node("btn", "button"),
334 node("win", "window"),
335 node("txt", "text"),
336 ],
337 );
338 tree.snapshot(root);
339 assert!(tree.find_window("btn").is_none());
340 assert!(tree.find_window("txt").is_none());
341 assert!(tree.find_window("win").is_some());
342 }
343
344 #[test]
345 fn find_window_searches_grandchildren() {
346 let mut tree = Tree::new();
347 let root = node_with_children(
348 "root",
349 "column",
350 vec![node_with_children(
351 "inner",
352 "row",
353 vec![node("deep_win", "window")],
354 )],
355 );
356 tree.snapshot(root);
357 let found = tree.find_window("deep_win");
358 assert!(found.is_some());
359 assert_eq!(found.unwrap().id, "deep_win");
360 }
361
362 #[test]
363 fn find_window_deeply_nested() {
364 let mut tree = Tree::new();
365 let root = node_with_children(
366 "root",
367 "column",
368 vec![node_with_children(
369 "l1",
370 "row",
371 vec![node_with_children(
372 "l2",
373 "column",
374 vec![node_with_children(
375 "l3",
376 "row",
377 vec![node("buried_win", "window")],
378 )],
379 )],
380 )],
381 );
382 tree.snapshot(root);
383 let found = tree.find_window("buried_win");
384 assert!(found.is_some());
385 assert_eq!(found.unwrap().id, "buried_win");
386 }
387
388 #[test]
389 fn window_ids_finds_nested_windows() {
390 let mut tree = Tree::new();
391 let root = node_with_children(
392 "root",
393 "column",
394 vec![
395 node("w1", "window"),
396 node_with_children("inner", "row", vec![node("w2", "window")]),
397 ],
398 );
399 tree.snapshot(root);
400 let ids = tree.window_ids();
401 assert_eq!(ids.len(), 2);
402 assert!(ids.contains(&"w1".to_string()));
403 assert!(ids.contains(&"w2".to_string()));
404 }
405
406 #[test]
411 fn window_ids_when_root_is_window() {
412 let mut tree = Tree::new();
413 tree.snapshot(node("main", "window"));
414 let ids = tree.window_ids();
415 assert_eq!(ids, vec!["main".to_string()]);
416 }
417
418 #[test]
419 fn window_ids_collects_child_windows() {
420 let mut tree = Tree::new();
421 let root = node_with_children(
422 "root",
423 "column",
424 vec![
425 node("w1", "window"),
426 node("w2", "window"),
427 node("w3", "window"),
428 ],
429 );
430 tree.snapshot(root);
431 let ids = tree.window_ids();
432 assert_eq!(ids.len(), 3);
433 assert!(ids.contains(&"w1".to_string()));
434 assert!(ids.contains(&"w2".to_string()));
435 assert!(ids.contains(&"w3".to_string()));
436 }
437
438 #[test]
439 fn window_ids_skips_non_windows() {
440 let mut tree = Tree::new();
441 let root = node_with_children(
442 "root",
443 "column",
444 vec![
445 node("w1", "window"),
446 node("btn", "button"),
447 node("w2", "window"),
448 ],
449 );
450 tree.snapshot(root);
451 let ids = tree.window_ids();
452 assert_eq!(ids.len(), 2);
453 assert!(!ids.contains(&"btn".to_string()));
454 }
455
456 #[test]
457 fn window_ids_empty_when_no_windows() {
458 let mut tree = Tree::new();
459 tree.snapshot(node("root", "column"));
460 assert!(tree.window_ids().is_empty());
461 }
462
463 #[test]
464 fn window_ids_empty_on_empty_tree() {
465 let tree = Tree::new();
466 assert!(tree.window_ids().is_empty());
467 }
468
469 #[test]
474 fn patch_replace_root() {
475 let mut tree = Tree::new();
476 tree.snapshot(node("old", "column"));
477 let op = make_patch_op(
478 "replace_node",
479 vec![],
480 json!({
481 "node": {"id": "new", "type": "row", "props": {}, "children": []}
482 }),
483 );
484 tree.apply_patch(vec![op]);
485 assert_eq!(tree.root().unwrap().id, "new");
486 assert_eq!(tree.root().unwrap().type_name, "row");
487 }
488
489 #[test]
490 fn patch_replace_child() {
491 let mut tree = Tree::new();
492 let root = node_with_children(
493 "root",
494 "column",
495 vec![node("a", "text"), node("b", "button")],
496 );
497 tree.snapshot(root);
498 let op = make_patch_op(
499 "replace_node",
500 vec![1],
501 json!({
502 "node": {"id": "c", "type": "text", "props": {"content": "replaced"}, "children": []}
503 }),
504 );
505 tree.apply_patch(vec![op]);
506 assert_eq!(tree.root().unwrap().children[1].id, "c");
507 assert_eq!(
508 tree.root().unwrap().children[1].props["content"],
509 "replaced"
510 );
511 }
512
513 #[test]
514 fn patch_replace_nested_child() {
515 let mut tree = Tree::new();
516 let root = node_with_children(
517 "root",
518 "column",
519 vec![node_with_children(
520 "row",
521 "row",
522 vec![node("inner", "text")],
523 )],
524 );
525 tree.snapshot(root);
526 let op = make_patch_op(
527 "replace_node",
528 vec![0, 0],
529 json!({
530 "node": {"id": "replaced", "type": "button", "props": {}, "children": []}
531 }),
532 );
533 tree.apply_patch(vec![op]);
534 assert_eq!(tree.root().unwrap().children[0].children[0].id, "replaced");
535 assert_eq!(
536 tree.root().unwrap().children[0].children[0].type_name,
537 "button"
538 );
539 }
540
541 #[test]
542 fn patch_replace_out_of_bounds_does_not_panic() {
543 let mut tree = Tree::new();
544 tree.snapshot(node("root", "column"));
545 let op = make_patch_op(
546 "replace_node",
547 vec![5],
548 json!({
549 "node": {"id": "x", "type": "text", "props": {}, "children": []}
550 }),
551 );
552 tree.apply_patch(vec![op]);
554 assert_eq!(tree.root().unwrap().id, "root");
556 }
557
558 #[test]
563 fn patch_update_props_on_root() {
564 let mut tree = Tree::new();
565 tree.snapshot(node_with_props("root", "column", json!({"spacing": 5})));
566 let op = make_patch_op(
567 "update_props",
568 vec![],
569 json!({
570 "props": {"spacing": 10, "padding": 20}
571 }),
572 );
573 tree.apply_patch(vec![op]);
574 assert_eq!(tree.root().unwrap().props["spacing"], 10);
575 assert_eq!(tree.root().unwrap().props["padding"], 20);
576 }
577
578 #[test]
579 fn patch_update_props_removes_null_keys() {
580 let mut tree = Tree::new();
581 tree.snapshot(node_with_props(
582 "root",
583 "text",
584 json!({"content": "hi", "size": 14}),
585 ));
586 let op = make_patch_op(
587 "update_props",
588 vec![],
589 json!({
590 "props": {"size": null}
591 }),
592 );
593 tree.apply_patch(vec![op]);
594 assert_eq!(tree.root().unwrap().props["content"], "hi");
595 assert!(tree.root().unwrap().props.get("size").is_none());
596 }
597
598 #[test]
599 fn patch_update_props_on_child() {
600 let mut tree = Tree::new();
601 let root = node_with_children(
602 "root",
603 "column",
604 vec![node_with_props("txt", "text", json!({"content": "old"}))],
605 );
606 tree.snapshot(root);
607 let op = make_patch_op(
608 "update_props",
609 vec![0],
610 json!({
611 "props": {"content": "new"}
612 }),
613 );
614 tree.apply_patch(vec![op]);
615 assert_eq!(tree.root().unwrap().children[0].props["content"], "new");
616 }
617
618 #[test]
619 fn patch_update_props_non_object_target_props_does_not_panic() {
620 let mut tree = Tree::new();
621 tree.snapshot(node_with_props("root", "text", json!("not an object")));
623 let op = make_patch_op(
624 "update_props",
625 vec![],
626 json!({
627 "props": {"content": "new"}
628 }),
629 );
630 tree.apply_patch(vec![op]);
631 assert_eq!(tree.root().unwrap().props, json!("not an object"));
633 }
634
635 #[test]
636 fn patch_update_props_non_object_patch_props_does_not_panic() {
637 let mut tree = Tree::new();
638 tree.snapshot(node_with_props("root", "text", json!({"content": "hi"})));
639 let op = make_patch_op(
641 "update_props",
642 vec![],
643 json!({
644 "props": "not an object"
645 }),
646 );
647 tree.apply_patch(vec![op]);
648 assert_eq!(tree.root().unwrap().props["content"], "hi");
650 }
651
652 #[test]
657 fn patch_insert_child_at_beginning() {
658 let mut tree = Tree::new();
659 let root = node_with_children("root", "column", vec![node("a", "text")]);
660 tree.snapshot(root);
661 let op = make_patch_op(
662 "insert_child",
663 vec![],
664 json!({
665 "index": 0,
666 "node": {"id": "b", "type": "button", "props": {}, "children": []}
667 }),
668 );
669 tree.apply_patch(vec![op]);
670 assert_eq!(tree.root().unwrap().children.len(), 2);
671 assert_eq!(tree.root().unwrap().children[0].id, "b");
672 assert_eq!(tree.root().unwrap().children[1].id, "a");
673 }
674
675 #[test]
676 fn patch_insert_child_at_end() {
677 let mut tree = Tree::new();
678 let root = node_with_children("root", "column", vec![node("a", "text")]);
679 tree.snapshot(root);
680 let op = make_patch_op(
681 "insert_child",
682 vec![],
683 json!({
684 "index": 1,
685 "node": {"id": "b", "type": "button", "props": {}, "children": []}
686 }),
687 );
688 tree.apply_patch(vec![op]);
689 assert_eq!(tree.root().unwrap().children.len(), 2);
690 assert_eq!(tree.root().unwrap().children[1].id, "b");
691 }
692
693 #[test]
694 fn patch_insert_child_beyond_length_appends() {
695 let mut tree = Tree::new();
696 tree.snapshot(node("root", "column"));
697 let op = make_patch_op(
698 "insert_child",
699 vec![],
700 json!({
701 "index": 99,
702 "node": {"id": "x", "type": "text", "props": {}, "children": []}
703 }),
704 );
705 tree.apply_patch(vec![op]);
706 assert_eq!(tree.root().unwrap().children.len(), 1);
707 assert_eq!(tree.root().unwrap().children[0].id, "x");
708 }
709
710 #[test]
711 fn patch_insert_child_into_nested_parent() {
712 let mut tree = Tree::new();
713 let root = node_with_children(
714 "root",
715 "column",
716 vec![node_with_children(
717 "row",
718 "row",
719 vec![node("existing", "text")],
720 )],
721 );
722 tree.snapshot(root);
723 let op = make_patch_op(
724 "insert_child",
725 vec![0],
726 json!({
727 "index": 0,
728 "node": {"id": "new", "type": "button", "props": {}, "children": []}
729 }),
730 );
731 tree.apply_patch(vec![op]);
732 let row = &tree.root().unwrap().children[0];
733 assert_eq!(row.children.len(), 2);
734 assert_eq!(row.children[0].id, "new");
735 assert_eq!(row.children[1].id, "existing");
736 }
737
738 #[test]
743 fn patch_remove_child() {
744 let mut tree = Tree::new();
745 let root = node_with_children(
746 "root",
747 "column",
748 vec![node("a", "text"), node("b", "button"), node("c", "text")],
749 );
750 tree.snapshot(root);
751 let op = make_patch_op("remove_child", vec![], json!({"index": 1}));
752 tree.apply_patch(vec![op]);
753 assert_eq!(tree.root().unwrap().children.len(), 2);
754 assert_eq!(tree.root().unwrap().children[0].id, "a");
755 assert_eq!(tree.root().unwrap().children[1].id, "c");
756 }
757
758 #[test]
759 fn patch_remove_child_first() {
760 let mut tree = Tree::new();
761 let root = node_with_children(
762 "root",
763 "column",
764 vec![node("a", "text"), node("b", "button")],
765 );
766 tree.snapshot(root);
767 let op = make_patch_op("remove_child", vec![], json!({"index": 0}));
768 tree.apply_patch(vec![op]);
769 assert_eq!(tree.root().unwrap().children.len(), 1);
770 assert_eq!(tree.root().unwrap().children[0].id, "b");
771 }
772
773 #[test]
774 fn patch_remove_child_last() {
775 let mut tree = Tree::new();
776 let root = node_with_children(
777 "root",
778 "column",
779 vec![node("a", "text"), node("b", "button")],
780 );
781 tree.snapshot(root);
782 let op = make_patch_op("remove_child", vec![], json!({"index": 1}));
783 tree.apply_patch(vec![op]);
784 assert_eq!(tree.root().unwrap().children.len(), 1);
785 assert_eq!(tree.root().unwrap().children[0].id, "a");
786 }
787
788 #[test]
789 fn patch_remove_child_out_of_bounds_does_not_panic() {
790 let mut tree = Tree::new();
791 tree.snapshot(node("root", "column"));
792 let op = make_patch_op("remove_child", vec![], json!({"index": 0}));
793 tree.apply_patch(vec![op]);
795 assert!(tree.root().unwrap().children.is_empty());
796 }
797
798 #[test]
803 fn patch_unknown_op_does_not_panic() {
804 let mut tree = Tree::new();
805 tree.snapshot(node("root", "column"));
806 let op = make_patch_op("frobnicate", vec![], json!({}));
807 tree.apply_patch(vec![op]);
808 assert_eq!(tree.root().unwrap().id, "root");
810 }
811
812 #[test]
817 fn patch_multiple_ops_applied_in_order() {
818 let mut tree = Tree::new();
819 tree.snapshot(node("root", "column"));
820
821 let ops = vec![
822 make_patch_op(
823 "insert_child",
824 vec![],
825 json!({
826 "index": 0,
827 "node": {"id": "a", "type": "text", "props": {}, "children": []}
828 }),
829 ),
830 make_patch_op(
831 "insert_child",
832 vec![],
833 json!({
834 "index": 1,
835 "node": {"id": "b", "type": "text", "props": {}, "children": []}
836 }),
837 ),
838 make_patch_op(
839 "insert_child",
840 vec![],
841 json!({
842 "index": 1,
843 "node": {"id": "c", "type": "text", "props": {}, "children": []}
844 }),
845 ),
846 ];
847 tree.apply_patch(ops);
848 let children = &tree.root().unwrap().children;
849 assert_eq!(children.len(), 3);
850 assert_eq!(children[0].id, "a");
851 assert_eq!(children[1].id, "c");
852 assert_eq!(children[2].id, "b");
853 }
854
855 #[test]
860 fn patch_on_empty_tree_does_not_panic() {
861 let mut tree = Tree::new();
862 let op = make_patch_op(
863 "replace_node",
864 vec![],
865 json!({
866 "node": {"id": "x", "type": "text", "props": {}, "children": []}
867 }),
868 );
869 tree.apply_patch(vec![op]);
870 assert!(tree.root().is_none());
872 }
873
874 #[test]
879 fn patch_deep_path_navigation() {
880 let mut tree = Tree::new();
881 let root = node_with_children(
882 "root",
883 "column",
884 vec![node_with_children(
885 "r0",
886 "row",
887 vec![node_with_children(
888 "r0c0",
889 "column",
890 vec![node("deep", "text")],
891 )],
892 )],
893 );
894 tree.snapshot(root);
895 let op = make_patch_op(
896 "update_props",
897 vec![0, 0, 0],
898 json!({
899 "props": {"content": "updated deep"}
900 }),
901 );
902 tree.apply_patch(vec![op]);
903 let deep = &tree.root().unwrap().children[0].children[0].children[0];
904 assert_eq!(deep.props["content"], "updated deep");
905 }
906
907 #[test]
908 fn patch_invalid_path_does_not_panic() {
909 let mut tree = Tree::new();
910 tree.snapshot(node("root", "column"));
911 let op = make_patch_op(
912 "update_props",
913 vec![0, 1, 2],
914 json!({
915 "props": {"x": 1}
916 }),
917 );
918 tree.apply_patch(vec![op]);
919 assert_eq!(tree.root().unwrap().id, "root");
921 }
922
923 #[test]
928 fn patch_replace_node_missing_node_field_does_not_panic() {
929 let mut tree = Tree::new();
930 tree.snapshot(node("root", "column"));
931 let op = make_patch_op("replace_node", vec![], json!({}));
933 tree.apply_patch(vec![op]);
934 assert_eq!(tree.root().unwrap().id, "root");
936 }
937
938 #[test]
939 fn patch_replace_node_invalid_node_json_does_not_panic() {
940 let mut tree = Tree::new();
941 tree.snapshot(node("root", "column"));
942 let op = make_patch_op("replace_node", vec![], json!({"node": {"garbage": true}}));
944 tree.apply_patch(vec![op]);
945 assert_eq!(tree.root().unwrap().id, "root");
946 }
947
948 #[test]
949 fn patch_update_props_missing_props_field_does_not_panic() {
950 let mut tree = Tree::new();
951 tree.snapshot(node_with_props("root", "text", json!({"content": "hi"})));
952 let op = make_patch_op("update_props", vec![], json!({}));
953 tree.apply_patch(vec![op]);
954 assert_eq!(tree.root().unwrap().props["content"], "hi");
956 }
957
958 #[test]
959 fn patch_insert_child_missing_index_does_not_panic() {
960 let mut tree = Tree::new();
961 tree.snapshot(node("root", "column"));
962 let op = make_patch_op(
963 "insert_child",
964 vec![],
965 json!({
966 "node": {"id": "x", "type": "text", "props": {}, "children": []}
967 }),
968 );
969 tree.apply_patch(vec![op]);
970 assert!(tree.root().unwrap().children.is_empty());
972 }
973
974 #[test]
975 fn patch_insert_child_missing_node_does_not_panic() {
976 let mut tree = Tree::new();
977 tree.snapshot(node("root", "column"));
978 let op = make_patch_op("insert_child", vec![], json!({"index": 0}));
979 tree.apply_patch(vec![op]);
980 assert!(tree.root().unwrap().children.is_empty());
981 }
982
983 #[test]
984 fn patch_remove_child_missing_index_does_not_panic() {
985 let mut tree = Tree::new();
986 let root = node_with_children("root", "column", vec![node("a", "text")]);
987 tree.snapshot(root);
988 let op = make_patch_op("remove_child", vec![], json!({}));
989 tree.apply_patch(vec![op]);
990 assert_eq!(tree.root().unwrap().children.len(), 1);
992 }
993}