fix: harden habit input and lifecycle
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m21s

This commit is contained in:
2026-09-09 14:08:19 +08:00
parent 489d120159
commit bc4c281658
10 changed files with 735 additions and 83 deletions
+30 -1
View File
@@ -1,5 +1,5 @@
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'
import { calendarModeLabel, changedHabitFields, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, formatHabitApiError, habitActionState, habitButtonNotice, habitButtonValue, habitWeek, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, normalizeRequiredName, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, validateHabitForm, writeCountdownCache, writeHabitGridCache, writeStoredBoolean, writeStoredNavigation } from './mvp-utils'
describe('MVP view utilities', () => {
it('formats a local date as YYYY-MM-DD', () => {
@@ -142,6 +142,35 @@ describe('MVP view utilities', () => {
expect(clampFabPosition(120, 300, 390, 844)).toEqual({ x: 120, y: 300 })
})
it('validates safe habit input and emits only changed patch fields', () => {
expect(normalizeRequiredName(' 喝水 ')).toEqual({ value: '喝水', error: '' })
expect(normalizeRequiredName(' \n ')).toEqual({ value: '', error: '名称不能为空,请输入至少一个可见字符。' })
expect(validateHabitForm({ name: ' ', kind: 'numeric', target: 0, max_value: 0, schedule_type: 'daily' })).toMatchObject({ name: '请输入习惯名称', target: '目标值必须大于 0' })
expect(validateHabitForm({ name: '跑步', kind: 'numeric', target: 3, max_value: 2, schedule_type: 'daily' })).toMatchObject({ max_value: '最大值不能小于目标值' })
expect(validateHabitForm({ name: '跑步', kind: 'boolean', target: 1, max_value: 1, schedule_type: 'weekly', weekdays: [] })).toMatchObject({ weekdays: '至少选择一个星期' })
expect(validateHabitForm({ name: '跑步', kind: 'boolean', target: 1, max_value: 1, schedule_type: 'monthly', month_days: [1, 1] })).toMatchObject({ month_days: '日期不能重复' })
expect(validateHabitForm({ name: '跑步', kind: 'boolean', target: 1, max_value: 1, schedule_type: 'interval', interval_days: 0 })).toMatchObject({ interval_days: '间隔天数至少为 1' })
expect(changedHabitFields(
{ name: '跑步', kind: 'numeric', target: 3, max_value: 5, schedule_type: 'weekly', weekdays: [0, 2] },
{ name: '跑步', kind: 'numeric', target: 4, max_value: 5, schedule_type: 'weekly', weekdays: [0, 2] },
)).toEqual({ target: 4 })
})
it('maps habit conflicts and validation arrays to actionable Chinese errors', () => {
expect(formatHabitApiError('暂停日不可记录正向进度')).toBe('今天已暂停,不能记录进度。')
expect(formatHabitApiError('非计划日不可记录正向进度')).toBe('今天未安排该习惯,不能记录进度。')
expect(formatHabitApiError([{ loc: ['body', 'max_value'], msg: 'Input should be greater than 0' }])).toBe('最大值必须大于 0。')
expect(formatHabitApiError([{ loc: ['body', 'weekdays'], msg: 'bad' }])).toBe('每周计划至少选择一天,且不能重复。')
expect(formatHabitApiError({ detail: '请先归档再永久删除' })).toBe('请先归档该习惯,再永久删除。')
})
it('blocks writes on paused, unscheduled, and archived habit rows', () => {
expect(habitActionState({ scheduled: true, paused: false })).toEqual({ writable: true, reason: '' })
expect(habitActionState({ scheduled: true, paused: true })).toEqual({ writable: false, reason: '今天已暂停' })
expect(habitActionState({ scheduled: false, paused: false })).toEqual({ writable: false, reason: '今天未安排' })
expect(habitActionState({ scheduled: true, paused: false }, true)).toEqual({ writable: false, reason: '该习惯已归档' })
})
it('distinguishes tapping the add button from dragging it', () => {
expect(isFabDrag(3, 4)).toBe(false)
expect(isFabDrag(8, 0)).toBe(true)