[
{"fn": "validateDraft", "name": "a plain title and nothing else is valid", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "every field filled in and consistent", "args": [{"title": "Pay rent", "notes": "Standing order failed", "priority": "high", "due": "2026-10-01", "tags": ["home", "bills-2026"], "recurrence": {"frequency": "monthly", "interval": 1, "anchor": "2026-10-01"}}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "a blank title (spaces only) needs a title", "args": [{"title": " ", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"title": "Enter a title."}}},
{"fn": "validateDraft", "name": "an empty title needs a title", "args": [{"title": "", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"title": "Enter a title."}}},
{"fn": "validateDraft", "name": "spaces around the title are fine in a draft: it is judged trimmed", "args": [{"title": " Buy milk \t", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "200 characters is the longest title", "args": [{"title": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "201 characters is too long", "args": [{"title": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"title": "Use no more than 200 characters for the title."}}},
{"fn": "validateDraft", "name": "200 emoji is 200 characters, not 400 UTF-16 units or 800 bytes", "args": [{"title": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "201 accented letters count as 201, not as 402 bytes", "args": [{"title": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"title": "Use no more than 200 characters for the title."}}},
{"fn": "validateDraft", "name": "spaces do not count towards the length", "args": [{"title": " aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "a line break inside the title is refused", "args": [{"title": "Buy\nmilk", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"title": "Keep the title to one line, without control characters."}}},
{"fn": "validateDraft", "name": "a trailing line break is trimmed away, not refused", "args": [{"title": "Buy milk\n", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "2000 characters of notes is allowed", "args": [{"title": "Buy milk", "notes": "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "2001 characters of notes is too long", "args": [{"title": "Buy milk", "notes": "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", "priority": "none", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"notes": "Use no more than 2000 characters for the notes."}}},
{"fn": "validateDraft", "name": "an unknown priority", "args": [{"title": "Buy milk", "notes": null, "priority": "urgent", "due": null, "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"priority": "Choose a priority: none, low, medium or high."}}},
{"fn": "validateDraft", "name": "a due date that never existed", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-02-30", "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"due": "Enter the due date as a real date, YYYY-MM-DD."}}},
{"fn": "validateDraft", "name": "29 February in a leap year is a real date", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2028-02-29", "tags": [], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "a due date with a trailing newline is not a date", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-09-28\n", "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"due": "Enter the due date as a real date, YYYY-MM-DD."}}},
{"fn": "validateDraft", "name": "a due date in Arabic-Indic digits is not a date", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "٢٠٢٦-09-28", "tags": [], "recurrence": null}], "expect": {"valid": false, "errors": {"due": "Enter the due date as a real date, YYYY-MM-DD."}}},
{"fn": "validateDraft", "name": "a tag with a capital letter is not in normal form", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": ["Home"], "recurrence": null}], "expect": {"valid": false, "errors": {"tags": "Tag \"Home\" can only use lower-case letters, digits and single hyphens."}}},
{"fn": "validateDraft", "name": "a tag with a # is not in normal form", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": ["#home"], "recurrence": null}], "expect": {"valid": false, "errors": {"tags": "Tag \"#home\" can only use lower-case letters, digits and single hyphens."}}},
{"fn": "validateDraft", "name": "a double hyphen is not in normal form", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": ["a--b"], "recurrence": null}], "expect": {"valid": false, "errors": {"tags": "Tag \"a--b\" can only use lower-case letters, digits and single hyphens."}}},
{"fn": "validateDraft", "name": "the same tag twice", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": ["home", "work", "home"], "recurrence": null}], "expect": {"valid": false, "errors": {"tags": "Tag \"home\" is listed twice."}}},
{"fn": "validateDraft", "name": "ten tags is the most", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9"], "recurrence": null}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "eleven tags is too many", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10"], "recurrence": null}], "expect": {"valid": false, "errors": {"tags": "Use no more than 10 tags."}}},
{"fn": "validateDraft", "name": "a 31-character tag is too long; 30 is fine", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"], "recurrence": null}], "expect": {"valid": false, "errors": {"tags": "Tag \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\" is longer than 30 characters."}}},
{"fn": "validateDraft", "name": "a repeat needs a due date", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": null, "tags": [], "recurrence": {"frequency": "weekly", "interval": 1, "anchor": "2026-09-28"}}], "expect": {"valid": false, "errors": {"recurrence": "A repeating todo needs a due date."}}},
{"fn": "validateDraft", "name": "weekday repeats cannot skip weeks", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-09-28", "tags": [], "recurrence": {"frequency": "weekdays", "interval": 2, "anchor": "2026-09-28"}}], "expect": {"valid": false, "errors": {"recurrence": "Weekday repeats cannot skip weeks; use an interval of 1."}}},
{"fn": "validateDraft", "name": "an interval of 0 repeats nothing", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-09-28", "tags": [], "recurrence": {"frequency": "daily", "interval": 0, "anchor": "2026-09-28"}}], "expect": {"valid": false, "errors": {"recurrence": "Repeat every 1 to 999 days, weeks, months or years."}}},
{"fn": "validateDraft", "name": "999 is the largest interval", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-09-28", "tags": [], "recurrence": {"frequency": "yearly", "interval": 999, "anchor": "2026-09-28"}}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "an unknown frequency", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-09-28", "tags": [], "recurrence": {"frequency": "hourly", "interval": 1, "anchor": "2026-09-28"}}], "expect": {"valid": false, "errors": {"recurrence": "Choose how it repeats: daily, weekdays, weekly, monthly or yearly."}}},
{"fn": "validateDraft", "name": "a repeat start that is not a date", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-09-28", "tags": [], "recurrence": {"frequency": "daily", "interval": 1, "anchor": "2026-13-01"}}], "expect": {"valid": false, "errors": {"recurrence": "Enter the repeat start as a real date, YYYY-MM-DD."}}},
{"fn": "validateDraft", "name": "a repeat cannot start after the due date", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-09-28", "tags": [], "recurrence": {"frequency": "daily", "interval": 1, "anchor": "2026-09-29"}}], "expect": {"valid": false, "errors": {"recurrence": "The repeat cannot start after the due date."}}},
{"fn": "validateDraft", "name": "a later occurrence of a series started earlier is fine", "args": [{"title": "Buy milk", "notes": null, "priority": "none", "due": "2026-11-30", "tags": [], "recurrence": {"frequency": "monthly", "interval": 1, "anchor": "2026-01-31"}}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateDraft", "name": "several problems are all reported, one message per field", "args": [{"title": "", "notes": "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", "priority": "urgent", "due": "2026-02-30", "tags": ["Home"], "recurrence": null}], "expect": {"valid": false, "errors": {"title": "Enter a title.", "notes": "Use no more than 2000 characters for the notes.", "priority": "Choose a priority: none, low, medium or high.", "due": "Enter the due date as a real date, YYYY-MM-DD.", "tags": "Tag \"Home\" can only use lower-case letters, digits and single hyphens."}}},
{"fn": "validateTodo", "name": "a stored open todo", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": null, "order": 0}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateTodo", "name": "a stored done todo with its completion time", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": true, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": "2026-09-28T18:00:00Z", "order": 0}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateTodo", "name": "a full stored todo with fractional-second timestamps, as JavaScript writes them", "args": [{"id": "a-1", "title": "Pay rent", "notes": "via bank", "done": true, "priority": "high", "due": "2026-10-01", "tags": ["home"], "recurrence": {"frequency": "monthly", "interval": 1, "anchor": "2026-01-31"}, "createdAt": "2026-09-28T09:30:00.123Z", "completedAt": "2026-10-01T07:00:00.5Z", "order": 3}], "expect": {"valid": true, "errors": {}}},
{"fn": "validateTodo", "name": "a done todo without a completion time", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": true, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"completedAt": "A done todo needs the time it was completed."}}},
{"fn": "validateTodo", "name": "an open todo cannot have a completion time", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": "2026-09-28T18:00:00Z", "order": 0}], "expect": {"valid": false, "errors": {"completedAt": "An open todo cannot have a completion time."}}},
{"fn": "validateTodo", "name": "a malformed completion time", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": true, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": "yesterday", "order": 0}], "expect": {"valid": false, "errors": {"completedAt": "Record when it was completed as a UTC timestamp, e.g. 2026-09-28T09:30:00Z."}}},
{"fn": "validateTodo", "name": "a stored title must already be trimmed", "args": [{"id": "t1", "title": " Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"title": "Remove the spaces around the title."}}},
{"fn": "validateTodo", "name": "a blank stored title says enter a title, not remove the spaces", "args": [{"id": "t1", "title": " ", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"title": "Enter a title."}}},
{"fn": "validateTodo", "name": "an empty id", "args": [{"id": " ", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"id": "Every todo needs an id."}}},
{"fn": "validateTodo", "name": "a created time with a space instead of T", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28 09:30:00Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"createdAt": "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z."}}},
{"fn": "validateTodo", "name": "a created time with an offset instead of Z", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00+01:00", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"createdAt": "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z."}}},
{"fn": "validateTodo", "name": "hour 24 is not a time", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T24:00:00Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"createdAt": "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z."}}},
{"fn": "validateTodo", "name": "a created time on 30 February", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-02-30T09:30:00Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"createdAt": "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z."}}},
{"fn": "validateTodo", "name": "a fraction of ten digits is too precise", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00.1234567890Z", "completedAt": null, "order": 0}], "expect": {"valid": false, "errors": {"createdAt": "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z."}}},
{"fn": "validateTodo", "name": "a negative order", "args": [{"id": "t1", "title": "Buy milk", "notes": null, "done": false, "priority": "none", "due": null, "tags": [], "recurrence": null, "createdAt": "2026-09-28T09:30:00Z", "completedAt": null, "order": -1}], "expect": {"valid": false, "errors": {"order": "Order must be a whole number, 0 or more."}}},
{"fn": "validateTodo", "name": "the draft rules apply to a stored todo, and every message comes back", "args": [{"id": "", "title": "", "notes": null, "done": false, "priority": "none", "due": null, "tags": ["home", "home"], "recurrence": null, "createdAt": "", "completedAt": null, "order": -2}], "expect": {"valid": false, "errors": {"id": "Every todo needs an id.", "title": "Enter a title.", "tags": "Tag \"home\" is listed twice.", "createdAt": "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z.", "order": "Order must be a whole number, 0 or more."}}}
]