mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 02:17:24 -06:00
* CI: line-table debug info for dev builds, none for dependencies * CI: one integration-test binary per crate * CI: nextest with doctests, test build without examples, one wasm check * CI: the interface audit reports missing catalog entries instead of failing * Docs: the interface audit, not CI as a whole, passes with missing catalog entries
137 lines
5.8 KiB
Rust
137 lines
5.8 KiB
Rust
//! #1690, finding 3: malformed command IDs must never target another comment.
|
|
use serde_json::{Value, json};
|
|
use wordcraft_doc::{Document, StoryRef};
|
|
use wordcraft_engine::{CmdError, Session};
|
|
|
|
fn commented() -> Session {
|
|
let mut s = Session::new(Document::new());
|
|
s.run("text.insert", &json!({"text": "Keep this text"})).unwrap();
|
|
s.run("select.text", &json!({"text": "this"})).unwrap();
|
|
assert_eq!(s.run("review.newComment", &json!({"text": "Keep this comment"})).unwrap()["id"], 0);
|
|
s.run("select.text", &json!({"text": "this"})).unwrap();
|
|
s.run("select.collapse", &json!({})).unwrap();
|
|
s
|
|
}
|
|
|
|
fn invalid_ids_leave_state_untouched(command: &str) {
|
|
for id in [json!(-1), json!(0.5), json!(0.0), json!("0"), Value::Null, json!(false), json!(4294967296u64)] {
|
|
for at_comment in [true, false] {
|
|
let mut s = commented();
|
|
if !at_comment {
|
|
s.run("caret.docEnd", &json!({})).unwrap();
|
|
s.run("text.newParagraph", &json!({})).unwrap();
|
|
}
|
|
// Keep a redo step too: a refused command must not clear it.
|
|
s.run("format.bold", &json!({})).unwrap();
|
|
s.run("edit.undo", &json!({})).unwrap();
|
|
assert!(s.can_redo());
|
|
let before = (s.doc.clone(), s.sel.clone(), s.undo_labels(), s.redo_label().map(str::to_owned), s.dirty);
|
|
let result = s.run(command, &json!({"id": id, "text": "Replacement"}));
|
|
assert!(matches!(result, Err(CmdError::Params(_))), "{command}, id={id}, at_comment={at_comment}: {result:?}");
|
|
assert_eq!(
|
|
(s.doc.clone(), s.sel.clone(), s.undo_labels(), s.redo_label().map(str::to_owned), s.dirty),
|
|
before,
|
|
"{command}, id={id}, at_comment={at_comment}"
|
|
);
|
|
let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap();
|
|
assert_eq!(back.comments.len(), 1);
|
|
let comment = back.comments.get(&0).unwrap();
|
|
assert!(!comment.resolved);
|
|
assert_eq!(back.plain_text(wordcraft_doc::StoryRef::Part(comment.part)), "Keep this comment");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn delete_comment_rejects_invalid_ids() {
|
|
invalid_ids_leave_state_untouched("review.deleteComment");
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_comment_rejects_invalid_ids() {
|
|
invalid_ids_leave_state_untouched("review.resolveComment");
|
|
}
|
|
|
|
#[test]
|
|
fn reply_rejects_invalid_comment_ids() {
|
|
invalid_ids_leave_state_untouched("review.reply");
|
|
}
|
|
|
|
#[test]
|
|
fn edit_comment_rejects_invalid_ids() {
|
|
invalid_ids_leave_state_untouched("review.editComment");
|
|
}
|
|
|
|
#[test]
|
|
fn delete_all_rejects_an_explicit_invalid_id() {
|
|
let mut s = commented();
|
|
let before = s.doc.clone();
|
|
assert!(matches!(s.run("review.deleteComment", &json!({"all": true, "id": null})), Err(CmdError::Params(_))));
|
|
assert_eq!(s.doc, before);
|
|
for params in [json!({"all": true}), json!({"all": true, "id": 0})] {
|
|
let mut s = commented();
|
|
let before = s.doc.clone();
|
|
s.run("review.deleteComment", ¶ms).unwrap();
|
|
assert!(s.doc.comments.is_empty());
|
|
s.run("edit.undo", &json!({})).unwrap();
|
|
assert_eq!(s.doc, before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn largest_comment_id_is_accepted_without_a_caret_fallback() {
|
|
let mut s = commented();
|
|
let comment = s.doc.comments.remove(&0).unwrap();
|
|
s.doc.comments.insert(u32::MAX, comment);
|
|
for object in &mut s.doc.para_mut(StoryRef::Body, &wordcraft_doc::Path::top(0)).unwrap().objects {
|
|
if let wordcraft_doc::para::InlineObject::CommentStart { id } | wordcraft_doc::para::InlineObject::CommentEnd { id } = object {
|
|
*id = u32::MAX;
|
|
}
|
|
}
|
|
s.run("caret.docEnd", &json!({})).unwrap();
|
|
s.run("text.newParagraph", &json!({})).unwrap();
|
|
s.run("review.resolveComment", &json!({"id": u32::MAX, "value": true})).unwrap();
|
|
assert!(s.doc.comments.get(&u32::MAX).unwrap().resolved);
|
|
s.run("review.editComment", &json!({"id": u32::MAX, "text": "Still here"})).unwrap();
|
|
s.run("review.deleteComment", &json!({"id": u32::MAX})).unwrap();
|
|
assert!(s.doc.comments.is_empty());
|
|
s.run("edit.undo", &json!({})).unwrap();
|
|
assert!(s.doc.comments.contains_key(&u32::MAX));
|
|
}
|
|
|
|
#[test]
|
|
fn valid_comment_ids_and_omitted_caret_ids_keep_undo_redo_and_docx_fidelity() {
|
|
for command in ["review.resolveComment", "review.deleteComment", "review.reply", "review.editComment"] {
|
|
let params = json!({"id": 0, "text": "Changed comment"});
|
|
let mut variants = vec![params];
|
|
if matches!(command, "review.resolveComment" | "review.deleteComment") {
|
|
variants.push(json!({}));
|
|
}
|
|
for params in variants {
|
|
let mut s = commented();
|
|
let before = s.doc.clone();
|
|
let depth = s.undo_depth();
|
|
s.run(command, ¶ms).unwrap();
|
|
let after = s.doc.clone();
|
|
assert_ne!(after, before);
|
|
assert_eq!(s.undo_depth(), depth + 1);
|
|
s.run("edit.undo", &json!({})).unwrap();
|
|
assert_eq!(s.doc, before);
|
|
s.run("edit.redo", &json!({})).unwrap();
|
|
assert_eq!(s.doc, after);
|
|
let comments = |doc: &Document| {
|
|
doc.comments
|
|
.values()
|
|
.map(|c| (c.author.clone(), c.initials.clone(), c.date.clone(), c.resolved, c.parent, doc.plain_text(StoryRef::Part(c.part))))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let mut back = after.clone();
|
|
for _ in 0..2 {
|
|
back = wordcraft_docx::read(&wordcraft_docx::write(&back).unwrap()).unwrap();
|
|
assert_eq!(comments(&back), comments(&after), "{command}");
|
|
assert_eq!(back.plain_text(StoryRef::Body), after.plain_text(StoryRef::Body), "{command}");
|
|
}
|
|
}
|
|
}
|
|
}
|