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, readStoredNavigation, shouldToggleRowSwipe, writeCountdownCache, writeHabitGridCache, writeStoredBoolean, writeStoredNavigation } 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('restores the last page and selected task list after refresh', () => { const storage = new Map() const fakeStorage = { getItem: (key: string) => storage.get(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), } expect(readStoredNavigation(fakeStorage, 'dodo.navigation')).toEqual({ view: 'today', listId: '' }) writeStoredNavigation(fakeStorage, 'dodo.navigation', 'tasks', 'list-2') expect(readStoredNavigation(fakeStorage, 'dodo.navigation')).toEqual({ view: 'tasks', listId: 'list-2' }) storage.set('dodo.navigation', JSON.stringify({ view: 'invalid', listId: 'list-2' })) expect(readStoredNavigation(fakeStorage, 'dodo.navigation')).toEqual({ view: 'today', listId: '' }) }) 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() 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) }) })