feat: refine archived habit management
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m28s

This commit is contained in:
2026-09-10 14:39:06 +08:00
parent bf61e69776
commit 3e80f2ecbb
8 changed files with 515 additions and 34 deletions
+44 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { calendarModeLabel, changedHabitFields, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, formatAuditAction, formatAuditEntity, formatHabitApiError, formatLocalShortDateTime, formatUserAgent, 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'
import { archivePanelFlags, calendarModeLabel, changedHabitFields, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, formatArchivedAt, formatAuditAction, formatAuditEntity, formatHabitApiError, formatLocalShortDateTime, formatUserAgent, habitActionState, habitButtonNotice, habitButtonValue, habitWeek, invalidateHabitGridCache, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, normalizeRequiredName, numericHabitInputValue, performHabitRestore, 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', () => {
@@ -196,6 +196,49 @@ describe('MVP view utilities', () => {
expect(formatLocalShortDateTime('2026-04-31T12:00:00+08:00', { timeZone: 'UTC' })).toBe('未知时间')
})
it('keeps a successful restore committed when the grid refresh fails', async () => {
const events: string[] = []
const result = await performHabitRestore({
restore: async () => { events.push('restored') },
commitRestore: () => { events.push('archive-removed') },
refreshGrid: async () => false,
})
expect(events).toEqual(['restored', 'archive-removed'])
expect(result).toEqual({ restored: true, refreshed: false })
})
it('invalidates a restored habit grid cache before reconciliation', () => {
writeHabitGridCache('2026-09-07', [{ id: 'restored-habit' }])
invalidateHabitGridCache()
expect(readHabitGridCache('2026-09-07')).toBeNull()
})
it('does not commit a failed restore or attempt a grid refresh', async () => {
const events: string[] = []
await expect(performHabitRestore({
restore: async () => { throw new Error('恢复失败') },
commitRestore: () => { events.push('archive-removed') },
refreshGrid: async () => { events.push('grid-refreshed') },
})).rejects.toThrow('恢复失败')
expect(events).toEqual([])
})
it('formats archive timestamps locally and rejects every malformed calendar value', () => {
expect(formatArchivedAt('2026-09-10T12:08:00Z', { timeZone: 'Asia/Shanghai' })).toMatch(/^归档于 2026\/9\/10 20:08$/)
for (const value of [null, '', 'not-a-date', '2026-02-30T12:00:00Z', '2026-13-01T00:00:00Z', '2026-09-10T25:00:00Z']) {
expect(formatArchivedAt(value, { timeZone: 'UTC' })).toBe('归档时间未知')
}
})
it('makes archive loading, error, and empty states mutually exclusive', () => {
expect(archivePanelFlags('loading', 0)).toEqual({ loading: true, error: false, empty: false, list: false })
expect(archivePanelFlags('error', 0)).toEqual({ loading: false, error: true, empty: false, list: false })
expect(archivePanelFlags('success', 0)).toEqual({ loading: false, error: false, empty: true, list: false })
expect(archivePanelFlags('success', 2)).toEqual({ loading: false, error: false, empty: false, list: true })
expect(archivePanelFlags('idle', 0)).toEqual({ loading: false, error: false, empty: false, list: false })
})
it('localizes known audit actions and entities and hides unknown actions', () => {
expect(['create', 'update', 'complete', 'delete', 'archive', 'restore', 'move', 'import'].map(formatAuditAction)).toEqual(['创建', '更新', '完成', '删除', '归档', '恢复', '移动', '导入'])
expect(['task', 'list', 'folder', 'countdown', 'backup'].map(formatAuditEntity)).toEqual(['任务', '清单', '文件夹', '倒数日', '备份'])