import { describe, expect, it } from 'vitest' import { buildTaskRrule, classifyTaskForToday, defaultTaskDueAt, filterTasks, groupTaskTree, isSameTaskSortTier, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal } from './task-utils' type SearchTask = { id: string title: string description?: string parent_id?: string | null completed?: boolean list_name?: string subtasks?: SearchTask[] } const tasks: SearchTask[] = [ { id: '1', title: 'Write release notes', description: 'mention **API**', parent_id: null, completed: false }, { id: '2', title: 'Check links', description: '', parent_id: '1', completed: false }, { id: '3', title: 'Buy milk', description: '', parent_id: null, completed: true }, ] describe('task utilities', () => { it('filters task titles and markdown descriptions case-insensitively', () => { expect(filterTasks(tasks, 'api').map((task) => task.id)).toEqual(['1']) expect(filterTasks(tasks, 'WRITE').map((task) => task.id)).toEqual(['1']) }) it('keeps a one-level subtask tree without duplicating children', () => { expect(groupTaskTree(tasks)).toEqual([{ task: tasks[0], subtasks: [tasks[1]] }, { task: tasks[2], subtasks: [] }]) }) it('preserves subtasks already nested by the task API', () => { const child: SearchTask = { id: 'nested-child', title: 'Nested child', parent_id: 'nested-parent' } const parent: SearchTask = { id: 'nested-parent', title: 'Nested parent', parent_id: null, subtasks: [child] } expect(groupTaskTree([parent])).toEqual([{ task: parent, subtasks: [child] }]) }) it('classifies due edits for Today membership', () => { const start = new Date('2026-09-10T00:00:00+08:00') const end = new Date('2026-09-11T00:00:00+08:00') expect(classifyTaskForToday({ completed: false, due_at: '2026-09-09T12:00:00+08:00' }, start, end)).toBe('overdue') expect(classifyTaskForToday({ completed: false, due_at: '2026-09-10T12:00:00+08:00' }, start, end)).toBe('today') expect(classifyTaskForToday({ completed: false, due_at: '2026-09-11T12:00:00+08:00' }, start, end)).toBe('outside') expect(classifyTaskForToday({ completed: true, due_at: '2026-09-09T12:00:00+08:00' }, start, end)).toBe('outside') }) it('only allows manual movement within the same completion and deadline tier', () => { const noDueOpen = { completed: false, due_at: null } expect(isSameTaskSortTier(noDueOpen, { completed: false, due_at: null })).toBe(true) expect(isSameTaskSortTier({ completed: false, due_at: '2026-09-10T08:00:00Z' }, { completed: false, due_at: '2026-09-10T08:00:00Z' })).toBe(true) expect(isSameTaskSortTier({ completed: false, due_at: '2026-09-10T08:00:00Z' }, { completed: false, due_at: '2026-09-10T16:00:00+08:00' })).toBe(true) expect(isSameTaskSortTier(noDueOpen, { completed: false, due_at: '2026-09-10T08:00:00Z' })).toBe(false) expect(isSameTaskSortTier(noDueOpen, { completed: true, due_at: null })).toBe(false) expect(isSameTaskSortTier({ completed: false, due_at: '2026-09-10T08:00:00Z' }, { completed: false, due_at: '2026-09-11T08:00:00Z' })).toBe(false) }) it('moves an item before or after another item without changing other scopes', () => { const rows = [ { id: 'a', parent_id: null }, { id: 'a1', parent_id: 'a' }, { id: 'b', parent_id: null }, { id: 'c', parent_id: null }, ] expect(moveItemWithinScope(rows, 'c', 'a', 'before').map((row) => row.id)).toEqual(['c', 'a1', 'a', 'b']) expect(moveItemWithinScope(rows, 'a', 'c', 'after').map((row) => row.id)).toEqual(['b', 'a1', 'c', 'a']) expect(moveItemWithinScope(rows, 'a1', 'b', 'before')).toEqual(rows) }) it('builds and parses custom repeat rules like TickTick', () => { expect(buildTaskRrule({ frequency: 'weekly', interval: 2, weekdays: ['MO', 'WE', 'FR'], endMode: 'never' })).toBe('FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR') expect(buildTaskRrule({ frequency: 'monthly', interval: 1, monthDays: [1, 15, 31], endMode: 'count', count: 10 })).toBe('FREQ=MONTHLY;BYMONTHDAY=1,15,31;COUNT=10') expect(buildTaskRrule({ frequency: 'yearly', interval: 1, weekdays: [], monthDays: [], endMode: 'date', count: 10, until: '2027-12-31' })).toBe('FREQ=YEARLY;UNTIL=2027-12-31T23:59:59') expect(parseTaskRrule('FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;COUNT=8')).toEqual({ frequency: 'weekly', interval: 2, weekdays: ['MO', 'WE'], monthDays: [], endMode: 'count', count: 8, until: '' }) expect(() => buildTaskRrule({ frequency: 'weekly', interval: 2, weekdays: [], monthDays: [], endMode: 'never', count: 10, until: '' })).toThrow('至少选择一个重复日期') expect(() => buildTaskRrule({ frequency: 'daily', interval: 0, weekdays: [], monthDays: [], endMode: 'never', count: 10, until: '' })).toThrow('重复间隔至少为 1') }) it('renders safe basic markdown and strips unsafe html', () => { const html = renderMarkdown('# Plan\n**bold** [link](https://example.com)\n') expect(html).toContain('