use crate::document::edit::{is_valid_field_name, EditError};
use crate::document::meta::is_valid_kind_name;
use crate::document::{Card, Document};
use crate::value::QuillValue;
use crate::version::QuillReference;
use std::str::FromStr;
fn make_doc() -> Document {
Document::from_markdown(
"~~~card-yaml\n$quill: test_quill\n$kind: main\ntitle: Hello\n~~~\n\nBody text.\n",
)
.unwrap()
}
fn make_doc_with_cards() -> Document {
Document::from_markdown(
"~~~card-yaml\n$quill: test_quill\n$kind: main\ntitle: Hello\n~~~\n\nBody.\n\n~~~card-yaml\n$kind: note\nfoo: bar\n~~~\n\nCard body.\n\n~~~card-yaml\n$kind: summary\n~~~\n",
)
.unwrap()
}
fn qv(s: &str) -> QuillValue {
QuillValue::from_json(serde_json::json!(s))
}
fn qv_int(n: i64) -> QuillValue {
QuillValue::from_json(serde_json::json!(n))
}
#[test]
fn test_valid_field_names() {
assert!(is_valid_field_name("title"));
assert!(is_valid_field_name("my_field"));
assert!(is_valid_field_name("_private"));
assert!(is_valid_field_name("abc123"));
assert!(is_valid_field_name("a1b2c3"));
assert!(is_valid_field_name("x"));
assert!(is_valid_field_name("_"));
}
#[test]
fn test_invalid_field_names() {
assert!(!is_valid_field_name(""));
assert!(!is_valid_field_name("Title")); assert!(!is_valid_field_name("123abc")); assert!(!is_valid_field_name("my-field")); assert!(!is_valid_field_name("my field")); assert!(!is_valid_field_name("BODY")); assert!(!is_valid_field_name("CARDS")); assert!(!is_valid_field_name("$body")); }
#[test]
fn test_edit_error_invalid_field_name() {
let mut doc = make_doc();
let result = doc.main_mut().set_field("My-Field", qv("value"));
assert_eq!(
result,
Err(EditError::InvalidFieldName("My-Field".to_string()))
);
}
#[test]
fn test_edit_error_uppercase_rejected() {
let mut doc = make_doc();
let result = doc.main_mut().set_field("BODY", qv("value"));
assert_eq!(
result,
Err(EditError::InvalidFieldName("BODY".to_string()))
);
}
#[test]
fn test_edit_error_invalid_kind_name() {
let result = Card::new("Invalid-Kind");
assert_eq!(
result,
Err(EditError::InvalidKindName("Invalid-Kind".to_string()))
);
}
#[test]
fn test_edit_error_index_out_of_range() {
let mut doc = make_doc(); let card = Card::new("note").unwrap();
let result = doc.insert_card(5, card);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: 5, len: 0 }));
}
#[test]
fn test_edit_error_display() {
assert!(EditError::InvalidFieldName("Bad-Name".to_string())
.to_string()
.contains("Bad-Name"));
assert!(EditError::InvalidKindName("Bad-Kind".to_string())
.to_string()
.contains("Bad-Kind"));
assert!(EditError::IndexOutOfRange { index: 3, len: 2 }
.to_string()
.contains("3"));
}
#[test]
fn test_document_set_field_rejects_legacy_uppercase_names() {
for name in ["BODY", "CARDS", "QUILL", "CARD", "$body", "$cards"] {
let mut doc = make_doc();
let result = doc.main_mut().set_field(name, qv("value"));
assert_eq!(
result,
Err(EditError::InvalidFieldName(name.to_string())),
"expected InvalidFieldName for '{}'",
name
);
}
}
#[test]
fn test_document_set_field_inserts() {
let mut doc = make_doc();
doc.main_mut().set_field("author", qv("Alice")).unwrap();
assert_eq!(
doc.main().payload().get("author").unwrap().as_str(),
Some("Alice")
);
}
#[test]
fn test_document_set_field_updates_existing() {
let mut doc = make_doc();
doc.main_mut().set_field("title", qv("New Title")).unwrap();
assert_eq!(
doc.main().payload().get("title").unwrap().as_str(),
Some("New Title")
);
}
#[test]
fn test_document_remove_field_existing() {
let mut doc = make_doc();
let removed = doc.main_mut().remove_field("title").unwrap();
assert_eq!(removed.unwrap().as_str(), Some("Hello"));
assert!(doc.main().payload().get("title").is_none());
}
#[test]
fn test_document_remove_field_absent() {
let mut doc = make_doc();
let removed = doc.main_mut().remove_field("nonexistent").unwrap();
assert!(removed.is_none());
}
#[test]
fn test_document_remove_field_legacy_uppercase_rejected() {
let mut doc = make_doc();
for name in ["BODY", "CARDS", "QUILL", "CARD"] {
match doc.main_mut().remove_field(name) {
Err(EditError::InvalidFieldName(got)) => assert_eq!(got, name),
other => panic!("expected InvalidFieldName for {name}, got {other:?}"),
}
}
}
#[test]
fn test_document_remove_field_invalid_name_throws() {
let mut doc = make_doc();
match doc.main_mut().remove_field("Bad-Name") {
Err(EditError::InvalidFieldName(name)) => assert_eq!(name, "Bad-Name"),
other => panic!("expected InvalidFieldName, got {other:?}"),
}
}
#[test]
fn test_document_set_quill_ref() {
let mut doc = make_doc();
let new_ref = QuillReference::from_str("new_quill").unwrap();
doc.set_quill_ref(new_ref);
assert_eq!(doc.quill_reference().name, "new_quill");
}
#[test]
fn test_document_replace_body() {
let mut doc = make_doc();
doc.main_mut().replace_body("New body content.");
assert_eq!(doc.main().body(), "New body content.");
}
#[test]
fn test_document_push_card() {
let mut doc = make_doc();
let card = Card::new("note").unwrap();
doc.push_card(card);
assert_eq!(doc.cards().len(), 1);
assert_eq!(doc.cards()[0].kind(), Some("note"));
}
#[test]
fn test_document_insert_card_at_zero() {
let mut doc = make_doc_with_cards(); let card = Card::new("intro").unwrap();
doc.insert_card(0, card).unwrap();
assert_eq!(doc.cards().len(), 3);
assert_eq!(doc.cards()[0].kind(), Some("intro"));
assert_eq!(doc.cards()[1].kind(), Some("note"));
}
#[test]
fn test_document_insert_card_at_end() {
let mut doc = make_doc_with_cards(); let len = doc.cards().len();
let card = Card::new("footer").unwrap();
doc.insert_card(len, card).unwrap();
assert_eq!(doc.cards()[len].kind(), Some("footer"));
}
#[test]
fn test_document_insert_card_out_of_range() {
let mut doc = make_doc(); let card = Card::new("note").unwrap();
let result = doc.insert_card(1, card);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: 1, len: 0 }));
}
#[test]
fn test_document_remove_card() {
let mut doc = make_doc_with_cards(); let removed = doc.remove_card(0);
assert!(removed.is_some());
assert_eq!(removed.unwrap().kind(), Some("note"));
assert_eq!(doc.cards().len(), 1);
assert_eq!(doc.cards()[0].kind(), Some("summary"));
}
#[test]
fn test_document_remove_card_out_of_range() {
let mut doc = make_doc();
let removed = doc.remove_card(0);
assert!(removed.is_none());
}
#[test]
fn test_document_card_mut() {
let mut doc = make_doc_with_cards();
{
let card = doc.card_mut(0).unwrap();
card.replace_body("Updated card body.");
}
assert_eq!(doc.cards()[0].body(), "Updated card body.");
}
#[test]
fn test_document_card_mut_out_of_range() {
let mut doc = make_doc();
assert!(doc.card_mut(0).is_none());
}
#[test]
fn test_move_card_no_op_same_index() {
let mut doc = make_doc_with_cards(); let result = doc.move_card(0, 0);
assert_eq!(result, Ok(()));
assert_eq!(doc.cards()[0].kind(), Some("note"));
assert_eq!(doc.cards()[1].kind(), Some("summary"));
}
#[test]
fn test_move_card_last_to_first() {
let mut doc = make_doc_with_cards(); doc.move_card(1, 0).unwrap();
assert_eq!(doc.cards()[0].kind(), Some("summary"));
assert_eq!(doc.cards()[1].kind(), Some("note"));
}
#[test]
fn test_move_card_first_to_last() {
let mut doc = make_doc_with_cards(); let last = doc.cards().len() - 1;
doc.move_card(0, last).unwrap();
assert_eq!(doc.cards()[0].kind(), Some("summary"));
assert_eq!(doc.cards()[last].kind(), Some("note"));
}
#[test]
fn test_move_card_from_out_of_range() {
let mut doc = make_doc_with_cards(); let len = doc.cards().len();
let result = doc.move_card(len, 0);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: len, len }));
}
#[test]
fn test_move_card_to_out_of_range() {
let mut doc = make_doc_with_cards(); let len = doc.cards().len();
let result = doc.move_card(0, len);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: len, len }));
}
#[test]
fn test_set_card_kind_renames_in_place() {
let mut doc = make_doc_with_cards(); doc.set_card_kind(0, "annotation").unwrap();
assert_eq!(doc.cards()[0].kind(), Some("annotation"));
assert_eq!(
doc.cards()[0].payload().get("foo").unwrap().as_str(),
Some("bar")
);
assert_eq!(doc.cards()[1].kind(), Some("summary"));
}
#[test]
fn test_set_card_kind_rejects_invalid_kind() {
let mut doc = make_doc_with_cards();
for bad in ["", "Bad", "with-dash", "1leading_digit"] {
match doc.set_card_kind(0, bad) {
Err(EditError::InvalidKindName(t)) => assert_eq!(t, bad),
other => panic!("expected InvalidKindName for {bad:?}, got {other:?}"),
}
}
assert_eq!(doc.cards()[0].kind(), Some("note"));
}
#[test]
fn test_set_card_kind_index_out_of_range() {
let mut doc = make_doc_with_cards();
let len = doc.cards().len();
let result = doc.set_card_kind(len, "annotation");
assert_eq!(result, Err(EditError::IndexOutOfRange { index: len, len }));
}
#[test]
fn test_set_card_kind_round_trips_via_markdown() {
let mut doc = make_doc_with_cards();
doc.set_card_kind(0, "annotation").unwrap();
let md = doc.to_markdown();
let reparsed = crate::Document::from_markdown(&md).unwrap();
assert_eq!(reparsed.cards()[0].kind(), Some("annotation"));
}
#[test]
fn test_card_new_valid() {
let card = Card::new("note").unwrap();
assert_eq!(card.kind(), Some("note"));
assert!(card.payload().is_empty());
assert_eq!(card.body(), "");
}
#[test]
fn test_card_new_invalid_kind_rejected() {
for kind in ["Note", "", "my-card"] {
assert_eq!(
Card::new(kind),
Err(EditError::InvalidKindName(kind.to_string()))
);
}
}
#[test]
fn test_card_set_field_valid() {
let mut card = Card::new("note").unwrap();
card.set_field("content", qv("Some text")).unwrap();
assert_eq!(
card.payload().get("content").unwrap().as_str(),
Some("Some text")
);
}
#[test]
fn test_card_set_field_invalid_name() {
let mut card = Card::new("note").unwrap();
let result = card.set_field("Content", qv("text"));
assert_eq!(
result,
Err(EditError::InvalidFieldName("Content".to_string()))
);
}
#[test]
fn test_card_remove_field_existing() {
let mut doc = make_doc_with_cards();
let card = doc.card_mut(0).unwrap();
let removed = card.remove_field("foo").unwrap();
assert_eq!(removed.unwrap().as_str(), Some("bar"));
assert!(card.payload().get("foo").is_none());
}
#[test]
fn test_card_remove_field_absent() {
let mut card = Card::new("note").unwrap();
assert!(card.remove_field("nonexistent").unwrap().is_none());
}
#[test]
fn test_card_remove_field_legacy_uppercase_rejected() {
let mut card = Card::new("note").unwrap();
for name in ["BODY", "CARDS", "QUILL", "CARD"] {
match card.remove_field(name) {
Err(EditError::InvalidFieldName(got)) => assert_eq!(got, name),
other => panic!("expected InvalidFieldName for {name}, got {other:?}"),
}
}
}
#[test]
fn test_card_remove_field_invalid_name_throws() {
let mut card = Card::new("note").unwrap();
match card.remove_field("Bad-Name") {
Err(EditError::InvalidFieldName(name)) => assert_eq!(name, "Bad-Name"),
other => panic!("expected InvalidFieldName, got {other:?}"),
}
}
#[test]
fn test_card_set_body() {
let mut card = Card::new("note").unwrap();
card.replace_body("Card body text.");
assert_eq!(card.body(), "Card body text.");
}
#[test]
fn test_invariants_after_mutation_sequence() {
let mut doc = make_doc();
doc.main_mut().set_field("author", qv("Alice")).unwrap();
doc.main_mut().set_field("version", qv_int(3)).unwrap();
let c1 = Card::new("note").unwrap();
let c2 = Card::new("summary").unwrap();
let c3 = Card::new("appendix").unwrap();
doc.push_card(c1);
doc.push_card(c2);
doc.insert_card(1, c3).unwrap();
doc.card_mut(0)
.unwrap()
.set_field("text", qv("Hello"))
.unwrap();
doc.move_card(2, 0).unwrap();
doc.remove_card(1);
doc.main_mut().replace_body("Updated body.");
doc.main_mut().remove_field("version").unwrap();
for key in doc.main().payload().keys() {
assert!(
is_valid_field_name(key),
"invalid key '{}' found in payload",
key
);
}
for card in doc.cards() {
if let Some(kind) = card.kind() {
assert!(is_valid_kind_name(kind), "invalid kind '{}' found", kind);
}
}
let json = doc.to_plate_json();
assert!(json.is_object());
assert_eq!(json["$quill"].as_str(), Some("test_quill"));
assert!(json["$cards"].is_array());
assert_eq!(json["$body"].as_str(), Some("Updated body."));
assert_eq!(
doc.main().payload().get("author").unwrap().as_str(),
Some("Alice")
);
assert!(doc.main().payload().get("version").is_none());
}