use serde_json::{json, Value};
use tiptap_rusty_parser::{apply, invert, Mark, Node, Transform};
fn node(v: &Value) -> Node {
serde_json::from_value(v.clone()).unwrap()
}
fn json_of(n: &Node) -> Value {
serde_json::to_value(n).unwrap()
}
fn undo_redo(base: &Value, f: impl FnOnce(&mut Transform<'_>)) -> Value {
let original = node(base);
let mut doc = original.clone();
let changes = {
let mut tr = doc.transform();
f(&mut tr);
tr.finish()
};
let edited = json_of(&doc);
let undo = invert(&original, &changes).unwrap();
apply(&mut doc, &undo).unwrap();
assert_eq!(&json_of(&doc), base, "undo lost shape; undo = {undo:?}");
let redo = invert(&node(&edited), &undo).unwrap();
apply(&mut doc, &redo).unwrap();
assert_eq!(json_of(&doc), edited, "redo lost shape; redo = {redo:?}");
edited
}
#[test]
fn insert_into_contentless_undoes_to_no_content_key() {
let base = json!({"type":"paragraph"});
let edited = undo_redo(&base, |tr| {
tr.insert(vec![], 0, Node::text("x")).unwrap();
});
assert_eq!(
edited,
json!({"type":"paragraph","content":[{"type":"text","text":"x"}]})
);
}
#[test]
fn insert_into_nested_contentless_undoes_to_no_content_key() {
let base = json!({"type":"doc","content":[{"type":"paragraph"}]});
undo_redo(&base, |tr| {
tr.insert(vec![0], 0, Node::text("x")).unwrap();
});
}
#[test]
fn insert_into_explicit_empty_undoes_to_empty_array() {
let base = json!({"type":"doc","content":[{"type":"paragraph","content":[]}]});
undo_redo(&base, |tr| {
tr.insert(vec![0], 0, Node::text("x")).unwrap();
});
}
#[test]
fn remove_last_child_of_originally_empty_node() {
let base = json!({"type":"doc","content":[{"type":"paragraph","content":[]}]});
undo_redo(&base, |tr| {
tr.insert(vec![0], 0, Node::text("x")).unwrap();
tr.remove(vec![0], 0).unwrap();
});
}
#[test]
fn remove_only_child_undoes_to_populated() {
let base = json!({"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"x"}]}]});
let edited = undo_redo(&base, |tr| {
tr.remove(vec![0], 0).unwrap();
});
assert_eq!(
edited,
json!({"type":"doc","content":[{"type":"paragraph"}]})
);
}
#[test]
fn set_marks_none_vs_empty() {
for marks in [json!(null), json!([])] {
let mut t = json!({"type":"text","text":"x"});
if !marks.is_null() {
t["marks"] = marks;
}
let base = json!({"type":"paragraph","content":[t]});
undo_redo(&base, |tr| {
tr.set_marks(vec![0], Some(vec![Mark::new("bold")]))
.unwrap();
});
undo_redo(&base, |tr| {
tr.set_marks(vec![0], None).unwrap();
});
undo_redo(&base, |tr| {
tr.set_marks(vec![0], Some(Vec::new())).unwrap();
});
}
}
#[test]
fn set_and_remove_attr_none_vs_empty() {
for attrs in [json!(null), json!({})] {
let mut p = json!({"type":"paragraph"});
if !attrs.is_null() {
p["attrs"] = attrs;
}
let base = json!({"type":"doc","content":[p]});
undo_redo(&base, |tr| {
tr.set_attr(vec![0], "level", 1).unwrap();
});
undo_redo(&base, |tr| {
tr.set_attr(vec![0], "level", 1).unwrap();
tr.remove_attr(vec![0], "level").unwrap();
});
}
let base = json!({"type":"doc","content":[{"type":"heading","attrs":{"level":1}}]});
let edited = undo_redo(&base, |tr| {
tr.remove_attr(vec![0], "level").unwrap();
});
assert_eq!(edited, json!({"type":"doc","content":[{"type":"heading"}]}));
}
fn shapes() -> Vec<Value> {
let mut out = Vec::new();
for content in [
None,
Some(json!([])),
Some(json!([{"type":"text","text":"a"}])),
] {
for attrs in [None, Some(json!({})), Some(json!({"k":1}))] {
for marks in [None, Some(json!([])), Some(json!([{"type":"bold"}]))] {
let mut n = json!({"type":"x"});
if let Some(c) = &content {
n["content"] = c.clone();
}
if let Some(a) = &attrs {
n["attrs"] = a.clone();
}
if let Some(m) = &marks {
n["marks"] = m.clone();
}
out.push(n);
}
}
}
out
}
type Op = fn(&mut Transform<'_>, Vec<usize>, &Node);
fn ops() -> Vec<(&'static str, Op)> {
vec![
("insert", |tr, p, _| {
tr.insert(p, 0, Node::text("n")).unwrap();
}),
("insert+remove", |tr, p, _| {
tr.insert(p.clone(), 0, Node::text("n")).unwrap();
tr.remove(p, 0).unwrap();
}),
("remove-all", |tr, p, n| {
for _ in 0..n.child_count() {
tr.remove(p.clone(), 0).unwrap();
}
}),
("set_attr", |tr, p, _| {
tr.set_attr(p, "z", 2).unwrap();
}),
("set+remove_attr", |tr, p, _| {
tr.set_attr(p.clone(), "z", 2).unwrap();
tr.remove_attr(p, "z").unwrap();
}),
("remove_attr k", |tr, p, _| {
tr.remove_attr(p, "k").unwrap();
}),
("set_marks some", |tr, p, _| {
tr.set_marks(p, Some(vec![Mark::new("italic")])).unwrap();
}),
("set_marks empty", |tr, p, _| {
tr.set_marks(p, Some(Vec::new())).unwrap();
}),
("set_marks none", |tr, p, _| {
tr.set_marks(p, None).unwrap();
}),
]
}
#[test]
fn shape_matrix_roundtrips_at_root_and_nested() {
for shape in shapes() {
let target = node(&shape);
for (name, op) in ops() {
let r = std::panic::catch_unwind(|| {
undo_redo(&shape, |tr| op(tr, vec![], &target));
});
assert!(r.is_ok(), "root {name} on {shape}");
let base = json!({"type":"doc","content":[{"type":"s"}, shape.clone(), {"type":"s"}]});
let r = std::panic::catch_unwind(|| {
undo_redo(&base, |tr| op(tr, vec![1], &target));
});
assert!(r.is_ok(), "nested {name} on {shape}");
}
}
}