86 lines
4.2 KiB
TypeScript
86 lines
4.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { buildTaskRrule, defaultTaskDueAt, filterTasks, groupTaskTree, 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('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<script>alert(1)</script>')
|
|
expect(html).toContain('<h1>Plan</h1>')
|
|
expect(html).toContain('<strong>bold</strong>')
|
|
expect(html).toContain('rel="noopener noreferrer"')
|
|
expect(html).not.toContain('<script>')
|
|
})
|
|
|
|
it('filters titles, descriptions, and list names', () => {
|
|
const searchable: SearchTask[] = [
|
|
...tasks,
|
|
{ id: '4', title: 'Plan', list_name: '工作清单' },
|
|
]
|
|
expect(filterTasks(searchable, '工作').map((task) => task.id)).toEqual(['4'])
|
|
})
|
|
|
|
it('defaults new tasks to today without a time', () => {
|
|
const due = defaultTaskDueAt(new Date(2026, 8, 8, 21, 30))
|
|
expect(due).toBe('2026-09-08')
|
|
})
|
|
|
|
it('formats API dates in local wall-clock time', () => {
|
|
const original = process.env.TZ
|
|
process.env.TZ = 'Asia/Shanghai'
|
|
expect(toDateTimeLocal('2026-09-05T12:30:00Z')).toBe('2026-09-05T20:30')
|
|
process.env.TZ = original
|
|
expect(toDateTimeLocal(null)).toBe('')
|
|
})
|
|
})
|