138 lines
6.6 KiB
TypeScript
138 lines
6.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { calendarModeLabel, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, habitButtonNotice, habitButtonValue, habitWeek, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, shouldToggleRowSwipe, writeCountdownCache, writeHabitGridCache, writeStoredBoolean } from './mvp-utils'
|
|
|
|
describe('MVP view utilities', () => {
|
|
it('formats a local date as YYYY-MM-DD', () => {
|
|
expect(dateKey(new Date(2026, 8, 5))).toBe('2026-09-05')
|
|
})
|
|
|
|
it('returns the monday-to-sunday week containing the requested day', () => {
|
|
expect(habitWeek(new Date(2026, 8, 5)).map(dateKey)).toEqual([
|
|
'2026-08-31', '2026-09-01', '2026-09-02', '2026-09-03', '2026-09-04', '2026-09-05', '2026-09-06',
|
|
])
|
|
})
|
|
|
|
it('identifies only task-backed views as task data loaders', () => {
|
|
expect(['tasks', 'today', 'upcoming'].filter(isTaskView)).toEqual(['tasks', 'today', 'upcoming'])
|
|
expect(['habits', 'settings', 'trash'].filter(isTaskView)).toEqual([])
|
|
})
|
|
|
|
it('only completes numeric habits when progress reaches the target', () => {
|
|
expect(isHabitComplete('numeric', 3, 5)).toBe(false)
|
|
expect(isHabitComplete('numeric', 5, 5)).toBe(true)
|
|
expect(isHabitComplete('boolean', 1, 5)).toBe(true)
|
|
})
|
|
|
|
it('does not invent a numeric value when the input is empty', () => {
|
|
expect(numericHabitInputValue(undefined)).toBeNull()
|
|
expect(numericHabitInputValue('')).toBeNull()
|
|
expect(numericHabitInputValue(4)).toBe(4)
|
|
})
|
|
|
|
it('uses today as the default landing view', () => {
|
|
expect(defaultView()).toBe('today')
|
|
})
|
|
|
|
it('quick-adds Today tasks to Inbox at noon in Asia/Shanghai', () => {
|
|
const now = new Date('2026-09-05T16:30:00.000Z')
|
|
expect(quickTaskFields('today', 'selected-list', 'inbox-list', now)).toEqual({
|
|
list_id: 'inbox-list',
|
|
due_at: '2026-09-06T12:00:00+08:00',
|
|
})
|
|
})
|
|
|
|
it('keeps quick-add tasks in the selected list outside Today', () => {
|
|
expect(quickTaskFields('tasks', 'selected-list', 'inbox-list', new Date('2026-09-05T16:30:00.000Z'))).toEqual({
|
|
list_id: 'selected-list',
|
|
})
|
|
})
|
|
|
|
it('only includes scheduled, unpaused habits today regardless of kind or completion', () => {
|
|
const day = '2026-09-06'
|
|
expect(isHabitScheduledToday({ kind: 'boolean', cells: [{ day, scheduled: true, paused: false, value: true }] }, day)).toBe(true)
|
|
expect(isHabitScheduledToday({ kind: 'numeric', cells: [{ day, scheduled: true, paused: false, value: 5 }] }, day)).toBe(true)
|
|
expect(isHabitScheduledToday({ kind: 'boolean', cells: [{ day, scheduled: false, paused: false, value: true }] }, day)).toBe(false)
|
|
expect(isHabitScheduledToday({ kind: 'numeric', cells: [{ day, scheduled: true, paused: true, value: 0 }] }, day)).toBe(false)
|
|
expect(isHabitScheduledToday({ kind: 'boolean', cells: [] }, day)).toBe(false)
|
|
})
|
|
|
|
it('increments count habits one swipe at a time and caps at target', () => {
|
|
expect(nextHabitSwipeValue('numeric', 2, 5)).toBe(3)
|
|
expect(nextHabitSwipeValue('numeric', 5, 5)).toBe(5)
|
|
expect(previousHabitSwipeValue('numeric', 3)).toBe(2)
|
|
expect(previousHabitSwipeValue('numeric', 0)).toBe(0)
|
|
})
|
|
|
|
it('resets a completed numeric habit when its round button is clicked again', () => {
|
|
expect(habitButtonValue('numeric', 2, 3)).toBe(3)
|
|
expect(habitButtonValue('numeric', 3, 3)).toBe(0)
|
|
expect(habitButtonValue('boolean', 0, 1)).toBe(1)
|
|
expect(habitButtonValue('boolean', 1, 1)).toBe(0)
|
|
})
|
|
|
|
it('uses reset wording when a completed numeric habit button returns to zero', () => {
|
|
expect(habitButtonNotice('numeric', 3, 0)).toBe('今日进度已重置')
|
|
expect(habitButtonNotice('numeric', 2, 3)).toBe('已记录一次 🎉')
|
|
expect(habitButtonNotice('numeric', 2, 1)).toBe('已减少一次')
|
|
expect(habitButtonNotice('boolean', 1, 0)).toBe('已取消完成')
|
|
})
|
|
|
|
it('persists boolean display preferences across page refreshes', () => {
|
|
const storage = new Map<string, string>()
|
|
const fakeStorage = {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => storage.set(key, value),
|
|
}
|
|
expect(readStoredBoolean(fakeStorage, 'dodo.show-completed', true)).toBe(true)
|
|
writeStoredBoolean(fakeStorage, 'dodo.show-completed', false)
|
|
expect(readStoredBoolean(fakeStorage, 'dodo.show-completed', true)).toBe(false)
|
|
storage.set('dodo.show-completed', 'invalid')
|
|
expect(readStoredBoolean(fakeStorage, 'dodo.show-completed', true)).toBe(true)
|
|
})
|
|
|
|
it('reuses the current week habit grid while refreshing in the background', () => {
|
|
const habits = [{ id: 'habit-1', name: '喝水' }]
|
|
writeHabitGridCache('2026-09-07', habits)
|
|
expect(readHabitGridCache('2026-09-07')).toEqual(habits)
|
|
expect(readHabitGridCache('2026-09-14')).toBeNull()
|
|
})
|
|
|
|
it('reuses countdown data while the page refreshes in the background', () => {
|
|
const active = [{ id: 'countdown-1', title: '旅行' }]
|
|
const archived = [{ id: 'countdown-2', title: '旧日期' }]
|
|
writeCountdownCache(active, archived)
|
|
expect(readCountdownCache()).toEqual({ items: active, archived })
|
|
})
|
|
|
|
it('renders countdown labels from date-only day values', () => {
|
|
expect(countdownDayText(8)).toBe('还有 8 天')
|
|
expect(countdownDayText(0)).toBe('就是今天')
|
|
expect(countdownDayText(-12)).toBe('已经 12 天')
|
|
expect(countdownKindLabel('countdown')).toBe('倒数日')
|
|
expect(countdownKindLabel('anniversary')).toBe('纪念日')
|
|
expect(countdownKindLabel('birthday')).toBe('生日')
|
|
expect(calendarModeLabel('solar')).toBe('公历')
|
|
expect(calendarModeLabel('lunar')).toBe('农历')
|
|
})
|
|
|
|
it('toggles a row for a deliberate mostly-horizontal swipe', () => {
|
|
expect(shouldToggleRowSwipe(78, 8)).toBe(true)
|
|
expect(shouldToggleRowSwipe(88, 28)).toBe(true)
|
|
expect(shouldToggleRowSwipe(-78, 8)).toBe(false)
|
|
expect(shouldToggleRowSwipe(30, 2)).toBe(false)
|
|
expect(shouldToggleRowSwipe(80, 60)).toBe(false)
|
|
})
|
|
|
|
it('keeps the draggable mobile add button inside the viewport', () => {
|
|
expect(clampFabPosition(-10, -20, 390, 844)).toEqual({ x: 14, y: 14 })
|
|
expect(clampFabPosition(500, 900, 390, 844)).toEqual({ x: 324, y: 718 })
|
|
expect(clampFabPosition(120, 300, 390, 844)).toEqual({ x: 120, y: 300 })
|
|
})
|
|
|
|
it('distinguishes tapping the add button from dragging it', () => {
|
|
expect(isFabDrag(3, 4)).toBe(false)
|
|
expect(isFabDrag(8, 0)).toBe(true)
|
|
expect(isFabDrag(6, 7)).toBe(true)
|
|
})
|
|
})
|