//! Tests of `tp.file.include` and of the Bases fallback (spec D3). use std::collections::BTreeMap; use notescraft_bases::Value; use notescraft_templater::{Answers, Ctx, Rendered, parse}; use notescraft_templates::{Date, DateTime, Weekday}; /// The reference moment of the tests: Tuesday, 2026-10-13 15:04:05. fn now() -> DateTime { DateTime::new(Date::new(2026, 10, 13).unwrap(), 15, 4, 5).unwrap() } /// The notes of the tests, by path. fn notes() -> BTreeMap { let mut notes = BTreeMap::new(); notes.insert("Note.md".to_owned(), "# Title\n\nIntro.\n\n## Section\n\nSection body.\n\n## Other\n\nOther body.\n".to_owned()); notes.insert("Block.md".to_owned(), "First paragraph.\n\nSecond paragraph with an id ^abc\n\nThird paragraph.\n".to_owned()); notes.insert("Templated.md".to_owned(), "Hello <% tp.file.title %>!\n".to_owned()); notes.insert("A.md".to_owned(), "<% tp.file.include(\"[[A]]\") %>\n".to_owned()); notes } /// The tags of the target note. fn tags() -> Vec { vec!["alpha".to_owned()] } /// The frontmatter of the target note. fn frontmatter() -> Vec<(String, Value)> { vec![("project".to_owned(), Value::Str("NotesCraft".to_owned()))] } /// A context with the notes of the tests. fn ctx<'a>(read_note: &'a dyn Fn(&str) -> Option, exists: &'a dyn Fn(&str) -> bool, tags: &'a [String], frontmatter: &'a [(String, Value)]) -> Ctx<'a> { Ctx { title: "Note", folder: "Folder", path: "Folder/Note.md", absolute_path: Some("/vault/Folder/Note.md"), now: now(), week_start: Weekday::Monday, created: Some(now()), modified: Some(now()), tags, frontmatter, selection: "", clipboard: "", read_note, exists, } } /// Renders a template with the notes of the tests. fn render(template: &str) -> Rendered { let notes = notes(); let read_note = |path: &str| notes.get(path).cloned(); let exists = |_: &str| -> bool { true }; let tags = tags(); let frontmatter = frontmatter(); let ctx = ctx(&read_note, &exists, &tags, &frontmatter); let template = parse(template).expect("the template parses"); template.render(&ctx, &Answers::default()) } #[test] fn a_whole_note_is_included() { let rendered = render("<% tp.file.include(\"[[Note]]\") %>"); assert_eq!(rendered.text, notes()["Note.md"]); assert_eq!(rendered.errors, Vec::new()); } #[test] fn a_note_is_included_without_brackets() { let rendered = render("<% tp.file.include(\"Note\") %>"); assert_eq!(rendered.text, notes()["Note.md"]); assert_eq!(rendered.errors, Vec::new()); } #[test] fn a_heading_section_is_included() { let rendered = render("<% tp.file.include(\"[[Note#Section]]\") %>"); assert_eq!(rendered.text, "## Section\n\nSection body.\n\n"); assert_eq!(rendered.errors, Vec::new()); } #[test] fn a_block_is_included() { let rendered = render("<% tp.file.include(\"[[Block#^abc]]\") %>"); assert_eq!(rendered.text, "Second paragraph with an id"); assert_eq!(rendered.errors, Vec::new()); } #[test] fn the_templates_inside_an_included_note_are_rendered() { let rendered = render("<% tp.file.include(\"[[Templated]]\") %>"); assert_eq!(rendered.text, "Hello Note!\n"); assert_eq!(rendered.errors, Vec::new()); } #[test] fn a_cycle_stops_with_an_error() { let rendered = render("<% tp.file.include(\"[[A]]\") %>"); assert_eq!(rendered.errors.len(), 1, "{:?}", rendered.errors); assert!(rendered.errors[0].message.contains("deeper than 4 levels"), "{}", rendered.errors[0].message); assert_eq!(rendered.text, "<% tp.file.include(\"[[A]]\") %>\n\n\n\n"); } #[test] fn a_missing_note_is_reported() { let rendered = render("<% tp.file.include(\"[[Missing]]\") %>"); assert_eq!(rendered.errors.len(), 1); assert!(rendered.errors[0].message.contains("does not exist"), "{}", rendered.errors[0].message); assert_eq!(rendered.text, "<% tp.file.include(\"[[Missing]]\") %>"); } #[test] fn a_missing_section_is_reported() { let rendered = render("<% tp.file.include(\"[[Note#Nowhere]]\") %>"); assert_eq!(rendered.errors.len(), 1); assert!(rendered.errors[0].message.contains("is not in the note"), "{}", rendered.errors[0].message); } #[test] fn a_bases_expression_is_evaluated() { let rendered = render("<% file.name.upper() %>"); assert_eq!(rendered.text, "NOTE.MD"); assert_eq!(rendered.errors, Vec::new()); } #[test] fn a_bases_path_reads_the_note_of_the_context() { let rendered = render("<% file.folder %>|<% file.basename %>"); assert_eq!(rendered.text, "Folder|Note"); assert_eq!(rendered.errors, Vec::new()); } #[test] fn a_bases_path_works_inside_a_template_command() { let rendered = render("<% tp.file.title + file.name %>"); assert_eq!(rendered.text, "NoteNote.md"); assert_eq!(rendered.errors, Vec::new()); } #[test] fn the_date_of_the_fallback_is_the_date_of_the_module() { let fallback = render("<% now().format(\"YYYY\") %>"); let module = render("<% tp.date.now(\"YYYY\") %>"); assert_eq!(fallback.text, "2026"); assert_eq!(fallback.text, module.text); assert_eq!(fallback.errors, Vec::new()); let dated = render("<% date(now()).format(\"YYYY\") %>"); assert_eq!(dated.text, module.text); assert_eq!(dated.errors, Vec::new()); } #[test] fn a_failing_bases_expression_is_reported() { let rendered = render("<% file.nope() %>"); assert_eq!(rendered.errors.len(), 1, "{:?}", rendered.errors); assert_eq!(rendered.text, "<% file.nope() %>"); } #[test] fn an_unknown_template_command_is_not_a_bases_expression() { let rendered = render("<% tp.foo.bar %>"); assert_eq!(rendered.errors.len(), 1); assert_eq!(rendered.errors[0].message, "tp.foo.bar is not supported"); assert_eq!(rendered.text, "<% tp.foo.bar %>"); }