216 lines
10 KiB
Rust
216 lines
10 KiB
Rust
//! Integration tests of the decoration commands: the icons, tints and banners
|
|
//! of a vault, read from the index and written through the properties (D3).
|
|
|
|
mod common;
|
|
|
|
use common::{on_disk, seed, temp_vault};
|
|
use notescraft_engine::{DecorConfig, Error, Registry, Session, SessionConfig, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
/// A registry with the core commands and a session on a small decorated vault.
|
|
fn fixture(name: &str) -> (Registry<Session>, Session, common::TempVault) {
|
|
let vault = temp_vault(name);
|
|
seed(
|
|
vault.path(),
|
|
&[
|
|
("note.md", "---\nicon: LiRocket\niconColor: \"#ff8800\"\n---\n# Note\n\nThe body stays.\n"),
|
|
("plain.md", "# Plain\n"),
|
|
("custom.md", "---\ncover: rocket\n---\n# Custom\n"),
|
|
("banner.md", "---\nbanner: \"[[cover.jpg]]\"\nbanner_y: 0.2\n---\n# Banner\n"),
|
|
("remote.md", "---\nbanner: https://example.com/a.png\n---\n# Remote\n"),
|
|
("assets/cover.jpg", "not really a jpg"),
|
|
("Projects/Projects.md", "---\nicon: folder\n---\n# Projects\n"),
|
|
],
|
|
);
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register the core commands");
|
|
let session = Session::open(vault.path()).expect("open the vault");
|
|
(reg, session, vault)
|
|
}
|
|
|
|
/// The items of a `decor.list` answer.
|
|
fn items(reg: &Registry<Session>, session: &mut Session) -> serde_json::Map<String, Value> {
|
|
let result = reg.run("decor.list", session, &json!({})).expect("decor.list");
|
|
result.get("items").and_then(Value::as_object).expect("the items").clone()
|
|
}
|
|
|
|
/// The decorated note is listed, the plain one is not.
|
|
#[test]
|
|
fn the_decorated_note_is_listed_and_the_plain_one_is_not() {
|
|
let (reg, mut session, _vault) = fixture("decor-list");
|
|
let items = items(®, &mut session);
|
|
assert!(items.contains_key("note.md"), "{items:?}");
|
|
assert!(!items.contains_key("plain.md"), "{items:?}");
|
|
assert_eq!(items["note.md"]["icon"]["kind"], "lucide");
|
|
assert_eq!(items["note.md"]["icon"]["value"], "rocket");
|
|
assert_eq!(items["note.md"]["color"]["kind"], "rgb");
|
|
assert_eq!(items["note.md"]["color"]["value"], "#ff8800");
|
|
}
|
|
|
|
/// The folder note decorates the folder itself.
|
|
#[test]
|
|
fn the_folder_note_decorates_the_folder() {
|
|
let (reg, mut session, _vault) = fixture("decor-folder");
|
|
let items = items(®, &mut session);
|
|
assert_eq!(items["Projects"]["icon"]["value"], "folder", "{items:?}");
|
|
}
|
|
|
|
/// A banner link resolves like a wiki link.
|
|
#[test]
|
|
fn the_banner_link_resolves_to_the_file_it_names() {
|
|
let (reg, mut session, _vault) = fixture("decor-banner");
|
|
let items = items(®, &mut session);
|
|
assert_eq!(items["banner.md"]["banner"]["path"], "assets/cover.jpg", "{items:?}");
|
|
assert!(items["banner.md"]["banner"]["y"].as_f64().is_some_and(|y| (y - 0.2).abs() < 1e-6), "{items:?}");
|
|
assert_eq!(items["banner.md"]["banner"]["x"], 0.5);
|
|
}
|
|
|
|
/// A remote banner stays a URL: it is never a path and never fetched.
|
|
#[test]
|
|
fn a_remote_banner_is_a_url_and_not_a_path() {
|
|
let (reg, mut session, _vault) = fixture("decor-remote");
|
|
let items = items(®, &mut session);
|
|
let banner = &items["remote.md"]["banner"];
|
|
assert_eq!(banner["url"], "https://example.com/a.png", "{items:?}");
|
|
assert!(banner.get("path").is_none(), "{banner:?}");
|
|
}
|
|
|
|
/// `decor.set` writes the property and leaves the body byte for byte.
|
|
#[test]
|
|
fn decor_set_writes_the_property_and_leaves_the_body_alone() {
|
|
let (reg, mut session, vault) = fixture("decor-set");
|
|
let before = on_disk(vault.path(), "plain.md");
|
|
let result = reg.run("decor.set", &mut session, &json!({ "path": "plain.md", "icon": "rocket" })).expect("decor.set");
|
|
assert_eq!(result.get("path"), Some(&json!("plain.md")));
|
|
assert_eq!(result.get("changed"), Some(&json!(true)));
|
|
let after = on_disk(vault.path(), "plain.md");
|
|
assert!(after.contains("icon: rocket"), "{after}");
|
|
assert!(after.ends_with("# Plain\n"), "the body moved: {after}");
|
|
assert_eq!(body_of(&after), body_of(&before));
|
|
}
|
|
|
|
/// A null value removes the property again.
|
|
#[test]
|
|
fn a_null_value_removes_the_property() {
|
|
let (reg, mut session, vault) = fixture("decor-remove");
|
|
reg.run("decor.set", &mut session, &json!({ "path": "note.md", "icon": null })).expect("decor.set");
|
|
let text = on_disk(vault.path(), "note.md");
|
|
assert!(!text.contains("icon:"), "{text}");
|
|
assert!(text.contains("iconColor"), "the other properties stay: {text}");
|
|
}
|
|
|
|
/// An open buffer gets one undoable edit.
|
|
#[test]
|
|
fn an_open_buffer_gets_one_undo_step() {
|
|
let (reg, mut session, _vault) = fixture("decor-undo");
|
|
reg.run("buffer.open", &mut session, &json!({ "path": "note.md" })).expect("buffer.open");
|
|
let before = session.buffer("note.md").map(|buffer| buffer.text().to_owned()).expect("the buffer is open");
|
|
reg.run("decor.set", &mut session, &json!({ "path": "note.md", "icon": "star" })).expect("decor.set");
|
|
let buffer = session.buffer("note.md").expect("the buffer is open");
|
|
assert!(buffer.text().contains("icon: star"), "{}", buffer.text());
|
|
let undone = reg.run("buffer.undo", &mut session, &json!({ "path": "note.md" })).expect("buffer.undo");
|
|
assert_eq!(undone.get("undone"), Some(&json!(true)));
|
|
let buffer = session.buffer("note.md").expect("the buffer is open");
|
|
assert_eq!(buffer.text(), before, "the undo took the whole edit back");
|
|
assert!(!buffer.can_undo(), "one edit, one undo step");
|
|
}
|
|
|
|
/// The property names are settings, so a vault may use another one.
|
|
#[test]
|
|
fn the_property_names_are_configurable() {
|
|
let (reg, mut session, _vault) = fixture("decor-keys");
|
|
assert!(!items(®, &mut session).contains_key("custom.md"), "the default key does not match");
|
|
let config = SessionConfig { decor: DecorConfig { property_icon: String::from("cover"), ..DecorConfig::default() }, ..SessionConfig::default() };
|
|
session.set_config(config).expect("set the config");
|
|
let items = items(®, &mut session);
|
|
assert_eq!(items["custom.md"]["icon"]["value"], "rocket", "{items:?}");
|
|
assert!(items["note.md"].get("icon").is_none(), "the icon key moved: {items:?}");
|
|
}
|
|
|
|
/// A banner height outside the range is refused.
|
|
#[test]
|
|
fn an_out_of_range_banner_height_is_refused() {
|
|
for height in [0, 79, 401] {
|
|
let config = DecorConfig { banner_height: height, ..DecorConfig::default() };
|
|
assert!(config.validate().is_err(), "{height} must be refused");
|
|
}
|
|
let config = DecorConfig::default();
|
|
assert!(config.validate().is_ok());
|
|
}
|
|
|
|
/// A read-only session never writes a decoration.
|
|
#[test]
|
|
fn a_read_only_session_refuses_to_set() {
|
|
let vault = temp_vault("decor-read-only");
|
|
seed(vault.path(), &[("a.md", "# A\n")]);
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register the core commands");
|
|
let mut session = Session::open_read_only(vault.path()).expect("open the vault read-only");
|
|
let result = reg.run("decor.set", &mut session, &json!({ "path": "a.md", "icon": "rocket" }));
|
|
assert!(matches!(result, Err(Error::Disabled(ref id)) if id == "decor.set"), "{result:?}");
|
|
assert_eq!(on_disk(vault.path(), "a.md"), "# A\n");
|
|
}
|
|
|
|
/// The decorations never reach the network.
|
|
#[test]
|
|
fn the_decor_commands_never_touch_the_network() {
|
|
for source in [include_str!("../src/decor/resolve.rs"), include_str!("../src/decor/commands.rs"), include_str!("../src/decor/mod.rs")] {
|
|
for needle in ["TcpStream", "ureq", "reqwest", "hyper", "std::net"] {
|
|
assert!(!source.contains(needle), "`{needle}` must not appear in the decor commands");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The generation moves when the decorations move, and stays when they do not.
|
|
#[test]
|
|
fn the_generation_follows_the_decorations() {
|
|
let (reg, mut session, _vault) = fixture("decor-generation");
|
|
let first = reg.run("decor.list", &mut session, &json!({})).expect("decor.list");
|
|
let again = reg.run("decor.list", &mut session, &json!({})).expect("decor.list");
|
|
assert_eq!(first.get("generation"), again.get("generation"), "the same decorations give the same generation");
|
|
reg.run("decor.set", &mut session, &json!({ "path": "plain.md", "icon": "rocket" })).expect("decor.set");
|
|
let moved = reg.run("decor.list", &mut session, &json!({})).expect("decor.list");
|
|
assert_ne!(first.get("generation"), moved.get("generation"), "a new decoration gives a new generation");
|
|
}
|
|
|
|
/// The property names travel through `settings.set` like every other key.
|
|
#[test]
|
|
fn the_property_names_round_trip_through_settings_set() {
|
|
let vault = temp_vault("decor-settings");
|
|
seed(vault.path(), &[("custom.md", "---\ncover: rocket\n---\n# Custom\n")]);
|
|
let config = common::temp_config("decor-settings-cfg");
|
|
let mut session = Session::open(vault.path()).expect("open the vault");
|
|
session.use_settings_dir(config.path());
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register the core commands");
|
|
let set = reg.run("settings.set", &mut session, &json!({ "key": "session.decor.property_icon", "value": "cover" })).expect("settings.set");
|
|
assert!(set.get("key").is_some(), "{set:?}");
|
|
assert_eq!(session.config().decor.property_icon, "cover");
|
|
let result = reg.run("decor.list", &mut session, &json!({})).expect("decor.list");
|
|
assert!(result["items"].get("custom.md").is_some(), "{result}");
|
|
}
|
|
|
|
/// The `paths` and `prefix` filters narrow the list.
|
|
#[test]
|
|
fn the_paths_and_prefix_filters_narrow_the_list() {
|
|
let (reg, mut session, _vault) = fixture("decor-filter");
|
|
let only = reg.run("decor.list", &mut session, &json!({ "paths": ["note.md"] })).expect("decor.list");
|
|
let items = only["items"].as_object().expect("items");
|
|
assert_eq!(items.len(), 1, "{items:?}");
|
|
assert!(items.contains_key("note.md"), "{items:?}");
|
|
let under = reg.run("decor.list", &mut session, &json!({ "prefix": "Projects" })).expect("decor.list");
|
|
let items = under["items"].as_object().expect("items");
|
|
assert!(items.contains_key("Projects"), "the folder itself: {items:?}");
|
|
assert!(items.contains_key("Projects/Projects.md"), "its folder note: {items:?}");
|
|
let bad = reg.run("decor.list", &mut session, &json!({ "paths": [3] })).expect_err("a number is not a path");
|
|
assert!(bad.to_string().contains("paths"), "{bad}");
|
|
}
|
|
|
|
/// The body of a note, without its frontmatter.
|
|
fn body_of(text: &str) -> &str {
|
|
match text.split_once("---\n") {
|
|
Some((_, rest)) => rest.split_once("---\n").map_or(rest, |(_, body)| body),
|
|
None => text,
|
|
}
|
|
}
|