use std::convert::TryInto; use automerge_frontend::{Frontend, Path, Primitive, Value}; use automerge_protocol as amp; use maplit::hashmap; use unicode_segmentation::UnicodeSegmentation; #[test] fn set_object_root_properties() { let actor = amp::ActorId::random(); let patch = amp::Patch { actor: None, seq: None, max_op: 1, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "bird".into() => hashmap!{ actor.op_id_at(1) => "magpie".into() } }, })), }; let mut frontend = Frontend::new(); frontend.apply_patch(patch).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"bird" => "magpie"}) ); } #[test] fn reveal_conflicts_on_root_properties() { // We don't just use random actor IDs because we need to have a specific // ordering (actor1 > actor2) let actor1 = amp::ActorId::from_bytes( uuid::Uuid::parse_str("02ef21f3-c9eb-4087-880e-bedd7c4bbe43") .unwrap() .as_bytes(), ); let actor2 = amp::ActorId::from_bytes( uuid::Uuid::parse_str("2a1d376b-24f7-4400-8d4a-f58252d644dd") .unwrap() .as_bytes(), ); let patch = amp::Patch { actor: None, seq: None, max_op: 2, pending_changes: 0, clock: hashmap! { actor1.clone() => 1, actor2.clone() => 2, }, deps: Vec::new(), diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "favouriteBird".into() => hashmap!{ actor1.op_id_at(1) => amp::Diff::Value("robin".into()), actor2.op_id_at(1) => amp::Diff::Value("wagtail".into()), } }, })), }; let mut doc = Frontend::new(); doc.apply_patch(patch).unwrap(); assert_eq!( doc.state(), &Into::::into(hashmap! {"favouriteBird" => "wagtail"}) ); let conflicts = doc.get_conflicts(&Path::root().key("favouriteBird")); assert_eq!( conflicts, Some(hashmap! { actor1.op_id_at(1) => "robin".into(), actor2.op_id_at(1) => "wagtail".into(), }) ) } #[test] fn create_nested_maps() { let actor = amp::ActorId::random(); let patch = amp::Patch { actor: None, seq: None, max_op: 3, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: actor.op_id_at(2).into(), obj_type: amp::MapType::Map, props: hashmap!{ "wrens".into() => hashmap!{ actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(3)) } } }) } }, })), }; let mut frontend = Frontend::new(); frontend.apply_patch(patch).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"birds" => hashmap!{"wrens" => Primitive::Int(3)}}) ); } #[test] fn apply_updates_inside_nested_maps() { let actor = amp::ActorId::random(); let patch1 = amp::Patch { actor: None, seq: None, max_op: 2, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: actor.op_id_at(2).into(), obj_type: amp::MapType::Map, props: hashmap!{ "wrens".into() => hashmap!{ actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(3)) } } }) } }, })), }; let mut frontend = Frontend::new(); frontend.apply_patch(patch1).unwrap(); let birds_id = frontend.get_object_id(&Path::root().key("birds")).unwrap(); let patch2 = amp::Patch { actor: None, seq: None, max_op: 3, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 2, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: birds_id, obj_type: amp::MapType::Map, props: hashmap!{ "sparrows".into() => hashmap!{ actor.op_id_at(3) => amp::Diff::Value(amp::ScalarValue::Int(15)) } } }) } }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into( hashmap! {"birds" => hashmap!{"wrens" => Primitive::Int(3), "sparrows" => Primitive::Int(15)}} ) ); } #[test] fn apply_updates_inside_map_conflicts() { // We don't just use random actor IDs because we need to have a specific // ordering (actor1 < actor2) let actor1 = amp::ActorId::from_bytes( uuid::Uuid::parse_str("02ef21f3-c9eb-4087-880e-bedd7c4bbe43") .unwrap() .as_bytes(), ); let actor2 = amp::ActorId::from_bytes( uuid::Uuid::parse_str("2a1d376b-24f7-4400-8d4a-f58252d644dd") .unwrap() .as_bytes(), ); let patch1 = amp::Patch { actor: None, seq: None, max_op: 2, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor1.clone() => 1, actor2.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "favouriteBirds".into() => hashmap!{ actor1.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: actor1.op_id_at(1).into(), obj_type: amp::MapType::Map, props: hashmap!{ "blackbirds".into() => hashmap!{ actor1.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(1)), } }, }), actor2.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: actor2.op_id_at(1).into(), obj_type: amp::MapType::Map, props: hashmap!{ "wrens".into() => hashmap!{ actor2.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(3)), } }, }) } }, })), }; let mut frontend = Frontend::new(); frontend.apply_patch(patch1).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"favouriteBirds" => hashmap!{"wrens" => Primitive::Int(3)}}) ); assert_eq!( frontend .get_conflicts(&Path::root().key("favouriteBirds")) .unwrap(), hashmap! { actor1.op_id_at(1) => hashmap!{"blackbirds" => Primitive::Int(1)}.into(), actor2.op_id_at(1) => hashmap!{"wrens" => Primitive::Int(3)}.into(), } ); let patch2 = amp::Patch { actor: None, seq: None, max_op: 1, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor1.clone() => 2, actor2.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "favouriteBirds".into() => hashmap!{ actor1.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: actor1.op_id_at(1).into(), obj_type: amp::MapType::Map, props: hashmap!{ "blackbirds".into() => hashmap!{ actor1.op_id_at(3) => amp::Diff::Value(amp::ScalarValue::Int(2)), } }, }), actor2.op_id_at(1) => amp::Diff::Unchanged(amp::ObjDiff{ object_id: actor2.op_id_at(1).into(), obj_type: amp::ObjType::Map(amp::MapType::Map), }) } }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"favouriteBirds" => hashmap!{"wrens" => Primitive::Int(3)}}) ); assert_eq!( frontend .get_conflicts(&Path::root().key("favouriteBirds")) .unwrap(), hashmap! { actor1.op_id_at(1) => hashmap!{"blackbirds" => Primitive::Int(2)}.into(), actor2.op_id_at(1) => hashmap!{"wrens" => Primitive::Int(3)}.into(), } ); } #[test] fn delete_keys_in_maps() { let actor = amp::ActorId::random(); let mut frontend = Frontend::new(); let patch1 = amp::Patch { actor: None, max_op: 2, pending_changes: 0, seq: None, deps: Vec::new(), clock: hashmap! { actor.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "magpies".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Value(amp::ScalarValue::Int(2)) }, "sparrows".into() => hashmap!{ actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(15)) } }, })), }; frontend.apply_patch(patch1).unwrap(); assert_eq!( frontend.state(), &Into::::into( hashmap! {"magpies" => Primitive::Int(2), "sparrows" => Primitive::Int(15)} ) ); let patch2 = amp::Patch { actor: None, seq: None, max_op: 3, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor => 2, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "magpies".into() => hashmap!{} }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"sparrows" => Primitive::Int(15)}) ); } #[test] fn create_lists() { let actor = amp::ActorId::random(); let mut frontend = Frontend::new(); let patch = amp::Patch { actor: None, seq: None, max_op: 2, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 2, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::SequenceType::List, edits: vec![amp::DiffEdit::Insert { index: 0, elem_id: actor.op_id_at(2).into() }], props: hashmap!{ 0 => hashmap!{ actor.op_id_at(2) => amp::Diff::Value("chaffinch".into()) } } }) } }, })), }; frontend.apply_patch(patch).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"birds" => vec!["chaffinch"]}) ) } #[test] fn apply_updates_inside_lists() { let actor = amp::ActorId::random(); let mut frontend = Frontend::new(); let patch = amp::Patch { actor: None, seq: None, max_op: 1, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::SequenceType::List, edits: vec![amp::DiffEdit::Insert { index: 0, elem_id: actor.op_id_at(2).into() }], props: hashmap!{ 0 => hashmap!{ actor.op_id_at(2) => amp::Diff::Value("chaffinch".into()) } } }) } }, })), }; frontend.apply_patch(patch).unwrap(); let patch2 = amp::Patch { actor: None, seq: None, max_op: 3, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 2, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::SequenceType::List, edits: vec![], props: hashmap!{ 0 => hashmap!{ actor.op_id_at(3) => amp::Diff::Value("greenfinch".into()) } } }) } }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"birds" => vec!["greenfinch"]}) ) } #[test] fn apply_updates_inside_list_conflicts() { // We don't just use random actor IDs because we need to have a specific // ordering (actor1 < actor2) let actor1 = amp::ActorId::from_bytes( uuid::Uuid::parse_str("02ef21f3-c9eb-4087-880e-bedd7c4bbe43") .unwrap() .as_bytes(), ); let actor2 = amp::ActorId::from_bytes( uuid::Uuid::parse_str("2a1d376b-24f7-4400-8d4a-f58252d644dd") .unwrap() .as_bytes(), ); let other_actor = amp::ActorId::random(); let patch1 = amp::Patch { actor: None, seq: None, max_op: 2, pending_changes: 0, deps: Vec::new(), clock: hashmap! { other_actor.clone() => 1, actor1.clone() => 1, actor2.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ other_actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: other_actor.op_id_at(1).into(), obj_type: amp::SequenceType::List, edits: vec![amp::DiffEdit::Insert{ index: 0, elem_id: actor1.op_id_at(2).into()}], props: hashmap!{ 0 => hashmap!{ actor1.op_id_at(2) => amp::Diff::Map(amp::MapDiff{ object_id: actor1.op_id_at(2).into(), obj_type: amp::MapType::Map, props: hashmap!{ "species".into() => hashmap!{ actor1.op_id_at(3) => amp::Diff::Value("woodpecker".into()), }, "numSeen".into() => hashmap!{ actor1.op_id_at(4) => amp::Diff::Value(amp::ScalarValue::Int(1)), }, } }), actor2.op_id_at(2) => amp::Diff::Map(amp::MapDiff{ object_id: actor2.op_id_at(2).into(), obj_type: amp::MapType::Map, props: hashmap!{ "species".into() => hashmap!{ actor2.op_id_at(3) => amp::Diff::Value("lapwing".into()), }, "numSeen".into() => hashmap!{ actor2.op_id_at(4) => amp::Diff::Value(amp::ScalarValue::Int(2)), }, } }), }, } }) } }, })), }; let mut frontend = Frontend::new(); frontend.apply_patch(patch1).unwrap(); assert_eq!( frontend.state(), &Into::::into( hashmap! {"birds" => vec![hashmap!{"species" => Primitive::Str("lapwing".to_string()), "numSeen" => Primitive::Int(2)}]} ) ); assert_eq!( frontend .get_conflicts(&Path::root().key("birds").index(0)) .unwrap(), hashmap! { actor1.op_id_at(2) => hashmap!{ "species" => Primitive::Str("woodpecker".into()), "numSeen" => Primitive::Int(1), }.into(), actor2.op_id_at(2) => hashmap!{ "species" => Primitive::Str("lapwing".into()), "numSeen" => Primitive::Int(2), }.into(), } ); let patch2 = amp::Patch { actor: None, seq: None, max_op: 5, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor1.clone() => 2, actor2.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ other_actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: other_actor.op_id_at(1).into(), obj_type: amp::SequenceType::List, edits: Vec::new(), props: hashmap!{ 0 => hashmap!{ actor1.op_id_at(2) => amp::Diff::Map(amp::MapDiff{ object_id: actor1.op_id_at(2).into(), obj_type: amp::MapType::Map, props: hashmap!{ "numSeen".into() => hashmap!{ actor1.op_id_at(5) => amp::Diff::Value(amp::ScalarValue::Int(2)), }, } }), actor2.op_id_at(2) => amp::Diff::Unchanged(amp::ObjDiff{ object_id: actor2.op_id_at(2).into(), obj_type: amp::ObjType::Map(amp::MapType::Map), }), }, } }) } }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into( hashmap! {"birds" => vec![hashmap!{"species" => Primitive::Str("lapwing".to_string()), "numSeen" => Primitive::Int(2)}]} ) ); assert_eq!( frontend .get_conflicts(&Path::root().key("birds").index(0)) .unwrap(), hashmap! { actor1.op_id_at(2) => hashmap!{ "species" => Primitive::Str("woodpecker".into()), "numSeen" => Primitive::Int(2), }.into(), actor2.op_id_at(2) => hashmap!{ "species" => Primitive::Str("lapwing".into()), "numSeen" => Primitive::Int(2), }.into(), } ); } #[test] fn delete_list_elements() { let actor = amp::ActorId::random(); let mut frontend = Frontend::new(); let patch = amp::Patch { actor: None, seq: None, max_op: 3, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 1, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::SequenceType::List, edits: vec![ amp::DiffEdit::Insert { index: 0, elem_id: actor.op_id_at(2).into() }, amp::DiffEdit::Insert { index: 1, elem_id: actor.op_id_at(3).into() }, ], props: hashmap!{ 0 => hashmap!{ actor.op_id_at(2) => amp::Diff::Value("chaffinch".into()) }, 1 => hashmap!{ actor.op_id_at(3) => amp::Diff::Value("goldfinch".into()) } } }) } }, })), }; frontend.apply_patch(patch).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"birds" => vec!["chaffinch", "goldfinch"]}) ); let patch2 = amp::Patch { actor: None, seq: None, max_op: 4, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 2, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "birds".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::SequenceType::List, edits: vec![amp::DiffEdit::Remove{ index: 0 }], props: hashmap!{} }) } }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! {"birds" => vec!["goldfinch"]}) ); } #[test] fn apply_updates_at_different_levels_of_object_tree() { let actor = amp::ActorId::random(); let patch1 = amp::Patch { clock: hashmap! {actor.clone() => 1}, seq: None, max_op: 6, pending_changes: 0, actor: None, deps: Vec::new(), diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "counts".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::MapType::Map, props: hashmap!{ "magpie".into() => hashmap!{ actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(2)) } } }) }, "details".into() => hashmap!{ actor.op_id_at(3) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(3).into(), obj_type: amp::SequenceType::List, edits: vec![amp::DiffEdit::Insert{ index: 0, elem_id: actor.op_id_at(4).into() }], props: hashmap!{ 0 => hashmap!{ actor.op_id_at(4) => amp::Diff::Map(amp::MapDiff{ object_id: actor.op_id_at(4).into(), obj_type: amp::MapType::Map, props: hashmap!{ "species".into() => hashmap!{ actor.op_id_at(5) => amp::Diff::Value("magpie".into()) }, "family".into() => hashmap!{ actor.op_id_at(6) => amp::Diff::Value("Corvidae".into()) } } }) } } }) }, }, })), }; let mut frontend = Frontend::new(); frontend.apply_patch(patch1).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! { "counts" => Into::::into(hashmap!{"magpie".to_string() => Primitive::Int(2)}), "details" => vec![Into::::into(hashmap!{ "species" => "magpie", "family" => "Corvidae", })].into() }) ); let patch2 = amp::Patch { clock: hashmap! {actor.clone() => 2}, seq: None, max_op: 7, pending_changes: 0, actor: None, deps: Vec::new(), diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "counts".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::MapType::Map, props: hashmap!{ "magpie".into() => hashmap!{ actor.op_id_at(7) => amp::Diff::Value(amp::ScalarValue::Int(3)) } } }) }, "details".into() => hashmap!{ actor.op_id_at(3) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(3).into(), obj_type: amp::SequenceType::List, edits: Vec::new(), props: hashmap!{ 0 => hashmap!{ actor.op_id_at(4) => amp::Diff::Map(amp::MapDiff{ object_id: actor.op_id_at(4).into(), obj_type: amp::MapType::Map, props: hashmap!{ "species".into() => hashmap!{ actor.op_id_at(8) => amp::Diff::Value("Eurasian magpie".into()) }, } }) } } }) }, }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into(hashmap! { "counts" => Into::::into(hashmap!{"magpie".to_string() => Primitive::Int(3)}), "details" => vec![Into::::into(hashmap!{ "species" => "Eurasian magpie", "family" => "Corvidae", })].into() }) ); } #[test] fn test_text_objects() { let actor = amp::ActorId::random(); let mut frontend = Frontend::new(); let patch = amp::Patch { actor: None, seq: None, max_op: 4, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 2, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "name".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::SequenceType::Text, edits: vec![ amp::DiffEdit::Insert { index: 0, elem_id: actor.op_id_at(2).into() }, amp::DiffEdit::Insert { index: 1, elem_id: actor.op_id_at(3).into() }, amp::DiffEdit::Insert { index: 2, elem_id: actor.op_id_at(4).into() }, ], props: hashmap!{ 0 => hashmap!{ actor.op_id_at(2) => amp::Diff::Value("b".into()) }, 1 => hashmap!{ actor.op_id_at(3) => amp::Diff::Value("e".into()) }, 2 => hashmap!{ actor.op_id_at(4) => amp::Diff::Value("n".into()) } } }) } }, })), }; frontend.apply_patch(patch).unwrap(); assert_eq!( frontend.state(), &Into::::into( hashmap! {"name" => Value::Text("ben".graphemes(true).map(|s|s.to_owned()).collect())} ) ); let patch2 = amp::Patch { actor: None, seq: None, max_op: 5, pending_changes: 0, deps: Vec::new(), clock: hashmap! { actor.clone() => 3, }, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "name".into() => hashmap!{ actor.op_id_at(1) => amp::Diff::Seq(amp::SeqDiff{ object_id: actor.op_id_at(1).into(), obj_type: amp::SequenceType::Text, edits: vec![ amp::DiffEdit::Remove { index: 1 }, ], props: hashmap!{ 1 => hashmap! { actor.op_id_at(5) => amp::Diff::Value(amp::ScalarValue::Str("i".to_string())) } } }) } }, })), }; frontend.apply_patch(patch2).unwrap(); assert_eq!( frontend.state(), &Into::::into( hashmap! {"name" => Value::Text("bi".graphemes(true).map(|s|s.to_owned()).collect())} ) ); } #[test] fn test_unchanged_diff_creates_empty_objects() { let mut doc = Frontend::new(); let patch = amp::Patch { actor: Some(doc.actor_id.clone()), seq: Some(1), clock: hashmap! {doc.actor_id.clone() => 1}, deps: Vec::new(), max_op: 1, pending_changes: 0, diffs: Some(amp::Diff::Map(amp::MapDiff { object_id: amp::ObjectId::Root, obj_type: amp::MapType::Map, props: hashmap! { "text".to_string() => hashmap!{ "1@cfe5fefb771f4c15a716d488012cbf40".try_into().unwrap() => amp::Diff::Unchanged(amp::ObjDiff{ object_id: "1@cfe5fefb771f4c15a716d488012cbf40".try_into().unwrap(), obj_type: amp::ObjType::Sequence(amp::SequenceType::Text) }) } }, })), }; doc.apply_patch(patch).unwrap(); assert_eq!( doc.state(), &Value::Map( hashmap! {"text".to_string() => Value::Text(Vec::new())}, amp::MapType::Map ), ); }