Files
Marcos Paulo 16a0786a2d
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: recurring task toggle, Templater render, icons in Explorer rows and tabs (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 11:16:23 -03:00

224 lines
7.4 KiB
Rust

//! Tests of the renderer (spec D4).
use notescraft_bases::Value;
use notescraft_templater::{Answers, Ctx, 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 tags of the target note.
fn tags() -> Vec<String> {
vec!["alpha".to_owned(), "beta".to_owned()]
}
/// The frontmatter of the target note.
fn frontmatter() -> Vec<(String, Value)> {
vec![("project".to_owned(), Value::Str("NotesCraft".to_owned())), ("my key".to_owned(), Value::Str("spaced".to_owned()))]
}
/// A context with the note of the tests.
fn ctx<'a>(read_note: &'a dyn Fn(&str) -> Option<String>, 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: "chosen",
clipboard: "copied",
read_note,
exists,
}
}
/// Renders a template with the context of the tests.
fn render(template: &str) -> notescraft_templater::Rendered {
let read_note = |_: &str| -> Option<String> { Some("body".to_owned()) };
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_golden_template_renders_the_context() {
let template = r#"# <% tp.file.title %>
- folder: <% tp.file.folder %>
- path: <% tp.file.path %>
- tags: <% tp.file.tags %>
- project: <% tp.frontmatter.project %>
- created: <% tp.file.creation_date %>
- date: <% tp.date.now("YYYY-MM-DD") %>
"#;
let expected = "# Note
- folder: Folder
- path: /vault/Folder/Note.md
- tags: alpha, beta
- project: NotesCraft
- created: 2026-10-13T15:04:05
- date: 2026-10-13
";
let rendered = render(template);
assert_eq!(rendered.text, expected);
assert_eq!(rendered.errors, Vec::new());
assert_eq!(rendered.cursor, None);
}
#[test]
fn a_relative_path_is_the_path_of_the_vault() {
let read_note = |_: &str| -> Option<String> { None };
let exists = |_: &str| -> bool { false };
let tags = tags();
let frontmatter = frontmatter();
let ctx = ctx(&read_note, &exists, &tags, &frontmatter);
let template = parse("<% tp.file.path(true) %>|<% tp.file.folder(true) %>").unwrap();
let rendered = template.render(&ctx, &Answers::default());
assert_eq!(rendered.text, "Folder/Note.md|/vault/Folder");
}
#[test]
fn an_unknown_function_is_kept_verbatim() {
let rendered = render("x <% tp.foo.bar %> y");
assert_eq!(rendered.text, "x <% tp.foo.bar %> y");
assert_eq!(rendered.errors.len(), 1);
assert_eq!(rendered.errors[0].line, 1);
assert_eq!(rendered.errors[0].message, "tp.foo.bar is not supported");
}
#[test]
fn an_unknown_call_is_kept_verbatim() {
let rendered = render("x <% tp.file.nope(1) %> y");
assert_eq!(rendered.text, "x <% tp.file.nope(1) %> y");
assert_eq!(rendered.errors.len(), 1);
assert_eq!(rendered.errors[0].message, "tp.file.nope is not supported");
let deeper = render("x <% tp.file.tags.join(\", \") %> y");
assert_eq!(deeper.text, "x <% tp.file.tags.join(\", \") %> y");
assert_eq!(deeper.errors[0].message, "tp.file.tags.join is not supported");
}
#[test]
fn a_javascript_block_is_kept_verbatim() {
let rendered = render("<%* let x = 1 %>ok");
assert_eq!(rendered.text, "<%* let x = 1 %>ok");
assert_eq!(rendered.errors.len(), 1);
assert_eq!(rendered.errors[0].message, "JavaScript is not supported");
}
#[test]
fn a_broken_expression_is_kept_verbatim() {
let rendered = render("x <% a ? b : c %> y");
assert_eq!(rendered.text, "x <% a ? b : c %> y");
assert_eq!(rendered.errors.len(), 1);
}
#[test]
fn the_cursor_sits_after_the_expanded_tags() {
let rendered = render("Hello <% tp.file.title %> world <% tp.file.cursor() %>");
assert_eq!(rendered.text, "Hello Note world ");
assert_eq!(rendered.cursor, Some(17));
assert_eq!(rendered.errors, Vec::new());
}
#[test]
fn the_lowest_cursor_order_wins() {
let rendered = render("a<% tp.file.cursor(1) %>b<% tp.file.cursor(0) %>c");
assert_eq!(rendered.text, "abc");
assert_eq!(rendered.cursor, Some(2));
assert_eq!(rendered.errors.len(), 1);
assert_eq!(rendered.errors[0].line, 1);
}
#[test]
fn cursor_append_writes_text_and_moves_the_cursor() {
let rendered = render("a<% tp.file.cursor_append(\"x\") %>b");
assert_eq!(rendered.text, "axb");
assert_eq!(rendered.cursor, Some(2));
}
#[test]
fn a_missing_frontmatter_key_has_a_default() {
let rendered = render("<% tp.frontmatter.missing ?? \"none\" %>");
assert_eq!(rendered.text, "none");
assert_eq!(rendered.errors, Vec::new());
let known = render("<% tp.frontmatter.project ?? \"none\" %>");
assert_eq!(known.text, "NotesCraft");
}
#[test]
fn an_expression_over_the_limit_is_kept_verbatim() {
let long = "x".repeat(10_000);
let template = format!("<% {long} %>");
let rendered = render(&template);
assert_eq!(rendered.text, format!("<% {long} %>"));
assert_eq!(rendered.errors.len(), 1);
}
#[test]
fn a_frontmatter_key_may_hold_spaces() {
let rendered = render("<% tp.frontmatter[\"my key\"] %>");
assert_eq!(rendered.text, "spaced");
}
#[test]
fn rename_and_move_are_recorded() {
let rendered = render("<% tp.file.rename(\"New\") %><% tp.file.move(\"Other/Note.md\") %>");
assert_eq!(rendered.text, "");
assert_eq!(rendered.rename, Some("New".to_owned()));
assert_eq!(rendered.move_to, Some("Other/Note.md".to_owned()));
assert_eq!(rendered.errors, Vec::new());
}
#[test]
fn a_prompt_renders_nothing() {
let rendered = render("<% tp.system.prompt(\"Name\") %>after");
assert_eq!(rendered.text, "after");
assert_eq!(rendered.errors, Vec::new());
}
#[test]
fn a_note_is_read_and_checked() {
let rendered = render("<% tp.file.content %>|<% tp.file.exists(\"a.md\") %>|<% tp.file.selection() %>");
assert_eq!(rendered.text, "body|true|chosen");
}
#[test]
fn values_are_added_as_text_or_as_numbers() {
assert_eq!(render("<% 1 + 2 %>").text, "3");
assert_eq!(render("<% \"a\" + 1 %>").text, "a1");
assert_eq!(render("<% tp.file.title + \"!\" %>").text, "Note!");
assert_eq!(render("<% 1 + \"a\" %>").text, "1a");
assert_eq!(render("<% 1 + true %>").errors.len(), 1);
}
#[test]
fn the_output_is_capped() {
let read_note = |_: &str| -> Option<String> { Some("x".repeat(1024 * 1024)) };
let exists = |_: &str| -> bool { false };
let tags = tags();
let frontmatter = frontmatter();
let ctx = ctx(&read_note, &exists, &tags, &frontmatter);
let template = parse("<% tp.file.content %>").unwrap();
let limit = template.output_limit();
let rendered = template.render(&ctx, &Answers::default());
assert_eq!(rendered.text.len(), limit);
assert_eq!(rendered.errors.len(), 1);
assert_eq!(rendered.errors[0].message, "the output is too large");
}
#[test]
fn a_template_without_commands_is_copied() {
let rendered = render("plain text");
assert_eq!(rendered.text, "plain text");
assert_eq!(rendered.errors, Vec::new());
}