224 lines
7.4 KiB
Rust
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());
|
|
}
|