//! Integration tests of the open buffers and of the watcher events applied to a //! session (SPEC 4.2). mod common; use std::ops::Range; use common::{TempVault, on_disk, seed, temp_vault}; use notescraft_engine::{Applied, Error, Session}; use notescraft_vault::VaultEvent; /// A vault with one note, ready to be edited. fn one_note(name: &str) -> (TempVault, Session) { let vault = temp_vault(name); seed(vault.path(), &[("note.md", "hello\n")]); let session = Session::open(vault.path()).expect("open the vault"); (vault, session) } /// Opening, editing, saving and closing, and what the disk looks like after. #[test] fn a_buffer_opens_edits_saves_and_closes() { let (vault, mut session) = one_note("buffer"); let buffer = session.open_buffer("note.md").expect("open"); assert_eq!(buffer.text(), "hello\n"); assert!(!buffer.is_dirty()); assert!(session.buffers().contains_key("note.md")); let buffer = session.buffer_mut("note.md").expect("the buffer is open"); buffer.edit(0..5, "HELLO").expect("edit"); assert!(buffer.is_dirty()); assert_eq!(on_disk(vault.path(), "note.md"), "hello\n", "an edit is not a save"); assert!(session.save_buffer("note.md").expect("save"), "the bytes changed"); assert_eq!(on_disk(vault.path(), "note.md"), "HELLO\n"); assert!(!session.buffer("note.md").expect("still open").is_dirty()); assert!(!session.save_buffer("note.md").expect("an unchanged save"), "nothing to write"); session.close_buffer("note.md", false).expect("close a clean buffer"); assert!(session.buffer("note.md").is_none()); assert_eq!(session.buffers().len(), 0); assert_eq!(session.open_buffer("note.md").expect("reopen").text(), "HELLO\n"); } /// Undo and redo walk the stack of an open buffer. #[test] fn undo_and_redo_walk_the_buffer_stack() { let (_vault, mut session) = one_note("undo"); session.open_buffer("note.md").expect("open"); session.buffer_mut("note.md").expect("open").edit(0..0, "a ").expect("insert"); session.buffer_mut("note.md").expect("open").edit(0..1, "b").expect("replace"); assert_eq!(session.buffer("note.md").expect("open").text(), "b hello\n"); assert!(session.buffer_mut("note.md").expect("open").undo().expect("undo")); assert_eq!(session.buffer("note.md").expect("open").text(), "a hello\n"); assert!(session.buffer_mut("note.md").expect("open").redo().expect("redo")); assert_eq!(session.buffer("note.md").expect("open").text(), "b hello\n"); assert!(session.buffer_mut("note.md").expect("open").undo().expect("undo")); assert!(session.buffer_mut("note.md").expect("open").undo().expect("undo")); assert!(!session.buffer_mut("note.md").expect("open").undo().expect("undo"), "the stack is empty"); assert!(!session.buffer("note.md").expect("open").is_dirty(), "back to the saved bytes"); assert!(session.buffer_mut("note.md").expect("open").redo().expect("redo")); assert_eq!(session.buffer("note.md").expect("open").text(), "a hello\n"); } /// A dirty buffer is only closed when the caller says so. #[test] fn closing_a_dirty_buffer_needs_force() { let (_vault, mut session) = one_note("dirty"); session.open_buffer("note.md").expect("open"); session.buffer_mut("note.md").expect("open").edit(0..1, "H").expect("edit"); assert_eq!(session.close_buffer("note.md", false), Err(Error::Dirty(String::from("note.md")))); assert!(session.buffer("note.md").is_some()); assert_eq!(session.close_buffer("note.md", true), Ok(())); assert_eq!(session.buffers().len(), 0); assert_eq!(session.close_buffer("note.md", true), Err(Error::NoBuffer(String::from("note.md")))); assert!(session.buffer_mut("note.md").is_err(), "there is no buffer to edit"); } /// Bad ranges and missing buffers are errors, never a panic. #[test] fn bad_buffer_operations_are_errors() { let (_vault, mut session) = one_note("buffer-errors"); session.open_buffer("note.md").expect("open"); let buffer = session.buffer_mut("note.md").expect("open"); let len = buffer.text().len(); for range in [len..len.saturating_add(1), 0..len.saturating_add(9), Range { start: 4, end: 1 }] { assert!(matches!(buffer.edit(range.clone(), "x"), Err(Error::InvalidRange(_))), "{range:?} must be refused"); } assert_eq!(buffer.text(), "hello\n", "a refused edit changes nothing"); assert_eq!(session.save_buffer("nope.md"), Err(Error::NoBuffer(String::from("nope.md")))); assert_eq!(session.open_buffer("nope.md").expect_err("missing"), Error::NotFound(String::from("`nope.md` does not exist"))); assert_eq!(session.open_buffer("notes.txt").expect_err("not a note"), Error::InvalidPath(String::from("`notes.txt` is not a note: it must end in `.md`"))); } /// A clean buffer reloads when the file changes outside the app. #[test] fn an_external_change_reloads_a_clean_buffer() { let (vault, mut session) = one_note("external-clean"); session.open_buffer("note.md").expect("open"); seed(vault.path(), &[("note.md", "# changed\n")]); let applied = session.apply_event(VaultEvent::Modified(String::from("note.md"))).expect("apply"); assert_eq!(applied, Applied::Indexed); assert_eq!(session.buffer("note.md").expect("open").text(), "# changed\n"); assert!(!session.buffer("note.md").expect("open").is_dirty()); assert_eq!(session.index().note("note.md").map(|note| note.title.as_str()), Some("note")); assert_eq!(session.conflicts(), &[] as &[String]); } /// A change landing on a dirty buffer is a conflict, and nothing is overwritten. #[test] fn an_external_change_on_a_dirty_buffer_is_a_conflict() { let (vault, mut session) = one_note("external-dirty"); session.open_buffer("note.md").expect("open"); session.buffer_mut("note.md").expect("open").edit(0..5, "MINE").expect("edit"); seed(vault.path(), &[("note.md", "THEIRS\n")]); let applied = session.apply_event(VaultEvent::Modified(String::from("note.md"))).expect("apply"); assert_eq!(applied, Applied::Conflict); assert_eq!(session.conflicts(), &[String::from("note.md")]); assert_eq!(session.buffer("note.md").expect("open").text(), "MINE\n"); assert_eq!(on_disk(vault.path(), "note.md"), "THEIRS\n"); assert_eq!(session.index().note("note.md").map(|note| note.tags.len()), Some(0), "the index still has the old content"); // The same event twice records one conflict, not two. session.apply_event(VaultEvent::Modified(String::from("note.md"))).expect("apply again"); assert_eq!(session.conflicts().len(), 1); } /// Created, removed and renamed events keep the index and the buffers in step. #[test] fn created_removed_and_renamed_events_are_applied() { let (vault, mut session) = one_note("external-events"); seed(vault.path(), &[("moved.md", "moved body\n")]); assert_eq!(session.apply_event(VaultEvent::Created(String::from("moved.md"))).expect("created"), Applied::Indexed); assert_eq!(session.index().len(), 2); std::fs::rename(vault.path().join("moved.md"), vault.path().join("other.md")).expect("the file moved on disk"); assert_eq!(session.apply_event(VaultEvent::Renamed { from: String::from("moved.md"), to: String::from("other.md") }).expect("renamed"), Applied::Indexed); assert_eq!(session.index().notes(), vec!["note.md", "other.md"]); assert!(session.index().note("moved.md").is_none()); assert_eq!(session.apply_event(VaultEvent::Removed(String::from("other.md"))).expect("removed"), Applied::Forgotten); assert_eq!(session.index().notes(), vec!["note.md"]); // An attachment is registered on create and forgotten on removal. seed(vault.path(), &[("img.png", "x")]); assert_eq!(session.apply_event(VaultEvent::Created(String::from("img.png"))).expect("attachment"), Applied::Indexed); assert_eq!(session.index().files(), vec!["img.png"]); assert_eq!(session.apply_event(VaultEvent::Removed(String::from("img.png"))).expect("attachment gone"), Applied::Forgotten); assert!(session.index().files().is_empty(), "removed attachment is forgotten"); assert!(vault.path().join("img.png").exists()); } /// A rename of a note with an open buffer moves the buffer with it. #[test] fn a_rename_moves_the_open_buffer() { let (vault, mut session) = one_note("rename-buffer"); session.open_buffer("note.md").expect("open"); session.rename_note("note.md", "moved/note.md", false).expect("rename"); assert!(session.buffer("note.md").is_none()); let buffer = session.buffer("moved/note.md").expect("the buffer followed"); assert_eq!(buffer.path(), "moved/note.md"); assert_eq!(buffer.text(), "hello\n"); session.buffer_mut("moved/note.md").expect("open").edit(0..5, "HELLO").expect("edit"); assert!(session.save_buffer("moved/note.md").expect("save")); assert_eq!(on_disk(vault.path(), "moved/note.md"), "HELLO\n"); }