15d7f5e873
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity - Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys - Visual view: tables, note embeds, %% comments, wrapping property chips - Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin - Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
129 lines
5.3 KiB
Rust
129 lines
5.3 KiB
Rust
//! Quick-add phrases. Today is Saturday 2026-10-17.
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
use notescraft_calendar::{Error, Event, parse_quick};
|
|
use notescraft_templates::Date;
|
|
|
|
fn today() -> Date {
|
|
Date::new(2026, 10, 17).unwrap()
|
|
}
|
|
|
|
fn ok(text: &str) -> Event {
|
|
parse_quick(text, today()).unwrap_or_else(|e| panic!("{text:?}: {e}"))
|
|
}
|
|
|
|
fn when(text: &str) -> (String, String) {
|
|
let event = ok(text);
|
|
(event.start.to_string(), event.end.map(|e| e.to_string()).unwrap_or_default())
|
|
}
|
|
|
|
fn failing_part(text: &str) -> String {
|
|
match parse_quick(text, today()) {
|
|
Err(Error::QuickAdd { part, .. }) => part,
|
|
other => panic!("{text:?} should fail, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_bare_title_is_an_all_day_event_today() {
|
|
let event = ok("Buy milk");
|
|
assert_eq!((event.title.as_str(), event.start.to_string().as_str()), ("Buy milk", "2026-10-17"));
|
|
assert!(event.end.is_none() && event.repeat.is_none() && event.reminders.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn days() {
|
|
assert_eq!(when("Call tomorrow").0, "2026-10-18");
|
|
assert_eq!(when("Call today").0, "2026-10-17");
|
|
assert_eq!(when("Call monday").0, "2026-10-19");
|
|
assert_eq!(when("Call saturday").0, "2026-10-17");
|
|
assert_eq!(when("Call next saturday").0, "2026-10-24");
|
|
assert_eq!(when("Call on Friday").0, "2026-10-23");
|
|
assert_eq!(when("Call 2026-12-25").0, "2026-12-25");
|
|
assert_eq!(when("Call oct 25").0, "2026-10-25");
|
|
assert_eq!(when("Call 25th October").0, "2026-10-25");
|
|
assert_eq!(when("Call on march 3").0, "2027-03-03");
|
|
assert_eq!(when("Call 3 jan").0, "2027-01-03");
|
|
assert_eq!(ok("Call on").title, "Call on");
|
|
assert_eq!(ok("May the force").title, "May the force");
|
|
}
|
|
|
|
#[test]
|
|
fn times_ranges_and_lengths() {
|
|
assert_eq!(when("Lunch tomorrow at 12:30"), ("2026-10-18T12:30".into(), String::new()));
|
|
assert_eq!(when("Lunch at 9").0, "2026-10-17T09:00");
|
|
assert_eq!(when("Lunch 3pm").0, "2026-10-17T15:00");
|
|
assert_eq!(when("Lunch 12am").0, "2026-10-17T00:00");
|
|
assert_eq!(when("Lunch noon").0, "2026-10-17T12:00");
|
|
assert_eq!(when("Lunch 14:30").0, "2026-10-17T14:30");
|
|
assert_eq!(when("Lunch at 9 for 1h"), ("2026-10-17T09:00".into(), "2026-10-17T10:00".into()));
|
|
assert_eq!(when("Lunch at 9 for 90 min").1, "2026-10-17T10:30");
|
|
assert_eq!(when("Lunch at 23:30 for 2 hours").1, "2026-10-18T01:30");
|
|
assert_eq!(when("Meet 9-10:30"), ("2026-10-17T09:00".into(), "2026-10-17T10:30".into()));
|
|
assert_eq!(when("Meet 9-10am").0, "2026-10-17T09:00");
|
|
assert_eq!(when("Meet 11-1pm"), ("2026-10-17T11:00".into(), "2026-10-17T13:00".into()));
|
|
assert_eq!(when("Meet from 9 to 10"), ("2026-10-17T09:00".into(), "2026-10-17T10:00".into()));
|
|
assert_eq!(ok("Lunch tomorrow at 12:30 for 1h").title, "Lunch");
|
|
}
|
|
|
|
#[test]
|
|
fn repeats_reminders_tags_and_calendars() {
|
|
assert_eq!(ok("Standup every weekday at 9:30").repeat.unwrap(), "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR");
|
|
assert_eq!(ok("Gym every mon,wed").repeat.unwrap(), "FREQ=WEEKLY;BYDAY=MO,WE");
|
|
assert_eq!(ok("Trash every 2 weeks").repeat.unwrap(), "FREQ=WEEKLY;INTERVAL=2");
|
|
assert_eq!(ok("Water plants every day").repeat.unwrap(), "FREQ=DAILY");
|
|
assert_eq!(ok("Rent monthly").repeat.unwrap(), "FREQ=MONTHLY");
|
|
assert_eq!(ok("Review every friday").repeat.unwrap(), "FREQ=WEEKLY;BYDAY=FR");
|
|
let event = ok("Dentist friday 3pm remind 15 min before remind me 1 hour #health @Personal");
|
|
assert_eq!(event.reminders.iter().map(|r| r.minutes_before).collect::<Vec<_>>(), [15, 60]);
|
|
assert_eq!((event.tags, event.calendar.as_deref(), event.title.as_str()), (vec!["health".to_owned()], Some("Personal"), "Dentist"));
|
|
assert_eq!(ok("Dentist remind 10m").reminders[0].minutes_before, 10);
|
|
}
|
|
|
|
#[test]
|
|
fn bad_parts_are_named_and_nothing_is_saved() {
|
|
assert_eq!(failing_part("Lunch at 25:00"), "25:00");
|
|
assert_eq!(failing_part("Lunch at 9:5"), "9:5");
|
|
assert_eq!(failing_part("Lunch 13pm"), "13pm");
|
|
assert_eq!(failing_part("Lunch at noonish"), "noonish");
|
|
assert_eq!(failing_part("Lunch at"), "at");
|
|
assert_eq!(failing_part("Lunch 2026-02-30"), "2026-02-30");
|
|
assert_eq!(failing_part("Lunch for banana"), "banana");
|
|
assert_eq!(failing_part("Lunch at 9 for 5 parsecs"), "5 parsecs");
|
|
assert_eq!(failing_part("Lunch every blue moon"), "every blue moon");
|
|
assert_eq!(failing_part("Lunch every 0 days"), "every 0 days");
|
|
assert_eq!(failing_part("Lunch tomorrow friday"), "friday");
|
|
assert_eq!(failing_part("Lunch at 9 at 10"), "10");
|
|
assert_eq!(failing_part("Lunch daily weekly"), "weekly");
|
|
assert_eq!(failing_part("Lunch for 1h"), "for");
|
|
assert_eq!(failing_part("Lunch 10-9"), "");
|
|
assert_eq!(failing_part("Lunch #"), "#");
|
|
assert_eq!(failing_part("tomorrow at 9"), "");
|
|
assert_eq!(failing_part(""), "");
|
|
assert_eq!(failing_part("Lunch for 99999 days"), "99999");
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_input_is_an_error_not_a_panic() {
|
|
for text in [
|
|
"x ".repeat(300).as_str(),
|
|
&"word ".repeat(150),
|
|
"日本語 を 食べる 明日",
|
|
"Lunch at 09:00",
|
|
"💥 at 💥",
|
|
"Lunch every 4294967296 days",
|
|
"a\u{0}b at 9",
|
|
"-",
|
|
"9-",
|
|
"-9",
|
|
"at -",
|
|
"from to",
|
|
"remind",
|
|
"remind me",
|
|
"next",
|
|
"on on on",
|
|
] {
|
|
let _ = parse_quick(text, today());
|
|
}
|
|
}
|