designcraft/crates/engine/tests/text_thread_regressions.rs

278 lines
12 KiB
Rust

use designcraft_doc::{ParaFormat, Story, StoryId};
use designcraft_engine::Session;
use serde_json::{Value, json};
use std::sync::Arc;
fn setup() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
s
}
fn frame(s: &mut Session, text: &str) -> (u64, u64) {
let r = s.execute("frame.create", &json!({"rect":[72,72,300,300],"content":"text","text":text,"caret":false})).unwrap();
(r["id"].as_u64().unwrap(), r["story"].as_u64().unwrap())
}
fn snapshot(s: &Session) -> Value {
let d = s.doc().unwrap();
json!({"doc":d.doc,"selection":d.selection,"revision":d.revision,"dirty":d.is_dirty(),"undo":d.history.undo.len(),"redo":d.history.redo.len(),"journal":s.journal})
}
#[test]
fn rejected_thread_is_atomic_with_new_frame_and_redo() {
let mut s = setup();
let (a, _) = frame(&mut s, "hello");
frame(&mut s, "undo");
s.execute("edit.undo", &json!({})).unwrap();
let before = snapshot(&s);
assert!(s.execute("text.thread", &json!({"from":999999,"rect":[0,0,100,100]})).is_err());
assert_eq!(snapshot(&s), before);
assert!(s.execute("text.thread", &json!({"from":a,"rect":[0,0,100,100],"spread":999})).is_err());
assert_eq!(snapshot(&s), before);
}
#[test]
fn locked_frame_cannot_be_unthreaded() {
let mut s = setup();
let (a, _) = frame(&mut s, "hello");
let (b, _) = frame(&mut s, "");
s.execute("text.thread", &json!({"from":a,"to":b})).unwrap();
s.execute("object.lock", &json!({"ids":[a]})).unwrap();
let before = snapshot(&s);
let r = s.execute("text.unthread", &json!({"frame":a}));
assert!(r.is_err(), "unthread accepted locked frame: {r:?}");
assert_eq!(snapshot(&s), before);
}
#[test]
fn locked_frame_cannot_be_removed_from_thread() {
let mut s = setup();
let (a, _) = frame(&mut s, "hello");
let (b, _) = frame(&mut s, "");
s.execute("text.thread", &json!({"from":a,"to":b})).unwrap();
s.execute("object.lock", &json!({"ids":[a]})).unwrap();
let before = snapshot(&s);
let r = s.execute("text.removeFromThread", &json!({"frame":a}));
assert!(r.is_err(), "remove accepted locked frame: {r:?}");
assert_eq!(snapshot(&s), before);
}
#[test]
fn rejected_selection_preserves_pending_typing() {
let mut s = setup();
let (_, sid) = frame(&mut s, "hello");
s.execute("text.select", &json!({"story":sid,"anchor":3})).unwrap();
s.execute("type.char", &json!({"attrs":{"size":30}})).unwrap();
let before = s.execute("type.selectionAttrs", &json!({})).unwrap();
assert!(s.execute("text.select", &json!({"story":999999,"anchor":0})).is_err());
let after = s.execute("type.selectionAttrs", &json!({})).unwrap();
assert_eq!(after, before, "rejected selection reset pending typing");
}
#[test]
fn append_max_id_preserves_both_tables() {
use designcraft_doc::table::Table;
let mut a = Story::with_text(StoryId(1), &designcraft_doc::TABLE_ANCHOR.to_string(), ParaFormat::default());
let mut b = Story::with_text(StoryId(2), &designcraft_doc::TABLE_ANCHOR.to_string(), ParaFormat::default());
let mut ta = Table::new(u64::MAX, 1, 1, 0, 0, 100.);
ta.cells[0].text = Story::with_text(StoryId(0), "ORIGINAL", ParaFormat::default());
a.tables.insert(u64::MAX, Arc::new(ta));
let mut tb = Table::new(1, 1, 1, 0, 0, 100.);
tb.cells[0].text = Story::with_text(StoryId(0), "INCOMING", ParaFormat::default());
b.tables.insert(1, Arc::new(tb));
a.paras[0].table = Some(u64::MAX);
b.paras[0].table = Some(1);
a.check().unwrap();
b.check().unwrap();
a.append_story(b);
let contents: Vec<_> = a.tables.values().map(|t| t.cells[0].text.text.clone()).collect();
assert!(contents.contains(&"ORIGINAL".into()) && contents.contains(&"INCOMING".into()), "table loss: {contents:?}");
}
#[test]
fn idml_dropcap_detail_preserves_inherited_other_bit() {
use designcraft_doc::{Document, ParaAttrs, ParagraphStyle, build::NewDocument};
let mut d = Document::new(&NewDocument::default());
let parent = ParagraphStyle {
name: "Parent".into(),
based_on: None,
next_style: None,
para: ParaAttrs { drop_cap_align_left: Some(false), drop_cap_scale_descenders: Some(true), ..Default::default() },
chars: Default::default(),
shortcut: String::new(),
};
let child = ParagraphStyle {
name: "Child".into(),
based_on: Some("Parent".into()),
next_style: None,
para: ParaAttrs { drop_cap_scale_descenders: Some(false), ..Default::default() },
chars: Default::default(),
shortcut: String::new(),
};
d.styles_mut().paragraph.extend([parent, child]);
let before = d.styles.resolve_para_style("Child").0;
let bytes = designcraft_idml::export_idml(&d);
let back = designcraft_idml::import_idml(&bytes).unwrap();
let after = back.styles.resolve_para_style("Child").0;
assert_eq!(
(after.drop_cap_align_left, after.drop_cap_scale_descenders),
(before.drop_cap_align_left, before.drop_cap_scale_descenders),
"IDML export resets inherited align-left bit"
);
}
#[test]
fn threading_preserves_incoming_text_hyperlink() {
let mut s = setup();
let (a, sa) = frame(&mut s, "First");
let (b, sb) = frame(&mut s, "Second");
s.execute("text.select", &json!({"story":sb,"anchor":0,"focus":6})).unwrap();
s.execute("hyperlink.create", &json!({"url":"https://example.com"})).unwrap();
s.execute("text.thread", &json!({"from":a,"to":b})).unwrap();
let d = &s.doc().unwrap().doc;
let back = designcraft_format::load(&designcraft_format::save(d).unwrap()).unwrap();
match &back.hyperlinks[0].source {
designcraft_doc::HyperlinkSource::Text { story, start, end } => {
assert_eq!(story.0, sa, "hyperlink points to deleted incoming story");
assert_eq!((*start, *end), (6, 12));
}
_ => panic!("wrong source"),
};
}
#[test]
fn thread_cannot_destroy_body_by_joining_generated_endnotes() {
let mut s = setup();
let (body, sid) = frame(&mut s, "KEEP THIS BODY");
s.execute("text.select", &json!({"story":sid,"anchor":14})).unwrap();
let e = s.execute("endnote.insert", &json!({"text":"a note"})).unwrap();
let ef = e["frame"].as_u64().unwrap();
let before = snapshot(&s);
let r = s.execute("text.thread", &json!({"from":ef,"to":body}));
assert!(
r.is_err(),
"thread into generated endnotes accepted: {r:?}; stories: {:?}",
s.doc().unwrap().doc.stories.values().map(|s| &s.text).collect::<Vec<_>>()
);
assert_eq!(snapshot(&s), before);
}
#[test]
fn generated_endnotes_accept_empty_extensions_but_reject_reverse_body_merge() {
let mut s = setup();
let (body, sid) = frame(&mut s, "Body");
s.execute("text.select", &json!({"story":sid,"anchor":4})).unwrap();
let r = s.execute("endnote.insert", &json!({"text":"note"})).unwrap();
let ef = r["frame"].as_u64().unwrap();
let esid = s.doc().unwrap().doc.endnote_story.unwrap();
let before = snapshot(&s);
assert!(s.execute("text.thread", &json!({"from":body,"to":ef})).is_err());
assert_eq!(snapshot(&s), before);
s.execute("text.thread", &json!({"from":ef,"rect":[72,72,300,300]})).unwrap();
assert_eq!(s.doc().unwrap().doc.endnote_story, Some(esid));
s.execute("text.thread", &json!({"to":ef,"rect":[72,72,300,300]})).unwrap();
assert_eq!(s.doc().unwrap().doc.endnote_story, Some(esid));
assert_eq!(s.doc().unwrap().doc.story(esid).unwrap().frames.len(), 3);
assert!(s.doc().unwrap().doc.story(StoryId(sid)).unwrap().text.starts_with("Body"));
s.doc().unwrap().doc.check().unwrap();
}
#[test]
fn threading_moves_data_placeholders_and_primary_story_with_text() {
use designcraft_doc::{Placeholder, PlaceholderAnchor, PlaceholderRole};
let mut s = setup();
let (a, sa) = frame(&mut s, "First");
let (b, sb) = frame(&mut s, "<<name>>");
s.edit(|d, _| {
d.settings.primary_story = Some(StoryId(sb));
d.data_merge.placeholders.push(Placeholder {
id: 123,
source_id: 0,
field: "name".into(),
role: PlaceholderRole::Text,
anchor: PlaceholderAnchor::Text { story: StoryId(sb), start: 0, end: 8 },
});
Ok(())
})
.unwrap();
let before = s.doc().unwrap().doc.clone();
s.execute("text.thread", &json!({"from":a,"to":b})).unwrap();
let d = &s.doc().unwrap().doc;
assert_eq!(d.settings.primary_story, Some(StoryId(sa)));
assert_eq!(d.data_merge.placeholders.len(), 1);
assert_eq!(d.data_merge.placeholders[0].anchor, PlaceholderAnchor::Text { story: StoryId(sa), start: 6, end: 14 });
let after = d.clone();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, before);
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, after);
}
#[test]
fn append_max_ids_preserves_addressable_footnotes_and_endnotes() {
let mut a = Story::with_text(StoryId(1), "A", ParaFormat::default());
let mut b = Story::with_text(StoryId(2), "B", ParaFormat::default());
a.insert_note(1, "original footnote", ParaFormat::default());
b.insert_note(1, "incoming footnote", ParaFormat::default());
Arc::make_mut(&mut a.notes[0]).id = u64::MAX;
a.insert_endnote(1, "original endnote", ParaFormat::default());
b.insert_endnote(1, "incoming endnote", ParaFormat::default());
Arc::make_mut(&mut a.endnotes[0]).id = u64::MAX;
a.check().unwrap();
b.check().unwrap();
a.append_story(b);
a.check().unwrap();
assert_ne!(a.notes[0].id, a.notes[1].id);
assert_ne!(a.endnotes[0].id, a.endnotes[1].id);
assert_eq!(a.notes[0].text.text, "original footnote");
assert_eq!(a.endnotes[1].text.text, "incoming endnote");
}
#[test]
fn locked_layer_and_neighbour_reject_unthread_atomically() {
for lock_layer in [false, true] {
let mut s = setup();
let (a, _) = frame(&mut s, "A");
let (b, _) = frame(&mut s, "B");
s.execute("text.thread", &json!({"from":a,"to":b})).unwrap();
if lock_layer {
let lid = s.doc().unwrap().active_layer.0;
s.execute("layer.set", &json!({"id":lid,"locked":true})).unwrap();
} else {
s.execute("object.lock", &json!({"ids":[b]})).unwrap();
}
let before = snapshot(&s);
assert!(s.execute("text.unthread", &json!({"frame":a})).is_err());
assert_eq!(snapshot(&s), before);
}
}
#[test]
fn idml_partial_dropcap_bits_resolve_both_style_and_paragraph_inheritance() {
use designcraft_doc::{Document, ParaAttrs, ParagraphStyle, SpreadRef, build::NewDocument};
use designcraft_geom::Rect;
for parent_align in [false, true] {
for parent_scale in [false, true] {
for local_align in [false, true] {
let mut d = Document::new(&NewDocument::default());
d.styles_mut().paragraph.push(ParagraphStyle {
name: "Parent".into(),
based_on: None,
next_style: None,
para: ParaAttrs { drop_cap_align_left: Some(parent_align), drop_cap_scale_descenders: Some(parent_scale), ..Default::default() },
chars: Default::default(),
shortcut: String::new(),
});
let layer = d.default_layer();
let (_, sid) = d
.add_text_frame(
SpreadRef::Doc(0),
Rect::new(10., 10., 200., 200.),
layer,
"Body",
ParaFormat {
style: "Parent".into(),
para: ParaAttrs { drop_cap_align_left: Some(local_align), ..Default::default() },
..Default::default()
},
)
.unwrap();
let bytes = designcraft_idml::export_idml(&d);
let back = designcraft_idml::import_idml(&bytes).unwrap();
let st = back.stories.values().find(|s| s.text == "Body").unwrap();
let actual = back.styles.resolve_para(&st.paras[0]).0;
let expected = d.styles.resolve_para(&d.story(sid).unwrap().paras[0]).0;
assert_eq!(
(actual.drop_cap_align_left, actual.drop_cap_scale_descenders),
(expected.drop_cap_align_left, expected.drop_cap_scale_descenders)
);
}
}
}
}