mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 18:15:09 +00:00 
			
		
		
		
	chore: add update node body test
This commit is contained in:
		
							parent
							
								
									2a5d9d5530
								
							
						
					
					
						commit
						1d7d4092a5
					
				| @ -39,8 +39,8 @@ impl NodeAttributes { | ||||
|         self.0.is_empty() | ||||
|     } | ||||
| 
 | ||||
|     pub fn delete(&mut self, key: &AttributeKey) { | ||||
|         self.insert(key.clone(), AttributeValue(None)); | ||||
|     pub fn delete<K: ToString>(&mut self, key: K) { | ||||
|         self.insert(key.to_string(), AttributeValue(None)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -139,8 +139,34 @@ impl std::convert::From<bool> for AttributeValue { | ||||
|     fn from(val: bool) -> Self { | ||||
|         let val = match val { | ||||
|             true => Some("true".to_owned()), | ||||
|             false => None, | ||||
|             false => Some("false".to_owned()), | ||||
|         }; | ||||
|         AttributeValue(val) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct NodeAttributeBuilder { | ||||
|     attributes: NodeAttributes, | ||||
| } | ||||
| 
 | ||||
| impl NodeAttributeBuilder { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             attributes: NodeAttributes::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn insert<K: ToString, V: Into<AttributeValue>>(mut self, key: K, value: V) -> Self { | ||||
|         self.attributes.insert(key, value); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn delete<K: ToString>(mut self, key: K) -> Self { | ||||
|         self.attributes.delete(key); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn build(self) -> NodeAttributes { | ||||
|         self.attributes | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -26,6 +26,13 @@ impl NodeTree { | ||||
|         Some(self.arena.get(node_id)?.get()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_node_at_path(&self, path: &Path) -> Option<&Node> { | ||||
|         { | ||||
|             let node_id = self.node_id_at_path(path)?; | ||||
|             self.get_node(node_id) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ///
 | ||||
|     /// # Examples
 | ||||
|     ///
 | ||||
| @ -41,7 +48,7 @@ impl NodeTree { | ||||
|     /// let node_path = node_tree.path_of_node(node_id);
 | ||||
|     /// debug_assert_eq!(node_path, root_path);
 | ||||
|     /// ```
 | ||||
|     pub fn node_at_path<T: Into<Path>>(&self, path: T) -> Option<NodeId> { | ||||
|     pub fn node_id_at_path<T: Into<Path>>(&self, path: T) -> Option<NodeId> { | ||||
|         let path = path.into(); | ||||
|         if path.is_empty() { | ||||
|             return Some(self.root); | ||||
| @ -54,7 +61,7 @@ impl NodeTree { | ||||
|         Some(iterate_node) | ||||
|     } | ||||
| 
 | ||||
|     pub fn path_of_node(&self, node_id: NodeId) -> Path { | ||||
|     pub fn path_from_node_id(&self, node_id: NodeId) -> Path { | ||||
|         let mut path = vec![]; | ||||
|         let mut current_node = node_id; | ||||
|         // Use .skip(1) on the ancestors iterator to skip the root node.
 | ||||
| @ -137,7 +144,7 @@ impl NodeTree { | ||||
|     } | ||||
| 
 | ||||
|     pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { | ||||
|         for op in &transaction.operations { | ||||
|         for op in transaction.operations.iter() { | ||||
|             self.apply_op(op)?; | ||||
|         } | ||||
|         Ok(()) | ||||
| @ -164,7 +171,7 @@ impl NodeTree { | ||||
|         let (parent_path, last_path) = path.split_at(path.0.len() - 1); | ||||
|         let last_index = *last_path.first().unwrap(); | ||||
|         let parent_node = self | ||||
|             .node_at_path(parent_path) | ||||
|             .node_id_at_path(parent_path) | ||||
|             .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; | ||||
| 
 | ||||
|         self.insert_nodes_at_index(parent_node, last_index, nodes) | ||||
| @ -228,7 +235,7 @@ impl NodeTree { | ||||
| 
 | ||||
|     fn delete_node(&mut self, path: &Path, nodes: &[NodeData]) -> Result<(), OTError> { | ||||
|         let mut update_node = self | ||||
|             .node_at_path(path) | ||||
|             .node_id_at_path(path) | ||||
|             .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; | ||||
| 
 | ||||
|         for _ in 0..nodes.len() { | ||||
| @ -255,7 +262,7 @@ impl NodeTree { | ||||
|         F: Fn(&mut Node) -> Result<(), OTError>, | ||||
|     { | ||||
|         let node_id = self | ||||
|             .node_at_path(path) | ||||
|             .node_id_at_path(path) | ||||
|             .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; | ||||
|         match self.arena.get_mut(node_id) { | ||||
|             None => tracing::warn!("The path: {:?} does not contain any nodes", path), | ||||
|  | ||||
| @ -1,8 +1,10 @@ | ||||
| use crate::core::document::operation_serde::*; | ||||
| use crate::core::document::path::Path; | ||||
| use crate::core::{NodeAttributes, NodeBodyChangeset, NodeData}; | ||||
| use crate::errors::OTError; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| #[serde(tag = "op")] | ||||
| pub enum NodeOperation { | ||||
|     #[serde(rename = "insert")] | ||||
| @ -99,59 +101,37 @@ impl NodeOperation { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use crate::core::{NodeAttributes, NodeBodyChangeset, NodeBuilder, NodeData, NodeOperation, Path, TextDelta}; | ||||
|     #[test] | ||||
|     fn test_serialize_insert_operation() { | ||||
|         let insert = NodeOperation::Insert { | ||||
|             path: Path(vec![0, 1]), | ||||
|             nodes: vec![NodeData::new("text".to_owned())], | ||||
|         }; | ||||
|         let result = serde_json::to_string(&insert).unwrap(); | ||||
|         assert_eq!(result, r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}"#); | ||||
|     } | ||||
| #[derive(Serialize, Deserialize, Default)] | ||||
| pub struct NodeOperationList { | ||||
|     operations: Vec<NodeOperation>, | ||||
| } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_serialize_insert_sub_trees() { | ||||
|         let insert = NodeOperation::Insert { | ||||
|             path: Path(vec![0, 1]), | ||||
|             nodes: vec![NodeBuilder::new("text") | ||||
|                 .add_node(NodeData::new("text".to_owned())) | ||||
|                 .build()], | ||||
|         }; | ||||
|         let result = serde_json::to_string(&insert).unwrap(); | ||||
|         assert_eq!( | ||||
|             result, | ||||
|             r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","children":[{"type":"text"}]}]}"# | ||||
|         ); | ||||
|     } | ||||
| impl std::ops::Deref for NodeOperationList { | ||||
|     type Target = Vec<NodeOperation>; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_serialize_update_operation() { | ||||
|         let insert = NodeOperation::UpdateAttributes { | ||||
|             path: Path(vec![0, 1]), | ||||
|             attributes: NodeAttributes::new(), | ||||
|             old_attributes: NodeAttributes::new(), | ||||
|         }; | ||||
|         let result = serde_json::to_string(&insert).unwrap(); | ||||
|         assert_eq!( | ||||
|             result, | ||||
|             r#"{"op":"update","path":[0,1],"attributes":{},"oldAttributes":{}}"# | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_serialize_text_edit_operation() { | ||||
|         let changeset = NodeBodyChangeset::Delta { | ||||
|             delta: TextDelta::new(), | ||||
|             inverted: TextDelta::new(), | ||||
|         }; | ||||
|         let insert = NodeOperation::UpdateBody { | ||||
|             path: Path(vec![0, 1]), | ||||
|             changeset, | ||||
|         }; | ||||
|         let result = serde_json::to_string(&insert).unwrap(); | ||||
|         assert_eq!(result, r#"{"op":"edit-body","path":[0,1],"delta":[],"inverted":[]}"#); | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         &self.operations | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::ops::DerefMut for NodeOperationList { | ||||
|     fn deref_mut(&mut self) -> &mut Self::Target { | ||||
|         &mut self.operations | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl NodeOperationList { | ||||
|     pub fn new(operations: Vec<NodeOperation>) -> Self { | ||||
|         Self { operations } | ||||
|     } | ||||
| 
 | ||||
|     pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> { | ||||
|         let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?; | ||||
|         Ok(operation_list) | ||||
|     } | ||||
| 
 | ||||
|     pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> { | ||||
|         let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?; | ||||
|         Ok(bytes) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,26 +2,28 @@ use crate::core::document::path::Path; | ||||
| use crate::core::{NodeAttributes, NodeData, NodeOperation, NodeTree}; | ||||
| use indextree::NodeId; | ||||
| 
 | ||||
| use super::{NodeBodyChangeset, NodeOperationList}; | ||||
| 
 | ||||
| pub struct Transaction { | ||||
|     pub operations: Vec<NodeOperation>, | ||||
|     pub operations: NodeOperationList, | ||||
| } | ||||
| 
 | ||||
| impl Transaction { | ||||
|     fn new(operations: Vec<NodeOperation>) -> Transaction { | ||||
|     fn new(operations: NodeOperationList) -> Transaction { | ||||
|         Transaction { operations } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct TransactionBuilder<'a> { | ||||
|     node_tree: &'a NodeTree, | ||||
|     operations: Vec<NodeOperation>, | ||||
|     operations: NodeOperationList, | ||||
| } | ||||
| 
 | ||||
| impl<'a> TransactionBuilder<'a> { | ||||
|     pub fn new(node_tree: &'a NodeTree) -> TransactionBuilder { | ||||
|         TransactionBuilder { | ||||
|             node_tree, | ||||
|             operations: Vec::new(), | ||||
|             operations: NodeOperationList::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -80,23 +82,39 @@ impl<'a> TransactionBuilder<'a> { | ||||
|         self.insert_nodes_at_path(path, vec![node]) | ||||
|     } | ||||
| 
 | ||||
|     pub fn update_attributes_at_path(self, path: &Path, attributes: NodeAttributes) -> Self { | ||||
|     pub fn update_attributes_at_path(mut self, path: &Path, attributes: NodeAttributes) -> Self { | ||||
|         match self.node_tree.get_node_at_path(path) { | ||||
|             Some(node) => { | ||||
|                 let mut old_attributes = NodeAttributes::new(); | ||||
|         let node = self.node_tree.node_at_path(path).unwrap(); | ||||
|         let node_data = self.node_tree.get_node(node).unwrap(); | ||||
| 
 | ||||
|                 for key in attributes.keys() { | ||||
|             let old_attrs = &node_data.attributes; | ||||
|                     let old_attrs = &node.attributes; | ||||
|                     if let Some(value) = old_attrs.get(key.as_str()) { | ||||
|                         old_attributes.insert(key.clone(), value.clone()); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|         self.push(NodeOperation::UpdateAttributes { | ||||
|                 self.operations.push(NodeOperation::UpdateAttributes { | ||||
|                     path: path.clone(), | ||||
|                     attributes, | ||||
|                     old_attributes, | ||||
|         }) | ||||
|                 }); | ||||
|             } | ||||
|             None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path), | ||||
|         } | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self { | ||||
|         match self.node_tree.node_id_at_path(path) { | ||||
|             Some(_) => { | ||||
|                 self.operations.push(NodeOperation::UpdateBody { | ||||
|                     path: path.clone(), | ||||
|                     changeset, | ||||
|                 }); | ||||
|             } | ||||
|             None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path), | ||||
|         } | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn delete_node_at_path(self, path: &Path) -> Self { | ||||
| @ -104,7 +122,7 @@ impl<'a> TransactionBuilder<'a> { | ||||
|     } | ||||
| 
 | ||||
|     pub fn delete_nodes_at_path(mut self, path: &Path, length: usize) -> Self { | ||||
|         let mut node = self.node_tree.node_at_path(path).unwrap(); | ||||
|         let mut node = self.node_tree.node_id_at_path(path).unwrap(); | ||||
|         let mut deleted_nodes = vec![]; | ||||
|         for _ in 0..length { | ||||
|             deleted_nodes.push(self.get_deleted_nodes(node)); | ||||
|  | ||||
| @ -37,6 +37,7 @@ impl OTError { | ||||
|     static_ot_error!(duplicate_revision, OTErrorCode::DuplicatedRevision); | ||||
|     static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict); | ||||
|     static_ot_error!(internal, OTErrorCode::Internal); | ||||
|     static_ot_error!(serde, OTErrorCode::SerdeError); | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for OTError { | ||||
|  | ||||
| @ -1,2 +1,3 @@ | ||||
| mod operation_test; | ||||
| mod script; | ||||
| mod test; | ||||
| mod tree_test; | ||||
|  | ||||
							
								
								
									
										61
									
								
								shared-lib/lib-ot/tests/node/operation_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								shared-lib/lib-ot/tests/node/operation_test.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| use lib_ot::core::{ | ||||
|     NodeAttributeBuilder, NodeBodyChangeset, NodeBuilder, NodeData, NodeOperation, Path, TextDeltaBuilder, | ||||
| }; | ||||
| 
 | ||||
| #[test] | ||||
| fn operation_insert_node_serde_test() { | ||||
|     let insert = NodeOperation::Insert { | ||||
|         path: Path(vec![0, 1]), | ||||
|         nodes: vec![NodeData::new("text".to_owned())], | ||||
|     }; | ||||
|     let result = serde_json::to_string(&insert).unwrap(); | ||||
|     assert_eq!(result, r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}"#); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn operation_insert_node_with_children_serde_test() { | ||||
|     let node = NodeBuilder::new("text") | ||||
|         .add_node(NodeData::new("sub_text".to_owned())) | ||||
|         .build(); | ||||
| 
 | ||||
|     let insert = NodeOperation::Insert { | ||||
|         path: Path(vec![0, 1]), | ||||
|         nodes: vec![node], | ||||
|     }; | ||||
|     assert_eq!( | ||||
|         serde_json::to_string(&insert).unwrap(), | ||||
|         r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","children":[{"type":"sub_text"}]}]}"# | ||||
|     ); | ||||
| } | ||||
| #[test] | ||||
| fn operation_update_node_attributes_serde_test() { | ||||
|     let operation = NodeOperation::UpdateAttributes { | ||||
|         path: Path(vec![0, 1]), | ||||
|         attributes: NodeAttributeBuilder::new().insert("bold", true).build(), | ||||
|         old_attributes: NodeAttributeBuilder::new().insert("bold", false).build(), | ||||
|     }; | ||||
| 
 | ||||
|     let result = serde_json::to_string(&operation).unwrap(); | ||||
| 
 | ||||
|     assert_eq!( | ||||
|         result, | ||||
|         r#"{"op":"update","path":[0,1],"attributes":{"bold":"true"},"oldAttributes":{"bold":"false"}}"# | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn operation_update_node_body_serde_test() { | ||||
|     let delta = TextDeltaBuilder::new().insert("AppFlowy...").build(); | ||||
|     let inverted = delta.invert_str(""); | ||||
|     let changeset = NodeBodyChangeset::Delta { delta, inverted }; | ||||
|     let insert = NodeOperation::UpdateBody { | ||||
|         path: Path(vec![0, 1]), | ||||
|         changeset, | ||||
|     }; | ||||
|     let result = serde_json::to_string(&insert).unwrap(); | ||||
|     assert_eq!( | ||||
|         result, | ||||
|         r#"{"op":"edit-body","path":[0,1],"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}"# | ||||
|     ); | ||||
|     //
 | ||||
| } | ||||
| @ -1,11 +1,15 @@ | ||||
| use lib_ot::core::{NodeAttributes, NodeData, NodeTree, Path, TransactionBuilder}; | ||||
| use lib_ot::core::{ | ||||
|     NodeAttributes, NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TextDelta, TransactionBuilder, | ||||
| }; | ||||
| 
 | ||||
| pub enum NodeScript { | ||||
|     InsertNode { path: Path, node: NodeData }, | ||||
|     InsertAttributes { path: Path, attributes: NodeAttributes }, | ||||
|     UpdateAttributes { path: Path, attributes: NodeAttributes }, | ||||
|     UpdateBody { path: Path, changeset: NodeBodyChangeset }, | ||||
|     DeleteNode { path: Path }, | ||||
|     AssertNumberOfChildrenAtPath { path: Option<Path>, len: usize }, | ||||
|     AssertNode { path: Path, expected: Option<NodeData> }, | ||||
|     AssertNodeDelta { path: Path, expected: TextDelta }, | ||||
| } | ||||
| 
 | ||||
| pub struct NodeTest { | ||||
| @ -34,12 +38,19 @@ impl NodeTest { | ||||
| 
 | ||||
|                 self.node_tree.apply(transaction).unwrap(); | ||||
|             } | ||||
|             NodeScript::InsertAttributes { path, attributes } => { | ||||
|             NodeScript::UpdateAttributes { path, attributes } => { | ||||
|                 let transaction = TransactionBuilder::new(&self.node_tree) | ||||
|                     .update_attributes_at_path(&path, attributes) | ||||
|                     .finalize(); | ||||
|                 self.node_tree.apply(transaction).unwrap(); | ||||
|             } | ||||
|             NodeScript::UpdateBody { path, changeset } => { | ||||
|                 //
 | ||||
|                 let transaction = TransactionBuilder::new(&self.node_tree) | ||||
|                     .update_body_at_path(&path, changeset) | ||||
|                     .finalize(); | ||||
|                 self.node_tree.apply(transaction).unwrap(); | ||||
|             } | ||||
|             NodeScript::DeleteNode { path } => { | ||||
|                 let transaction = TransactionBuilder::new(&self.node_tree) | ||||
|                     .delete_node_at_path(&path) | ||||
| @ -47,7 +58,7 @@ impl NodeTest { | ||||
|                 self.node_tree.apply(transaction).unwrap(); | ||||
|             } | ||||
|             NodeScript::AssertNode { path, expected } => { | ||||
|                 let node_id = self.node_tree.node_at_path(path); | ||||
|                 let node_id = self.node_tree.node_id_at_path(path); | ||||
| 
 | ||||
|                 match node_id { | ||||
|                     None => assert!(node_id.is_none()), | ||||
| @ -66,11 +77,19 @@ impl NodeTest { | ||||
|                     assert_eq!(len, expected_len) | ||||
|                 } | ||||
|                 Some(path) => { | ||||
|                     let node_id = self.node_tree.node_at_path(path).unwrap(); | ||||
|                     let node_id = self.node_tree.node_id_at_path(path).unwrap(); | ||||
|                     let len = self.node_tree.number_of_children(Some(node_id)); | ||||
|                     assert_eq!(len, expected_len) | ||||
|                 } | ||||
|             }, | ||||
|             NodeScript::AssertNodeDelta { path, expected } => { | ||||
|                 let node = self.node_tree.get_node_at_path(&path).unwrap(); | ||||
|                 if let NodeBody::Delta(delta) = node.body.clone() { | ||||
|                     debug_assert_eq!(delta, expected); | ||||
|                 } else { | ||||
|                     panic!("Node body type not match, expect Delta"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,9 @@ | ||||
| use crate::node::script::NodeScript::*; | ||||
| use crate::node::script::NodeTest; | ||||
| use lib_ot::core::NodeBody; | ||||
| use lib_ot::core::NodeBodyChangeset; | ||||
| use lib_ot::core::OperationTransform; | ||||
| use lib_ot::core::TextDeltaBuilder; | ||||
| use lib_ot::core::{NodeBuilder, NodeData, Path}; | ||||
| 
 | ||||
| #[test] | ||||
| @ -147,7 +151,7 @@ fn node_insert_with_attributes_test() { | ||||
|             path: path.clone(), | ||||
|             node: inserted_node.clone(), | ||||
|         }, | ||||
|         InsertAttributes { | ||||
|         UpdateAttributes { | ||||
|             path: path.clone(), | ||||
|             attributes: inserted_node.attributes.clone(), | ||||
|         }, | ||||
| @ -175,3 +179,32 @@ fn node_delete_test() { | ||||
|     ]; | ||||
|     test.run_scripts(scripts); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn node_update_body_test() { | ||||
|     let mut test = NodeTest::new(); | ||||
|     let path: Path = 0.into(); | ||||
| 
 | ||||
|     let s = "Hello".to_owned(); | ||||
|     let init_delta = TextDeltaBuilder::new().insert(&s).build(); | ||||
|     let delta = TextDeltaBuilder::new().retain(s.len()).insert(" AppFlowy").build(); | ||||
|     let inverted = delta.invert(&init_delta); | ||||
|     let expected = init_delta.compose(&delta).unwrap(); | ||||
| 
 | ||||
|     let node = NodeBuilder::new("text") | ||||
|         .insert_body(NodeBody::Delta(init_delta)) | ||||
|         .build(); | ||||
| 
 | ||||
|     let scripts = vec![ | ||||
|         InsertNode { | ||||
|             path: path.clone(), | ||||
|             node: node.clone(), | ||||
|         }, | ||||
|         UpdateBody { | ||||
|             path: path.clone(), | ||||
|             changeset: NodeBodyChangeset::Delta { delta, inverted }, | ||||
|         }, | ||||
|         AssertNodeDelta { path, expected }, | ||||
|     ]; | ||||
|     test.run_scripts(scripts); | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 nathan
						nathan